diff --git a/README.md b/README.md index 0b00c955..45311392 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,12 @@ transforms are available: the same way as [babel-plugin-jest-hoist](https://github.com/facebook/jest/tree/master/packages/babel-plugin-jest-hoist). Does not validate the arguments passed to `jest.mock`, but the same rules still apply. +When the `imports` transform is *not* specified (i.e. when targeting ESM), the +`injectCreateRequireForImportRequire` option can be specified to transform TS +`import foo = require("foo");` in a way that matches the +[TypeScript 4.7 behavior](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#commonjs-interoperability) +with `module: nodenext`. + These newer JS features are transformed by default: * [Optional chaining](https://github.com/tc39/proposal-optional-chaining): `a?.b` diff --git a/src/HelperManager.ts b/src/HelperManager.ts index 0bc5162c..8571e492 100644 --- a/src/HelperManager.ts +++ b/src/HelperManager.ts @@ -1,6 +1,10 @@ import type NameManager from "./NameManager"; const HELPERS: {[name: string]: string} = { + require: ` + import {createRequire as CREATE_REQUIRE_NAME} from "module"; + const require = CREATE_REQUIRE_NAME(import.meta.url); + `, interopRequireWildcard: ` function interopRequireWildcard(obj) { if (obj && obj.__esModule) { @@ -125,6 +129,7 @@ const HELPERS: {[name: string]: string} = { export class HelperManager { helperNames: {[baseName in keyof typeof HELPERS]?: string} = {}; + createRequireName: string | null = null; constructor(readonly nameManager: NameManager) {} getHelperName(baseName: keyof typeof HELPERS): string { @@ -155,6 +160,11 @@ export class HelperManager { "ASYNC_OPTIONAL_CHAIN_NAME", this.helperNames.asyncOptionalChain!, ); + } else if (baseName === "require") { + if (this.createRequireName === null) { + this.createRequireName = this.nameManager.claimFreeName("_createRequire"); + } + helperCode = helperCode.replace(/CREATE_REQUIRE_NAME/g, this.createRequireName); } if (helperName) { resultCode += " "; diff --git a/src/Options-gen-types.ts b/src/Options-gen-types.ts index 1b835c98..7dd91b82 100644 --- a/src/Options-gen-types.ts +++ b/src/Options-gen-types.ts @@ -29,6 +29,7 @@ export const Options = t.iface([], { disableESTransforms: t.opt("boolean"), addUseStrict: t.opt("boolean"), preserveDynamicImport: t.opt("boolean"), + injectCreateRequireForImportRequire: t.opt("boolean"), }); const exportedTypeSuite: t.ITypeSuite = { diff --git a/src/Options.ts b/src/Options.ts index 0915b4ce..bcf9028f 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -60,6 +60,17 @@ export interface Options { * expressions into require() calls. */ preserveDynamicImport?: boolean; + /** + * Only relevant when targeting ESM (i.e. when the imports transform is *not* + * specified). This flag changes the behavior of TS require imports: + * + * import Foo = require("foo"); + * + * to import createRequire, create a require function, and use that function. + * This is the TS behavior with module: nodenext and makes it easier for the + * same code to target ESM and CJS. + */ + injectCreateRequireForImportRequire?: boolean; } export function validateOptions(options: Options): void { diff --git a/src/transformers/ESMImportTransformer.ts b/src/transformers/ESMImportTransformer.ts index 2a2c394b..84e64c4d 100644 --- a/src/transformers/ESMImportTransformer.ts +++ b/src/transformers/ESMImportTransformer.ts @@ -1,3 +1,4 @@ +import type {HelperManager} from "../HelperManager"; import type {Options} from "../index"; import type NameManager from "../NameManager"; import {ContextualKeyword} from "../parser/tokenizer/keywords"; @@ -21,10 +22,12 @@ import Transformer from "./Transformer"; export default class ESMImportTransformer extends Transformer { private nonTypeIdentifiers: Set; private declarationInfo: DeclarationInfo; + private injectCreateRequireForImportRequire: boolean; constructor( readonly tokens: TokenProcessor, readonly nameManager: NameManager, + readonly helperManager: HelperManager, readonly reactHotLoaderTransformer: ReactHotLoaderTransformer | null, readonly isTypeScriptTransformEnabled: boolean, options: Options, @@ -36,6 +39,7 @@ export default class ESMImportTransformer extends Transformer { this.declarationInfo = isTypeScriptTransformEnabled ? getDeclarationInfo(tokens) : EMPTY_DECLARATION_INFO; + this.injectCreateRequireForImportRequire = Boolean(options.injectCreateRequireForImportRequire); } process(): boolean { @@ -109,8 +113,19 @@ export default class ESMImportTransformer extends Transformer { if (this.isTypeName(importName)) { // If this name is only used as a type, elide the whole import. elideImportEquals(this.tokens); + } else if (this.injectCreateRequireForImportRequire) { + // We're using require in an environment (Node ESM) that doesn't provide + // it as a global, so generate a helper to import it. + // import -> const + this.tokens.replaceToken("const"); + // Foo + this.tokens.copyToken(); + // = + this.tokens.copyToken(); + // require + this.tokens.replaceToken(this.helperManager.getHelperName("require")); } else { - // Otherwise, switch `import` to `const`. + // Otherwise, just switch `import` to `const`. this.tokens.replaceToken("const"); } return true; diff --git a/src/transformers/RootTransformer.ts b/src/transformers/RootTransformer.ts index 8dd32141..ada87b6a 100644 --- a/src/transformers/RootTransformer.ts +++ b/src/transformers/RootTransformer.ts @@ -94,6 +94,7 @@ export default class RootTransformer { new ESMImportTransformer( tokenProcessor, this.nameManager, + this.helperManager, reactHotLoaderTransformer, transforms.includes("typescript"), options, diff --git a/test/prefixes.ts b/test/prefixes.ts index 99633b02..aba561b3 100644 --- a/test/prefixes.ts +++ b/test/prefixes.ts @@ -1,4 +1,6 @@ export const JSX_PREFIX = 'const _jsxFileName = "";'; +export const CREATE_REQUIRE_PREFIX = ` import {createRequire as _createRequire} from "module"; \ +const _require = _createRequire(import.meta.url);`; export const IMPORT_DEFAULT_PREFIX = ` function _interopRequireDefault(obj) { \ return obj && obj.__esModule ? obj : { default: obj }; }`; export const IMPORT_WILDCARD_PREFIX = ` function _interopRequireWildcard(obj) { \ diff --git a/test/typescript-test.ts b/test/typescript-test.ts index 47f42e97..6a1b4afa 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1,5 +1,6 @@ import type {Options} from "../src/Options"; import { + CREATE_REQUIRE_PREFIX, CREATE_STAR_EXPORT_PREFIX, ESMODULE_PREFIX, IMPORT_DEFAULT_PREFIX, @@ -893,6 +894,47 @@ describe("typescript transform", () => { ); }); + it("ignores injectCreateRequireForImportRequire when targeting CJS", () => { + assertTypeScriptResult( + ` + import a = require('a'); + console.log(a); + `, + `"use strict"; + const a = require('a'); + console.log(a); + `, + {injectCreateRequireForImportRequire: true}, + ); + }); + + it("preserves import = require by default when targeting ESM", () => { + assertTypeScriptESMResult( + ` + import a = require('a'); + console.log(a); + `, + ` + const a = require('a'); + console.log(a); + `, + ); + }); + + it("transforms import = require when targeting ESM and injectCreateRequireForImportRequire is enabled", () => { + assertTypeScriptESMResult( + ` + import a = require('a'); + console.log(a); + `, + `${CREATE_REQUIRE_PREFIX} + const a = _require('a'); + console.log(a); + `, + {injectCreateRequireForImportRequire: true}, + ); + }); + it("allows this types in functions", () => { assertTypeScriptResult( `