Skip to content

Commit

Permalink
Add hook for dynamic entry chunk emission (#2809)
Browse files Browse the repository at this point in the history
* Merge compact and non-compact import.meta.url mechanisms

* Extract more common code

* Add option to configure import.meta.url resolution

* Extract more common code

* Switch to using a plugin hook

* Move functionality into default plugin

* Improve SystemJS handling

* Refactor import.meta.url handling

* Fix, refactor and test asset emission

* Extract ModuleLoader

* Use sets for colouring hashes

* Simplify alias generation

* Attach aliases to modules

* Transform loading into a process that dynamically accepts new entry points
and manual chunks

* Pass alias objects through the module loader

* Implement basic chunk emission

* Allow duplicate entry points again

* Rename addEntry -> emitEntryChunk

* Simplify alias handling by immediately assigning a chunkAlias to entry
points and introducing a manualChunkAlias for colouring to resolve this
confusing double use of chunkAlias

* * Allow manual chunks to contain nested entry points
* Allow manual chunks to contain entry points without name or with the same name
* Throw if an emitted chunk is not found
* Throw if there is a conflict between manual chunk entries
* Allow nesting of manual chunks without requiring a specific order

* Manual chunks never conflict with entry points:
- if the alias matches, the manual chunk becomes the entry chunk
- otherwise a facade is created

* Return correct file name if a facade is created for an emitted chunk

* Start using central error handlers

* Improve plugin driver type, add generic resolveFileUrl hook

* Test new resolveFileUrl hook, make meta properties tree-shakeable

* Move setAssetSource failure tests to function

* Test and extract all errors thrown when emitting assets

* Extract and refine error when chunk id cannot be found

* Fail if filename is not yet available

* Fail when adding a chunk after loading has finished

* Do not access process.cwd() unchecked

* Move isExternal to module loader

* Fix typing of resolveId context hook

* Refine worker test

* Revert to "wrong" resolveId type as this will probably be fixed in a separate PR

* Suppress .js extensions for AMD, fix issue with empty dynamically imported chunks

* Use generated chunk naming scheme for emitted chunks and rename context
function to `emitChunk`

* Allow emitted chunks to be named

* Add paint worklet example

* Update documentation

* Add reference ids to resolveFileUrl and replace type

* Do not require `input` to be set if a dynamic entry is emitted

* Use facade module id as [name] for dynamic imports
  • Loading branch information
lukastaegert committed May 3, 2019
1 parent 980903b commit 18829da
Show file tree
Hide file tree
Showing 644 changed files with 3,752 additions and 1,126 deletions.
5 changes: 2 additions & 3 deletions bin/src/run/loadConfigFile.ts
Expand Up @@ -19,9 +19,8 @@ export default function loadConfigFile(

return rollup
.rollup({
external: (id: string) => {
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
},
external: (id: string) =>
(id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json',
input: configFile,
onwarn: warnings.add,
treeshake: false
Expand Down
131 changes: 107 additions & 24 deletions docs/05-plugins.md
Expand Up @@ -181,37 +181,39 @@ Kind: `async, parallel`

Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks.

#### `resolveAssetUrl`
Type: `({assetFileName: string, relativeAssetPath: string, chunkId: string, moduleId: string, format: string}) => string | null`<br>
#### `resolveDynamicImport`
Type: `(specifier: string | ESTree.Node, importer: string) => string | false | null`<br>
Kind: `async, first`

Defines a custom resolver for dynamic imports. In case a dynamic import is not passed a string as argument, this hook gets access to the raw AST nodes to analyze. Returning `null` will defer to other resolvers and eventually to `resolveId` if this is possible; returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Note that the return value of this hook will not be passed to `resolveId` afterwards; if you need access to the static resolution algorithm, you can use `this.resolveId(importee, importer)` on the plugin context.

#### `resolveFileUrl`
Type: `({assetReferenceId: string | null, chunkId: string, chunkReferenceId: string | null, fileName: string, format: string, moduleId: string, relativePath: string}) => string | null`<br>
Kind: `sync, first`

Allows to customize how Rollup resolves URLs of assets emitted via `this.emitAsset` by plugins. By default, Rollup will generate code for `import.meta.ROLLUP_ASSET_URL_[assetId]` that should correctly generate absolute URLs of emitted assets independent of the output format and the host system where the code is deployed.
Allows to customize how Rollup resolves URLs of files that were emitted by plugins via `this.emitAsset` or `this.emitChunk`. By default, Rollup will generate code for `import.meta.ROLLUP_ASSET_URL_assetReferenceId` and `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` that should correctly generate absolute URLs of emitted files independent of the output format and the host system where the code is deployed.

For that, all formats except CommonJS and UMD assume that they run in a browser environment where `URL` and `document` are available. In case that fails or to generate more optimized code, this hook can be used to customize this behaviour. To do that, the following information is available:

- `assetFileName`: The path and file name of the emitted asset, relative to `output.dir` without a leading `./`.
- `relativeAssetPath`: The path and file name of the emitted asset, relative to the chunk from which the asset is referenced via `import.meta.ROLLUP_ASSET_URL_[assetId]`. This will also contain no leading `./` but may contain a leading `../`.
- `moduleId`: The id of the original module this asset is referenced from. Useful for conditionally resolving certain assets differently.
- `chunkId`: The id of the chunk this asset is referenced from.
- `assetReferenceId`: The asset reference id if we are resolving `import.meta.ROLLUP_ASSET_URL_assetReferenceId`, otherwise `null`.
- `chunkId`: The id of the chunk this file is referenced from.
- `chunkReferenceId`: The chunk reference id if we are resolving `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId`, otherwise `null`.
- `fileName`: The path and file name of the emitted asset, relative to `output.dir` without a leading `./`.
- `format`: The rendered output format.
- `moduleId`: The id of the original module this file is referenced from. Useful for conditionally resolving certain assets differently.
- `relativePath`: The path and file name of the emitted file, relative to the chunk the file is referenced from. This will path will contain no leading `./` but may contain a leading `../`.

Note that since this hook has access to the filename of the current chunk, its return value will not be considered when generating the hash of this chunk.

The following plugin will always resolve all assets relative to the current document:
The following plugin will always resolve all files relative to the current document:

```javascript
// rollup.config.js
resolveAssetUrl({assetFileName}) {
return `new URL('${assetFileName}', document.baseURI).href`;
resolveFileUrl({fileName}) {
return `new URL('${fileName}', document.baseURI).href`;
}
```

#### `resolveDynamicImport`
Type: `(specifier: string | ESTree.Node, importer: string) => string | false | null`<br>
Kind: `async, first`

Defines a custom resolver for dynamic imports. In case a dynamic import is not passed a string as argument, this hook gets access to the raw AST nodes to analyze. Returning `null` will defer to other resolvers and eventually to `resolveId` if this is possible; returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Note that the return value of this hook will not be passed to `resolveId` afterwards; if you need access to the static resolution algorithm, you can use `this.resolveId(importee, importer)` on the plugin context.

#### `resolveId`
Type: `(importee: string, importer: string) => string | false | null | {id: string, external?: boolean}`<br>
Kind: `async, first`
Expand Down Expand Up @@ -281,6 +283,8 @@ called when `bundle.generate()` is being executed.
called when `bundle.write()` is being executed, after the file has been written
to disk.

- `resolveAssetUrl` - _**Use [`resolveFileUrl`](guide/en#resolvefileurl)**_ - Function hook that allows to customize the generated code for asset URLs.

- `transformBundle`_**Use [`renderChunk`](guide/en#renderchunk)**_ - A `( source, { format } ) =>
code` or `( source, { format } ) => { code, map }` bundle transformer function.

Expand All @@ -303,15 +307,31 @@ In general, it is recommended to use `this.addWatchfile` from within the hook th

#### `this.emitAsset(assetName: string, source: string) => string`

Emits a custom file to include in the build output, returning its `assetId`. You can defer setting the source if you provide it later via `this.setAssetSource(assetId, source)`. A string or Buffer source must be set for each asset through either method or an error will be thrown on generate completion.
Emits a custom file that is included in the build output, returning an `assetReferenceId` that can be used to reference the emitted file. You can defer setting the source if you provide it later via [`this.setAssetSource(assetReferenceId, source)`](guide/en#this-setassetsource-assetreferenceid-string-source-string-buffer-void). A string or Buffer source must be set for each asset through either method or an error will be thrown on generate completion.

Emitted assets will follow the [`output.assetFileNames`](guide/en#output-assetfilenames) naming scheme. You can reference the URL of the file in any code returned by a [`load`](guide/en#load) or [`transform`](guide/en#transform) plugin hook via `import.meta.ROLLUP_ASSET_URL_assetReferenceId`. See [Asset URLs](guide/en#asset-urls) for more details and an example.

The generated code that replaces `import.meta.ROLLUP_ASSET_URL_assetReferenceId` can be customized via the [`resolveFileUrl`](guide/en#resolvefileurl) plugin hook. Once the asset has been finalized during `generate`, you can also use [`this.getAssetFileName(assetReferenceId)`](guide/en#this-getassetfilename-assetreferenceid-string-string) to determine the file name.

#### `this.emitChunk(moduleId: string, options?: {name?: string}) => string`

Emits a new chunk with the given module as entry point. This will not result in duplicate modules in the graph, instead if necessary, existing chunks will be split. It returns a `chunkReferenceId` that can be used to later access the generated file name of the chunk.

Emitted chunks will follow the [`output.chunkFileNames`](guide/en#output-chunkfilenames), [`output.entryFileNames`](guide/en#output-entryfilenames) naming scheme. If a `name` is provided, this will be used for the `[name]` file name placeholder, otherwise the name will be derived from the file name. If a `name` is provided, this name must not conflict with any other entry point names unless the entry points reference the same entry module. You can reference the URL of the emitted chunk in any code returned by a [`load`](guide/en#load) or [`transform`](guide/en#transform) plugin hook via `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId`.

The generated code that replaces `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` can be customized via the [`resolveFileUrl`](guide/en#resolvefileurl) plugin hook. Once the chunk has been rendered during `generate`, you can also use [`this.getChunkFileName(chunkReferenceId)`](guide/en#this-getchunkfilename-chunkreferenceid-string-string) to determine the file name.

#### `this.error(error: string | Error, position?: number) => void`

Structurally equivalent to `this.warn`, except that it will also abort the bundling process.

#### `this.getAssetFileName(assetId: string) => string`
#### `this.getAssetFileName(assetReferenceId: string) => string`

Get the file name of an asset, according to the `assetFileNames` output option pattern. The file name will be relative to `outputOptions.dir`.

Get the file name of an asset, according to the `assetFileNames` output option pattern.
#### `this.getChunkFileName(chunkReferenceId: string) => string`

Get the file name of an emitted chunk. The file name will be relative to `outputOptions.dir`.

#### `this.getModuleInfo(moduleId: string) => ModuleInfo`

Expand Down Expand Up @@ -349,11 +369,11 @@ or converted into an Array via `Array.from(this.moduleIds)`.

Use Rollup's internal acorn instance to parse code to an AST.

#### `this.resolveId(importee: string, importer: string) => string`
#### `this.resolveId(importee: string, importer: string) => Promise<string>`

Resolve imports to module ids (i.e. file names). Uses the same hooks as Rollup itself.

#### `this.setAssetSource(assetId: string, source: string | Buffer) => void`
#### `this.setAssetSource(assetReferenceId: string, source: string | Buffer) => void`

Set the deferred source of an asset.

Expand All @@ -375,7 +395,7 @@ The `position` argument is a character index where the warning was raised. If pr

### Asset URLs

To reference an asset URL reference from within JS code, use the `import.meta.ROLLUP_ASSET_URL_[assetId]` replacement. This will generate code that depends on the output format and generates a URL that points to the emitted file in the target environment. Note that all formats except CommonJS and UMD assume that they run in a browser environment where `URL` and `document` are available.
To reference an asset URL reference from within JS code, use the `import.meta.ROLLUP_ASSET_URL_assetReferenceId` replacement. This will generate code that depends on the output format and generates a URL that points to the emitted file in the target environment. Note that all formats except CommonJS and UMD assume that they run in a browser environment where `URL` and `document` are available.

The following example will detect imports of `.svg` files, emit the imported files as assets, and return their URLs to be used e.g. as the `src` attribute of an `img` tag:

Expand All @@ -390,11 +410,11 @@ export default function svgResolverPlugin () {
},
load(id) {
if (id.endsWith('.svg')) {
const assetId = this.emitAsset(
const assetReferenceId = this.emitAsset(
path.basename(id),
fs.readFileSync(id)
);
return `export default import.meta.ROLLUP_ASSET_URL_${assetId};`;
return `export default import.meta.ROLLUP_ASSET_URL_${assetReferenceId};`;
}
}
});
Expand All @@ -407,6 +427,69 @@ image.src = logo;
document.body.appendChild(image);
```

### Chunk URLs

Similar to assets, emitted chunks can be referenced from within JS code via the `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` replacement.

The following example will detect imports prefixed with `register-paint-worklet:` and generate the necessary code and separate chunk to generate a CSS paint worklet. Note that this will only work in modern browsers and will only work if the output format is set to `esm`.

```js
// plugin
const REGISTER_WORKLET = 'register-paint-worklet:';
export default function paintWorkletPlugin () {
return ({
load(id) {
if (id.startsWith(REGISTER_WORKLET)) {
return `CSS.paintWorklet.addModule(import.meta.ROLLUP_CHUNK_URL_${this.emitChunk(
id.slice(REGISTER_WORKLET.length)
)});`;
}
},
resolveId(id, importee) {
// We remove the prefix, resolve everything to absolute ids and add the prefix again
// This makes sure that you can use relative imports to define worklets
if (id.startsWith(REGISTER_WORKLET)) {
return this.resolveId(id.slice(REGISTER_WORKLET.length), importee).then(
id => REGISTER_WORKLET + id
);
}
return null;
}
});
}
```

Usage:

```js
// main.js
import 'register-paint-worklet:./worklet.js';
import { color, size } from './config.js';
document.body.innerHTML += `<h1 style="background-image: paint(vertical-lines);">color: ${color}, size: ${size}</h1>`;

// worklet.js
import { color, size } from './config.js';
registerPaint(
'vertical-lines',
class {
paint(ctx, geom) {
for (let x = 0; x < geom.width / size; x++) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.rect(x * size, 0, 2, geom.height);
ctx.fill();
}
}
}
);

// config.js
export const color = 'greenyellow';
export const size = 6;
```

If you build this code, both the main chunk and the worklet will share the code from `config.js` via a shared chunk. This enables us to make use of the browser cache to reduce transmitted data and speed up loading the worklet.

### Advanced Loaders

The `load` hook can optionally return a `{ code, ast }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node.
Expand Down
2 changes: 1 addition & 1 deletion docs/999-big-list-of-options.md
Expand Up @@ -322,7 +322,7 @@ The pattern to use for naming custom emitted assets to include in the build outp
* `[hash]`: A hash based on the name and content of the asset.
* `[name]`: The file name of the asset excluding any extension.

Forward slashes `/` can be used to place files in sub-directories. See also [`output.chunkFileNames`](guide/en#output-chunkfilenames), [`output.entryFileNames`](guide/en#output-entryfilenames).
Forward slashes `/` can be used to place files in sub-directories. See also `[`output.chunkFileNames`](guide/en#output-chunkfilenames)`, [`output.entryFileNames`](guide/en#output-entryfilenames).

#### output.banner/output.footer
Type: `string | (() => string | Promise<string>)`<br>
Expand Down
48 changes: 32 additions & 16 deletions src/Chunk.ts
Expand Up @@ -30,7 +30,7 @@ import { sortByExecutionOrder } from './utils/executionOrder';
import getIndentString from './utils/getIndentString';
import { makeLegal } from './utils/identifierHelpers';
import { basename, dirname, isAbsolute, normalize, relative, resolve } from './utils/path';
import relativeId from './utils/relativeId';
import relativeId, { getAliasName } from './utils/relativeId';
import renderChunk from './utils/renderChunk';
import { RenderOptions } from './utils/renderHelpers';
import { makeUnique, renderNamePattern } from './utils/renderNamePattern';
Expand Down Expand Up @@ -107,6 +107,10 @@ function getGlobalName(
}
}

export function isChunkRendered(chunk: Chunk): boolean {
return !chunk.isEmpty || chunk.entryModules.length > 0 || chunk.manualChunkAlias !== null;
}

export default class Chunk {
entryModules: Module[] = [];
execIndex: number;
Expand All @@ -117,7 +121,7 @@ export default class Chunk {
id: string = undefined;
indentString: string = undefined;
isEmpty: boolean;
isManualChunk: boolean = false;
manualChunkAlias: string | null = null;
orderedModules: Module[];
renderedModules: {
[moduleId: string]: RenderedModule;
Expand Down Expand Up @@ -152,8 +156,8 @@ export default class Chunk {
if (this.isEmpty && module.isIncluded()) {
this.isEmpty = false;
}
if (module.chunkAlias) {
this.isManualChunk = true;
if (module.manualChunkAlias) {
this.manualChunkAlias = module.manualChunkAlias;
}
module.chunk = this;
if (
Expand All @@ -164,14 +168,16 @@ export default class Chunk {
}
}

if (this.entryModules.length > 0) {
const entryModule = this.entryModules[0];
if (entryModule) {
this.variableName = makeLegal(
basename(
this.entryModules.map(module => module.chunkAlias).find(Boolean) ||
this.entryModules[0].id
entryModule.chunkAlias || entryModule.manualChunkAlias || getAliasName(entryModule.id)
)
);
} else this.variableName = '__chunk_' + ++graph.curChunkIndex;
} else {
this.variableName = '__chunk_' + ++graph.curChunkIndex;
}
}

generateEntryExportsOrMarkAsTainted() {
Expand All @@ -187,6 +193,13 @@ export default class Chunk {
const exposedVariables = Array.from(this.exports);
checkNextEntryModule: for (const { map, module } of exportVariableMaps) {
if (!this.graph.preserveModules) {
if (
this.manualChunkAlias &&
module.chunkAlias &&
this.manualChunkAlias !== module.chunkAlias
) {
continue checkNextEntryModule;
}
for (const exposedVariable of exposedVariables) {
if (!map.has(exposedVariable)) {
continue checkNextEntryModule;
Expand Down Expand Up @@ -590,7 +603,7 @@ export default class Chunk {
renderedDependency.id = relPath;
}

this.finaliseDynamicImports();
this.finaliseDynamicImports(options.format);
const needsAmdModule = this.finaliseImportMetas(options);

const hasExports =
Expand Down Expand Up @@ -748,8 +761,11 @@ export default class Chunk {
}

private computeChunkName(): string {
if (this.facadeModule !== null && this.facadeModule.chunkAlias) {
return sanitizeFileName(this.facadeModule.chunkAlias);
if (this.manualChunkAlias) {
return sanitizeFileName(this.manualChunkAlias);
}
if (this.facadeModule !== null) {
return sanitizeFileName(this.facadeModule.chunkAlias || getAliasName(this.facadeModule.id));
}
for (const module of this.orderedModules) {
if (module.chunkAlias) return sanitizeFileName(module.chunkAlias);
Expand All @@ -772,23 +788,23 @@ export default class Chunk {
return hash.digest('hex').substr(0, 8);
}

private finaliseDynamicImports() {
private finaliseDynamicImports(format: string) {
for (let i = 0; i < this.orderedModules.length; i++) {
const module = this.orderedModules[i];
const code = this.renderedModuleSources[i];
for (const { node, resolution } of module.dynamicImports) {
if (!resolution) continue;
if (resolution instanceof Module) {
if (!resolution.chunk.isEmpty && resolution.chunk !== this) {
if (resolution.chunk !== this && isChunkRendered(resolution.chunk)) {
const resolutionChunk = resolution.facadeChunk || resolution.chunk;
let relPath = normalize(relative(dirname(this.id), resolutionChunk.id));
if (!relPath.startsWith('../')) relPath = './' + relPath;
node.renderFinalResolution(code, `'${relPath}'`);
node.renderFinalResolution(code, `'${relPath}'`, format);
}
} else if (resolution instanceof ExternalModule) {
node.renderFinalResolution(code, `'${resolution.id}'`);
node.renderFinalResolution(code, `'${resolution.id}'`, format);
} else {
node.renderFinalResolution(code, resolution);
node.renderFinalResolution(code, resolution, format);
}
}
}
Expand Down

0 comments on commit 18829da

Please sign in to comment.