react-saasify-chrisvxd
Version:
React components for Saasify web clients.
252 lines (218 loc) • 7.6 kB
JavaScript
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;