Skip to content

Commit

Permalink
enable code splitting for the esm format (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 30, 2020
1 parent d7c20a4 commit a5f73e0
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 0 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## Unreleased

* Experimental code splitting with `--splitting` ([#16](https://github.com/evanw/esbuild/issues/16))

This release includes experimental support for code splitting. Enable it with the `--splitting` flag. This currently only works with the `esm` output format. Support for the `cjs` and `iife` formats will come later. It's being released early so people can try it out and provide feedback.

When enabled, code splitting does two things:

* An asynchronous `import('path')` expression will create another chunk that will only be loaded when that expression is evaluated. This is intended to be used for lazily loading additional code. All additional chunks will be written to the directory configured with `outdir`.

Note that when code splitting is disabled (i.e. the default behavior), an `import('path')` expression behaves similar to `Promise.resolve(require('path'))` and still bundles the imported file into the entry point bundle. No additional chunks are generated in this case.

* Multiple entry points will cause additional chunks to be created for code that is shared between entry points. Chunks are generated automatically based on simple principles: code should only ever be in one chunk (i.e. no duplication) and no unnecessary code should be loaded (i.e. chunk boundaries are minimal).

The way this works is by traversing through the module dependency graph and marking which top-level statements are reachable from which entry points. The set of entry points for a given top-level statement determines which chunk that statement is in.

This is an advanced form of code splitting where even a single file may end up being split into different chunks. This is not something most other bundlers can do at the moment.

Note that using code splitting with many entry points may generate many chunks for shared code reachable from different combinations of entry points. This should work fine and should still be efficient with HTTP/2. If you want to only let certain entry points share code, you can run esbuild multiple times for different groups of entry points.

Please try it out and report any issues on [#16](https://github.com/evanw/esbuild/issues/16).

## 0.5.15

* Remove some unnecessary helper functions ([#206](https://github.com/evanw/esbuild/issues/206))
Expand Down
1 change: 1 addition & 0 deletions lib/api-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean): stri
if (options.sourcemap) flags.push(`--sourcemap${options.sourcemap === true ? '' : `=${options.sourcemap}`}`);
if (options.globalName) flags.push(`--global-name=${options.globalName}`);
if (options.bundle) flags.push('--bundle');
if (options.splitting) flags.push('--splitting');
if (options.metafile) flags.push(`--metafile=${options.metafile}`);
if (options.outfile) flags.push(`--outfile=${options.outfile}`);
if (options.outdir) flags.push(`--outdir=${options.outdir}`);
Expand Down
1 change: 1 addition & 0 deletions lib/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface CommonOptions {
export interface BuildOptions extends CommonOptions {
globalName?: string;
bundle?: boolean;
splitting?: boolean;
outfile?: string;
metafile?: string;
outdir?: string;
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ type BuildOptions struct {

GlobalName string
Bundle bool
Splitting bool
Outfile string
Metafile string
Outdir string
Expand Down
9 changes: 9 additions & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ func buildImpl(options BuildOptions) BuildResult {
MinifyIdentifiers: options.MinifyIdentifiers,
ModuleName: options.GlobalName,
IsBundling: options.Bundle,
CodeSplitting: options.Splitting,
OutputFormat: validateFormat(options.Format),
AbsOutputFile: validatePath(log, realFS, options.Outfile),
AbsOutputDir: validatePath(log, realFS, options.Outdir),
Expand All @@ -350,6 +351,9 @@ func buildImpl(options BuildOptions) BuildResult {
if bundleOptions.AbsOutputDir == "" && len(entryPaths) > 1 {
log.AddError(nil, ast.Loc{},
"Must use \"outdir\" when there are multiple input files")
} else if bundleOptions.AbsOutputDir == "" && bundleOptions.CodeSplitting {
log.AddError(nil, ast.Loc{},
"Must use \"outdir\" when code splitting is enabled")
} else if bundleOptions.AbsOutputFile != "" && bundleOptions.AbsOutputDir != "" {
log.AddError(nil, ast.Loc{}, "Cannot use both \"outfile\" and \"outdir\"")
} else if bundleOptions.AbsOutputFile != "" {
Expand Down Expand Up @@ -389,6 +393,11 @@ func buildImpl(options BuildOptions) BuildResult {
}
}

// Code splitting is experimental and currently only enabled for ES6 modules
if bundleOptions.CodeSplitting && bundleOptions.OutputFormat != printer.FormatESModule {
log.AddError(nil, ast.Loc{}, "Spltting currently only works with the \"esm\" format")
}

var outputFiles []OutputFile

// Stop now if there were errors
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func parseOptionsImpl(osArgs []string, buildOpts *api.BuildOptions, transformOpt
case arg == "--bundle" && buildOpts != nil:
buildOpts.Bundle = true

case arg == "--splitting" && buildOpts != nil:
buildOpts.Splitting = true

case arg == "--minify":
if buildOpts != nil {
buildOpts.MinifySyntax = true
Expand Down
65 changes: 65 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,71 @@
}),
)

// Code splitting tests
tests.push(
// Code splitting via sharing
test(['a.js', 'b.js', '--outdir=out', '--splitting', '--format=esm', '--bundle'], {
'a.js': `
import * as ns from './common'
export let a = 'a' + ns.foo
`,
'b.js': `
import * as ns from './common'
export let b = 'b' + ns.foo
`,
'common.js': `
export let foo = 123
`,
'node.js': `
import {a} from './out/a.js'
import {b} from './out/b.js'
if (a !== 'a123' || b !== 'b123') throw 'fail'
`,
}),

// Code splitting via ES6 module double-imported with sync and async imports
test(['a.js', '--outdir=out', '--splitting', '--format=esm', '--bundle'], {
'a.js': `
import * as ns1 from './b'
export default async function () {
const ns2 = await import('./b')
return [ns1.foo, -ns2.foo]
}
`,
'b.js': `
export let foo = 123
`,
'node.js': `
export let async = async () => {
const {default: fn} = await import('./out/a.js')
const [a, b] = await fn()
if (a !== 123 || b !== -123) throw 'fail'
}
`,
}, { async: true }),

// Code splitting via CommonJS module double-imported with sync and async imports
test(['a.js', '--outdir=out', '--splitting', '--format=esm', '--bundle'], {
'a.js': `
import * as ns1 from './b'
export default async function () {
const ns2 = await import('./b')
return [ns1.foo, -ns2.default.foo]
}
`,
'b.js': `
exports.foo = 123
`,
'node.js': `
export let async = async () => {
const {default: fn} = await import('./out/a.js')
const [a, b] = await fn()
if (a !== 123 || b !== -123) throw 'fail'
}
`,
}, { async: true }),
)

// Test writing to stdout
tests.push(
// These should succeed
Expand Down

0 comments on commit a5f73e0

Please sign in to comment.