|
|
|
// @flow strict-local
|
|
|
|
import type {BundleGraph, PluginOptions, NamedBundle} from '@parcel/types';
|
|
|
|
|
|
|
|
import {PromiseQueue, relativeBundlePath, countLines} from '@parcel/utils';
|
|
|
|
import SourceMap from '@parcel/source-map';
|
|
|
|
import invariant from 'assert';
|
|
|
|
import path from 'path';
|
|
|
|
import fs from 'fs';
|
|
|
|
import {replaceScriptDependencies, getSpecifier} from './utils';
|
|
|
|
|
|
|
|
const PRELUDE = fs
|
|
|
|
.readFileSync(path.join(__dirname, 'dev-prelude.js'), 'utf8')
|
|
|
|
.trim()
|
|
|
|
.replace(/;$/, '');
|
|
|
|
|
|
|
|
export class DevPackager {
|
|
|
|
options: PluginOptions;
|
|
|
|
bundleGraph: BundleGraph<NamedBundle>;
|
|
|
|
bundle: NamedBundle;
|
|
|
|
parcelRequireName: string;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
options: PluginOptions,
|
|
|
|
bundleGraph: BundleGraph<NamedBundle>,
|
|
|
|
bundle: NamedBundle,
|
|
|
|
parcelRequireName: string,
|
|
|
|
) {
|
|
|
|
this.options = options;
|
|
|
|
this.bundleGraph = bundleGraph;
|
|
|
|
this.bundle = bundle;
|
|
|
|
this.parcelRequireName = parcelRequireName;
|
|
|
|
}
|
|
|
|
|
|
|
|
async package(): Promise<{|contents: string, map: ?SourceMap|}> {
|
|
|
|
// Load assets
|
|
|
|
let queue = new PromiseQueue({maxConcurrent: 32});
|
|
|
|
this.bundle.traverse(node => {
|
|
|
|
if (node.type === 'asset') {
|
|
|
|
queue.add(async () => {
|
|
|
|
let [code, mapBuffer] = await Promise.all([
|
|
|
|
node.value.getCode(),
|
|
|
|
this.bundle.env.sourceMap && node.value.getMapBuffer(),
|
|
|
|
]);
|
|
|
|
return {code, mapBuffer};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let results = await queue.run();
|
|
|
|
|
|
|
|
let assets = '';
|
|
|
|
let i = 0;
|
|
|
|
let first = true;
|
|
|
|
let map = new SourceMap(this.options.projectRoot);
|
|
|
|
|
|
|
|
let prefix = this.getPrefix();
|
|
|
|
let lineOffset = countLines(prefix);
|
|
|
|
let script: ?{|code: string, mapBuffer: ?Buffer|} = null;
|
|
|
|
|
|
|
|
this.bundle.traverse(node => {
|
|
|
|
let wrapped = first ? '' : ',';
|
|
|
|
|
|
|
|
if (node.type === 'dependency') {
|
|
|
|
let resolved = this.bundleGraph.getResolvedAsset(
|
|
|
|
node.value,
|
|
|
|
this.bundle,
|
|
|
|
);
|
|
|
|
if (resolved && resolved.type !== 'js') {
|
|
|
|
// if this is a reference to another javascript asset, we should not include
|
|
|
|
// its output, as its contents should already be loaded.
|
|
|
|
invariant(!this.bundle.hasAsset(resolved));
|
|
|
|
wrapped +=
|
|
|
|
JSON.stringify(this.bundleGraph.getAssetPublicId(resolved)) +
|
|
|
|
':[function() {},{}]';
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.type === 'asset') {
|
|
|
|
let asset = node.value;
|
|
|
|
invariant(
|
|
|
|
asset.type === 'js',
|
|
|
|
'all assets in a js bundle must be js assets',
|
|
|
|
);
|
|
|
|
|
|
|
|
// If this is the main entry of a script rather than a module, we need to hoist it
|
|
|
|
// outside the bundle wrapper function so that its variables are exposed as globals.
|
|
|
|
if (
|
|
|
|
this.bundle.env.sourceType === 'script' &&
|
|
|
|
asset === this.bundle.getMainEntry()
|
|
|
|
) {
|
|
|
|
script = results[i++];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let deps = {};
|
|
|
|
let dependencies = this.bundleGraph.getDependencies(asset);
|
|
|
|
for (let dep of dependencies) {
|
|
|
|
let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
|
|
|
|
if (resolved) {
|
|
|
|
deps[getSpecifier(dep)] = this.bundleGraph.getAssetPublicId(
|
|
|
|
resolved,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let {code, mapBuffer} = results[i];
|
|
|
|
let output = code || '';
|
|
|
|
wrapped +=
|
|
|
|
JSON.stringify(this.bundleGraph.getAssetPublicId(asset)) +
|
|
|
|
':[function(require,module,exports) {\n' +
|
|
|
|
output +
|
|
|
|
'\n},';
|
|
|
|
wrapped += JSON.stringify(deps);
|
|
|
|
wrapped += ']';
|
|
|
|
|
|
|
|
if (this.bundle.env.sourceMap) {
|
|
|
|
if (mapBuffer) {
|
|
|
|
map.addBuffer(mapBuffer, lineOffset);
|
|
|
|
} else {
|
|
|
|
map.addEmptyMap(
|
|
|
|
path
|
|
|
|
.relative(this.options.projectRoot, asset.filePath)
|
|
|
|
.replace(/\\+/g, '/'),
|
|
|
|
output,
|
|
|
|
lineOffset,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
lineOffset += countLines(output) + 1;
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
assets += wrapped;
|
|
|
|
first = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
let entries = this.bundle.getEntryAssets();
|
|
|
|
let mainEntry = this.bundle.getMainEntry();
|
|
|
|
if (
|
|
|
|
(!this.isEntry() && this.bundle.env.outputFormat === 'global') ||
|
|
|
|
this.bundle.env.sourceType === 'script'
|
|
|
|
) {
|
|
|
|
// In async bundles we don't want the main entry to execute until we require it
|
|
|
|
// as there might be dependencies in a sibling bundle that hasn't loaded yet.
|
|
|
|
entries = entries.filter(a => a.id !== mainEntry?.id);
|
|
|
|
mainEntry = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let contents =
|
|
|
|
prefix +
|
|
|
|
'({' +
|
|
|
|
assets +
|
|
|
|
'},' +
|
|
|
|
JSON.stringify(
|
|
|
|
entries.map(asset => this.bundleGraph.getAssetPublicId(asset)),
|
|
|
|
) +
|
|
|
|
', ' +
|
|
|
|
JSON.stringify(
|
|
|
|
mainEntry ? this.bundleGraph.getAssetPublicId(mainEntry) : null,
|
|
|
|
) +
|
|
|
|
', ' +
|
|
|
|
JSON.stringify(this.parcelRequireName) +
|
|
|
|
')' +
|
|
|
|
'\n';
|
|
|
|
|
|
|
|
// The entry asset of a script bundle gets hoisted outside the bundle wrapper function
|
|
|
|
// so that its variables become globals. We need to replace any require calls for
|
|
|
|
// runtimes with a parcelRequire call.
|
|
|
|
if (this.bundle.env.sourceType === 'script' && script) {
|
|
|
|
let entryMap;
|
|
|
|
let mapBuffer = script.mapBuffer;
|
|
|
|
if (mapBuffer) {
|
|
|
|
entryMap = new SourceMap(this.options.projectRoot, mapBuffer);
|
|
|
|
}
|
|
|
|
contents += replaceScriptDependencies(
|
|
|
|
this.bundleGraph,
|
|
|
|
this.bundle,
|
|
|
|
script.code,
|
|
|
|
entryMap,
|
|
|
|
this.parcelRequireName,
|
|
|
|
);
|
|
|
|
if (this.bundle.env.sourceMap && entryMap) {
|
|
|
|
map.addSourceMap(entryMap, lineOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
contents,
|
|
|
|
map,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
getPrefix(): string {
|
|
|
|
let interpreter: ?string;
|
|
|
|
let mainEntry = this.bundle.getMainEntry();
|
|
|
|
if (mainEntry && this.isEntry() && !this.bundle.target.env.isBrowser()) {
|
|
|
|
let _interpreter = mainEntry.meta.interpreter;
|
|
|
|
invariant(_interpreter == null || typeof _interpreter === 'string');
|
|
|
|
interpreter = _interpreter;
|
|
|
|
}
|
|
|
|
|
|
|
|
let importScripts = '';
|
|
|
|
if (this.bundle.env.isWorker()) {
|
|
|
|
let bundles = this.bundleGraph.getReferencedBundles(this.bundle);
|
|
|
|
for (let b of bundles) {
|
|
|
|
importScripts += `importScripts("${relativeBundlePath(
|
|
|
|
this.bundle,
|
|
|
|
b,
|
|
|
|
)}");\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
// If the entry asset included a hashbang, repeat it at the top of the bundle
|
|
|
|
(interpreter != null ? `#!${interpreter}\n` : '') +
|
|
|
|
importScripts +
|
|
|
|
PRELUDE
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
isEntry(): boolean {
|
|
|
|
return (
|
|
|
|
!this.bundleGraph.hasParentBundleOfType(this.bundle, 'js') ||
|
|
|
|
this.bundle.env.isIsolated() ||
|
|
|
|
this.bundle.bundleBehavior === 'isolated'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|