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