UNPKG

7.6 kBJavaScriptView Raw
1const path = require('path');
2const Packager = require('./Packager');
3const getExisting = require('../utils/getExisting');
4const urlJoin = require('../utils/urlJoin');
5const lineCounter = require('../utils/lineCounter');
6const objectHash = require('../utils/objectHash');
7
8const prelude = getExisting(
9 path.join(__dirname, '../builtins/prelude.min.js'),
10 path.join(__dirname, '../builtins/prelude.js')
11);
12
13class JSPackager extends Packager {
14 async start() {
15 this.first = true;
16 this.dedupe = new Map();
17 this.bundleLoaders = new Set();
18 this.externalModules = new Set();
19
20 let preludeCode = this.options.minify ? prelude.minified : prelude.source;
21 if (this.options.target === 'electron') {
22 preludeCode =
23 `process.env.HMR_PORT=${
24 this.options.hmrPort
25 };process.env.HMR_HOSTNAME=${JSON.stringify(
26 this.options.hmrHostname
27 )};` + preludeCode;
28 }
29 await this.write(preludeCode + '({');
30 this.lineOffset = lineCounter(preludeCode);
31 }
32
33 async addAsset(asset) {
34 // If this module is referenced by another JS bundle, it needs to be exposed externally.
35 // In that case, don't dedupe the asset as it would affect the module ids that are referenced by other bundles.
36 let isExposed = !Array.from(asset.parentDeps).every(dep => {
37 let depAsset = this.bundler.loadedAssets.get(dep.parent);
38 return this.bundle.assets.has(depAsset) || depAsset.type !== 'js';
39 });
40
41 if (!isExposed) {
42 let key = this.dedupeKey(asset);
43 if (this.dedupe.has(key)) {
44 return;
45 }
46
47 // Don't dedupe when HMR is turned on since it messes with the asset ids
48 if (!this.options.hmr) {
49 this.dedupe.set(key, asset.id);
50 }
51 }
52
53 let deps = {};
54 for (let [dep, mod] of asset.depAssets) {
55 // For dynamic dependencies, list the child bundles to load along with the module id
56 if (dep.dynamic) {
57 let bundles = [this.getBundleSpecifier(mod.parentBundle)];
58 for (let child of mod.parentBundle.siblingBundles) {
59 if (!child.isEmpty) {
60 bundles.push(this.getBundleSpecifier(child));
61 this.bundleLoaders.add(child.type);
62 }
63 }
64
65 bundles.push(mod.id);
66 deps[dep.name] = bundles;
67 this.bundleLoaders.add(mod.type);
68 } else {
69 deps[dep.name] = this.dedupe.get(this.dedupeKey(mod)) || mod.id;
70
71 // If the dep isn't in this bundle, add it to the list of external modules to preload.
72 // Only do this if this is the root JS bundle, otherwise they will have already been
73 // loaded in parallel with this bundle as part of a dynamic import.
74 if (!this.bundle.assets.has(mod)) {
75 this.externalModules.add(mod);
76 if (
77 !this.bundle.parentBundle ||
78 this.bundle.isolated ||
79 this.bundle.parentBundle.type !== 'js'
80 ) {
81 this.bundleLoaders.add(mod.type);
82 }
83 }
84 }
85 }
86
87 this.bundle.addOffset(asset, this.lineOffset);
88 await this.writeModule(
89 asset.id,
90 asset.generated.js,
91 deps,
92 asset.generated.map
93 );
94 }
95
96 getBundleSpecifier(bundle) {
97 let name = path.relative(path.dirname(this.bundle.name), bundle.name);
98 if (bundle.entryAsset) {
99 return [name, bundle.entryAsset.id];
100 }
101
102 return name;
103 }
104
105 dedupeKey(asset) {
106 // cannot rely *only* on generated JS for deduplication because paths like
107 // `../` can cause 2 identical JS files to behave differently depending on
108 // where they are located on the filesystem
109 let deps = Array.from(asset.depAssets.values(), dep => dep.name).sort();
110 return objectHash([asset.generated.js, deps]);
111 }
112
113 async writeModule(id, code, deps = {}, map) {
114 let wrapped = this.first ? '' : ',';
115 wrapped +=
116 JSON.stringify(id) +
117 ':[function(require,module,exports) {\n' +
118 (code || '') +
119 '\n},';
120 wrapped += JSON.stringify(deps);
121 wrapped += ']';
122
123 this.first = false;
124 await this.write(wrapped);
125
126 // Use the pre-computed line count from the source map if possible
127 let lineCount = map && map.lineCount ? map.lineCount : lineCounter(code);
128 this.lineOffset += 1 + lineCount;
129 }
130
131 async addAssetToBundle(asset) {
132 if (this.bundle.assets.has(asset)) {
133 return;
134 }
135 this.bundle.addAsset(asset);
136 if (!asset.parentBundle) {
137 asset.parentBundle = this.bundle;
138 }
139
140 // Add all dependencies as well
141 for (let child of asset.depAssets.values()) {
142 await this.addAssetToBundle(child);
143 }
144
145 await this.addAsset(asset);
146 }
147
148 async writeBundleLoaders() {
149 if (this.bundleLoaders.size === 0) {
150 return false;
151 }
152
153 let bundleLoader = this.bundler.loadedAssets.get(
154 require.resolve('../builtins/bundle-loader')
155 );
156 if (this.externalModules.size > 0 && !bundleLoader) {
157 bundleLoader = await this.bundler.getAsset('_bundle_loader');
158 }
159
160 if (bundleLoader) {
161 await this.addAssetToBundle(bundleLoader);
162 } else {
163 return;
164 }
165
166 // Generate a module to register the bundle loaders that are needed
167 let loads = 'var b=require(' + JSON.stringify(bundleLoader.id) + ');';
168 for (let bundleType of this.bundleLoaders) {
169 let loader = this.options.bundleLoaders[bundleType];
170 if (loader) {
171 let target = this.options.target === 'node' ? 'node' : 'browser';
172 let asset = await this.bundler.getAsset(loader[target]);
173 await this.addAssetToBundle(asset);
174 loads +=
175 'b.register(' +
176 JSON.stringify(bundleType) +
177 ',require(' +
178 JSON.stringify(asset.id) +
179 '));';
180 }
181 }
182
183 // Preload external modules before running entry point if needed
184 if (this.externalModules.size > 0) {
185 let preload = [];
186 for (let mod of this.externalModules) {
187 // Find the bundle that has the module as its entry point
188 let bundle = Array.from(mod.bundles).find(b => b.entryAsset === mod);
189 if (bundle) {
190 preload.push([path.basename(bundle.name), mod.id]);
191 }
192 }
193
194 loads += 'b.load(' + JSON.stringify(preload) + ')';
195 if (this.bundle.entryAsset) {
196 loads += `.then(function(){require(${JSON.stringify(
197 this.bundle.entryAsset.id
198 )});})`;
199 }
200
201 loads += ';';
202 }
203
204 // Asset ids normally start at 1, so this should be safe.
205 await this.writeModule(0, loads, {});
206 return true;
207 }
208
209 async end() {
210 let entry = [];
211
212 // Add the HMR runtime if needed.
213 if (this.options.hmr) {
214 let asset = await this.bundler.getAsset(
215 require.resolve('../builtins/hmr-runtime')
216 );
217 await this.addAssetToBundle(asset);
218 entry.push(asset.id);
219 }
220
221 if (await this.writeBundleLoaders()) {
222 entry.push(0);
223 }
224
225 if (this.bundle.entryAsset && this.externalModules.size === 0) {
226 entry.push(this.bundle.entryAsset.id);
227 }
228
229 await this.write(
230 '},{},' +
231 JSON.stringify(entry) +
232 ', ' +
233 JSON.stringify(this.options.global || null) +
234 ')'
235 );
236 if (this.options.sourceMaps) {
237 // Add source map url if a map bundle exists
238 let mapBundle = this.bundle.siblingBundlesMap.get('map');
239 if (mapBundle) {
240 let mapUrl = urlJoin(
241 this.options.publicURL,
242 path.relative(this.options.outDir, mapBundle.name)
243 );
244 await this.write(`\n//# sourceMappingURL=${mapUrl}`);
245 }
246 }
247 await super.end();
248 }
249}
250
251module.exports = JSPackager;