Skip to content

Commit

Permalink
Add flag to preserve dynamic import (#727)
Browse files Browse the repository at this point in the history
Progress toward #726

This PR adds a new `preserveDynamicImport` flag that, when enabled, changes the
`imports` transform so that only static `import`s are transformed, while dynamic
`import()` expressions are passed through as-is.

This behavior matches what TypeScript does when handling a `.cjs` file when
`module: nodenext` is specified, and ts-node expects transpilers to handle this
mode, so this PR is a necessary step toward Sucrase as a ts-node plugin.

This mode also seems like the best interpretation of `import()` in new code
these days; if `import()` is transpiled, then it's impossible to access ESM
code from CJS in Node, so leaving it alone allows a mechanism to import
ESM-only libraries. My tentative plan is to change this mode to be the default
behavior in an upcoming semver-major release.
  • Loading branch information
alangpierce committed Jul 25, 2022
1 parent 5268e6c commit 8a5fdf8
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 2 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ transforms are available:
* **flow**: Removes Flow type annotations. Does not check types.
* **imports**: Transforms ES Modules (`import`/`export`) to CommonJS
(`require`/`module.exports`) using the same approach as Babel and TypeScript
with `--esModuleInterop`. Also includes dynamic `import`.
with `--esModuleInterop`. If `preserveDynamicImport` is specified in the Sucrase
options, then dynamic `import` expressions are left alone, which is particularly
useful in Node to load ESM-only libraries. If `preserveDynamicImport` is not
specified, `import` expressions are transformed into a promise-wrapped call to
`require`.
* **react-hot-loader**: Performs the equivalent of the `react-hot-loader/babel`
transform in the [react-hot-loader](https://github.com/gaearon/react-hot-loader)
project. This enables advanced hot reloading use cases such as editing of
Expand Down
1 change: 1 addition & 0 deletions src/Options-gen-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Options = t.iface([], {
filePath: t.opt("string"),
production: t.opt("boolean"),
disableESTransforms: t.opt("boolean"),
preserveDynamicImport: t.opt("boolean"),
});

const exportedTypeSuite: t.ITypeSuite = {
Expand Down
5 changes: 5 additions & 0 deletions src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface Options {
* separators, etc.
*/
disableESTransforms?: boolean;
/**
* If specified, the imports transform does not attempt to change dynamic import()
* expressions into require() calls.
*/
preserveDynamicImport?: boolean;
}

export function validateOptions(options: Options): void {
Expand Down
6 changes: 6 additions & 0 deletions src/transformers/CJSImportTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class CJSImportTransformer extends Transformer {
readonly reactHotLoaderTransformer: ReactHotLoaderTransformer | null,
readonly enableLegacyBabel5ModuleInterop: boolean,
readonly isTypeScriptTransformEnabled: boolean,
readonly preserveDynamicImport: boolean,
) {
super();
this.declarationInfo = isTypeScriptTransformEnabled
Expand Down Expand Up @@ -115,6 +116,11 @@ export default class CJSImportTransformer extends Transformer {
*/
private processImport(): void {
if (this.tokens.matches2(tt._import, tt.parenL)) {
if (this.preserveDynamicImport) {
// Bail out, only making progress for this one token.
this.tokens.copyToken();
return;
}
this.tokens.replaceToken("Promise.resolve().then(() => require");
const contextId = this.tokens.currentToken().contextId;
if (contextId == null) {
Expand Down
1 change: 1 addition & 0 deletions src/transformers/RootTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class RootTransformer {
reactHotLoaderTransformer,
enableLegacyBabel5ModuleInterop,
transforms.includes("typescript"),
Boolean(options.preserveDynamicImport),
),
);
} else {
Expand Down
22 changes: 21 additions & 1 deletion test/imports-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ module.exports = exports.default;
);
});

it("handles dynamic imports", () => {
it("transforms dynamic imports by default", () => {
assertResult(
`
async function loadThing() {
Expand All @@ -1040,6 +1040,26 @@ module.exports = exports.default;
);
});

it("preserves dynamic import when configured to do so", () => {
assertResult(
`
import Bar from './Bar.js';
async function loadThing() {
const foo = await import('foo');
console.log(Bar);
}
`,
`"use strict";${IMPORT_DEFAULT_PREFIX}
var _Barjs = require('./Bar.js'); var _Barjs2 = _interopRequireDefault(_Barjs);
async function loadThing() {
const foo = await import('foo');
console.log(_Barjs2.default);
}
`,
{transforms: ["imports"], preserveDynamicImport: true},
);
});

it("properly handles object destructuring with imported names", () => {
assertResult(
`
Expand Down

0 comments on commit 8a5fdf8

Please sign in to comment.