1 | const {version} = require('../package.json');
|
2 | const trace = require('./trace');
|
3 | const {cleanPath, parse, mapId} = require('dumber-module-loader/dist/id-utils');
|
4 | const defaultPackageFileReader = require('./package-file-reader/default');
|
5 | const PackageReader = require('./package-reader');
|
6 | const Package = require('./package');
|
7 | const stubModule = require('./stub-module');
|
8 | const {info, error, warn} = require('./log');
|
9 | const {generateHash, stripJsExtension, contentOrFile} = require('./shared');
|
10 | const resolvePackage = require('./resolve-package');
|
11 | const cache = require('./cache/default');
|
12 | const path = require('path');
|
13 | const ModulesDone = require('./modules-done');
|
14 | const ModulesTodo = require('./modules-todo');
|
15 |
|
16 | const 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 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | module.exports = class Bundler {
|
34 | constructor(opts, mock) {
|
35 | printVersion();
|
36 |
|
37 | opts = opts || {};
|
38 |
|
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 |
|
50 | this._cache = cache;
|
51 | }
|
52 | } else {
|
53 |
|
54 | this._cache = cache;
|
55 | }
|
56 |
|
57 | if (opts.hasOwnProperty('injectCss') && !opts.injectCss) {
|
58 | this._injectCss = false;
|
59 | } else {
|
60 |
|
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 |
|
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 |
|
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 |
|
101 | this.dirty = {[this._entryBundle]: true};
|
102 |
|
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 |
|
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 |
|
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 |
|
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 |
|
184 | this._modules_done.addUnit(tracedUnit);
|
185 |
|
186 | this._modules_todo.process(tracedUnit);
|
187 |
|
188 | const bundle = this.bundleOf(tracedUnit);
|
189 |
|
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 |
|
252 | if (this._modules_todo.hasTodo()) {
|
253 | return this.resolve();
|
254 | }
|
255 | });
|
256 | }
|
257 |
|
258 |
|
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 |
|
267 |
|
268 | return Promise.resolve();
|
269 | }
|
270 |
|
271 | if (this._modules_done.has(parsedId.bareId, checkUserSpace, checkPackageSpace)) {
|
272 | return Promise.resolve();
|
273 | }
|
274 |
|
275 |
|
276 |
|
277 | if (checkUserSpace && !checkPackageSpace) {
|
278 |
|
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 |
|
288 | if (result === false) return true;
|
289 |
|
290 |
|
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 |
|
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 |
|
311 | },
|
312 |
|
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 |
|
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 |
|
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 |
|
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 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | const special = [];
|
388 | while (true) {
|
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 |
|
401 |
|
402 |
|
403 |
|
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 |
|
424 | this._prepends.forEach(f => files.push(f));
|
425 | }
|
426 |
|
427 | if (userSpaceUnits.length) {
|
428 |
|
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 |
|
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 |
|
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 |
|
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 |
|
500 | this.dirty = {[this._entryBundle]: true};
|
501 |
|
502 |
|
503 | this._inWatchMode = true;
|
504 | return bundles;
|
505 | }
|
506 | };
|