Skip to content

Commit

Permalink
Add support for multiple entry points (#1119)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed May 1, 2018
1 parent 95f1dd5 commit 0c8fc1f
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 74 deletions.
32 changes: 17 additions & 15 deletions packages/core/parcel/src/Bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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()) {
Expand Down
79 changes: 61 additions & 18 deletions packages/core/parcel/src/Bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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';
Expand All @@ -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:
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
}
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/parcel/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <port>',
Expand Down Expand Up @@ -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 <path>',
Expand Down Expand Up @@ -101,7 +101,7 @@ program
.action(bundle);

program
.command('build [input]')
.command('build [input...]')
.description('bundles for production')
.option(
'-d, --out-dir <path>',
Expand Down
5 changes: 4 additions & 1 deletion packages/core/parcel/src/utils/bundleReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
31 changes: 31 additions & 0 deletions packages/core/parcel/src/utils/getRootDir.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 4 additions & 1 deletion packages/core/parcel/src/utils/getTargetEngines.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const browserslist = require('browserslist');
const semver = require('semver');
const Path = require('path');

const DEFAULT_ENGINES = {
browsers: ['> 0.25%'],
Expand All @@ -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});
Expand Down
51 changes: 49 additions & 2 deletions packages/core/parcel/test/bundler.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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() {
let b = bundler(__dirname + '/integration/bundler-middleware/index.js');
b.middleware();

await nextBundle(b);
assert(b.mainAsset);
assert(b.entryAssets);
});

it('should defer bundling if a bundle is pending', async () => {
Expand Down Expand Up @@ -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: []
}
]);
});
});

0 comments on commit 0c8fc1f

Please sign in to comment.