Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dynamicImportFunction option #2723

Merged
1 change: 1 addition & 0 deletions bin/src/help.md
Expand Up @@ -51,6 +51,7 @@ Basic options:
--no-treeshake.annotations Ignore pure call annotations
--no-treeshake.propertyReadSideEffects Ignore property access side-effects
--treeshake.pureExternalModules Assume side-effect free externals
--dynamicImportFunction <name> Rename the dynamic `import()` function

Examples:

Expand Down
7 changes: 7 additions & 0 deletions docs/999-big-list-of-options.md
Expand Up @@ -667,6 +667,13 @@ Default: `true`

Whether to include the 'use strict' pragma at the top of generated non-ESM bundles. Strictly-speaking, ES modules are *always* in strict mode, so you shouldn't disable this without good reason.

#### output.dynamicImportFunction
Type: `string`<br>
CLI: `--dynamicImportFunction <name>`<br>
Default: `import`

This will rename the dynamic import function to the chosen name when outputting ESM bundles. This is useful for generating code that uses a dynamic import polyfill such as [this one](https://github.com/uupaa/dynamic-import-polyfill).

#### preserveSymlinks
Type: `boolean`<br>
CLI: `--preserveSymlinks`<br>
Expand Down
9 changes: 8 additions & 1 deletion src/Chunk.ts
Expand Up @@ -435,6 +435,7 @@ export default class Chunk {

const renderOptions: RenderOptions = {
compact: options.compact,
dynamicImportFunction: options.dynamicImportFunction,
format: options.format,
freeze: options.freeze !== false,
indent: this.indentString,
Expand Down Expand Up @@ -545,7 +546,13 @@ export default class Chunk {
code: 'INVALID_OPTION',
message: `Invalid format: ${options.format} - valid options are ${Object.keys(
finalisers
).join(', ')}`
).join(', ')}.`
});
}
if (options.dynamicImportFunction && options.format !== 'es') {
this.graph.warn({
code: 'INVALID_OPTION',
message: '"output.dynamicImportFunction" is ignored for formats other than "esm".'
});
}

Expand Down
19 changes: 12 additions & 7 deletions src/ast/nodes/Import.ts
Expand Up @@ -11,10 +11,10 @@ interface DynamicImportMechanism {
right: string;
}

const getDynamicImportMechanism = (format: string, compact: boolean): DynamicImportMechanism => {
switch (format) {
const getDynamicImportMechanism = (options: RenderOptions): DynamicImportMechanism => {
switch (options.format) {
case 'cjs': {
const _ = compact ? '' : ' ';
const _ = options.compact ? '' : ' ';
return {
interopLeft: `Promise.resolve({${_}default:${_}require(`,
interopRight: `)${_}})`,
Expand All @@ -23,9 +23,9 @@ const getDynamicImportMechanism = (format: string, compact: boolean): DynamicImp
};
}
case 'amd': {
const _ = compact ? '' : ' ';
const resolve = compact ? 'c' : 'resolve';
const reject = compact ? 'e' : 'reject';
const _ = options.compact ? '' : ' ';
const resolve = options.compact ? 'c' : 'resolve';
const reject = options.compact ? 'e' : 'reject';
return {
interopLeft: `new Promise(function${_}(${resolve},${_}${reject})${_}{${_}require([`,
interopRight: `],${_}function${_}(m)${_}{${_}${resolve}({${_}default:${_}m${_}})${_}},${_}${reject})${_}})`,
Expand All @@ -38,6 +38,11 @@ const getDynamicImportMechanism = (format: string, compact: boolean): DynamicImp
left: 'module.import(',
right: ')'
};
case 'es':
return {
left: `${options.dynamicImportFunction || 'import'}(`,
right: ')'
};
}
};

Expand Down Expand Up @@ -72,7 +77,7 @@ export default class Import extends NodeBase {
return;
}

const importMechanism = getDynamicImportMechanism(options.format, options.compact);
const importMechanism = getDynamicImportMechanism(options);
if (importMechanism) {
const leftMechanism =
(this.resolutionInterop && importMechanism.interopLeft) || importMechanism.left;
Expand Down
2 changes: 1 addition & 1 deletion src/finalisers/iife.ts
Expand Up @@ -50,7 +50,7 @@ export default function iife(
if (hasExports && !name) {
error({
code: 'INVALID_OPTION',
message: `You must supply output.name for IIFE bundles`
message: `You must supply "output.name" for IIFE bundles.`
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/finalisers/umd.ts
Expand Up @@ -45,7 +45,7 @@ export default function umd(
if (hasExports && !options.name) {
error({
code: 'INVALID_OPTION',
message: 'You must supply output.name for UMD bundles'
message: 'You must supply "output.name" for UMD bundles.'
});
}

Expand Down
33 changes: 17 additions & 16 deletions src/rollup/index.ts
Expand Up @@ -28,14 +28,14 @@ import {
function checkOutputOptions(options: OutputOptions) {
if (<string>options.format === 'es6') {
error({
message: 'The `es6` output format is deprecated – use `esm` instead',
message: 'The "es6" output format is deprecated – use "esm" instead',
url: `https://rollupjs.org/guide/en#output-format`
});
}

