UNPKG

15.5 kBJavaScriptView Raw
1const {version} = require('../package.json');
2const trace = require('./trace');
3const {cleanPath, parse, mapId} = require('dumber-module-loader/dist/id-utils');
4const defaultPackageFileReader = require('./package-file-reader/default');
5const PackageReader = require('./package-reader');
6const Package = require('./package');
7const stubModule = require('./stub-module');
8const {info, error, warn} = require('./log');
9const {generateHash, stripJsExtension, contentOrFile} = require('./shared');
10const resolvePackage = require('./resolve-package');
11const cache = require('./cache/default');
12const path = require('path');
13const ModulesDone = require('./modules-done');
14const ModulesTodo = require('./modules-todo');
15
16const printVersion = (function() {
17 let did = false;
18
19 return function() {
20 if (did) return;
21 did = true;
22 info(`Starting dumber bundler v${version} https://dumber.js.org`);
23 };
24})();
25
26// Bundler does
27// 1. capture: capture units (unit is a file like object plus meta data)
28// 2. resolve: resolve all dependencies
29// 3. bundle: write to bundles (objects, not final file contents)
30
31// gulp-dumber or dumberify will then process bundles objects to files
32
33module.exports = class Bundler {
34 constructor(opts, mock) {
35 printVersion();
36
37 opts = opts || {};
38 // decoupling for testing
39 this._trace = (mock && mock.trace) || trace;
40 this._resolve = (mock && mock.resolve) || resolvePackage;
41 this._contentOrFile = (mock && mock.contentOrFile) || contentOrFile;
42
43 if (opts.hasOwnProperty('cache')) {
44 if (opts.cache === false) {
45 this._cache = null;
46 } else if (opts.cache.getCache && opts.cache.setCache && opts.cache.clearCache) {
47 this._cache = opts.cache;
48 } else {
49 // turn on cache by default
50 this._cache = cache;
51 }
52 } else {
53 // turn on cache by default
54 this._cache = cache;
55 }
56
57 if (opts.hasOwnProperty('injectCss') && !opts.injectCss) {
58 this._injectCss = false;
59 } else {
60 // by default turn on injection of css (inject onto html head)
61 this._injectCss = true;
62 }
63
64 let _paths = {};
65 if (opts.paths) {
66 Object.keys(opts.paths).forEach(path => {
67 let alias = opts.paths[path];
68 _paths[cleanPath(path)] = cleanPath(alias);
69 });
70 }
71 this._paths = _paths;
72
73 this._unitsMap = {};
74
75 this._modules_done = new ModulesDone();
76 this._modules_todo = new ModulesTodo();
77
78 this._readersMap = {};
79 this._fileReader = opts.packageFileReader || defaultPackageFileReader;
80
81 // baseUrl default to "dist"
82 this._baseUrl = opts.baseUrl || '/dist';
83 this._depsFinder = opts.depsFinder;
84 this._onRequire = opts.onRequire || opts.onrequire || opts.onRequiringModule;
85
86 this._prepends = (opts.prepends || opts.prepend || []).filter(t => t);
87
88 if (!opts.skipModuleLoader) {
89 this._prepends.push(
90 // load dumber-module-loader after prepends
91 path.join(path.relative(process.cwd(), this._resolve('dumber-module-loader')), 'dist', 'index.debug.js')
92 );
93 }
94
95 this._appends = (opts.appends || opts.append || []).filter(t => t);
96
97 this._dependencies = (opts.dependencies || opts.deps || []).filter(t => t).map(d => new Package(d));
98 this._entryBundle = stripJsExtension(opts.entryBundle) || 'entry-bundle';
99 this._codeSplit = opts.codeSplit || function(){};
100 // mark dirtiness of bundles
101 this.dirty = {[this._entryBundle]: true};
102 // persist bundles in watch mode
103 this._bundles = {};
104 this._inWatchMode = false;
105
106 this._onAcquire = this._onAcquire.bind(this);
107 this._supportInjectCssIfNeeded = this._supportInjectCssIfNeeded.bind(this);
108 this._resolveExplicitDepsIfNeeded = this._resolveExplicitDepsIfNeeded.bind(this);
109 }
110
111 clearCache() {
112 if(this._cache) return this._cache.clearCache();
113 return Promise.resolve();
114 }
115
116 packageReaderFor(packageConfig) {
117 if (this._readersMap.hasOwnProperty(packageConfig.name)) {
118 return Promise.resolve(this._readersMap[packageConfig.name]);
119 }
120
121 return this._fileReader(packageConfig).then(fileReader => {
122 const reader = new PackageReader(fileReader);
123 this._readersMap[packageConfig.name] = reader;
124 return reader;
125 });
126 }
127
128 shimFor(packageName) {
129 const dep = this._dependencies.find(d => d.name === packageName);
130 if (dep) return dep.shim;
131 }
132
133 bundleOf(unit) {
134 const bundleName = this._codeSplit(unit.moduleId, unit.packageName);
135 if (!bundleName) return this._entryBundle;
136 return stripJsExtension(bundleName);
137 }
138
139 capture(unit) {
140 const hash = generateHash(JSON.stringify(unit));
141
142 if (this._inWatchMode && !unit.packageName) {
143 if (this._unitsMap[unit.path]) {
144 const oldHash = this._unitsMap[unit.path].hash;
145
146 if (oldHash === hash) {
147 // ignore unchanged file in watch mode
148 return Promise.resolve(this._unitsMap[unit.path]);
149 }
150
151 info(`Update ${unit.path}`);
152 } else {
153 info(`Add ${unit.path}`);
154 }
155 }
156
157 if (unit.packageName) {
158 unit.shim = this.shimFor(unit.packageName);
159 }
160
161 // return tracedUnit
162 return this._trace(unit, {
163 cache: this._cache,
164 depsFinder: this._depsFinder
165 }).then(
166 tracedUnit => {
167 tracedUnit.hash = hash;
168 return this._capture(tracedUnit);
169 },
170 err => {
171 // just print error, but not stopping
172 error('Tracing failed for ' + unit.path);
173 error(err);
174 }
175 );
176 }
177
178 _capture(tracedUnit) {
179 let key = tracedUnit.path;
180 if (tracedUnit.packageName) key = tracedUnit.packageName + ':' + key;
181 this._unitsMap[key] = tracedUnit;
182
183 // mark as done.
184 this._modules_done.addUnit(tracedUnit);
185 // process deps.
186 this._modules_todo.process(tracedUnit);
187
188 const bundle = this.bundleOf(tracedUnit);
189 // mark related bundle dirty
190 tracedUnit.bundle = bundle;
191 this.dirty[bundle] = true;
192
193 return tracedUnit;
194 }
195
196 _resolveExplicitDepsIfNeeded() {
197 if (this._isExplicitDepsResolved) return Promise.resolve();
198 this._isExplicitDepsResolved = true;
199
200 let p = Promise.resolve();
201
202 this._dependencies.forEach(pkg => {
203 p = p.then(() => this.packageReaderFor(pkg)).then(reader => {
204 if (!pkg.lazyMain) {
205 return reader.readMain()
206 .then(unit => this.capture(unit));
207 }
208 });
209 });
210
211 return p;
212 }
213
214 _resolvePrependsAndAppends() {
215 if (this._isPrependsAndAppendsResolved) return Promise.resolve();
216 this._isPrependsAndAppendsResolved = true;
217
218 let {_prepends, _appends} = this;
219 let prepends = new Array(_prepends.length);
220 let appends = new Array(_appends.length);
221
222 return Promise.all([
223 ..._prepends.map((p, i) => this._contentOrFile(p).then(f => prepends[i] = f)),
224 ..._appends.map((p, i) => this._contentOrFile(p).then(f => appends[i] = f)),
225 ]).then(() => {
226 this._prepends = prepends;
227 this._appends = appends;
228 })
229 }
230
231 _supportInjectCssIfNeeded() {
232 if (!this._modules_todo.needCssInjection || !this._injectCss || this._isInjectCssTurnedOn) {
233 return Promise.resolve();
234 }
235 this._isInjectCssTurnedOn = true;
236
237 return this.capture({
238 path:'__stub__/ext-css.js',
239 contents: "define(['dumber/lib/inject-css'],function(m){return m;});",
240 moduleId: 'ext:css',
241 alias: ['ext:less', 'ext:scss', 'ext:sass', 'ext:styl']
242 });
243 }
244
245 resolve() {
246 return this._resolvePrependsAndAppends()
247 .then(this._resolveExplicitDepsIfNeeded)
248 .then(() => this._modules_todo.acquire(this._onAcquire))
249 .then(this._supportInjectCssIfNeeded)
250 .then(() => {
251 // recursively resolve
252 if (this._modules_todo.hasTodo()) {
253 return this.resolve();
254 }
255 });
256 }
257
258 // trace missing dep
259 _onAcquire(id, opts) {
260 const checkUserSpace = opts.user;
261 const checkPackageSpace = opts.package;
262 const requiredBy = opts.requiredBy;
263 const parsedId = parse(mapId(id, this._paths));
264
265 if (parsedId.bareId.match(/^(?:https?:)?\//)) {
266 // ignore https:// and /root/path.
267 // they are for runtime loading.
268 return Promise.resolve();
269 }
270
271 if (this._modules_done.has(parsedId.bareId, checkUserSpace, checkPackageSpace)) {
272 return Promise.resolve();
273 }
274
275 // TODO add a callback point to fillup missing local dep.
276 // This is needed by dumberify.
277 if (checkUserSpace && !checkPackageSpace) {
278 // detected missing local dep
279 warn(`local dependency ${parsedId.bareId} (requiredBy ${requiredBy.join(', ')}) is missing`);
280 return Promise.resolve();
281 }
282
283 return new Promise(resolve => {
284 resolve(this._onRequire && this._onRequire(parsedId.bareId, parsedId));
285 }).then(
286 result => {
287 // ignore this module id
288 if (result === false) return true;
289
290 // require other module ids instead
291 if (Array.isArray(result) && result.length) {
292 this._modules_todo.process({
293 moduleId: parsedId.bareId,
294 packageName: (!checkUserSpace && checkPackageSpace) ? parsedId.parts[0] : undefined,
295 deps: result
296 });
297 return true;
298 }
299
300 // got full content of this module
301 if (typeof result === 'string') {
302 return this.capture({
303 path: '__on_require__/' + parsedId.bareId + (parsedId.ext ? '' : '.js'),
304 contents: result,
305 moduleId: parsedId.bareId,
306 packageName: parsedId.parts[0]
307 }).then(() => true);
308 }
309
310 // process normally if result is not recognizable
311 },
312 // proceed normally after error
313 err => {
314 error('onRequire call failed for ' + parsedId.bareId);
315 error(err);
316 }
317 ).then(didRequire => {
318 if (didRequire === true) return;
319
320 const bareId = parsedId.bareId;
321 const packageName = parsedId.parts[0];
322 const resource = bareId.slice(packageName.length + 1);
323
324 const stub = stubModule(bareId, this._resolve);
325
326 if (typeof stub === 'string') {
327 return this.capture({
328 // not a real file path
329 path:'__stub__/' + bareId + '.js',
330 contents: stub,
331 moduleId: bareId,
332 packageName
333 });
334 }
335
336 return this.packageReaderFor(stub || {name: packageName})
337 .then(reader => resource ? reader.readResource(resource) : reader.readMain())
338 .then(unit => this.capture(unit))
339 .catch(err => {
340 error('Resolving failed for module ' + bareId);
341 error(err);
342 });
343 });
344 }
345
346 _unitsForBundle(bundle) {
347 let units = [];
348
349 Object.keys(this._unitsMap).forEach(key => {
350 const unit = this._unitsMap[key];
351 if (unit.bundle === bundle) units.push(unit);
352 });
353
354 // Alphabetical sorting based on moduleId
355 units.sort((a, b) => {
356 if (a.moduleId > b.moduleId) return 1;
357 if (b.moduleId > a.moduleId) return -1;
358 return 0;
359 });
360
361 // Topological sort for shim packages
362 const sorted = [];
363 const visited = {};
364
365 const visit = file => {
366 const {moduleId, deps, shimed} = file;
367 if (visited[moduleId]) return;
368 visited[moduleId] = true;
369
370 if (shimed && deps) {
371 deps.forEach(packageName => {
372 units.filter(u => u.packageName === packageName).forEach(visit);
373 });
374 }
375
376 sorted.push(file);
377 };
378
379 units.forEach(visit);
380
381 // Special treatment for jquery and moment, put them in front of everything else,
382 // so that jquery and moment can create global vars as early as possible.
383 // This improves compatibility with some legacy jquery plugins.
384 // Note as of momentjs version 2.10.0, momentjs no longer exports global object
385 // in AMD module environment. There is special code in lib/transformers/hack-moment.js
386 // to bring up global var "moment".
387 const special = [];
388 while (true) { // eslint-disable-line no-constant-condition
389 const idx = sorted.findIndex(unit =>
390 unit.packageName === 'jquery' || unit.packageName === 'moment'
391 );
392
393 if (idx === -1) break;
394 special.push(...sorted.splice(idx, 1));
395 }
396
397 return [...special, ...sorted];
398 }
399
400 // return promise of a map
401 // {
402 // 'bundle-name' : {files: [{path, contents, sourceMap}]},
403 // 'bunele-entry-name': {files: [{path, contents, sourceMap}], config: {...}},
404 // }
405 bundle() {
406 Object.keys(this.dirty).forEach(bundle => {
407 const files = [];
408
409 const userSpaceUnits = [];
410 const packageSpaceUnits = [];
411 const userSpaceModuleIds = new Set();
412 const packageSpaceModuleIds = new Set();
413
414 this._unitsForBundle(bundle).forEach(unit => {
415 if (unit.packageName) {
416 packageSpaceUnits.push(unit);
417 } else {
418 userSpaceUnits.push(unit);
419 }
420 });
421
422 if (bundle === this._entryBundle) {
423 // write prepends
424 this._prepends.forEach(f => files.push(f));
425 }
426
427 if (userSpaceUnits.length) {
428 // write userSpaceUnits
429 files.push({contents: 'define.switchToUserSpace();'});
430
431 userSpaceUnits.forEach(unit => {
432 files.push({
433 path: unit.path,
434 contents: unit.contents,
435 sourceMap: unit.sourceMap
436 });
437 userSpaceModuleIds.add(unit.moduleId);
438 unit.defined.forEach(d => userSpaceModuleIds.add(d));
439 });
440 }
441
442 if (packageSpaceUnits.length) {
443 // write packageSpaceUnits
444 files.push({contents: 'define.switchToPackageSpace();'});
445 packageSpaceUnits.forEach(unit => {
446 files.push({
447 path: unit.path,
448 contents: unit.contents,
449 sourceMap: unit.sourceMap
450 });
451 packageSpaceModuleIds.add(unit.moduleId);
452 unit.defined.forEach(d => packageSpaceModuleIds.add(d));
453 });
454
455 // reset to userSpaceUnits
456 files.push({contents: 'define.switchToUserSpace();'});
457 }
458
459 if (!this._bundles[bundle]) this._bundles[bundle] = {};
460
461 this._bundles[bundle].files = files;
462 this._bundles[bundle].modules = {
463 user: Array.from(userSpaceModuleIds).sort(),
464 package: Array.from(packageSpaceModuleIds).sort()
465 };
466
467 if (bundle === this._entryBundle && this._appends.length) {
468 let appendFiles = [];
469 // write appends
470 this._appends.forEach(f => appendFiles.push(f));
471 this._bundles[bundle].appendFiles = appendFiles;
472 }
473 });
474
475 const bundleWithConfig = this._bundles[this._entryBundle];
476 if (!bundleWithConfig) {
477 throw new Error(`Entry bundle "${this._entryBundle}" is missing`);
478 }
479
480 const bundlesConfig = (bundleWithConfig.config && bundleWithConfig.config.bundles) || {};
481
482 Object.keys(this.dirty).forEach(bundle => {
483 if (bundle !== this._entryBundle) {
484 bundlesConfig[bundle] = this._bundles[bundle].modules;
485 }
486 delete this._bundles[bundle].modules;
487 });
488
489 bundleWithConfig.config = {
490 baseUrl: this._baseUrl,
491 paths: JSON.parse(JSON.stringify(this._paths)),
492 bundles: bundlesConfig
493 };
494
495 const bundles = {};
496 Object.keys(this.dirty).forEach(bundle => {
497 bundles[bundle] = this._bundles[bundle];
498 });
499 // reset dirty flags
500 this.dirty = {[this._entryBundle]: true};
501
502 // turn on watch node after first "bundle()"
503 this._inWatchMode = true;
504 return bundles;
505 }
506};