-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
186 lines (173 loc) · 6.09 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
const R = require('ramda')
const fs = require('fs')
const postcss = require('postcss')
const chalk = require('chalk')
const fileExists = path => {
try {
fs.accessSync(path)
return true
} catch(e) {
return false
}
}
// Create the full directory tree for a file path
const createDirs =
R.compose(
R.map(dir => fs.mkdirSync(dir)) // create all missing directories
, R.filter(dir => !fs.existsSync(dir)) // filter out only dirs that do not exist
, R.dropLast(1) // we don't want the path with the filename at the end (last element in the scan)
, R.drop(1) // we don't want the first empty array from the scan
, R.map(R.join('/')) // Array of directory levels ['', 'css', 'css/nonprofits', 'css/nonprofits/recurring_donations.css']
, R.scan((arr, p) => R.append(p, arr), []) // an array of arrays of directory levels [[], ['css'], ['css', 'nonprofits'], ['css', 'nonprofits', 'recurring_donations.css']]
, R.split('/')
)
const logCompileErr = err =>
process.stderr.write(chalk.red('!! error: ' + err.message + '\n'))
// Postcss compile an input file to an output path using a postcss compiler object
const compile = (plugins, from, to) => {
postcss(plugins)
.process(fs.readFileSync(from), {from, to, map: true})
.then(result => {
fs.writeFileSync(to, result.css)
if(result.map) fs.writeFileSync(to + '.map', result.map)
log(chalk.green.bold('=> compiled: ' + from + ' to ' + to))
result.warnings().forEach(warn => log(chalk.red('!! warning: ' + warn.toString())))
// Get all dependency paths and watch them
const recurseCompile = ()=> compile(plugins, from, to)
const notModule = p => p.indexOf('node_modules') === -1
const isDep = m => m && m.type === 'dependency'
R.compose(
R.map(watchDep(from, recurseCompile))
, R.filter(notModule)
, unwatchRemovedDeps(from)
, R.map(R.prop('file'))
, R.filter(isDep)
)(result.messages || [])
})
.catch(logCompileErr)
}
var watching = {}
// Unwatch all dependents that have been removed from an index file
const unwatchRemovedDeps = R.curryN(2, (index, deps) => {
if(watching[index]) {
const keys = R.keys(watching[index].deps)
const missing = R.without(deps, keys)
R.map(f => unwatchDep(index, f), missing)
}
return deps
})
// Unwatch an index file (plus all its dependents)
const unwatchIndex = index => {
if(!watching[index]) return
log(chalk.gray('<> closing: ' + index))
watching[index].watcher.close()
R.map(w => w.close(), watching[index].deps)
delete watching[index]
}
// Unwatch a dependent file
const unwatchDep = (index, file) => {
if(!watching[index].deps[file]) return
log(chalk.gray('<> closing: ' + file))
watching[index].deps[file].close()
delete watching[index].deps[file]
}
// Start watching an index file (unwatch it if it is removed)
const watchIndex = (file, cb) => {
watching[file] = {
deps: {}
, watcher: watch(file, ()=> { fileExists(file) ? cb() : unwatchIndex(file) })
}
}
// Start watching a depndent file (compile the parent on dependent file changes)
// Unwatch the index file if it is removed, and unwatch the dependent if it is removed
const watchDep = R.curryN(3, (index, cb, file) => {
if(!watching[index] || watching[index].deps[file]) return
watching[index].deps[file] = watch(file, ()=> {
if(!fileExists(index)) {
unwatchIndex(index)
} else if(!fileExists(file)) {
unwatchDep(index, file)
} else {
cb()
}
})
})
const watch = (file, cb) => {
log(chalk.gray('<> watching: ' + file))
return fs.watch(file, {}, cb)
}
var log = ()=>{}
// options.plugins is an array of postcss plugin modules
// options.input is the top-level directory of containing all input css files
// options.output is the top-level directory that will contain all output/compiled css files
const initialize = options => {
// Set defaults
options = R.merge({
indexName: 'index.css'
, plugins: []
, input: ''
, output: ''
, watch: false
}, options)
// Log is a no-op unless options.log is true
if(options.log) log = console.log.bind(console)
walkDir(options.input, options)
}
// Recursively walk through a directory, finding all index css files
// Uses a stack, not actual recursion
const walkDir = (input, options) => {
const inputRegex = new RegExp("^" + options.input.replace('/', '\/'))
var stack = [input]
// Tree traversal of directory structure using stack recursion
while(stack.length) {
input = stack.pop()
const stats = fs.lstatSync(input)
const output = R.replace(inputRegex, options.output, input)
if(stats.isDirectory()) {
// Watch the directory for new files
// Push all children in the directory to the stack
if(options.watch) watchDir(input, output, options)
const children = R.map(R.concat(input + '/'), fs.readdirSync(input))
stack.push.apply(stack, children)
} else if(stats.isFile()) {
watchNewFile(input, output, options)
}
}
}
var watchingDirs = {}
const unwatchDir = (input, output, options) => {
if(!watchingDirs[input]) return
watchingDirs[input].close()
delete watchingDirs[input]
}
const watchDir = (input, output, options) => {
watchingDirs[input] = fs.watch(input, (ev, filename) => {
if(ev === 'rename') {
handleRename(input + '/' + filename, output + '/' + filename, options)
}
})
}
const handleRename = (input, output, options) => {
if(fileExists(input)) {
const stats = fs.lstatSync(input)
// New file created
if(stats.isFile()) {
watchNewFile(input, output, options)
// New directory created
} else if(stats.isDirectory()) {
watchDir(input, output, options)
walkDir(input, options)
}
} else { // File has been removed
if(watching[input]) unwatchIndex(input)
if(watchingDirs[input]) unwatchDir(input)
}
}
const watchNewFile = (input, output, options) => {
if(input.match(new RegExp('\/' + options.indexName + '$'))) {
createDirs(output)
compile(options.plugins, input, output)
if(options.watch) watchIndex(input, ()=> compile(options.plugins, input, output))
}
}
module.exports = initialize