Skip to content

Commit

Permalink
Improve and fix asset emission (#2796)
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

* Switch to using a plugin hook

* Extract more common code

* Fix, refactor and test asset emission

* Allow asset URLs to be configured

* Move functionality into default plugin

* Improve SystemJS handling
  • Loading branch information
lukastaegert committed Apr 11, 2019
1 parent 2ae0811 commit 9ffe6f5
Show file tree
Hide file tree
Showing 94 changed files with 802 additions and 93 deletions.
55 changes: 51 additions & 4 deletions docs/05-plugins.md
Expand Up @@ -179,6 +179,31 @@ 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>
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.

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.
- `format`: The rendered output format.

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:

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

#### `resolveDynamicImport`
Type: `(specifier: string | ESTree.Node, importer: string) => string | false | null`<br>
Kind: `async, first`
Expand Down Expand Up @@ -348,14 +373,36 @@ 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. The following example represents emitting a CSS file for a module that then exports a URL that is constructed to correctly point to the emitted file from the target runtime environment.
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.

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:

```js
load (id) {
const assetId = this.emitAsset('style.css', fs.readFileSync(path.resolve(assets, 'style.css')));
return `export default import.meta.ROLLUP_ASSET_URL_${assetId}`;
// plugin
export default function svgResolverPlugin () {
return ({
resolveId(id, importee) {
if (id.endsWith('.svg')) {
return path.resolve(path.dirname(importee), id);
}
},
load(id) {
if (id.endsWith('.svg')) {
const assetId = this.emitAsset(
path.basename(id),
fs.readFileSync(id)
);
return `export default import.meta.ROLLUP_ASSET_URL_${assetId};`;
}
}
});
}

// usage
import logo from '../images/logo.svg';
const image = document.createElement('img');
image.src = logo;
document.body.appendChild(image);
```

### Advanced Loaders
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -59,7 +59,7 @@
"homepage": "https://github.com/rollup/rollup",
"dependencies": {
"@types/estree": "0.0.39",
"@types/node": "^11.13.2",
"@types/node": "^11.13.4",
"acorn": "^6.1.1"
},
"devDependencies": {
Expand Down Expand Up @@ -98,12 +98,12 @@
"remap-istanbul": "^0.13.0",
"require-relative": "^0.8.7",
"requirejs": "^2.3.6",
"rollup": "^1.9.1",
"rollup": "^1.9.3",
"rollup-plugin-alias": "^1.5.1",
"rollup-plugin-buble": "^0.19.6",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.2",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^4.0.4",
Expand All @@ -115,7 +115,7 @@
"source-map": "^0.6.1",
"source-map-support": "^0.5.12",
"sourcemap-codec": "^1.4.4",
"systemjs": "^3.1.0",
"systemjs": "^3.1.1",
"terser": "^3.17.0",
"tslib": "^1.9.3",
"tslint": "^5.15.0",
Expand Down
6 changes: 3 additions & 3 deletions src/Chunk.ts
Expand Up @@ -795,19 +795,19 @@ export default class Chunk {
}

private finaliseImportMetas(options: OutputOptions): boolean {
let usesMechanism = false;
let needsAmdModule = false;
for (let i = 0; i < this.orderedModules.length; i++) {
const module = this.orderedModules[i];
const code = this.renderedModuleSources[i];
for (const importMeta of module.importMetas) {
if (
importMeta.renderFinalMechanism(code, this.id, options.format, this.graph.pluginDriver)
) {
usesMechanism = true;
needsAmdModule = true;
}
}
}
return usesMechanism;
return needsAmdModule;
}

private getChunkDependencyDeclarations(
Expand Down
4 changes: 2 additions & 2 deletions src/Module.ts
Expand Up @@ -499,8 +499,8 @@ export default class Module {

this.resolvedIds = resolvedIds || Object.create(null);

// By default, `id` is the filename. Custom resolvers and loaders
// can change that, but it makes sense to use it for the source filename
// By default, `id` is the file name. Custom resolvers and loaders
// can change that, but it makes sense to use it for the source file name
const fileName = this.id;

this.magicString = new MagicString(code, {
Expand Down
38 changes: 12 additions & 26 deletions src/ast/nodes/MetaProperty.ts
Expand Up @@ -6,30 +6,6 @@ import MemberExpression from './MemberExpression';
import * as NodeType from './NodeType';
import { NodeBase } from './shared/Node';

const getResolveUrl = (path: string, URL: string = 'URL') => `new ${URL}(${path}).href`;

const amdModuleUrl = `(typeof process !== 'undefined' && process.versions && process.versions.node ? 'file:' : '') + module.uri`;

const globalRelUrlMechanism = (relPath: string) => {
return getResolveUrl(
`(typeof document !== 'undefined' ? document.currentScript && document.currentScript.src || document.baseURI : 'file:' + __filename) + '/../${relPath}'`,
`(typeof URL !== 'undefined' ? URL : require('ur'+'l').URL)`
);
};

const relUrlMechanisms: Record<string, (relPath: string) => string> = {
amd: (relPath: string) => getResolveUrl(`${amdModuleUrl} + '/../${relPath}'`),
cjs: (relPath: string) =>
getResolveUrl(
`(process.browser ? '' : 'file:') + __dirname + '/${relPath}', process.browser && document.baseURI`,
`(typeof URL !== 'undefined' ? URL : require('ur'+'l').URL)`
),
es: (relPath: string) => getResolveUrl(`'../${relPath}', import.meta.url`),
iife: globalRelUrlMechanism,
system: (relPath: string) => getResolveUrl(`'../${relPath}', module.url`),
umd: globalRelUrlMechanism
};

export default class MetaProperty extends NodeBase {
meta: Identifier;
property: Identifier;
Expand Down Expand Up @@ -58,11 +34,21 @@ export default class MetaProperty extends NodeBase {
// support import.meta.ROLLUP_ASSET_URL_[ID]
if (importMetaProperty && importMetaProperty.startsWith('ROLLUP_ASSET_URL_')) {
const assetFileName = this.context.getAssetFileName(importMetaProperty.substr(17));
const relPath = normalize(relative(dirname(chunkId), assetFileName));
const relativeAssetPath = normalize(relative(dirname(chunkId), assetFileName));
const replacement = pluginDriver.hookFirstSync<string>('resolveAssetUrl', [
{
assetFileName,
chunkId,
format,
moduleId: this.context.module.id,
relativeAssetPath
}
]);

code.overwrite(
(parent as MemberExpression).start,
(parent as MemberExpression).end,
relUrlMechanisms[format](relPath)
replacement
);
return true;
}
Expand Down
6 changes: 3 additions & 3 deletions src/rollup/index.ts
Expand Up @@ -382,7 +382,7 @@ function writeOutputFile(
outputFile: OutputAsset | OutputChunk,
outputOptions: OutputOptions
): Promise<void> {
const filename = resolve(outputOptions.dir || dirname(outputOptions.file), outputFile.fileName);
const fileName = resolve(outputOptions.dir || dirname(outputOptions.file), outputFile.fileName);
let writeSourceMapPromise: Promise<void>;
let source: string | Buffer;
if (isOutputAsset(outputFile)) {
Expand All @@ -395,13 +395,13 @@ function writeOutputFile(
url = outputFile.map.toUrl();
} else {
url = `${basename(outputFile.fileName)}.map`;
writeSourceMapPromise = writeFile(`${filename}.map`, outputFile.map.toString());
writeSourceMapPromise = writeFile(`${fileName}.map`, outputFile.map.toString());
}
source += `//# ${SOURCEMAPPING_URL}=${url}\n`;
}
}

return writeFile(filename, source)
return writeFile(fileName, source)
.then(() => writeSourceMapPromise)
.then(
() =>
Expand Down
13 changes: 13 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -197,6 +197,17 @@ export type ResolveImportMetaHook = (
options: { chunkId: string; format: string; moduleId: string }
) => string | void;

export type ResolveAssetUrlHook = (
this: PluginContext,
options: {
assetFileName: string;
chunkId: string;
format: string;
moduleId: string;
relativeAssetPath: string;
}
) => string | void;

export type AddonHook = string | ((this: PluginContext) => string | Promise<string>);

/**
Expand Down Expand Up @@ -248,6 +259,7 @@ export interface Plugin {
renderChunk?: RenderChunkHook;
renderError?: (this: PluginContext, err?: Error) => Promise<void> | void;
renderStart?: (this: PluginContext) => Promise<void> | void;
resolveAssetUrl?: ResolveAssetUrlHook;
resolveDynamicImport?: ResolveDynamicImportHook;
resolveId?: ResolveIdHook;
resolveImportMeta?: ResolveImportMetaHook;
Expand Down Expand Up @@ -331,6 +343,7 @@ export interface OutputOptions {
format?: ModuleFormat;
freeze?: boolean;
globals?: GlobalsOption;
importMetaUrl?: (chunkId: string, moduleId: string) => string;
indent?: boolean;
interop?: boolean;
intro?: string | (() => string | Promise<string>);
Expand Down

0 comments on commit 9ffe6f5

Please sign in to comment.