UNPKG

16.6 kBJavaScriptView Raw
1const Packager = require('./Packager');
2const path = require('path');
3const concat = require('../scope-hoisting/concat');
4const urlJoin = require('../utils/urlJoin');
5const getExisting = require('../utils/getExisting');
6const walk = require('babylon-walk');
7const babylon = require('@babel/parser');
8const t = require('@babel/types');
9const {getName, getIdentifier} = require('../scope-hoisting/utils');
10
11const prelude = getExisting(
12 path.join(__dirname, '../builtins/prelude2.min.js'),
13 path.join(__dirname, '../builtins/prelude2.js')
14);
15
16const helpers = getExisting(
17 path.join(__dirname, '../builtins/helpers.min.js'),
18 path.join(__dirname, '../builtins/helpers.js')
19);
20
21class JSConcatPackager extends Packager {
22 async start() {
23 this.addedAssets = new Set();
24 this.assets = new Map();
25 this.exposedModules = new Set();
26 this.externalModules = new Set();
27 this.size = 0;
28 this.needsPrelude = false;
29 this.statements = [];
30 this.assetPostludes = new Map();
31
32 for (let asset of this.bundle.assets) {
33 // If this module is referenced by another JS bundle, it needs to be exposed externally.
34 let isExposed = !Array.from(asset.parentDeps).every(dep => {
35 let depAsset = this.bundler.loadedAssets.get(dep.parent);
36 return this.bundle.assets.has(depAsset) || depAsset.type !== 'js';
37 });
38
39 if (
40 isExposed ||
41 (this.bundle.entryAsset === asset &&
42 this.bundle.parentBundle &&
43 this.bundle.parentBundle.childBundles.size !== 1)
44 ) {
45 this.exposedModules.add(asset);
46 this.needsPrelude = true;
47 }
48
49 this.assets.set(asset.id, asset);
50
51 for (let mod of asset.depAssets.values()) {
52 if (
53 !this.bundle.assets.has(mod) &&
54 this.options.bundleLoaders[asset.type]
55 ) {
56 this.needsPrelude = true;
57 break;
58 }
59 }
60 }
61
62 if (this.bundle.entryAsset) {
63 this.markUsedExports(this.bundle.entryAsset);
64 }
65
66 if (this.needsPrelude) {
67 if (
68 this.bundle.entryAsset &&
69 this.options.bundleLoaders[this.bundle.entryAsset.type]
70 ) {
71 this.exposedModules.add(this.bundle.entryAsset);
72 }
73 }
74
75 this.write(helpers.minified);
76 }
77
78 write(string) {
79 this.statements.push(...this.parse(string));
80 }
81
82 getSize() {
83 return this.size;
84 }
85
86 markUsedExports(asset) {
87 if (asset.usedExports) {
88 return;
89 }
90
91 asset.usedExports = new Set();
92
93 for (let identifier in asset.cacheData.imports) {
94 let [source, name] = asset.cacheData.imports[identifier];
95 let dep = asset.depAssets.get(asset.dependencies.get(source));
96
97 if (dep) {
98 if (name === '*') {
99 this.markUsedExports(dep);
100 }
101
102 this.markUsed(dep, name);
103 }
104 }
105 }
106
107 markUsed(mod, name) {
108 let {id} = this.findExportModule(mod.id, name);
109 mod = this.assets.get(id);
110
111 if (!mod) {
112 return;
113 }
114
115 let exp = mod.cacheData.exports[name];
116 if (Array.isArray(exp)) {
117 let depMod = mod.depAssets.get(mod.dependencies.get(exp[0]));
118 return this.markUsed(depMod, exp[1]);
119 }
120
121 this.markUsedExports(mod);
122 mod.usedExports.add(name);
123 }
124
125 getExportIdentifier(asset) {
126 let id = getName(asset, 'exports');
127 if (this.shouldWrap(asset)) {
128 return `(${getName(asset, 'init')}(), ${id})`;
129 }
130
131 return id;
132 }
133
134 async addAsset(asset) {
135 if (this.addedAssets.has(asset)) {
136 return;
137 }
138 this.addedAssets.add(asset);
139 let {js} = asset.generated;
140
141 // If the asset has no side effects according to the its package's sideEffects flag,
142 // and there are no used exports marked, exclude the asset from the bundle.
143 if (
144 asset.cacheData.sideEffects === false &&
145 (!asset.usedExports || asset.usedExports.size === 0)
146 ) {
147 return;
148 }
149
150 for (let [dep, mod] of asset.depAssets) {
151 if (dep.dynamic) {
152 for (let child of mod.parentBundle.siblingBundles) {
153 if (!child.isEmpty) {
154 await this.addBundleLoader(child.type, asset);
155 }
156 }
157
158 await this.addBundleLoader(mod.type, asset, true);
159 } else {
160 // If the dep isn't in this bundle, add it to the list of external modules to preload.
161 // Only do this if this is the root JS bundle, otherwise they will have already been
162 // loaded in parallel with this bundle as part of a dynamic import.
163 if (
164 !this.bundle.assets.has(mod) &&
165 (!this.bundle.parentBundle ||
166 this.bundle.parentBundle.type !== 'js') &&
167 this.options.bundleLoaders[mod.type]
168 ) {
169 this.externalModules.add(mod);
170 await this.addBundleLoader(mod.type, asset);
171 }
172 }
173 }
174
175 // if (this.bundle.entryAsset === asset && this.externalModules.size > 0) {
176 // js = `
177 // function $parcel$entry() {
178 // ${js.trim()}
179 // }
180 // `;
181 // }
182
183 // js = js.trim() + '\n';
184 this.size += js.length;
185 }
186
187 shouldWrap(asset) {
188 if (!asset) {
189 return false;
190 }
191
192 if (asset.cacheData.shouldWrap != null) {
193 return asset.cacheData.shouldWrap;
194 }
195
196 // Set to false initially so circular deps work
197 asset.cacheData.shouldWrap = false;
198
199 // We need to wrap if any of the deps are marked by the hoister, e.g.
200 // when the dep is required inside a function or conditional.
201 // We also need to wrap if any of the parents are wrapped - transitive requires
202 // shouldn't be evaluated until their parents are.
203 let shouldWrap = [...asset.parentDeps].some(
204 dep =>
205 dep.shouldWrap ||
206 this.shouldWrap(this.bundler.loadedAssets.get(dep.parent))
207 );
208
209 asset.cacheData.shouldWrap = shouldWrap;
210 return shouldWrap;
211 }
212
213 addDeps(asset, included) {
214 if (!this.bundle.assets.has(asset) || included.has(asset)) {
215 return [];
216 }
217
218 included.add(asset);
219
220 let depAsts = new Map();
221 for (let depAsset of asset.depAssets.values()) {
222 if (!depAsts.has(depAsset)) {
223 let depAst = this.addDeps(depAsset, included);
224 depAsts.set(depAsset, depAst);
225 }
226 }
227
228 let statements;
229 if (
230 asset.cacheData.sideEffects === false &&
231 (!asset.usedExports || asset.usedExports.size === 0)
232 ) {
233 statements = [];
234 } else {
235 statements = this.parse(asset.generated.js, asset.name);
236 }
237
238 if (this.shouldWrap(asset)) {
239 statements = this.wrapModule(asset, statements);
240 }
241
242 if (statements[0]) {
243 if (!statements[0].leadingComments) {
244 statements[0].leadingComments = [];
245 }
246 statements[0].leadingComments.push({
247 type: 'CommentLine',
248 value: ` ASSET: ${path.relative(this.options.rootDir, asset.name)}`
249 });
250 }
251
252 let statementIndices = new Map();
253 for (let i = 0; i < statements.length; i++) {
254 let statement = statements[i];
255 if (t.isExpressionStatement(statement)) {
256 for (let depAsset of this.findRequires(asset, statement)) {
257 if (!statementIndices.has(depAsset)) {
258 statementIndices.set(depAsset, i);
259 }
260 }
261 }
262 }
263
264 let reverseDeps = [...asset.depAssets.values()].reverse();
265 for (let dep of reverseDeps) {
266 let index = statementIndices.has(dep) ? statementIndices.get(dep) : 0;
267 statements.splice(index, 0, ...depAsts.get(dep));
268 }
269
270 if (this.assetPostludes.has(asset)) {
271 statements.push(...this.parse(this.assetPostludes.get(asset)));
272 }
273
274 return statements;
275 }
276
277 wrapModule(asset, statements) {
278 let body = [];
279 let decls = [];
280 let fns = [];
281 for (let node of statements) {
282 // Hoist all declarations out of the function wrapper
283 // so that they can be referenced by other modules directly.
284 if (t.isVariableDeclaration(node)) {
285 for (let decl of node.declarations) {
286 decls.push(t.variableDeclarator(decl.id));
287 if (decl.init) {
288 body.push(
289 t.expressionStatement(
290 t.assignmentExpression(
291 '=',
292 t.identifier(decl.id.name),
293 decl.init
294 )
295 )
296 );
297 }
298 }
299 } else if (t.isFunctionDeclaration(node)) {
300 // Function declarations can be hoisted out of the module initialization function
301 fns.push(node);
302 } else if (t.isClassDeclaration(node)) {
303 // Class declarations are not hoisted. We declare a variable outside the
304 // function convert to a class expression assignment.
305 decls.push(t.variableDeclarator(t.identifier(node.id.name)));
306 body.push(
307 t.expressionStatement(
308 t.assignmentExpression(
309 '=',
310 t.identifier(node.id.name),
311 t.toExpression(node)
312 )
313 )
314 );
315 } else {
316 body.push(node);
317 }
318 }
319
320 let executed = getName(asset, 'executed');
321 decls.push(
322 t.variableDeclarator(t.identifier(executed), t.booleanLiteral(false))
323 );
324
325 let init = t.functionDeclaration(
326 getIdentifier(asset, 'init'),
327 [],
328 t.blockStatement([
329 t.ifStatement(t.identifier(executed), t.returnStatement()),
330 t.expressionStatement(
331 t.assignmentExpression(
332 '=',
333 t.identifier(executed),
334 t.booleanLiteral(true)
335 )
336 ),
337 ...body
338 ])
339 );
340
341 return [t.variableDeclaration('var', decls), ...fns, init];
342 }
343
344 parse(code, filename) {
345 let ast = babylon.parse(code, {
346 sourceFilename: filename,
347 allowReturnOutsideFunction: true
348 });
349
350 return ast.program.body;
351 }
352
353 findRequires(asset, ast) {
354 let result = [];
355 walk.simple(ast, {
356 CallExpression(node) {
357 let {arguments: args, callee} = node;
358
359 if (!t.isIdentifier(callee)) {
360 return;
361 }
362
363 if (callee.name === '$parcel$require') {
364 result.push(
365 asset.depAssets.get(asset.dependencies.get(args[1].value))
366 );
367 }
368 }
369 });
370
371 return result;
372 }
373
374 getBundleSpecifier(bundle) {
375 let name = path.relative(path.dirname(this.bundle.name), bundle.name);
376 if (bundle.entryAsset) {
377 return [name, bundle.entryAsset.id];
378 }
379
380 return name;
381 }
382
383 async addAssetToBundle(asset) {
384 if (this.bundle.assets.has(asset)) {
385 return;
386 }
387 this.assets.set(asset.id, asset);
388 this.bundle.addAsset(asset);
389 if (!asset.parentBundle) {
390 asset.parentBundle = this.bundle;
391 }
392
393 // Add all dependencies as well
394 for (let child of asset.depAssets.values()) {
395 await this.addAssetToBundle(child, this.bundle);
396 }
397
398 await this.addAsset(asset);
399 }
400
401 async addBundleLoader(bundleType, parentAsset, dynamic) {
402 let loader = this.options.bundleLoaders[bundleType];
403 if (!loader) {
404 return;
405 }
406
407 let bundleLoader = this.bundler.loadedAssets.get(
408 require.resolve('../builtins/bundle-loader')
409 );
410 if (!bundleLoader && !dynamic) {
411 bundleLoader = await this.bundler.getAsset('_bundle_loader');
412 }
413
414 if (bundleLoader) {
415 // parentAsset.depAssets.set({name: '_bundle_loader'}, bundleLoader);
416 await this.addAssetToBundle(bundleLoader);
417 } else {
418 return;
419 }
420
421 let target = this.options.target === 'node' ? 'node' : 'browser';
422 let asset = await this.bundler.getAsset(loader[target]);
423 if (!this.bundle.assets.has(asset)) {
424 let dep = {name: asset.name};
425 asset.parentDeps.add(dep);
426 parentAsset.dependencies.set(dep.name, dep);
427 parentAsset.depAssets.set(dep, asset);
428 this.assetPostludes.set(
429 asset,
430 `${this.getExportIdentifier(bundleLoader)}.register(${JSON.stringify(
431 bundleType
432 )},${this.getExportIdentifier(asset)});\n`
433 );
434
435 await this.addAssetToBundle(asset);
436 }
437 }
438
439 async end() {
440 let included = new Set();
441 for (let asset of this.bundle.assets) {
442 this.statements.push(...this.addDeps(asset, included));
443 }
444
445 // Preload external modules before running entry point if needed
446 if (this.externalModules.size > 0) {
447 let bundleLoader = this.bundler.loadedAssets.get(
448 require.resolve('../builtins/bundle-loader')
449 );
450
451 let preload = [];
452 for (let mod of this.externalModules) {
453 // Find the bundle that has the module as its entry point
454 let bundle = Array.from(mod.bundles).find(b => b.entryAsset === mod);
455 if (bundle) {
456 preload.push([path.basename(bundle.name), mod.id]);
457 }
458 }
459
460 let loads = `${this.getExportIdentifier(
461 bundleLoader
462 )}.load(${JSON.stringify(preload)})`;
463 if (this.bundle.entryAsset) {
464 loads += '.then($parcel$entry)';
465 }
466
467 loads += ';';
468 this.write(loads);
469 }
470
471 let entryExports =
472 this.bundle.entryAsset &&
473 this.getExportIdentifier(this.bundle.entryAsset);
474 if (
475 entryExports &&
476 this.bundle.entryAsset.generated.js.includes(entryExports)
477 ) {
478 this.write(`
479 if (typeof exports === "object" && typeof module !== "undefined") {
480 // CommonJS
481 module.exports = ${entryExports};
482 } else if (typeof define === "function" && define.amd) {
483 // RequireJS
484 define(function () {
485 return ${entryExports};
486 });
487 } ${
488 this.options.global
489 ? `else {
490 // <script>
491 this[${JSON.stringify(this.options.global)}] = ${entryExports};
492 }`
493 : ''
494 }
495 `);
496 }
497
498 if (this.needsPrelude) {
499 let exposed = [];
500 let prepareModule = [];
501 for (let m of this.exposedModules) {
502 if (m.cacheData.isES6Module) {
503 prepareModule.push(
504 `${this.getExportIdentifier(m)}.__esModule = true;`
505 );
506 }
507
508 exposed.push(`"${m.id}": ${this.getExportIdentifier(m)}`);
509 }
510
511 this.write(`
512 ${prepareModule.join('\n')}
513 return {${exposed.join(', ')}};
514 `);
515 }
516
517 try {
518 let ast = t.file(t.program(this.statements));
519 let {code: output} = concat(this, ast);
520
521 if (!this.options.minify) {
522 output = '\n' + output + '\n';
523 }
524
525 let preludeCode = this.options.minify ? prelude.minified : prelude.source;
526 if (this.needsPrelude) {
527 output = preludeCode + '(function (require) {' + output + '});';
528 } else {
529 output = '(function () {' + output + '})();';
530 }
531
532 this.size = output.length;
533
534 let {sourceMaps} = this.options;
535 if (sourceMaps) {
536 // Add source map url if a map bundle exists
537 let mapBundle = this.bundle.siblingBundlesMap.get('map');
538 if (mapBundle) {
539 let mapUrl = urlJoin(
540 this.options.publicURL,
541 path.basename(mapBundle.name)
542 );
543 output += `\n//# sourceMappingURL=${mapUrl}`;
544 }
545 }
546
547 await super.write(output);
548 } catch (e) {
549 throw e;
550 } finally {
551 await super.end();
552 }
553 }
554
555 resolveModule(id, name) {
556 let module = this.assets.get(id);
557 return module.depAssets.get(module.dependencies.get(name));
558 }
559
560 findExportModule(id, name, replacements) {
561 let asset = this.assets.get(id);
562 let exp =
563 asset &&
564 Object.prototype.hasOwnProperty.call(asset.cacheData.exports, name)
565 ? asset.cacheData.exports[name]
566 : null;
567
568 // If this is a re-export, find the original module.
569 if (Array.isArray(exp)) {
570 let mod = this.resolveModule(id, exp[0]);
571 return this.findExportModule(mod.id, exp[1], replacements);
572 }
573
574 // If this module exports wildcards, resolve the original module.
575 // Default exports are excluded from wildcard exports.
576 let wildcards = asset && asset.cacheData.wildcards;
577 if (wildcards && name !== 'default' && name !== '*') {
578 for (let source of wildcards) {
579 let mod = this.resolveModule(id, source);
580 let m = this.findExportModule(mod.id, name, replacements);
581 if (m.identifier) {
582 return m;
583 }
584 }
585 }
586
587 // If this is a wildcard import, resolve to the exports object.
588 if (asset && name === '*') {
589 exp = getName(asset, 'exports');
590 }
591
592 if (replacements && replacements.has(exp)) {
593 exp = replacements.get(exp);
594 }
595
596 return {
597 identifier: exp,
598 name,
599 id
600 };
601 }
602}
603
604module.exports = JSConcatPackager;