react-saasify-chrisvxd
Version:
React components for Saasify web clients.
853 lines (715 loc) • 24.9 kB
JavaScript
const fs = require('@parcel/fs');
const Resolver = require('./Resolver');
const Parser = require('./Parser');
const WorkerFarm = require('@parcel/workers');
const Path = require('path');
const Bundle = require('./Bundle');
const Watcher = require('@parcel/watcher');
const FSCache = require('./FSCache');
const HMRServer = require('./HMRServer');
const Server = require('./Server');
const {EventEmitter} = require('events');
const logger = require('@parcel/logger');
const PackagerRegistry = require('./packagers');
const localRequire = require('./utils/localRequire');
const config = require('./utils/config');
const loadEnv = require('./utils/env');
const PromiseQueue = require('./utils/PromiseQueue');
const installPackage = require('./utils/installPackage');
const bundleReport = require('./utils/bundleReport');
const prettifyTime = require('./utils/prettifyTime');
const getRootDir = require('./utils/getRootDir');
const {glob, isGlob} = require('./utils/glob');
/**
* The Bundler is the main entry point. It resolves and loads assets,
* creates the bundle tree, and manages the worker farm, cache, and file watcher.
*/
class Bundler extends EventEmitter {
constructor(entryFiles, options = {}) {
super();
entryFiles = this.normalizeEntries(entryFiles);
this.watchedGlobs = entryFiles.filter(entry => isGlob(entry));
this.entryFiles = this.findEntryFiles(entryFiles);
this.options = this.normalizeOptions(options);
this.resolver = new Resolver(this.options);
this.parser = new Parser(this.options);
this.packagers = new PackagerRegistry(this.options);
this.cache = this.options.cache ? new FSCache(this.options) : null;
this.delegate = options.delegate || {};
this.bundleLoaders = {};
this.addBundleLoader('wasm', {
browser: require.resolve('./builtins/loaders/browser/wasm-loader'),
node: require.resolve('./builtins/loaders/node/wasm-loader')
});
this.addBundleLoader('css', {
browser: require.resolve('./builtins/loaders/browser/css-loader'),
node: require.resolve('./builtins/loaders/node/css-loader')
});
this.addBundleLoader('js', {
browser: require.resolve('./builtins/loaders/browser/js-loader'),
node: require.resolve('./builtins/loaders/node/js-loader')
});
this.addBundleLoader('html', {
browser: require.resolve('./builtins/loaders/browser/html-loader'),
node: require.resolve('./builtins/loaders/node/html-loader')
});
this.pending = false;
this.loadedAssets = new Map();
this.watchedAssets = new Map();
this.farm = null;
this.watcher = null;
this.hmr = null;
this.bundleHashes = null;
this.error = null;
this.buildQueue = new PromiseQueue(this.processAsset.bind(this));
this.rebuildTimeout = null;
logger.setOptions(this.options);
}
normalizeEntries(entryFiles) {
// Support passing a single file
if (entryFiles && !Array.isArray(entryFiles)) {
entryFiles = [entryFiles];
}
// If no entry files provided, resolve the entry point from the current directory.
if (!entryFiles || entryFiles.length === 0) {
entryFiles = [process.cwd()];
}
return entryFiles;
}
findEntryFiles(entryFiles) {
// Match files as globs
return entryFiles
.reduce((p, m) => p.concat(glob.sync(m)), [])
.map(f => Path.resolve(f));
}
normalizeOptions(options) {
const isProduction =
options.production || process.env.NODE_ENV === 'production';
const publicURL = options.publicUrl || options.publicURL || '/';
const watch =
typeof options.watch === 'boolean' ? options.watch : !isProduction;
const target = options.target || 'browser';
const hmr =
target === 'node'
? false
: typeof options.hmr === 'boolean'
? options.hmr
: watch;
const scopeHoist =
options.scopeHoist !== undefined ? options.scopeHoist : false;
return {
production: isProduction,
outDir: Path.resolve(options.outDir || 'dist'),
outFile: options.outFile || '',
publicURL: publicURL,
watch: watch,
cache: typeof options.cache === 'boolean' ? options.cache : true,
cacheDir: Path.resolve(options.cacheDir || '.cache'),
killWorkers:
typeof options.killWorkers === 'boolean' ? options.killWorkers : true,
minify:
typeof options.minify === 'boolean' ? options.minify : isProduction,
target: target,
bundleNodeModules:
typeof options.bundleNodeModules === 'boolean'
? options.bundleNodeModules
: target === 'browser',
hmr: hmr,
https: options.https || false,
logLevel: isNaN(options.logLevel) ? 3 : options.logLevel,
entryFiles: this.entryFiles,
hmrPort: options.hmrPort || 0,
rootDir: getRootDir(this.entryFiles),
sourceMaps:
(typeof options.sourceMaps === 'boolean' ? options.sourceMaps : true) &&
!scopeHoist,
hmrHostname:
options.hmrHostname ||
options.host ||
(options.target === 'electron' ? 'localhost' : ''),
detailedReport: options.detailedReport || false,
global: options.global,
autoinstall:
typeof options.autoInstall === 'boolean'
? options.autoInstall
: process.env.PARCEL_AUTOINSTALL === 'false'
? false
: !isProduction,
scopeHoist: scopeHoist,
contentHash:
typeof options.contentHash === 'boolean'
? options.contentHash
: isProduction,
throwErrors:
typeof options.throwErrors === 'boolean' ? options.throwErrors : true
};
}
addAssetType(extension, path) {
if (typeof path !== 'string') {
throw new Error('Asset type should be a module path.');
}
if (this.farm) {
throw new Error('Asset types must be added before bundling.');
}
this.parser.registerExtension(extension, path);
}
addPackager(type, packager) {
if (this.farm) {
throw new Error('Packagers must be added before bundling.');
}
this.packagers.add(type, packager);
}
addBundleLoader(type, paths) {
if (typeof paths === 'string') {
paths = {node: paths, browser: paths};
} else if (typeof paths !== 'object') {
throw new Error('Bundle loaders should be an object.');
}
for (const target in paths) {
if (target !== 'node' && target !== 'browser') {
throw new Error(`Unknown bundle loader target "${target}".`);
}
if (typeof paths[target] !== 'string') {
throw new Error('Bundle loader should be a string.');
}
}
if (this.farm) {
throw new Error('Bundle loaders must be added before bundling.');
}
this.bundleLoaders[type] = paths;
}
async loadPlugins() {
let relative = Path.join(this.options.rootDir, 'index');
let pkg = await config.load(relative, ['package.json']);
if (!pkg) {
return;
}
let lastDep;
try {
let deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
for (let dep in deps) {
lastDep = dep;
const pattern = /^(@.*\/)?parcel-plugin-.+/;
if (pattern.test(dep)) {
let plugin = await localRequire(dep, relative);
await plugin(this);
}
}
} catch (err) {
logger.warn(
`Plugin ${lastDep} failed to initialize: ${err.stack ||
err.message ||
err}`
);
}
}
async bundle() {
// If another bundle is already pending, wait for that one to finish and retry.
if (this.pending) {
return new Promise((resolve, reject) => {
this.once('buildEnd', () => {
this.bundle().then(resolve, reject);
});
});
}
let isInitialBundle = !this.entryAssets;
let startTime = Date.now();
let initialised = !isInitialBundle;
this.pending = true;
this.error = null;
logger.clear();
logger.progress('Building...');
try {
// Start worker farm, watcher, etc. if needed
await this.start();
// Emit start event, after bundler is initialised
this.emit('buildStart', this.entryFiles);
// If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
if (isInitialBundle) {
await fs.mkdirp(this.options.outDir);
this.entryAssets = new Set();
for (let entry of this.entryFiles) {
try {
let asset = await this.resolveAsset(entry);
this.buildQueue.add(asset);
this.entryAssets.add(asset);
} catch (err) {
throw new Error(
`Cannot resolve entry "${entry}" from "${this.options.rootDir}"`
);
}
}
if (this.entryAssets.size === 0) {
throw new Error('No entries found.');
}
initialised = true;
}
// Build the queued assets.
let loadedAssets = await this.buildQueue.run();
// The changed assets are any that don't have a parent bundle yet
// plus the ones that were in the build queue.
let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];
// Invalidate bundles
for (let asset of this.loadedAssets.values()) {
asset.invalidateBundle();
}
logger.progress(`Producing bundles...`);
// Create a root bundle to hold all of the entry assets, and add them to the tree.
this.mainBundle = new Bundle();
for (let asset of this.entryAssets) {
this.createBundleTree(asset, this.mainBundle);
}
// If there is only one child bundle, replace the root with that bundle.
if (this.mainBundle.childBundles.size === 1) {
this.mainBundle = Array.from(this.mainBundle.childBundles)[0];
}
// Generate the final bundle names, and replace references in the built assets.
let numBundles = this.bundleNameMap ? this.bundleNameMap.size : 0;
this.bundleNameMap = this.mainBundle.getBundleNameMap(
this.options.contentHash
);
for (let asset of changedAssets) {
asset.replaceBundleNames(this.bundleNameMap);
}
// Emit an HMR update if this is not the initial bundle.
let bundlesChanged = numBundles !== this.bundleNameMap.size;
if (this.hmr && !isInitialBundle) {
this.hmr.emitUpdate(changedAssets, bundlesChanged);
}
logger.progress(`Packaging...`);
// Package everything up
this.bundleHashes = await this.mainBundle.package(
this,
bundlesChanged ? null : this.bundleHashes
);
// Unload any orphaned assets
this.unloadOrphanedAssets();
let buildTime = Date.now() - startTime;
let time = prettifyTime(buildTime);
logger.success(`Built in ${time}.`);
if (!this.watcher) {
bundleReport(this.mainBundle, this.options.detailedReport);
}
this.emit('bundled', this.mainBundle);
return this.mainBundle;
} catch (err) {
this.error = err;
logger.error(err);
this.emit('buildError', err);
if (this.hmr) {
this.hmr.emitError(err);
}
if (this.options.throwErrors && !this.hmr) {
throw err;
} else if (!this.options.watch || !initialised) {
await this.stop();
process.exit(1);
}
} finally {
this.pending = false;
this.emit('buildEnd');
// If not in watch mode, stop the worker farm so we don't keep the process running.
if (!this.watcher && this.options.killWorkers) {
await this.stop();
}
}
}
async start() {
if (this.farm) {
return;
}
await this.loadPlugins();
if (!this.options.env) {
await loadEnv(Path.join(this.options.rootDir, 'index'));
this.options.env = process.env;
}
this.options.extensions = Object.assign({}, this.parser.extensions);
this.options.bundleLoaders = this.bundleLoaders;
if (this.options.watch) {
this.watcher = new Watcher();
// Wait for ready event for reliable testing on watcher
if (process.env.NODE_ENV === 'test' && !this.watcher.ready) {
await new Promise(resolve => this.watcher.once('ready', resolve));
}
this.watchedGlobs.forEach(glob => {
this.watcher.add(glob);
});
this.watcher.on('add', this.onAdd.bind(this));
this.watcher.on('change', this.onChange.bind(this));
this.watcher.on('unlink', this.onUnlink.bind(this));
}
if (this.options.hmr) {
this.hmr = new HMRServer();
this.options.hmrPort = await this.hmr.start(this.options);
}
this.farm = await WorkerFarm.getShared(this.options, {
workerPath: require.resolve('./worker.js')
});
}
async stop() {
if (this.watcher) {
await this.watcher.stop();
}
if (this.hmr) {
this.hmr.stop();
}
// Watcher and hmr can cause workerfarm calls
// keep this as last to prevent unwanted errors
if (this.farm) {
await this.farm.end();
}
}
async getAsset(name, parent) {
let asset = await this.resolveAsset(name, parent);
this.buildQueue.add(asset);
await this.buildQueue.run();
return asset;
}
async resolveAsset(name, parent) {
let {path} = await this.resolver.resolve(name, parent);
return this.getLoadedAsset(path);
}
getLoadedAsset(path) {
if (this.loadedAssets.has(path)) {
return this.loadedAssets.get(path);
}
let asset = this.parser.getAsset(path, this.options);
this.loadedAssets.set(path, asset);
this.watch(path, asset);
return asset;
}
async watch(path, asset) {
if (!this.watcher) {
return;
}
path = await fs.realpath(path);
if (!this.watchedAssets.has(path)) {
this.watcher.watch(path);
this.watchedAssets.set(path, new Set());
}
this.watchedAssets.get(path).add(asset);
}
async unwatch(path, asset) {
path = await fs.realpath(path);
if (!this.watchedAssets.has(path)) {
return;
}
let watched = this.watchedAssets.get(path);
watched.delete(asset);
if (watched.size === 0) {
this.watchedAssets.delete(path);
this.watcher.unwatch(path);
}
}
async resolveDep(asset, dep, install = true) {
try {
if (dep.resolved) {
return this.getLoadedAsset(dep.resolved);
}
return await this.resolveAsset(dep.name, asset.name);
} catch (err) {
// If the dep is optional, return before we throw
if (dep.optional) {
return;
}
if (err.code === 'MODULE_NOT_FOUND') {
let isLocalFile = /^[/~.]/.test(dep.name);
let fromNodeModules = asset.name.includes(
`${Path.sep}node_modules${Path.sep}`
);
if (
!isLocalFile &&
!fromNodeModules &&
this.options.autoinstall &&
install
) {
return this.installDep(asset, dep);
}
err.message = `Cannot resolve dependency '${dep.name}'`;
if (isLocalFile) {
const absPath = Path.resolve(Path.dirname(asset.name), dep.name);
err.message += ` at '${absPath}'`;
}
await this.throwDepError(asset, dep, err);
}
throw err;
}
}
async installDep(asset, dep) {
// Check if module exists, prevents useless installs
let resolved = await this.resolver.resolveModule(dep.name, asset.name);
// If the module resolved (i.e. wasn't a local file), but the module directory wasn't found, install it.
if (resolved.moduleName && !resolved.moduleDir) {
try {
await installPackage(resolved.moduleName, asset.name, {
saveDev: false
});
} catch (err) {
await this.throwDepError(asset, dep, err);
}
}
return this.resolveDep(asset, dep, false);
}
async throwDepError(asset, dep, err) {
// Generate a code frame where the dependency was used
if (dep.loc) {
await asset.loadIfNeeded();
err.loc = dep.loc;
err = asset.generateErrorMessage(err);
}
err.fileName = asset.name;
throw err;
}
async processAsset(asset, isRebuild) {
if (isRebuild) {
asset.invalidate();
if (this.cache) {
this.cache.invalidate(asset.name);
}
}
await this.loadAsset(asset);
}
async loadAsset(asset) {
if (asset.processed) {
return;
}
if (!this.error) {
logger.progress(`Building ${asset.basename}...`);
}
// Mark the asset processed so we don't load it twice
asset.processed = true;
// First try the cache, otherwise load and compile in the background
asset.startTime = Date.now();
let processed = this.cache && (await this.cache.read(asset.name));
let cacheMiss = false;
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
processed = await this.farm.run(asset.name);
cacheMiss = true;
}
asset.endTime = Date.now();
asset.buildTime = asset.endTime - asset.startTime;
asset.id = processed.id;
asset.generated = processed.generated;
asset.sourceMaps = processed.sourceMaps;
asset.hash = processed.hash;
asset.cacheData = processed.cacheData;
// Call the delegate to get implicit dependencies
let dependencies = processed.dependencies;
if (this.delegate.getImplicitDependencies) {
let implicitDeps = await this.delegate.getImplicitDependencies(asset);
if (implicitDeps) {
dependencies = dependencies.concat(implicitDeps);
}
}
// Resolve and load asset dependencies
let assetDeps = await Promise.all(
dependencies.map(async dep => {
if (dep.includedInParent) {
// This dependency is already included in the parent's generated output,
// so no need to load it. We map the name back to the parent asset so
// that changing it triggers a recompile of the parent.
this.watch(dep.name, asset);
} else {
dep.parent = asset.name;
let assetDep = await this.resolveDep(asset, dep);
if (assetDep) {
await this.loadAsset(assetDep);
}
return assetDep;
}
})
);
// If there was a processing error, re-throw now that we've set up
// depdenency watchers. This keeps reloading working if there is an
// error in a dependency not directly handled by Parcel.
if (processed.error !== null) {
throw processed.error;
}
// Store resolved assets in their original order
dependencies.forEach((dep, i) => {
asset.dependencies.set(dep.name, dep);
let assetDep = assetDeps[i];
if (assetDep) {
asset.depAssets.set(dep, assetDep);
dep.resolved = assetDep.name;
}
});
logger.verbose(`Built ${asset.relativeName}...`);
if (this.cache && cacheMiss) {
this.cache.write(asset.name, processed);
}
}
createBundleTree(asset, bundle, dep, parentBundles = new Set()) {
if (dep) {
asset.parentDeps.add(dep);
}
if (asset.parentBundle && !bundle.isolated) {
// If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
if (asset.parentBundle !== bundle) {
let commonBundle = bundle.findCommonAncestor(asset.parentBundle);
// If the common bundle's type matches the asset's, move the asset to the common bundle.
// Otherwise, proceed with adding the asset to the new bundle below.
if (asset.parentBundle.type === commonBundle.type) {
this.moveAssetToBundle(asset, commonBundle);
return;
}
} else {
return;
}
// Detect circular bundles
if (parentBundles.has(asset.parentBundle)) {
return;
}
}
// Skip this asset if it's already in the bundle.
// Happens when circular dependencies are placed in an isolated bundle (e.g. a worker).
if (bundle.isolated && bundle.assets.has(asset)) {
return;
}
let isEntryAsset =
asset.parentBundle && asset.parentBundle.entryAsset === asset;
// If the asset generated a representation for the parent bundle type, and this
// is not an async import, add it to the current bundle
if (bundle.type && asset.generated[bundle.type] != null && !dep.dynamic) {
bundle.addAsset(asset);
}
if ((dep && dep.dynamic) || !bundle.type) {
// If the asset is already the entry asset of a bundle, don't create a duplicate.
if (isEntryAsset) {
return;
}
// Create a new bundle for dynamic imports
bundle = bundle.createChildBundle(asset, dep);
} else if (
asset.type &&
!this.packagers.get(asset.type).shouldAddAsset(bundle, asset)
) {
// If the asset is already the entry asset of a bundle, don't create a duplicate.
if (isEntryAsset) {
return;
}
// No packager is available for this asset type, or the packager doesn't support
// combining this asset into the bundle. Create a new bundle with only this asset.
bundle = bundle.createSiblingBundle(asset, dep);
} else {
// Add the asset to the common bundle of the asset's type
bundle.getSiblingBundle(asset.type).addAsset(asset);
}
// Add the asset to sibling bundles for each generated type
if (asset.type && asset.generated[asset.type]) {
for (let t in asset.generated) {
if (asset.generated[t]) {
bundle.getSiblingBundle(t).addAsset(asset);
}
}
}
asset.parentBundle = bundle;
parentBundles.add(bundle);
for (let [dep, assetDep] of asset.depAssets) {
this.createBundleTree(assetDep, bundle, dep, parentBundles);
}
parentBundles.delete(bundle);
return bundle;
}
moveAssetToBundle(asset, commonBundle) {
// Never move the entry asset of a bundle, as it was explicitly requested to be placed in a separate bundle.
if (
asset.parentBundle.entryAsset === asset ||
asset.parentBundle === commonBundle
) {
return;
}
for (let bundle of Array.from(asset.bundles)) {
if (!bundle.isolated) {
bundle.removeAsset(asset);
}
commonBundle.getSiblingBundle(bundle.type).addAsset(asset);
}
let oldBundle = asset.parentBundle;
asset.parentBundle = commonBundle;
// Move all dependencies as well
for (let child of asset.depAssets.values()) {
if (child.parentBundle === oldBundle) {
this.moveAssetToBundle(child, commonBundle);
}
}
}
*findOrphanAssets() {
for (let asset of this.loadedAssets.values()) {
if (!asset.parentBundle) {
yield asset;
}
}
}
unloadOrphanedAssets() {
for (let asset of this.findOrphanAssets()) {
this.unloadAsset(asset);
}
}
unloadAsset(asset) {
this.loadedAssets.delete(asset.name);
if (this.watcher) {
this.unwatch(asset.name, asset);
// Unwatch all included dependencies that map to this asset
for (let dep of asset.dependencies.values()) {
if (dep.includedInParent) {
this.unwatch(dep.name, asset);
}
}
}
}
async onAdd(path) {
path = Path.join(process.cwd(), path);
let asset = this.parser.getAsset(path, this.options);
this.loadedAssets.set(path, asset);
this.entryAssets.add(asset);
await this.watch(path, asset);
this.onChange(path);
}
async onChange(path) {
// The path to the newly-added items are not absolute.
if (!Path.isAbsolute(path)) {
path = Path.resolve(process.cwd(), path);
}
let assets = this.watchedAssets.get(path);
if (!assets || !assets.size) {
return;
}
logger.clear();
logger.progress(`Building ${Path.basename(path)}...`);
// Add the asset to the rebuild queue, and reset the timeout.
for (let asset of assets) {
this.buildQueue.add(asset, true);
}
clearTimeout(this.rebuildTimeout);
this.rebuildTimeout = setTimeout(async () => {
await this.bundle();
}, 100);
}
async onUnlink(path) {
// The path to the newly-added items are not absolute.
if (!Path.isAbsolute(path)) {
path = Path.resolve(process.cwd(), path);
}
let asset = this.getLoadedAsset(path);
this.entryAssets.delete(asset);
this.unloadAsset(asset);
this.bundle();
}
middleware() {
this.bundle();
return Server.middleware(this);
}
async serve(port = 1234, https = false, host) {
this.server = await Server.serve(this, port, host, https);
try {
await this.bundle();
} catch (e) {
// ignore: server can still work with errored bundler
}
return this.server;
}
}
module.exports = Bundler;
Bundler.Asset = require('./Asset');
Bundler.Packager = require('./packagers/Packager');