From 7cbbeef2bf5b6e2d83af55344218da72006d325c Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 1 May 2018 08:00:11 -0700 Subject: [PATCH] Add support for multiple entry points (#1119) --- src/Bundle.js | 32 ++++++----- src/Bundler.js | 79 ++++++++++++++++++++------ src/cli.js | 6 +- src/utils/bundleReport.js | 5 +- src/utils/getRootDir.js | 31 ++++++++++ src/utils/getTargetEngines.js | 5 +- test/bundler.js | 51 ++++++++++++++++- test/html.js | 14 ----- test/integration/multi-entry/one.html | 7 +++ test/integration/multi-entry/shared.js | 0 test/integration/multi-entry/two.html | 7 +++ test/parser.js | 14 ----- test/utils.js | 25 ++++++-- 13 files changed, 202 insertions(+), 74 deletions(-) create mode 100644 src/utils/getRootDir.js create mode 100644 test/integration/multi-entry/one.html create mode 100644 test/integration/multi-entry/shared.js create mode 100644 test/integration/multi-entry/two.html diff --git a/src/Bundle.js b/src/Bundle.js index f8be4fe0975..ff820d1e7ea 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -92,9 +92,11 @@ class Bundle { } getBundleNameMap(contentHash, hashes = new Map()) { - let hashedName = this.getHashedBundleName(contentHash); - hashes.set(Path.basename(this.name), hashedName); - this.name = Path.join(Path.dirname(this.name), hashedName); + if (this.name) { + let hashedName = this.getHashedBundleName(contentHash); + hashes.set(Path.basename(this.name), hashedName); + this.name = Path.join(Path.dirname(this.name), hashedName); + } for (let child of this.childBundles.values()) { child.getBundleNameMap(contentHash, hashes); @@ -113,9 +115,10 @@ class Bundle { ).slice(-8); let entryAsset = this.entryAsset || this.parentBundle.entryAsset; let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name)); - let isMainEntry = entryAsset.name === entryAsset.options.mainFile; + let isMainEntry = entryAsset.options.entryFiles[0] === entryAsset.name; let isEntry = - isMainEntry || Array.from(entryAsset.parentDeps).some(dep => dep.entry); + entryAsset.options.entryFiles.includes(entryAsset.name) || + Array.from(entryAsset.parentDeps).some(dep => dep.entry); // If this is the main entry file, use the output file option as the name if provided. if (isMainEntry && entryAsset.options.outFile) { @@ -127,7 +130,7 @@ class Bundle { if (isEntry) { return Path.join( Path.relative( - Path.dirname(entryAsset.options.mainFile), + entryAsset.options.rootDir, Path.dirname(entryAsset.name) ), name + ext @@ -145,17 +148,16 @@ class Bundle { } async package(bundler, oldHashes, newHashes = new Map()) { - if (this.isEmpty) { - return newHashes; - } - - let hash = this.getHash(); - newHashes.set(this.name, hash); - let promises = []; let mappings = []; - if (!oldHashes || oldHashes.get(this.name) !== hash) { - promises.push(this._package(bundler)); + + if (!this.isEmpty) { + let hash = this.getHash(); + newHashes.set(this.name, hash); + + if (!oldHashes || oldHashes.get(this.name) !== hash) { + promises.push(this._package(bundler)); + } } for (let bundle of this.childBundles.values()) { diff --git a/src/Bundler.js b/src/Bundler.js index b650280c337..fb44af209eb 100644 --- a/src/Bundler.js +++ b/src/Bundler.js @@ -19,15 +19,18 @@ 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 = require('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(main, options = {}) { + constructor(entryFiles, options = {}) { super(); - this.mainFile = Path.resolve(main || ''); + + this.entryFiles = this.normalizeEntries(entryFiles); this.options = this.normalizeOptions(options); this.resolver = new Resolver(this.options); @@ -64,6 +67,23 @@ class Bundler extends EventEmitter { 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()]; + } + + // Match files as globs + return entryFiles + .reduce((p, m) => p.concat(glob.sync(m, {nonull: true})), []) + .map(f => Path.resolve(f)); + } + normalizeOptions(options) { const isProduction = options.production || process.env.NODE_ENV === 'production'; @@ -90,9 +110,9 @@ class Bundler extends EventEmitter { : typeof options.hmr === 'boolean' ? options.hmr : watch, https: options.https || false, logLevel: isNaN(options.logLevel) ? 3 : options.logLevel, - mainFile: this.mainFile, + entryFiles: this.entryFiles, hmrPort: options.hmrPort || 0, - rootDir: Path.dirname(this.mainFile), + rootDir: getRootDir(this.entryFiles), sourceMaps: typeof options.sourceMaps === 'boolean' ? options.sourceMaps : true, hmrHostname: @@ -156,7 +176,8 @@ class Bundler extends EventEmitter { } async loadPlugins() { - let pkg = await config.load(this.mainFile, ['package.json']); + let relative = Path.join(this.options.rootDir, 'index'); + let pkg = await config.load(relative, ['package.json']); if (!pkg) { return; } @@ -166,7 +187,7 @@ class Bundler extends EventEmitter { for (let dep in deps) { const pattern = /^(@.*\/)?parcel-plugin-.+/; if (pattern.test(dep)) { - let plugin = await localRequire(dep, this.mainFile); + let plugin = await localRequire(dep, relative); await plugin(this); } } @@ -185,7 +206,7 @@ class Bundler extends EventEmitter { }); } - let isInitialBundle = !this.mainAsset; + let isInitialBundle = !this.entryAssets; let startTime = Date.now(); this.pending = true; this.errored = false; @@ -201,8 +222,12 @@ class Bundler extends EventEmitter { if (isInitialBundle) { await fs.mkdirp(this.options.outDir); - this.mainAsset = await this.resolveAsset(this.mainFile); - this.buildQueue.add(this.mainAsset); + this.entryAssets = new Set(); + for (let entry of this.entryFiles) { + let asset = await this.resolveAsset(entry); + this.buildQueue.add(asset); + this.entryAssets.add(asset); + } } // Build the queued assets. @@ -217,8 +242,16 @@ class Bundler extends EventEmitter { asset.invalidateBundle(); } - // Create a new bundle tree - this.mainBundle = this.createBundleTree(this.mainAsset); + // 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. this.bundleNameMap = this.mainBundle.getBundleNameMap( @@ -281,7 +314,7 @@ class Bundler extends EventEmitter { } await this.loadPlugins(); - await loadEnv(this.mainFile); + await loadEnv(Path.join(this.options.rootDir, 'index')); this.options.extensions = Object.assign({}, this.parser.extensions); this.options.bundleLoaders = this.bundleLoaders; @@ -508,7 +541,7 @@ class Bundler extends EventEmitter { }); } - createBundleTree(asset, dep, bundle, parentBundles = new Set()) { + createBundleTree(asset, bundle, dep, parentBundles = new Set()) { if (dep) { asset.parentDeps.add(dep); } @@ -534,13 +567,23 @@ class Bundler extends EventEmitter { } } - if (!bundle) { - // Create the root bundle if it doesn't exist - bundle = Bundle.createWithAsset(asset); - } else if (dep && dep.dynamic) { + let isEntryAsset = + asset.parentBundle && asset.parentBundle.entryAsset === 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); } else if (asset.type && !this.packagers.has(asset.type)) { + // 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. Create a new bundle with only this asset. bundle.createSiblingBundle(asset); } else { @@ -566,7 +609,7 @@ class Bundler extends EventEmitter { parentBundles.add(bundle); for (let [dep, assetDep] of asset.depAssets) { - this.createBundleTree(assetDep, dep, bundle, parentBundles); + this.createBundleTree(assetDep, bundle, dep, parentBundles); } parentBundles.delete(bundle); diff --git a/src/cli.js b/src/cli.js index 475cef421c8..047999c94a2 100755 --- a/src/cli.js +++ b/src/cli.js @@ -6,7 +6,7 @@ const version = require('../package.json').version; program.version(version); program - .command('serve [input]') + .command('serve [input...]') .description('starts a development server') .option( '-p, --port ', @@ -60,7 +60,7 @@ program .action(bundle); program - .command('watch [input]') + .command('watch [input...]') .description('starts the bundler in watch mode') .option( '-d, --out-dir ', @@ -101,7 +101,7 @@ program .action(bundle); program - .command('build [input]') + .command('build [input...]') .description('bundles for production') .option( '-d, --out-dir ', diff --git a/src/utils/bundleReport.js b/src/utils/bundleReport.js index 38fa67fa3c0..286bd8ae463 100644 --- a/src/utils/bundleReport.js +++ b/src/utils/bundleReport.js @@ -71,7 +71,10 @@ function bundleReport(mainBundle, detailed = false) { module.exports = bundleReport; function* iterateBundles(bundle) { - yield bundle; + if (!bundle.isEmpty) { + yield bundle; + } + for (let child of bundle.childBundles) { yield* iterateBundles(child); } diff --git a/src/utils/getRootDir.js b/src/utils/getRootDir.js new file mode 100644 index 00000000000..0a2e4848436 --- /dev/null +++ b/src/utils/getRootDir.js @@ -0,0 +1,31 @@ +const path = require('path'); + +function getRootDir(files) { + let cur = null; + + for (let file of files) { + let parsed = path.parse(file); + if (!cur) { + cur = parsed; + } else if (parsed.root !== cur.root) { + // bail out. there is no common root. + // this can happen on windows, e.g. C:\foo\bar vs. D:\foo\bar + return process.cwd(); + } else { + // find the common path parts. + let curParts = cur.dir.split(path.sep); + let newParts = parsed.dir.split(path.sep); + let len = Math.min(curParts.length, newParts.length); + let i = 0; + while (i < len && curParts[i] === newParts[i]) { + i++; + } + + cur.dir = i > 1 ? curParts.slice(0, i).join(path.sep) : cur.root; + } + } + + return cur ? cur.dir : process.cwd(); +} + +module.exports = getRootDir; diff --git a/src/utils/getTargetEngines.js b/src/utils/getTargetEngines.js index f099a9db78d..cfcdc300b5c 100644 --- a/src/utils/getTargetEngines.js +++ b/src/utils/getTargetEngines.js @@ -1,5 +1,6 @@ const browserslist = require('browserslist'); const semver = require('semver'); +const Path = require('path'); const DEFAULT_ENGINES = { browsers: ['> 0.25%'], @@ -15,7 +16,9 @@ const DEFAULT_ENGINES = { */ async function getTargetEngines(asset, isTargetApp) { let targets = {}; - let path = isTargetApp ? asset.options.mainFile : asset.name; + let path = isTargetApp + ? Path.join(asset.options.rootDir, 'index') + : asset.name; let compileTarget = asset.options.target === 'browser' ? 'browsers' : asset.options.target; let pkg = await asset.getConfig(['package.json'], {path}); diff --git a/test/bundler.js b/test/bundler.js index ee6b2b9e7af..f4ae8bfc43d 100644 --- a/test/bundler.js +++ b/test/bundler.js @@ -1,6 +1,6 @@ const assert = require('assert'); const sinon = require('sinon'); -const {bundler, nextBundle} = require('./utils'); +const {assertBundleTree, bundle, bundler, nextBundle} = require('./utils'); describe('bundler', function() { it('should bundle once before exporting middleware', async function() { @@ -8,7 +8,7 @@ describe('bundler', function() { b.middleware(); await nextBundle(b); - assert(b.mainAsset); + assert(b.entryAssets); }); it('should defer bundling if a bundle is pending', async () => { @@ -49,4 +49,51 @@ describe('bundler', function() { b.addPackager('type', 'packager'); }, 'before bundling'); }); + + it('should support multiple entry points', async function() { + let b = await bundle([ + __dirname + '/integration/multi-entry/one.html', + __dirname + '/integration/multi-entry/two.html' + ]); + + assertBundleTree(b, [ + { + type: 'html', + assets: ['one.html'], + childBundles: [ + { + type: 'js', + assets: ['shared.js'] + } + ] + }, + { + type: 'html', + assets: ['two.html'], + childBundles: [] + } + ]); + }); + + it('should support multiple entry points as a glob', async function() { + let b = await bundle(__dirname + '/integration/multi-entry/*.html'); + + assertBundleTree(b, [ + { + type: 'html', + assets: ['one.html'], + childBundles: [ + { + type: 'js', + assets: ['shared.js'] + } + ] + }, + { + type: 'html', + assets: ['two.html'], + childBundles: [] + } + ]); + }); }); diff --git a/test/html.js b/test/html.js index 7674010e415..ca9a65e466b 100644 --- a/test/html.js +++ b/test/html.js @@ -25,24 +25,10 @@ describe('html', function() { assets: ['index.css'], childBundles: [] }, - { - type: 'js', - assets: ['index.js'], - childBundles: [ - { - type: 'map' - } - ] - }, { type: 'html', assets: ['other.html'], childBundles: [ - { - type: 'css', - assets: ['index.css'], - childBundles: [] - }, { type: 'js', assets: ['index.js'], diff --git a/test/integration/multi-entry/one.html b/test/integration/multi-entry/one.html new file mode 100644 index 00000000000..650de11c03e --- /dev/null +++ b/test/integration/multi-entry/one.html @@ -0,0 +1,7 @@ + + + +

One

+ + + diff --git a/test/integration/multi-entry/shared.js b/test/integration/multi-entry/shared.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/multi-entry/two.html b/test/integration/multi-entry/two.html new file mode 100644 index 00000000000..1dfee6f038b --- /dev/null +++ b/test/integration/multi-entry/two.html @@ -0,0 +1,7 @@ + + + +

Two

+ + + diff --git a/test/parser.js b/test/parser.js index ec1db8e413c..05676789a66 100644 --- a/test/parser.js +++ b/test/parser.js @@ -22,24 +22,10 @@ describe('parser', function() { assets: ['index.cSs'], childBundles: [] }, - { - type: 'js', - assets: ['index.js'], - childBundles: [ - { - type: 'map' - } - ] - }, { type: 'html', assets: ['other.HTM'], childBundles: [ - { - type: 'css', - assets: ['index.cSs'], - childBundles: [] - }, { type: 'js', assets: ['index.js'], diff --git a/test/utils.js b/test/utils.js index d26f5664a97..443fa865df8 100644 --- a/test/utils.js +++ b/test/utils.js @@ -162,11 +162,19 @@ function run(bundle, globals, opts = {}) { function assertBundleTree(bundle, tree) { if (tree.name) { - assert.equal(path.basename(bundle.name), tree.name); + assert.equal( + path.basename(bundle.name), + tree.name, + 'bundle names mismatched' + ); } if (tree.type) { - assert.equal(bundle.type.toLowerCase(), tree.type.toLowerCase()); + assert.equal( + bundle.type.toLowerCase(), + tree.type.toLowerCase(), + 'bundle types mismatched' + ); } if (tree.assets) { @@ -178,7 +186,8 @@ function assertBundleTree(bundle, tree) { ); } - if (tree.childBundles) { + let childBundles = Array.isArray(tree) ? tree : tree.childBundles; + if (childBundles) { let children = Array.from(bundle.childBundles).sort( (a, b) => Array.from(a.assets).sort()[0].basename < @@ -186,12 +195,16 @@ function assertBundleTree(bundle, tree) { ? -1 : 1 ); - assert.equal(bundle.childBundles.size, tree.childBundles.length); - tree.childBundles.forEach((b, i) => assertBundleTree(children[i], b)); + assert.equal( + bundle.childBundles.size, + childBundles.length, + 'expected number of child bundles mismatched' + ); + childBundles.forEach((b, i) => assertBundleTree(children[i], b)); } if (/js|css/.test(bundle.type)) { - assert(fs.existsSync(bundle.name)); + assert(fs.existsSync(bundle.name), 'expected file does not exist'); } }