if (!options.format) {
error({
message: `You must specify output.format, which can be one of 'amd', 'cjs', 'system', 'esm', 'iife' or 'umd'`,
message: `You must specify "output.format", which can be one of "amd", "cjs", "system", "esm", "iife" or "umd".`,
url: `https://rollupjs.org/guide/en#output-format`
});
}
Expand Down Expand Up @@ -88,37 +88,37 @@ function getInputOptions(rawInputOptions: GenericConfigObject): any {
if (inputOptions.preserveModules)
error({
code: 'INVALID_OPTION',
message: `preserveModules does not support the inlineDynamicImports option.`
message: `"preserveModules" does not support the "inlineDynamicImports" option.`
});
if (inputOptions.manualChunks)
error({
code: 'INVALID_OPTION',
message: '"manualChunks" option is not supported for inlineDynamicImports.'
message: '"manualChunks" option is not supported for "inlineDynamicImports".'
});

if (inputOptions.experimentalOptimizeChunks)
error({
code: 'INVALID_OPTION',
message: '"experimentalOptimizeChunks" option is not supported for inlineDynamicImports.'
message: '"experimentalOptimizeChunks" option is not supported for "inlineDynamicImports".'
});
if (
(inputOptions.input instanceof Array && inputOptions.input.length > 1) ||
(typeof inputOptions.input === 'object' && Object.keys(inputOptions.input).length > 1)
)
error({
code: 'INVALID_OPTION',
message: 'Multiple inputs are not supported for inlineDynamicImports.'
message: 'Multiple inputs are not supported for "inlineDynamicImports".'
});
} else if (inputOptions.preserveModules) {
if (inputOptions.manualChunks)
error({
code: 'INVALID_OPTION',
message: 'preserveModules does not support the manualChunks option.'
message: '"preserveModules" does not support the "manualChunks" option.'
});
if (inputOptions.experimentalOptimizeChunks)
error({
code: 'INVALID_OPTION',
message: 'preserveModules does not support the experimentalOptimizeChunks option.'
message: '"preserveModules" does not support the "experimentalOptimizeChunks" option.'
});
}

