UNPKG

14.9 kBJavaScriptView Raw
1/*
2 * Copyright 2014-2016 Guy Bedford (http://guybedford.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16var ui = require('./ui');
17var path = require('path');
18var config = require('./config');
19var SystemJSBuilder = require('systemjs-builder');
20var fs = require('fs');
21var asp = require('bluebird').Promise.promisify;
22var extendSystemConfig = require('./common').extendSystemConfig;
23var toFileURL = require('./common').toFileURL;
24var JspmSystemConfig = require('./config/loader').JspmSystemConfig;
25
26function camelCase(name, capitalizeFirst) {
27 return name.split('-').map(function(part, index) {
28 return index || capitalizeFirst ? part[0].toUpperCase() + part.substr(1) : part;
29 }).join('');
30}
31
32
33// new Builder(baseURL)
34// new Builder(baseURL, {cfg})
35// new Builder({cfg})
36function Builder(baseURL, cfg) {
37 if (typeof baseURL == 'object') {
38 cfg = baseURL;
39 baseURL = null;
40 }
41 config.loadSync(true);
42 cfg = extendSystemConfig(config.getLoaderConfig(), cfg || {});
43 if (baseURL)
44 cfg.baseURL = baseURL;
45 SystemJSBuilder.call(this, cfg);
46}
47Builder.prototype = Object.create(SystemJSBuilder.prototype);
48
49var savingBuildConfiguration = false;
50
51// extend build functions with jspm 0.16 compatibility options
52Builder.prototype.bundle = function(expressionOrTree, outFile, opts) {
53 if (outFile && typeof outFile === 'object') {
54 opts = outFile;
55 outFile = undefined;
56 }
57
58 config.loadSync(true);
59
60 opts = opts || {};
61
62 if (outFile)
63 opts.outFile = outFile;
64
65 /* jspm default bundle options */
66
67 // by default we build for the browser
68 if (!('browser' in opts) && !('node' in opts))
69 opts.browser = true;
70
71 if (!('lowResSourceMaps' in opts))
72 opts.lowResSourceMaps = true;
73
74 opts.buildConfig = true;
75
76 return SystemJSBuilder.prototype.bundle.call(this, expressionOrTree, opts)
77 .then(function(output) {
78 // Add the bundle to config if the inject flag was given.
79 if (opts.injectConfig || opts.inject) {
80 config.loader.file.setValue(['browserConfig', 'bundles', output.bundleName], output.modules);
81
82 // a sync flag to ensure that configuration injection config file saves
83 // do not count as config file changes in the watcher
84 savingBuildConfiguration = true;
85 config.save();
86 savingBuildConfiguration = false;
87
88 return output;
89 }
90
91 return output;
92 });
93};
94
95Builder.prototype.buildStatic = function(expressionOrTree, outFile, opts) {
96 if (outFile && typeof outFile === 'object') {
97 opts = outFile;
98 outFile = undefined;
99 }
100
101 opts = opts || {};
102
103 if (outFile)
104 opts.outFile = outFile;
105
106 /* jspm default build options */
107
108 if (!('browser' in opts) && !('node' in opts))
109 opts.browser = true;
110
111 if (!('lowResSourceMaps' in opts))
112 opts.lowResSourceMaps = true;
113
114 opts.format = opts.format || 'umd';
115
116 if (!('rollup' in opts))
117 opts.rollup = true;
118
119 return SystemJSBuilder.prototype.buildStatic.call(this, expressionOrTree, opts);
120};
121
122
123exports.Builder = Builder;
124
125exports.depCache = function(expression) {
126 var systemBuilder = new Builder();
127
128 expression = expression || config.loader.main;
129
130 ui.log('info', 'Injecting the traced dependency tree for `' + expression + '`...');
131
132 return systemBuilder.trace(expression, { browser: true })
133 .then(function(tree) {
134 var depCacheConfig = config.loader.file.getObject(['depCache']) || {};
135 var depCache = systemBuilder.getDepCache(tree);
136 Object.keys(depCache).forEach(function(dep) {
137 depCacheConfig[dep] = depCache[dep];
138 });
139 var modules = Object.keys(tree).filter(function(moduleName) {
140 return tree[moduleName] && !tree[moduleName].conditional;
141 });
142 logTree(modules);
143 config.loader.file.setObject(['browserConfig', 'depCache'], depCacheConfig);
144 })
145 .then(config.save)
146 .then(function() {
147 ui.log('ok', 'Dependency tree injected');
148 })
149 .catch(function(e) {
150 ui.log('err', e.stack || e);
151 });
152};
153
154// options.inject, options.sourceMaps, options.minify
155exports.bundle = function(moduleExpression, fileName, opts) {
156 opts = opts || {};
157
158 function bundle(givenBuilder) {
159 fileName = fileName || path.resolve(config.pjson.baseURL, 'build.js');
160 return Promise.resolve()
161 .then(function() {
162 if (!opts.sourceMaps)
163 return removeExistingSourceMap(fileName);
164 })
165 .then(function() {
166 ui.log('info', 'Building the bundle tree for %' + moduleExpression + '%...');
167
168 return givenBuilder.bundle(moduleExpression, fileName, opts);
169 })
170 .then(function(output) {
171 logTree(output.modules);
172
173 if (opts.injectConfig) {
174 if (!output.bundleName)
175 throw '%' + fileName + '% does not have a canonical name to inject into (unable to calculate canonical name, ensure the output file is within the baseURL or a path).';
176 else
177 ui.log('ok', '`' + output.bundleName + '` added to config bundles.');
178 }
179
180 logBuild(path.relative(process.cwd(), fileName), opts);
181 return output;
182 });
183 }
184
185 var systemBuilder = new Builder();
186 return bundle(systemBuilder)
187 .then(function(output) {
188 if (!opts.watch)
189 return output;
190
191 return buildWatch(systemBuilder, output, bundle);
192 })
193 .catch(function(e) {
194 ui.log('err', e.stack || e);
195 throw e;
196 });
197};
198
199exports.unbundle = function() {
200 return config.load()
201 .then(function() {
202 config.loader.file.remove(['bundles']);
203 config.loader.file.remove(['depCache']);
204 config.loader.file.remove(['browserConfig', 'bundles']);
205 config.loader.file.remove(['browserConfig', 'depCache']);
206 return config.save();
207 })
208 .then(function() {
209 ui.log('ok', 'Bundle configuration removed.');
210 });
211};
212
213function logBuild(outFile, opts) {
214 var resolution = opts.lowResSourceMaps ? '' : 'high-res ';
215 ui.log('ok', 'Built into `' + outFile + '`' +
216 (opts.sourceMaps ? ' with ' + resolution + 'source maps' : '') + ', ' +
217 (opts.minify ? '' : 'un') + 'minified' +
218 (opts.minify ? (opts.mangle ? ', ' : ', un') + 'mangled' : '') +
219 (opts.extra ? opts.extra : '') + '.');
220}
221
222// options.minify, options.sourceMaps
223exports.build = function(expression, fileName, opts) {
224 opts = opts || {};
225
226 function build(givenBuilder) {
227 fileName = fileName || path.resolve(config.pjson.baseURL, 'build.js');
228
229 return Promise.resolve()
230 .then(function() {
231 if (!opts.sourceMaps)
232 return removeExistingSourceMap(fileName);
233 })
234 .then(function() {
235 ui.log('info', 'Creating the single-file build for %' + expression + '%...');
236
237 return givenBuilder.buildStatic(expression, fileName, opts);
238 })
239 .then(function(output) {
240 logTree(output.modules, output.inlineMap ? output.inlineMap : opts.rollup);
241 opts.extra = ' as %' + opts.format + '%';
242 logBuild(path.relative(process.cwd(), fileName), opts);
243 return output;
244 })
245 .catch(function(e) {
246 // catch sfx globals error to give a better error message
247 if (e.toString().indexOf('globalDeps option') != -1) {
248 var module = e.toString().match(/dependency "([^"]+)"/);
249 module = module && module[1];
250 throw 'Build exclusion "' + module + '" needs an external reference.\n\t\t\tEither output to a module format like %--format amd% or map the external module to an environment global ' +
251 'via %--global-deps "{\'' + module + '\': \'' + camelCase(module, true) + '\'}"%';
252 }
253 if (e.toString().indexOf('globalName option') != -1) {
254 var generatedGlobalName = camelCase((expression.substr(expression.length - 3, 3) == '.js' ? expression.substr(0, expression.length - 3) : expression).split(/ |\//)[0]);
255 ui.log('warn', 'Build output to %' + opts.format + '% requires the global name to be set.\n' +
256 'Added default %--global-name ' + generatedGlobalName + '% option.\n');
257 opts.globalName = generatedGlobalName;
258 return build(givenBuilder);
259 }
260 else
261 throw e;
262 });
263 }
264
265 var systemBuilder = new Builder();
266 return build(systemBuilder)
267 .then(function(output) {
268 if (!opts.watch)
269 return output;
270
271 // create a watcher
272 return buildWatch(systemBuilder, output, build);
273 })
274 .catch(function(e) {
275 ui.log('err', e.stack || e);
276 throw e;
277 });
278};
279
280// we only support a single watcher at a time (its a cli)
281var watchman = true;
282var curWatcher;
283var watchFiles;
284var watchDir;
285function createWatcher(files, opts) {
286 // get the lowest directory from the files listing
287 var lowestDir = path.dirname(files[0]);
288 files.forEach(function(file) {
289 var dir = path.dirname(file);
290 // dir contained in lowest dir
291 if (dir.substr(0, lowestDir.length) == lowestDir && (dir[lowestDir.length] == '/' || dir.length == lowestDir.length))
292 return;
293 // if not in lowest dir, create next lowest dir
294 var dirParts = dir.split(path.sep);
295 var lowestDirParts = lowestDir.split(path.sep);
296 lowestDir = '';
297 var i = 0;
298 while (lowestDirParts[i] === dirParts[i] && typeof dirParts[i] == 'string')
299 lowestDir += (i > 0 ? path.sep : '') + dirParts[i++];
300 });
301
302 var relFiles = files.map(function(file) {
303 if (lowestDir == '.')
304 return file;
305 return file.substr(lowestDir.length + 1);
306 });
307
308 var watcher;
309 // share the existing watcher if possible
310 if (watchman && lowestDir != watchDir ||
311 !watchman && (lowestDir != watchDir || JSON.stringify(watchFiles) != JSON.stringify(relFiles))) {
312 var sane = require('sane');
313 if (curWatcher)
314 ui.log('info', 'New module tree, creating new watcher...');
315 watcher = sane(lowestDir, { watchman: watchman, glob: watchman && watchFiles });
316 }
317 else {
318 watcher = curWatcher;
319 ready();
320 }
321
322 watcher.on('error', error);
323 watcher.on('ready', ready);
324 watcher.on('change', change);
325
326 function ready() {
327 if (curWatcher && curWatcher != watcher)
328 curWatcher.close();
329 curWatcher = watcher;
330 watchFiles = relFiles;
331 watchDir = lowestDir;
332 opts.ready(lowestDir, watchman);
333 }
334
335 function error(e) {
336 if (e.toString().indexOf('Watchman was not found in PATH') == -1) {
337 opts.error(e);
338 return;
339 }
340 watchman = false;
341 detach();
342 createWatcher(files, opts);
343 }
344
345 function change(filepath) {
346 if (!watchFiles || watchFiles.indexOf(filepath) == -1)
347 return;
348 opts.change(path.join(lowestDir, filepath));
349 }
350
351 function detach() {
352 watcher.removeListener('error', error);
353 watcher.removeListener('ready', ready);
354 watcher.removeListener('change', change);
355 }
356 return detach;
357}
358
359
360function buildWatch(builder, output, build) {
361 // return value Promise for the rebuild function, or error
362 return new Promise(function(resolve, reject) {
363 var files = output.modules.map(function(name) {
364 return output.tree[name] && output.tree[name].path;
365 }).filter(function(name) {
366 return name;
367 }).map(function(file) {
368 return path.resolve(config.pjson.baseURL, file);
369 });
370
371 var configFiles = [config.pjson.configFile];
372 if (config.pjson.configFileBrowser)
373 configFiles.push(config.pjson.configFileBrowser);
374 if (config.pjson.configFileDev)
375 configFiles.push(config.pjson.configFileDev);
376
377 files = files.concat(configFiles);
378
379 var unwatch = createWatcher(files, {
380 ready: function(dir, watchman) {
381 ui.log('info', 'Watching %' + (path.relative(process.cwd(), dir) || '.') + '% for changes ' + (watchman ? 'with Watchman' : 'with Node native watcher') + '...');
382
383 // we return rebuild so when passing from one watcher to another, rebuild can be passed on too
384 resolve(function() {
385 changed();
386 });
387 },
388 error: function(e) {
389 reject(e);
390 },
391 change: changed
392 });
393
394 var building = false;
395 var rebuild = false;
396 var changeWasConfigFile = false;
397 function changed(file) {
398 changeWasConfigFile = configFiles.indexOf(file) > -1;
399
400 if (changeWasConfigFile) {
401 if (!savingBuildConfiguration) {
402 config.loader = new JspmSystemConfig(config.pjson.configFile);
403 builder = new Builder();
404 }
405 return;
406 }
407
408 if (file) {
409 builder.invalidate(toFileURL(file));
410 // also invalidate any plugin syntax cases
411 builder.invalidate(toFileURL(file) + '!*');
412 }
413
414 if (building) {
415 rebuild = true;
416 return;
417 }
418
419 building = true;
420 rebuild = false;
421 if (file)
422 ui.log('ok', 'File `' + path.relative(process.cwd(), file) + '` changed, rebuilding...');
423 else
424 ui.log('ok', 'File changes made during previous build, rebuilding...');
425
426 return build(builder).then(function(output) {
427 return buildWatch(builder, output, build);
428 }, function(err) {
429 ui.log('err', err.stack || err);
430 return buildWatch(builder, output, build);
431 }).then(function(newWatcherRefresh) {
432 unwatch();
433 if (rebuild)
434 newWatcherRefresh();
435 });
436 }
437 });
438}
439
440function logTree(modules, inlineMap) {
441 inlineMap = inlineMap || {};
442 var inlinedModules = [];
443
444 Object.keys(inlineMap).forEach(function(inlineParent) {
445 inlinedModules = inlinedModules.concat(inlineMap[inlineParent]);
446 });
447
448 ui.log('info', '');
449
450 if (inlineMap['@dummy-entry-point'])
451 logDepTree(inlineMap['@dummy-entry-point'], false);
452
453 if (inlineMap !== true)
454 modules.sort().forEach(function(name) {
455 if (inlinedModules.indexOf(name) == -1)
456 ui.log('info', ' `' + name + '`');
457
458 if (inlineMap[name])
459 logDepTree(inlineMap[name], true);
460 });
461 else
462 logDepTree(modules, false);
463
464 if (inlinedModules.length || inlineMap === true)
465 ui.log('info', '');
466
467 if (inlinedModules.length)
468 ui.log('ok', '%Optimized% - modules in bold inlined via Rollup static optimizations.');
469 if (inlineMap === true)
470 ui.log('ok', '%Fully-optimized% - entire tree built via Rollup static optimization.');
471 ui.log('info', '');
472}
473
474function logDepTree(items, firstParent) {
475 items.forEach(function(item, index) {
476 ui.log('info', ' `' + (items.length == 1 ? '──' : index == items.length - 1 ? '└─' : index == 0 && !firstParent ? '┌─' : '├─') + ' %' + item + '%`');
477 });
478}
479
480function removeExistingSourceMap(fileName) {
481 return asp(fs.unlink)(fileName + '.map')
482 .catch(function(e) {
483 if (e.code === 'ENOENT')
484 return;
485 throw e;
486 });
487}