Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81159 views
1
var assert = require("assert");
2
var path = require("path");
3
var fs = require("fs");
4
var Q = require("q");
5
var iconv = require("iconv-lite");
6
var ReadFileCache = require("./cache").ReadFileCache;
7
var Watcher = require("./watcher").Watcher;
8
var contextModule = require("./context");
9
var BuildContext = contextModule.BuildContext;
10
var PreferredFileExtension = contextModule.PreferredFileExtension;
11
var ModuleReader = require("./reader").ModuleReader;
12
var output = require("./output");
13
var DirOutput = output.DirOutput;
14
var StdOutput = output.StdOutput;
15
var util = require("./util");
16
var log = util.log;
17
var Ap = Array.prototype;
18
var each = Ap.forEach;
19
20
// Better stack traces for promises.
21
Q.longStackSupport = true;
22
23
function Commoner() {
24
var self = this;
25
assert.ok(self instanceof Commoner);
26
27
Object.defineProperties(self, {
28
customVersion: { value: null, writable: true },
29
customOptions: { value: [] },
30
resolvers: { value: [] },
31
processors: { value: [] }
32
});
33
}
34
35
var Cp = Commoner.prototype;
36
37
Cp.version = function(version) {
38
this.customVersion = version;
39
return this; // For chaining.
40
};
41
42
// Add custom command line options
43
Cp.option = function() {
44
this.customOptions.push(Ap.slice.call(arguments));
45
return this; // For chaining.
46
};
47
48
// A resolver is a function that takes a module identifier and returns
49
// the unmodified source of the corresponding module, either as a string
50
// or as a promise for a string.
51
Cp.resolve = function() {
52
each.call(arguments, function(resolver) {
53
assert.strictEqual(typeof resolver, "function");
54
this.resolvers.push(resolver);
55
}, this);
56
57
return this; // For chaining.
58
};
59
60
// A processor is a function that takes a module identifier and a string
61
// representing the source of the module and returns a modified version of
62
// the source, either as a string or as a promise for a string.
63
Cp.process = function(processor) {
64
each.call(arguments, function(processor) {
65
assert.strictEqual(typeof processor, "function");
66
this.processors.push(processor);
67
}, this);
68
69
return this; // For chaining.
70
};
71
72
Cp.buildP = function(options, roots) {
73
var self = this;
74
var sourceDir = options.sourceDir;
75
var outputDir = options.outputDir;
76
var readFileCache = new ReadFileCache(sourceDir, options.sourceCharset);
77
var waiting = 0;
78
var output = outputDir
79
? new DirOutput(outputDir)
80
: new StdOutput;
81
82
if (self.watch) {
83
new Watcher(readFileCache).on("changed", function(file) {
84
log.err(file + " changed; rebuilding...", "yellow");
85
rebuild();
86
});
87
}
88
89
function outputModules(modules) {
90
// Note that output.outputModules comes pre-bound.
91
modules.forEach(output.outputModule);
92
return modules;
93
}
94
95
function finish(result) {
96
rebuild.ing = false;
97
98
if (waiting > 0) {
99
waiting = 0;
100
process.nextTick(rebuild);
101
}
102
103
return result;
104
}
105
106
function rebuild() {
107
if (rebuild.ing) {
108
waiting += 1;
109
return;
110
}
111
112
rebuild.ing = true;
113
114
var context = new BuildContext(options, readFileCache);
115
116
if (self.preferredFileExtension)
117
context.setPreferredFileExtension(
118
self.preferredFileExtension);
119
120
context.setCacheDirectory(self.cacheDir);
121
122
context.setIgnoreDependencies(self.ignoreDependencies);
123
124
context.setRelativize(self.relativize);
125
126
context.setUseProvidesModule(self.useProvidesModule);
127
128
return new ModuleReader(
129
context,
130
self.resolvers,
131
self.processors
132
).readMultiP(context.expandIdsOrGlobsP(roots))
133
.then(context.ignoreDependencies ? pass : collectDepsP)
134
.then(outputModules)
135
.then(outputDir ? printModuleIds : pass)
136
.then(finish, function(err) {
137
log.err(err.stack);
138
139
if (!self.watch) {
140
// If we're not building with --watch, throw the error
141
// so that cliBuildP can call process.exit(-1).
142
throw err;
143
}
144
145
finish();
146
});
147
}
148
149
return (
150
// If outputDir is falsy, we can't (and don't need to) mkdirP it.
151
outputDir ? util.mkdirP : Q
152
)(outputDir).then(rebuild);
153
};
154
155
function pass(modules) {
156
return modules;
157
}
158
159
function collectDepsP(rootModules) {
160
var modules = [];
161
var seenIds = {};
162
163
function traverse(module) {
164
if (seenIds.hasOwnProperty(module.id))
165
return Q(modules);
166
seenIds[module.id] = true;
167
168
return module.getRequiredP().then(function(reqs) {
169
return Q.all(reqs.map(traverse));
170
}).then(function() {
171
modules.push(module);
172
return modules;
173
});
174
}
175
176
return Q.all(rootModules.map(traverse)).then(
177
function() { return modules });
178
}
179
180
function printModuleIds(modules) {
181
log.out(JSON.stringify(modules.map(function(module) {
182
return module.id;
183
})));
184
185
return modules;
186
}
187
188
Cp.forceResolve = function(forceId, source) {
189
this.resolvers.unshift(function(id) {
190
if (id === forceId)
191
return source;
192
});
193
};
194
195
Cp.cliBuildP = function() {
196
var version = this.customVersion || require("../package.json").version;
197
return Q.spread([this, version], cliBuildP);
198
};
199
200
function cliBuildP(commoner, version) {
201
var options = require("commander");
202
var workingDir = process.cwd();
203
var sourceDir = workingDir;
204
var outputDir = null;
205
var roots;
206
207
options.version(version)
208
.usage("[options] <source directory> <output directory> [<module ID> [<module ID> ...]]")
209
.option("-c, --config [file]", "JSON configuration file (no file or - means STDIN)")
210
.option("-w, --watch", "Continually rebuild")
211
.option("-x, --extension <js | coffee | ...>",
212
"File extension to assume when resolving module identifiers")
213
.option("--relativize", "Rewrite all module identifiers to be relative")
214
.option("--follow-requires", "Scan modules for required dependencies")
215
.option("--use-provides-module", "Respect @providesModules pragma in files")
216
.option("--cache-dir <directory>", "Alternate directory to use for disk cache")
217
.option("--no-cache-dir", "Disable the disk cache")
218
.option("--source-charset <utf8 | win1252 | ...>",
219
"Charset of source (default: utf8)")
220
.option("--output-charset <utf8 | win1252 | ...>",
221
"Charset of output (default: utf8)");
222
223
commoner.customOptions.forEach(function(customOption) {
224
options.option.apply(options, customOption);
225
});
226
227
options.parse(process.argv.slice(0));
228
229
var pfe = new PreferredFileExtension(options.extension || "js");
230
231
// TODO Decide whether passing options to buildP via instance
232
// variables is preferable to passing them as arguments.
233
commoner.preferredFileExtension = pfe;
234
commoner.watch = options.watch;
235
commoner.ignoreDependencies = !options.followRequires;
236
commoner.relativize = options.relativize;
237
commoner.useProvidesModule = options.useProvidesModule;
238
commoner.sourceCharset = normalizeCharset(options.sourceCharset);
239
commoner.outputCharset = normalizeCharset(options.outputCharset);
240
241
function fileToId(file) {
242
file = absolutePath(workingDir, file);
243
assert.ok(fs.statSync(file).isFile(), file);
244
return pfe.trim(path.relative(sourceDir, file));
245
}
246
247
var args = options.args.slice(0);
248
var argc = args.length;
249
if (argc === 0) {
250
if (options.config === true) {
251
log.err("Cannot read --config from STDIN when reading " +
252
"source from STDIN");
253
process.exit(-1);
254
}
255
256
sourceDir = workingDir;
257
outputDir = null;
258
roots = ["<stdin>"];
259
commoner.forceResolve("<stdin>", util.readFromStdinP());
260
261
// Ignore dependencies because we wouldn't know how to find them.
262
commoner.ignoreDependencies = true;
263
264
} else {
265
var first = absolutePath(workingDir, args[0]);
266
var stats = fs.statSync(first);
267
268
if (argc === 1) {
269
var firstId = fileToId(first);
270
sourceDir = workingDir;
271
outputDir = null;
272
roots = [firstId];
273
commoner.forceResolve(
274
firstId,
275
util.readFileP(first, commoner.sourceCharset)
276
);
277
278
// Ignore dependencies because we wouldn't know how to find them.
279
commoner.ignoreDependencies = true;
280
281
} else if (stats.isDirectory(first)) {
282
sourceDir = first;
283
outputDir = absolutePath(workingDir, args[1]);
284
roots = args.slice(2);
285
if (roots.length === 0)
286
roots.push(commoner.preferredFileExtension.glob());
287
288
} else {
289
options.help();
290
process.exit(-1);
291
}
292
}
293
294
commoner.cacheDir = null;
295
if (options.cacheDir === false) {
296
// Received the --no-cache-dir option, so disable the disk cache.
297
} else if (typeof options.cacheDir === "string") {
298
commoner.cacheDir = absolutePath(workingDir, options.cacheDir);
299
} else if (outputDir) {
300
// The default cache directory lives inside the output directory.
301
commoner.cacheDir = path.join(outputDir, ".module-cache");
302
}
303
304
var promise = getConfigP(
305
workingDir,
306
options.config
307
).then(function(config) {
308
var cleanOptions = {};
309
310
options.options.forEach(function(option) {
311
var name = util.camelize(option.name());
312
if (options.hasOwnProperty(name)) {
313
cleanOptions[name] = options[name];
314
}
315
});
316
317
cleanOptions.version = version;
318
cleanOptions.config = config;
319
cleanOptions.sourceDir = sourceDir;
320
cleanOptions.outputDir = outputDir;
321
cleanOptions.sourceCharset = commoner.sourceCharset;
322
cleanOptions.outputCharset = commoner.outputCharset;
323
324
return commoner.buildP(cleanOptions, roots);
325
});
326
327
if (!commoner.watch) {
328
// If we're building from the command line without --watch, any
329
// build errors should immediately terminate the process with a
330
// non-zero error code.
331
promise = promise.catch(function(err) {
332
log.err(err.stack);
333
process.exit(-1);
334
});
335
}
336
337
return promise;
338
}
339
340
function normalizeCharset(charset) {
341
charset = charset
342
&& charset.replace(/[- ]/g, "").toLowerCase()
343
|| "utf8";
344
345
assert.ok(
346
iconv.encodingExists(charset),
347
"Unrecognized charset: " + charset
348
);
349
350
return charset;
351
}
352
353
function absolutePath(workingDir, pathToJoin) {
354
if (pathToJoin) {
355
workingDir = path.normalize(workingDir);
356
pathToJoin = path.normalize(pathToJoin);
357
// TODO: use path.isAbsolute when Node < 0.10 is unsupported
358
if (path.resolve(pathToJoin) !== pathToJoin) {
359
pathToJoin = path.join(workingDir, pathToJoin);
360
}
361
}
362
return pathToJoin;
363
}
364
365
function getConfigP(workingDir, configFile) {
366
if (typeof configFile === "undefined")
367
return Q({}); // Empty config.
368
369
if (configFile === true || // --config is present but has no argument
370
configFile === "<stdin>" ||
371
configFile === "-" ||
372
configFile === path.sep + path.join("dev", "stdin")) {
373
return util.readJsonFromStdinP(
374
1000, // Time limit in milliseconds before warning displayed.
375
"Expecting configuration from STDIN (pass --config <file> " +
376
"if stuck here)...",
377
"yellow"
378
);
379
}
380
381
return util.readJsonFileP(absolutePath(workingDir, configFile));
382
}
383
384
exports.Commoner = Commoner;
385
386