Expand Down Expand Up @@ -290,7 +290,7 @@ export default function rollup(rawInputOptions: GenericConfigObject): Promise<Ro
if (!outputOptions || (!outputOptions.dir && !outputOptions.file)) {
error({
code: 'MISSING_OPTION',
message: 'You must specify output.file or output.dir for the build.'
message: 'You must specify "output.file" or "output.dir" for the build.'
});
}
return generate(outputOptions, true).then(bundle => {
Expand All @@ -305,17 +305,17 @@ export default function rollup(rawInputOptions: GenericConfigObject): Promise<Ro
if (outputOptions.sourcemapFile)
error({
code: 'INVALID_OPTION',
message: '"sourcemapFile" is only supported for single-file builds.'
message: '"output.sourcemapFile" is only supported for single-file builds.'
});
if (typeof outputOptions.file === 'string')
error({
code: 'INVALID_OPTION',
message:
'When building multiple chunks, the output.dir option must be used, not output.file.' +
'When building multiple chunks, the "output.dir" option must be used, not "output.file".' +
(typeof inputOptions.input !== 'string' ||
inputOptions.inlineDynamicImports === true
? ''
: ' To inline dynamic imports set the inlineDynamicImports: true option.')
: ' To inline dynamic imports, set the "inlineDynamicImports" option.')
});
}
return Promise.all(
Expand Down Expand Up @@ -436,19 +436,19 @@ function normalizeOutputOptions(
error({
code: 'INVALID_OPTION',
message:
'You must set either output.file for a single-file build or output.dir when generating multiple chunks.'
'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.'
});
if (inputOptions.preserveModules) {
error({
code: 'INVALID_OPTION',
message:
'You must set output.dir instead of output.file when using the preserveModules option.'
'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.'
});
}
if (typeof inputOptions.input === 'object' && !Array.isArray(inputOptions.input))
error({
code: 'INVALID_OPTION',
message: 'You must set output.dir instead of output.file when providing named inputs.'
message: 'You must set "output.dir" instead of "output.file" when providing named inputs.'
});
}

Expand All @@ -461,7 +461,8 @@ function normalizeOutputOptions(
if (typeof outputOptions.file === 'string')
error({
code: 'INVALID_OPTION',
message: 'You must set output.dir instead of output.file when generating multiple chunks.'
message:
'You must set "output.dir" instead of "output.file" when generating multiple chunks.'
});
}

Expand Down
1 change: 1 addition & 0 deletions src/rollup/types.d.ts
Expand Up @@ -294,6 +294,7 @@ export interface OutputOptions {
compact?: boolean;
// only required for bundle.write
dir?: string;
dynamicImportFunction?: string;
entryFileNames?: string;
esModule?: boolean;
exports?: 'default' | 'named' | 'none' | 'auto';
Expand Down
1 change: 1 addition & 0 deletions src/utils/mergeOptions.ts
Expand Up @@ -238,6 +238,7 @@ function getOutputOptions(
chunkFileNames: getOption('chunkFileNames'),
compact: getOption('compact', false),
dir: getOption('dir'),
dynamicImportFunction: getOption('dynamicImportFunction'),
entryFileNames: getOption('entryFileNames'),
esModule: getOption('esModule', true),
exports: getOption('exports'),
Expand Down
1 change: 1 addition & 0 deletions src/utils/renderHelpers.ts
Expand Up @@ -4,6 +4,7 @@ import { treeshakeNode } from './treeshakeNode';

export interface RenderOptions {
compact: boolean;
dynamicImportFunction: string;
format: string;
freeze: boolean;
indent: string;
Expand Down
15 changes: 15 additions & 0 deletions test/chunking-form/samples/dynamic-import-name/_config.js
@@ -0,0 +1,15 @@
module.exports = {
description: 'allows specifying a custom importer function',
options: {
input: 'main.js',
onwarn() {},
plugins: {
resolveDynamicImport() {
return false;
}
},
output: {
dynamicImportFunction: 'foobar'
}
}
};
@@ -0,0 +1,5 @@
define(['require'], function (require) { 'use strict';

new Promise(function (resolve, reject) { require(['./foo.js'], resolve, reject) }).then(result => console.log(result));

});
@@ -0,0 +1,3 @@
'use strict';

Promise.resolve(require('./foo.js')).then(result => console.log(result));
@@ -0,0 +1 @@
foobar('./foo.js').then(result => console.log(result));
@@ -0,0 +1,10 @@
System.register([], function (exports, module) {
'use strict';
return {
execute: function () {

module.import('./foo.js').then(result => console.log(result));

}
};
});
1 change: 1 addition & 0 deletions test/chunking-form/samples/dynamic-import-name/main.js
@@ -0,0 +1 @@
import('./foo.js').then(result => console.log(result));
32 changes: 32 additions & 0 deletions test/function/samples/dynamic-import-name-warn/_config.js
@@ -0,0 +1,32 @@
const assert = require('assert');

module.exports = {
description: 'warns when specifying a custom importer function for formats other than "esm"',
context: {
require(path) {
assert.equal(path, './foo.js');
return 42;
}
},
options: {
input: 'main.js',
plugins: {
resolveDynamicImport() {
return false;
}
},
output: {
dynamicImportFunction: 'myImporter',
format: 'cjs'
}
},
exports(exports) {
return exports.fromFoo.then(value => assert.strictEqual(value, 42));
},
warnings: [
{
code: 'INVALID_OPTION',
message: '"output.dynamicImportFunction" is ignored for formats other than "esm".'
}
]
};
@@ -0,0 +1,5 @@
define(['require'], function (require) { 'use strict';

new Promise(function (resolve, reject) { require(['./foo.js'], resolve, reject) });

});
@@ -0,0 +1,3 @@
'use strict';

Promise.resolve(require('./foo.js'));
@@ -0,0 +1 @@
foobar('./foo.js');
@@ -0,0 +1,10 @@
System.register([], function (exports, module) {
'use strict';
return {
execute: function () {

module.import('./foo.js');

}
};
});
1 change: 1 addition & 0 deletions test/function/samples/dynamic-import-name-warn/main.js
@@ -0,0 +1 @@
export const fromFoo = import('./foo.js');
27 changes: 27 additions & 0 deletions test/function/samples/dynamic-import-name/_config.js
@@ -0,0 +1,27 @@
const assert = require('assert');
let imported = false;

module.exports = {
description: 'allows specifying a custom importer function',
context: {
myImporter(path) {
assert.equal(path, './foo.js');
imported = true;
}
},
options: {
input: 'main.js',
plugins: {
resolveDynamicImport() {
return false;
}
},
output: {
dynamicImportFunction: 'myImporter',
format: 'esm'
}
},
exports() {
assert.ok(imported);
}
};
@@ -0,0 +1,5 @@
define(['require'], function (require) { 'use strict';

new Promise(function (resolve, reject) { require(['./foo.js'], resolve, reject) });

});
@@ -0,0 +1,3 @@
'use strict';

Promise.resolve(require('./foo.js'));
@@ -0,0 +1 @@
foobar('./foo.js');