Skip to content

Commit

Permalink
Refactor handling of import.meta.url and add option to configure beha…
Browse files Browse the repository at this point in the history
…viour (#2785)

* Note topics to fix

* Merge compact and non-compact import.meta.url mechanisms

* Extract more common code

* Refactor import.meta.url handling

* Add option to configure import.meta.url resolution

* Fix dependencies again

* Fix windows tests

* Switch to using a plugin hook

* Add format to parameters and move functionality into default plugin

* Generalize hook to handle all occurrences of `import.meta`
  • Loading branch information
lukastaegert committed Apr 11, 2019
1 parent 516a06d commit 2ae0811
Show file tree
Hide file tree
Showing 62 changed files with 624 additions and 127 deletions.
4 changes: 2 additions & 2 deletions bin/src/index.ts
Expand Up @@ -9,9 +9,9 @@ const command = minimist(process.argv.slice(2), {
});

if (command.help || (process.argv.length <= 2 && process.stdin.isTTY)) {
console.log(`\n${help.replace('__VERSION__', version)}\n`); // eslint-disable-line no-console
console.log(`\n${help.replace('__VERSION__', version)}\n`);
} else if (command.version) {
console.log(`rollup v${version}`); // eslint-disable-line no-console
console.log(`rollup v${version}`);
} else {
try {
require('source-map-support').install();
Expand Down
2 changes: 1 addition & 1 deletion bin/src/logging.ts
Expand Up @@ -3,7 +3,7 @@ import { RollupError } from '../../src/rollup/types';
import relativeId from '../../src/utils/relativeId';

// log to stderr to keep `rollup main.js > bundle.js` from breaking
export const stderr = console.error.bind(console); // eslint-disable-line no-console
export const stderr = console.error.bind(console);

export function handleError(err: RollupError, recover = false) {
let description = err.message || err;
Expand Down
30 changes: 26 additions & 4 deletions docs/05-plugins.md
Expand Up @@ -195,13 +195,35 @@ If you return an object, then it is possible to resolve an import to a different

```js
resolveId(id) {
if (id === 'my-dependency') {
return {id: 'my-dependency-develop', external: true};
}
return null;
if (id === 'my-dependency') {
return {id: 'my-dependency-develop', external: true};
}
return null;
}
```

#### `resolveImportMeta`
Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`<br>
Kind: `sync, first`

Allows to customize how Rollup handles `import.meta` and `import.meta.someProperty`, in particular `import.meta.url`. In ES modules, `import.meta` is an object and `import.meta.url` contains the URL of the current module, e.g. `http://server.net/bundle.js` for browsers or `file:///path/to/bundle.js` in Node.

By default for formats other than ES modules, Rollup replaces `import.meta.url` with code that attempts to match this behaviour by returning the dynamic URL of the current chunk. Note that all formats except CommonJS and UMD assume that they run in a browser environment where `URL` and `document` are available. For other properties, `import.meta.someProperty` is replaced with `undefined` while `import.meta` is replaced with an object containing a `url` property.

This behaviour can be changed—also for ES modules—via this hook. For each occurrence of `import.meta<.someProperty>`, this hook is called with the name of the property or `null` if `import.meta` is accessed directly. For example, the following code will resolve `import.meta.url` using the relative path of the original module to the current working directory and again resolve this path against the base URL of the current document at runtime:

```javascript
// rollup.config.js
resolveImportMeta(property, {moduleId}) {
if (property === 'url') {
return `new URL('${path.relative(process.cwd(), moduleId)}', document.baseURI).href`;
}
return null;
}
```

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.

#### `transform`
Type: `(code: string, id: string) => string | { code: string, map?: string | SourceMap, ast? : ESTree.Program } | null`
<br>
Expand Down
18 changes: 15 additions & 3 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -97,6 +97,7 @@
"pretty-ms": "^4.0.0",
"remap-istanbul": "^0.13.0",
"require-relative": "^0.8.7",
"requirejs": "^2.3.6",
"rollup": "^1.9.1",
"rollup-plugin-alias": "^1.5.1",
"rollup-plugin-buble": "^0.19.6",
Expand All @@ -114,6 +115,7 @@
"source-map": "^0.6.1",
"source-map-support": "^0.5.12",
"sourcemap-codec": "^1.4.4",
"systemjs": "^3.1.0",
"terser": "^3.17.0",
"tslib": "^1.9.3",
"tslint": "^5.15.0",
Expand Down
5 changes: 4 additions & 1 deletion src/Chunk.ts
Expand Up @@ -800,8 +800,11 @@ export default class Chunk {
const module = this.orderedModules[i];
const code = this.renderedModuleSources[i];
for (const importMeta of module.importMetas) {
if (importMeta.renderFinalMechanism(code, this.id, options.format, options.compact))
if (
importMeta.renderFinalMechanism(code, this.id, options.format, this.graph.pluginDriver)
) {
usesMechanism = true;
}
}
}
return usesMechanism;
Expand Down
2 changes: 1 addition & 1 deletion src/Graph.ts
Expand Up @@ -41,7 +41,7 @@ function makeOnwarn() {
return (warning: any) => {
const str = warning.toString();
if (str in warned) return;
console.error(str); //eslint-disable-line no-console
console.error(str);
warned[str] = true;
};
}
Expand Down
109 changes: 44 additions & 65 deletions src/ast/nodes/MetaProperty.ts
@@ -1,109 +1,88 @@
import MagicString from 'magic-string';
import { dirname, normalize, relative } from '../../utils/path';
import { RenderOptions } from '../../utils/renderHelpers';
import { PluginDriver } from '../../utils/pluginDriver';
import Identifier from './Identifier';
import Literal from './Literal';
import MemberExpression from './MemberExpression';
import * as NodeType from './NodeType';
import { NodeBase } from './shared/Node';

const globalImportMetaUrlMechanism = `(typeof document !== 'undefined' ? document.currentScript && document.currentScript.src || document.baseURI : new (typeof URL !== 'undefined' ? URL : require('ur'+'l').URL)('file:' + __filename).href)`;
const importMetaUrlMechanisms: Record<string, string> = {
amd: `new URL((typeof process !== 'undefined' && process.versions && process.versions.node ? 'file:' : '') + module.uri).href`,
cjs: `new (typeof URL !== 'undefined' ? URL : require('ur'+'l').URL)((process.browser ? '' : 'file:') + __filename, process.browser && document.baseURI).href`,
iife: globalImportMetaUrlMechanism,
umd: globalImportMetaUrlMechanism
};
const getResolveUrl = (path: string, URL: string = 'URL') => `new ${URL}(${path}).href`;

const globalImportMetaUrlMechanismCompact = `(typeof document!=='undefined'?document.currentScript&&document.currentScript.src||document.baseURI:new(typeof URL!=='undefined'?URL:require('ur'+'l').URL)('file:'+__filename).href)`;
const importMetaUrlMechanismsCompact: Record<string, string> = {
amd: `new URL((typeof process!=='undefined'&&process.versions&&process.versions.node?'file:':'')+module.uri).href`,
cjs: `new(typeof URL!=='undefined'?URL:require('ur'+'l').URL)((process.browser?'':'file:')+__filename,process.browser&&document.baseURI).href`,
iife: globalImportMetaUrlMechanismCompact,
umd: globalImportMetaUrlMechanismCompact
};
const amdModuleUrl = `(typeof process !== 'undefined' && process.versions && process.versions.node ? 'file:' : '') + module.uri`;

const globalRelUrlMechanism = (relPath: string, compact: boolean) => {
const _ = compact ? '' : ' ';
return `new${_}(typeof URL${_}!==${_}'undefined'${_}?${_}URL${_}:${_}require('ur'+'l').URL)((typeof document${_}!==${_}'undefined'${_}?${_}document.currentScript${_}&&${_}document.currentScript.src${_}||${_}document.baseURI${_}:${_}'file:'${_}+${_}__filename)${_}+${_}'/../${relPath}').href`;
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, compact: boolean) => string> = {
amd: (relPath: string, compact: boolean) => {
const _ = compact ? '' : ' ';
return `new URL((typeof process${_}!==${_}'undefined'${_}&&${_}process.versions${_}&&${_}process.versions.node${_}?${_}'file:'${_}:${_}'')${_}+${_}module.uri${_}+${_}'/../${relPath}').href`;
},
cjs: (relPath: string, compact: boolean) => {
const _ = compact ? '' : ' ';
return `new${_}(typeof URL${_}!==${_}'undefined'${_}?${_}URL${_}:${_}require('ur'+'l').URL)((process.browser${_}?${_}''${_}:${_}'file:')${_}+${_}__dirname${_}+${_}'/${relPath}',${_}process.browser${_}&&${_}document.baseURI).href`;
},
es: (relPath: string, compact: boolean) => {
const _ = compact ? '' : ' ';
return `new URL('../${relPath}',${_}import.meta.url).href`;
},
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, compact: boolean) => {
const _ = compact ? '' : ' ';
return `new URL('../${relPath}',${_}module.url).href`;
},
system: (relPath: string) => getResolveUrl(`'../${relPath}', module.url`),
umd: globalRelUrlMechanism
};

export default class MetaProperty extends NodeBase {
meta: Identifier;
property: Identifier;
rendered: boolean;
type: NodeType.tMetaProperty;

initialise() {
if (this.meta.name === 'import') {
this.rendered = false;
this.context.addImportMeta(this);
}
this.included = false;
}

render(code: MagicString, options: RenderOptions) {
if (this.meta.name === 'import') this.rendered = true;
super.render(code, options);
}

renderFinalMechanism(
code: MagicString,
chunkId: string,
format: string,
compact: boolean
pluginDriver: PluginDriver
): boolean {
if (!this.rendered) return false;

if (this.parent instanceof MemberExpression === false) return false;

const parent = <MemberExpression>this.parent;

let importMetaProperty: string;
if (parent.property instanceof Identifier) importMetaProperty = parent.property.name;
else if (parent.property instanceof Literal && typeof parent.property.value === 'string')
importMetaProperty = parent.property.value;
else return false;
if (!this.included) return false;
const parent = this.parent;
const importMetaProperty =
parent instanceof MemberExpression && typeof parent.propertyKey === 'string'
? parent.propertyKey
: null;

// support import.meta.ROLLUP_ASSET_URL_[ID]
if (importMetaProperty.startsWith('ROLLUP_ASSET_URL_')) {
if (importMetaProperty && importMetaProperty.startsWith('ROLLUP_ASSET_URL_')) {
const assetFileName = this.context.getAssetFileName(importMetaProperty.substr(17));
const relPath = normalize(relative(dirname(chunkId), assetFileName));
code.overwrite(parent.start, parent.end, relUrlMechanisms[format](relPath, compact));
code.overwrite(
(parent as MemberExpression).start,
(parent as MemberExpression).end,
relUrlMechanisms[format](relPath)
);
return true;
}

if (format === 'system') {
code.overwrite(this.meta.start, this.meta.end, 'module');
} else if (importMetaProperty === 'url') {
const importMetaUrlMechanism = (compact
? importMetaUrlMechanismsCompact
: importMetaUrlMechanisms)[format];
if (importMetaUrlMechanism) code.overwrite(parent.start, parent.end, importMetaUrlMechanism);
const replacement = pluginDriver.hookFirstSync<string | void>('resolveImportMeta', [
importMetaProperty,
{
chunkId,
format,
moduleId: this.context.module.id
}
]);
if (typeof replacement === 'string') {
if (parent instanceof MemberExpression) {
code.overwrite(parent.start, parent.end, replacement);
} else {
code.overwrite(this.start, this.end, replacement);
}
return true;
}

return false;
}
}
7 changes: 7 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -191,6 +191,12 @@ export type ResolveDynamicImportHook = (
parentId: string
) => Promise<string | void> | string | void;

export type ResolveImportMetaHook = (
this: PluginContext,
prop: string | null,
options: { chunkId: string; format: string; moduleId: string }
) => string | void;

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

/**
Expand Down Expand Up @@ -244,6 +250,7 @@ export interface Plugin {
renderStart?: (this: PluginContext) => Promise<void> | void;
resolveDynamicImport?: ResolveDynamicImportHook;
resolveId?: ResolveIdHook;
resolveImportMeta?: ResolveImportMetaHook;
transform?: TransformHook;
/** @deprecated */
transformBundle?: TransformChunkHook;
Expand Down
29 changes: 28 additions & 1 deletion src/utils/defaultPlugin.ts
@@ -1,6 +1,6 @@
import { InputOptions, Plugin } from '../rollup/types';
import { error } from './error';
import { lstatSync, readdirSync, readFileSync, realpathSync } from './fs'; // eslint-disable-line
import { lstatSync, readdirSync, readFileSync, realpathSync } from './fs';
import { basename, dirname, isAbsolute, resolve } from './path';

export function getRollupDefaultPlugin(options: InputOptions): Plugin {
Expand All @@ -13,6 +13,12 @@ export function getRollupDefaultPlugin(options: InputOptions): Plugin {
resolveDynamicImport(specifier, parentId) {
if (typeof specifier === 'string' && !this.isExternal(specifier, parentId, false))
return <Promise<string>>this.resolveId(specifier, parentId);
},
resolveImportMeta(prop, { chunkId, format }) {
const mechanism = importMetaUrlMechanisms[format] && importMetaUrlMechanisms[format](chunkId);
if (mechanism) {
return prop === null ? `({ url: ${mechanism} })` : prop === 'url' ? mechanism : 'undefined';
}
}
};
}
Expand Down Expand Up @@ -67,3 +73,24 @@ function createResolveId(options: InputOptions) {
);
};
}

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

const getUrlFromDocument = (chunkId: string) =>
`(document.currentScript && document.currentScript.src || new URL('${chunkId}', document.baseURI).href)`;

const importMetaUrlMechanisms: Record<string, (chunkId: string) => string> = {
amd: () => getResolveUrl(`module.uri, document.baseURI`),
cjs: chunkId =>
`(typeof document === 'undefined' ? ${getResolveUrl(
`'file:' + __filename`,
`(require('u' + 'rl').URL)`
)} : ${getUrlFromDocument(chunkId)})`,
iife: chunkId => getUrlFromDocument(chunkId),
system: () => `module.meta.url`,
umd: chunkId =>
`(typeof document === 'undefined' ? ${getResolveUrl(
`'file:' + __filename`,
`(require('u' + 'rl').URL)`
)} : ${getUrlFromDocument(chunkId)})`
};
4 changes: 2 additions & 2 deletions src/utils/mergeOptions.ts
Expand Up @@ -39,9 +39,9 @@ const getObjectOption = (

const defaultOnWarn: WarningHandler = warning => {
if (typeof warning === 'string') {
console.warn(warning); // eslint-disable-line no-console
console.warn(warning);
} else {
console.warn(warning.message); // eslint-disable-line no-console
console.warn(warning.message);
}
};

Expand Down

0 comments on commit 2ae0811

Please sign in to comment.