Skip to content

Commit 7cbbeef

Browse files
authored
Add support for multiple entry points (#1119)
1 parent d517132 commit 7cbbeef

13 files changed

Lines changed: 202 additions & 74 deletions

File tree

src/Bundle.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ class Bundle {
9292
}
9393

9494
getBundleNameMap(contentHash, hashes = new Map()) {
95-
let hashedName = this.getHashedBundleName(contentHash);
96-
hashes.set(Path.basename(this.name), hashedName);
97-
this.name = Path.join(Path.dirname(this.name), hashedName);
95+
if (this.name) {
96+
let hashedName = this.getHashedBundleName(contentHash);
97+
hashes.set(Path.basename(this.name), hashedName);
98+
this.name = Path.join(Path.dirname(this.name), hashedName);
99+
}
98100

99101
for (let child of this.childBundles.values()) {
100102
child.getBundleNameMap(contentHash, hashes);
@@ -113,9 +115,10 @@ class Bundle {
113115
).slice(-8);
114116
let entryAsset = this.entryAsset || this.parentBundle.entryAsset;
115117
let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name));
116-
let isMainEntry = entryAsset.name === entryAsset.options.mainFile;
118+
let isMainEntry = entryAsset.options.entryFiles[0] === entryAsset.name;
117119
let isEntry =
118-
isMainEntry || Array.from(entryAsset.parentDeps).some(dep => dep.entry);
120+
entryAsset.options.entryFiles.includes(entryAsset.name) ||
121+
Array.from(entryAsset.parentDeps).some(dep => dep.entry);
119122

120123
// If this is the main entry file, use the output file option as the name if provided.
121124
if (isMainEntry && entryAsset.options.outFile) {
@@ -127,7 +130,7 @@ class Bundle {
127130
if (isEntry) {
128131
return Path.join(
129132
Path.relative(
130-
Path.dirname(entryAsset.options.mainFile),
133+
entryAsset.options.rootDir,
131134
Path.dirname(entryAsset.name)
132135
),
133136
name + ext
@@ -145,17 +148,16 @@ class Bundle {
145148
}
146149

147150
async package(bundler, oldHashes, newHashes = new Map()) {
148-
if (this.isEmpty) {
149-
return newHashes;
150-
}
151-
152-
let hash = this.getHash();
153-
newHashes.set(this.name, hash);
154-
155151
let promises = [];
156152
let mappings = [];
157-
if (!oldHashes || oldHashes.get(this.name) !== hash) {
158-
promises.push(this._package(bundler));
153+
154+
if (!this.isEmpty) {
155+
let hash = this.getHash();
156+
newHashes.set(this.name, hash);
157+
158+
if (!oldHashes || oldHashes.get(this.name) !== hash) {
159+
promises.push(this._package(bundler));
160+
}
159161
}
160162

161163
for (let bundle of this.childBundles.values()) {

src/Bundler.js

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ const PromiseQueue = require('./utils/PromiseQueue');
1919
const installPackage = require('./utils/installPackage');
2020
const bundleReport = require('./utils/bundleReport');
2121
const prettifyTime = require('./utils/prettifyTime');
22+
const getRootDir = require('./utils/getRootDir');
23+
const glob = require('glob');
2224

2325
/**
2426
* The Bundler is the main entry point. It resolves and loads assets,
2527
* creates the bundle tree, and manages the worker farm, cache, and file watcher.
2628
*/
2729
class Bundler extends EventEmitter {
28-
constructor(main, options = {}) {
30+
constructor(entryFiles, options = {}) {
2931
super();
30-
this.mainFile = Path.resolve(main || '');
32+
33+
this.entryFiles = this.normalizeEntries(entryFiles);
3134
this.options = this.normalizeOptions(options);
3235

3336
this.resolver = new Resolver(this.options);
@@ -64,6 +67,23 @@ class Bundler extends EventEmitter {
6467
logger.setOptions(this.options);
6568
}
6669

70+
normalizeEntries(entryFiles) {
71+
// Support passing a single file
72+
if (entryFiles && !Array.isArray(entryFiles)) {
73+
entryFiles = [entryFiles];
74+
}
75+
76+
// If no entry files provided, resolve the entry point from the current directory.
77+
if (!entryFiles || entryFiles.length === 0) {
78+
entryFiles = [process.cwd()];
79+
}
80+
81+
// Match files as globs
82+
return entryFiles
83+
.reduce((p, m) => p.concat(glob.sync(m, {nonull: true})), [])
84+
.map(f => Path.resolve(f));
85+
}
86+
6787
normalizeOptions(options) {
6888
const isProduction =
6989
options.production || process.env.NODE_ENV === 'production';
@@ -90,9 +110,9 @@ class Bundler extends EventEmitter {
90110
: typeof options.hmr === 'boolean' ? options.hmr : watch,
91111
https: options.https || false,
92112
logLevel: isNaN(options.logLevel) ? 3 : options.logLevel,
93-
mainFile: this.mainFile,
113+
entryFiles: this.entryFiles,
94114
hmrPort: options.hmrPort || 0,
95-
rootDir: Path.dirname(this.mainFile),
115+
rootDir: getRootDir(this.entryFiles),
96116
sourceMaps:
97117
typeof options.sourceMaps === 'boolean' ? options.sourceMaps : true,
98118
hmrHostname:
@@ -156,7 +176,8 @@ class Bundler extends EventEmitter {
156176
}
157177

158178
async loadPlugins() {
159-
let pkg = await config.load(this.mainFile, ['package.json']);
179+
let relative = Path.join(this.options.rootDir, 'index');
180+
let pkg = await config.load(relative, ['package.json']);
160181
if (!pkg) {
161182
return;
162183
}
@@ -166,7 +187,7 @@ class Bundler extends EventEmitter {
166187
for (let dep in deps) {
167188
const pattern = /^(@.*\/)?parcel-plugin-.+/;
168189
if (pattern.test(dep)) {
169-
let plugin = await localRequire(dep, this.mainFile);
190+
let plugin = await localRequire(dep, relative);
170191
await plugin(this);
171192
}
172193
}
@@ -185,7 +206,7 @@ class Bundler extends EventEmitter {
185206
});
186207
}
187208

188-
let isInitialBundle = !this.mainAsset;
209+
let isInitialBundle = !this.entryAssets;
189210
let startTime = Date.now();
190211
this.pending = true;
191212
this.errored = false;
@@ -201,8 +222,12 @@ class Bundler extends EventEmitter {
201222
if (isInitialBundle) {
202223
await fs.mkdirp(this.options.outDir);
203224

204-
this.mainAsset = await this.resolveAsset(this.mainFile);
205-
this.buildQueue.add(this.mainAsset);
225+
this.entryAssets = new Set();
226+
for (let entry of this.entryFiles) {
227+
let asset = await this.resolveAsset(entry);
228+
this.buildQueue.add(asset);
229+
this.entryAssets.add(asset);
230+
}
206231
}
207232

208233
// Build the queued assets.
@@ -217,8 +242,16 @@ class Bundler extends EventEmitter {
217242
asset.invalidateBundle();
218243
}
219244

220-
// Create a new bundle tree
221-
this.mainBundle = this.createBundleTree(this.mainAsset);
245+
// Create a root bundle to hold all of the entry assets, and add them to the tree.
246+
this.mainBundle = new Bundle();
247+
for (let asset of this.entryAssets) {
248+
this.createBundleTree(asset, this.mainBundle);
249+
}
250+
251+
// If there is only one child bundle, replace the root with that bundle.
252+
if (this.mainBundle.childBundles.size === 1) {
253+
this.mainBundle = Array.from(this.mainBundle.childBundles)[0];
254+
}
222255

223256
// Generate the final bundle names, and replace references in the built assets.
224257
this.bundleNameMap = this.mainBundle.getBundleNameMap(
@@ -281,7 +314,7 @@ class Bundler extends EventEmitter {
281314
}
282315

283316
await this.loadPlugins();
284-
await loadEnv(this.mainFile);
317+
await loadEnv(Path.join(this.options.rootDir, 'index'));
285318

286319
this.options.extensions = Object.assign({}, this.parser.extensions);
287320
this.options.bundleLoaders = this.bundleLoaders;
@@ -508,7 +541,7 @@ class Bundler extends EventEmitter {
508541
});
509542
}
510543

511-
createBundleTree(asset, dep, bundle, parentBundles = new Set()) {
544+
createBundleTree(asset, bundle, dep, parentBundles = new Set()) {
512545
if (dep) {
513546
asset.parentDeps.add(dep);
514547
}
@@ -534,13 +567,23 @@ class Bundler extends EventEmitter {
534567
}
535568
}
536569

537-
if (!bundle) {
538-
// Create the root bundle if it doesn't exist
539-
bundle = Bundle.createWithAsset(asset);
540-
} else if (dep && dep.dynamic) {
570+
let isEntryAsset =
571+
asset.parentBundle && asset.parentBundle.entryAsset === asset;
572+
573+
if ((dep && dep.dynamic) || !bundle.type) {
574+
// If the asset is already the entry asset of a bundle, don't create a duplicate.
575+
if (isEntryAsset) {
576+
return;
577+
}
578+
541579
// Create a new bundle for dynamic imports
542580
bundle = bundle.createChildBundle(asset);
543581
} else if (asset.type && !this.packagers.has(asset.type)) {
582+
// If the asset is already the entry asset of a bundle, don't create a duplicate.
583+
if (isEntryAsset) {
584+
return;
585+
}
586+
544587
// No packager is available for this asset type. Create a new bundle with only this asset.
545588
bundle.createSiblingBundle(asset);
546589
} else {
@@ -566,7 +609,7 @@ class Bundler extends EventEmitter {
566609
parentBundles.add(bundle);
567610

568611
for (let [dep, assetDep] of asset.depAssets) {
569-
this.createBundleTree(assetDep, dep, bundle, parentBundles);
612+
this.createBundleTree(assetDep, bundle, dep, parentBundles);
570613
}
571614

572615
parentBundles.delete(bundle);

src/cli.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const version = require('../package.json').version;
66
program.version(version);
77

88
program
9-
.command('serve [input]')
9+
.command('serve [input...]')
1010
.description('starts a development server')
1111
.option(
1212
'-p, --port <port>',
@@ -60,7 +60,7 @@ program
6060
.action(bundle);
6161

6262
program
63-
.command('watch [input]')
63+
.command('watch [input...]')
6464
.description('starts the bundler in watch mode')
6565
.option(
6666
'-d, --out-dir <path>',
@@ -101,7 +101,7 @@ program
101101
.action(bundle);
102102

103103
program
104-
.command('build [input]')
104+
.command('build [input...]')
105105
.description('bundles for production')
106106
.option(
107107
'-d, --out-dir <path>',

src/utils/bundleReport.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ function bundleReport(mainBundle, detailed = false) {
7171
module.exports = bundleReport;
7272

7373
function* iterateBundles(bundle) {
74-
yield bundle;
74+
if (!bundle.isEmpty) {
75+
yield bundle;
76+
}
77+
7578
for (let child of bundle.childBundles) {
7679
yield* iterateBundles(child);
7780
}

src/utils/getRootDir.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const path = require('path');
2+
3+
function getRootDir(files) {
4+
let cur = null;
5+
6+
for (let file of files) {
7+
let parsed = path.parse(file);
8+
if (!cur) {
9+
cur = parsed;
10+
} else if (parsed.root !== cur.root) {
11+
// bail out. there is no common root.
12+
// this can happen on windows, e.g. C:\foo\bar vs. D:\foo\bar
13+
return process.cwd();
14+
} else {
15+
// find the common path parts.
16+
let curParts = cur.dir.split(path.sep);
17+
let newParts = parsed.dir.split(path.sep);
18+
let len = Math.min(curParts.length, newParts.length);
19+
let i = 0;
20+
while (i < len && curParts[i] === newParts[i]) {
21+
i++;
22+
}
23+
24+
cur.dir = i > 1 ? curParts.slice(0, i).join(path.sep) : cur.root;
25+
}
26+
}
27+
28+
return cur ? cur.dir : process.cwd();
29+
}
30+
31+
module.exports = getRootDir;

src/utils/getTargetEngines.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const browserslist = require('browserslist');
22
const semver = require('semver');
3+
const Path = require('path');
34

45
const DEFAULT_ENGINES = {
56
browsers: ['> 0.25%'],
@@ -15,7 +16,9 @@ const DEFAULT_ENGINES = {
1516
*/
1617
async function getTargetEngines(asset, isTargetApp) {
1718
let targets = {};
18-
let path = isTargetApp ? asset.options.mainFile : asset.name;
19+
let path = isTargetApp
20+
? Path.join(asset.options.rootDir, 'index')
21+
: asset.name;
1922
let compileTarget =
2023
asset.options.target === 'browser' ? 'browsers' : asset.options.target;
2124
let pkg = await asset.getConfig(['package.json'], {path});

test/bundler.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
const assert = require('assert');
22
const sinon = require('sinon');
3-
const {bundler, nextBundle} = require('./utils');
3+
const {assertBundleTree, bundle, bundler, nextBundle} = require('./utils');
44

55
describe('bundler', function() {
66
it('should bundle once before exporting middleware', async function() {
77
let b = bundler(__dirname + '/integration/bundler-middleware/index.js');
88
b.middleware();
99

1010
await nextBundle(b);
11-
assert(b.mainAsset);
11+
assert(b.entryAssets);
1212
});
1313

1414
it('should defer bundling if a bundle is pending', async () => {
@@ -49,4 +49,51 @@ describe('bundler', function() {
4949
b.addPackager('type', 'packager');
5050
}, 'before bundling');
5151
});
52+
53+
it('should support multiple entry points', async function() {
54+
let b = await bundle([
55+
__dirname + '/integration/multi-entry/one.html',
56+
__dirname + '/integration/multi-entry/two.html'
57+
]);
58+
59+
assertBundleTree(b, [
60+
{
61+
type: 'html',
62+
assets: ['one.html'],
63+
childBundles: [
64+
{
65+
type: 'js',
66+
assets: ['shared.js']
67+
}
68+
]
69+
},
70+
{
71+
type: 'html',
72+
assets: ['two.html'],
73+
childBundles: []
74+
}
75+
]);
76+
});
77+
78+
it('should support multiple entry points as a glob', async function() {
79+
let b = await bundle(__dirname + '/integration/multi-entry/*.html');
80+
81+
assertBundleTree(b, [
82+
{
83+
type: 'html',
84+
assets: ['one.html'],
85+
childBundles: [
86+
{
87+
type: 'js',
88+
assets: ['shared.js']
89+
}
90+
]
91+
},
92+
{
93+
type: 'html',
94+
assets: ['two.html'],
95+
childBundles: []
96+
}
97+
]);
98+
});
5299
});

0 commit comments

Comments
 (0)