-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
323 lines (259 loc) · 8.99 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
"use strict"
let gulp = require("gulp")
let rename = require("gulp-rename")
let through = require("through2")
let async_done = require("async-done")
let Vinyl = require("vinyl")
module.exports = glupost
class Registry {
constructor() { this._tasks = {} }
init() { this._tasks = {} }
get(name) { return this._tasks[name] }
set(name, task) { this._tasks[name] = task }
tasks() { return this._tasks }
}
// Create gulp tasks.
function glupost(tasks={}, {template={}, logger=console, beep=false, register=false} = {}) {
// Second call to glupost clears previous tasks.
gulp.registry(new Registry())
if (logger === null)
logger = {log() {}, info() {}, debug() {}, warn() {}, error() {}}
// Expand template object with defaults.
expand(template, {transforms: [], dest: "."})
// Replace tasks with normalised objects.
let entries = Object.entries(tasks)
for (let [name, task] of entries)
tasks[name] = init(task, template, name)
// Create watch task (after other tasks are initialised).
let watch_task = create_watch_task(tasks, logger, beep)
if (watch_task)
tasks["watch"] = init(watch_task, {}, "watch")
// Compose gulp tasks (after watch task is ready).
let gulp_tasks = {}
entries = Object.entries(tasks)
for (let [name, task] of entries)
gulp_tasks[name] = compose(task, tasks)
if (register) {
entries = Object.entries(gulp_tasks)
for (let [name, task] of entries)
gulp.task(name, task)
}
return gulp_tasks
}
// Recursively validate and normalise task and its properties, add wrappers around
// strings and functions, and return the (wrapped) task.
function init(task, template, name=null) {
validate(task)
// 1. named task.
if (typeof task === "string") {
if (!name)
name = task
return {name, alias: task}
}
// 2. a function directly.
if (typeof task === "function") {
if (!name) {
let m = /^function (.+)\(/.exec(task.toString())
if (m)
name = m[1]
}
return {name, callback: task}
}
// 3. task object.
if (typeof task === "object") {
if (!task.task && !task.series && !task.parallel)
expand(task, template)
if (task.watch === true)
task.watch = task.src
if (task.task) {
task.task = init(task.task, template)
if (!name)
name = task.task.name
}
else if (task.series) {
task.series = task.series.map((task) => init(task, template))
}
else if (task.parallel) {
task.parallel = task.parallel.map((task) => init(task, template))
}
task.name = name
return task
}
}
// Recursively compose task's action and return it.
function compose(task, tasks, aliases=new Set()) {
if (task.action)
return task.action
let action
if (task.alias) {
let name = task.alias
let aliased_task = tasks[name]
if (!aliased_task)
throw new Error("Task never defined: " + name + ".")
if (aliases.has(name))
throw new Error("Circular aliases.")
aliases.add(name)
let f = compose(aliased_task, tasks, aliases)
aliases.delete(name)
action = (done) => async_done(f, done)
}
else if (task.callback) {
let f = task.callback
action = f.length ? f : async () => f()
}
else if (task.src) {
action = () => streamify(task)
}
else if (task.task) {
let f = compose(task.task, tasks, aliases)
action = (done) => async_done(f, done)
}
else if (task.series) {
let subtasks = task.series.map((task) => compose(task, tasks, aliases))
action = gulp.series(...subtasks)
}
else if (task.parallel) {
let subtasks = task.parallel.map((task) => compose(task, tasks, aliases))
action = gulp.parallel(...subtasks)
}
else {
throw new Error("Invalid task structure.") // Not expected.
}
action.displayName = task.name || "<anonymous>"
task.action = action
return action
}
// Check if task is valid.
function validate(task) {
if (typeof task !== "object" && typeof task !== "string" && typeof task !== "function")
throw new Error("A task must be a string, function, or object.")
if (typeof task === "object") {
// No transform function and no task/series/parallel.
if (!task.src && !(task.task || task.series || task.parallel))
throw new Error("A task must do something.")
// Transform function and task/series/parallel.
if (task.src && (task.task || task.series || task.parallel))
throw new Error("A task can't have both .src and .task/.series/.parallel properties.")
// Combining task/series/parallel.
if (task.hasOwnProperty("task") + task.hasOwnProperty("series") + task.hasOwnProperty("parallel") > 1)
throw new Error("A task can only have one of .task/.series/.parallel properties.")
// Invalid .src.
if (task.src && !(typeof task.src === "string" || task.src instanceof Vinyl))
throw new Error("Task's .src must be a string or a Vinyl file.")
// Invalid watch path.
if (task.watch === true && !task.src)
throw new Error("No path given to watch.")
}
}
// Generate a watch task based on .watch property of other tasks.
function create_watch_task(tasks, logger, beep) {
if (tasks["watch"]) {
logger.warn(timestamp() + " 'watch' task redefined.")
return null
}
// Filter tasks that need watching.
tasks = Object.values(tasks)
tasks = tasks.reduce(function flatten(watched, task) {
if (task.watch)
watched.add(task)
let subtasks = [].concat(task.task || task.series || task.parallel || [])
for (let subtask of subtasks)
flatten(watched, subtask)
return watched
}, new Set())
tasks = [...tasks]
if (!tasks.length)
return null
return (_done) => {
let running = 0
let watchers = tasks.map((task) => {
let watch = task.watch
let action
// Play a beep sound once all triggered watched tasks are finished.
if (beep) {
action = (done) => {
running++
async_done(task.action, (error) => {
done(error)
running--
setTimeout(() => {
if (running)
return
if (error)
process.stderr.write("\x07\x07\x07")
else
process.stdout.write("\x07")
}, 10)
})
}
}
// When the invoked task is series/parallel, only the subtasks are logged,
// regardless of displayName. Therefore, wrapper.
else {
action = (done) => async_done(task.action, done)
}
action.displayName = task.name
let watcher = gulp.watch(watch, {delay: 0}, action)
watcher.on("change", (path) => logger.info(timestamp() + " '" + path + "' was changed, running '" + task.name + "'..."))
logger.info(timestamp() + " Watching '" + watch + "' for changes...")
return watcher
})
return async function unwatch() {
return Promise.all(watchers.map((watcher) => watcher.close()))
}
}
}
// Convert task's transform functions to a Stream.
function streamify(task) {
let stream
if (typeof task.src === "string") {
let options = task.base ? {base: task.base} : {}
stream = gulp.src(task.src, options)
}
else {
stream = through.obj((file, encoding, done) => done(null, file))
stream.end(task.src)
}
for (let transform of task.transforms)
stream = stream.pipe(transform.pipe ? transform : pluginate(transform))
if (task.rename)
stream = stream.pipe(rename(task.rename))
if (task.dest)
stream = stream.pipe(gulp.dest(task.dest))
return stream
}
// Convert a transform function into a Stream.
function pluginate(transform) {
return through.obj((file, encoding, done) => {
// Transform function returns a vinyl file or file contents (in form of a
// stream, a buffer or a string), or a promise which resolves with those.
new Promise((resolve) => {
resolve(transform(file.contents, file))
}).then((result) => {
if (Vinyl.isVinyl(result))
return
if (result instanceof Buffer)
file.contents = result
else if (typeof result === "string")
file.contents = Buffer.from(result)
else
throw new Error("Transforms must return/resolve with a file, a buffer or a string.")
}).then(() => {
done(null, file)
}).catch((e) => {
done(e)
})
})
}
// Add new properties on 'from' to 'to'.
function expand(to, from) {
let keys = Object.keys(from)
for (let key of keys) {
if (!to.hasOwnProperty(key))
to[key] = from[key]
}
}
// Output current time in '[HH:MM:SS]' format.
function timestamp() {
return "[" + new Date().toLocaleTimeString("hr-HR") + "]"
}