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

Change TypeScript ES module interop to act like Babel by default #202

Merged
merged 1 commit into from
Apr 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,10 @@ are four main transforms that you may want to enable:
including adding `createReactClass` display names and JSX context information.
* **typescript**: Compiles TypeScript code to JavaScript, removing type
annotations and handling features like enums. Does not check types.
* **flow**: Removes Flow types, e.g. `const f = (x: number): string => "hi";`
to `const f = (x) => "hi";`. Does not check types.
* **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. With the
`typescript` transform enabled, the import conversion uses the behavior of the
TypeScript compiler (which is slightly more lenient). Also includes dynamic
`import`.
(`require`/`module.exports`) using the same approach as Babel 6 and TypeScript
with `--esModuleInterop`. Also includes dynamic `import`.

The following proposed JS features are built-in and always transformed:
* [Class fields](https://github.com/tc39/proposal-class-fields): `class C { x = 1; }`.
Expand All @@ -63,10 +60,19 @@ The following proposed JS features are built-in and always transformed:
* [Optional catch binding](https://github.com/tc39/proposal-optional-catch-binding):
`try { doThing(); } catch { }`.

There are some additional opt-in transforms that are useful in legacy situations:
* **add-module-exports**: Mimic the Babel 5 approach to CommonJS interop, so that
you can run `require('./MyModule')` instead of `require('./MyModule').default`.
Analogous to
When using the `import` transform, there are some options to enable legacy
CommonJS interop approaches:
* **enableLegacyTypeScriptModuleInterop**: Use the default TypeScript approach
to CommonJS interop instead of assuming that TypeScript's `--esModuleInterop`
flag is enabled. For example, if a CJS module exports a function, legacy
TypeScript interop requires you to write `import * as add from './add';`,
while Babel, Webpack, Node.js, and TypeScript with `--esModuleInterop` require
you to write `import add from './add';`. As mentioned in the
[docs](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#support-for-import-d-from-cjs-form-commonjs-modules-with---esmoduleinterop),
the TypeScript team recommends you always use `--esModuleInterop`.
* **enableLegacyBabel5ModuleInterop**: Use the Babel 5 approach to CommonJS
interop, so that you can run `require('./MyModule')` instead of
`require('./MyModule').default`. Analogous to
[babel-plugin-add-module-exports](https://github.com/59naga/babel-plugin-add-module-exports).

## Usage
Expand Down
2 changes: 1 addition & 1 deletion example-runner/example-configs/decaffeinate-parser.patch
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ index 1aee96b..d084985 100644
+++ b/test/mocha.opts
@@ -1,2 +1,2 @@
---compilers ts:ts-node/register,js:babel-register
+--compilers ts:sucrase/register/ts
+--compilers ts:sucrase/register/ts-legacy-module-interop
--recursive
32 changes: 28 additions & 4 deletions example-runner/example-configs/tslint.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/package.json b/package.json
index 826ad87f..650dbd39 100644
index 826ad87f..1ac6cee3 100644
--- a/package.json
+++ b/package.json
@@ -22,14 +22,14 @@
Expand All @@ -9,9 +9,9 @@ index 826ad87f..650dbd39 100644
- "compile:core": "tsc -p src",
- "compile:scripts": "tsc -p scripts",
- "compile:test": "tsc -p test",
+ "compile:core": "sucrase ./src -d ./lib --transforms typescript,imports",
+ "compile:scripts": "sucrase ./scripts -d ./scripts --transforms typescript,imports",
+ "compile:test": "mkdir -p build && sucrase ./test -d ./build/test --exclude-dirs files,rules --transforms typescript,imports && sucrase ./src -d ./build/src --transforms typescript,imports",
+ "compile:core": "sucrase ./src -d ./lib --transforms typescript,imports --enable-legacy-typescript-module-interop",
+ "compile:scripts": "sucrase ./scripts -d ./scripts --transforms typescript,imports --enable-legacy-typescript-module-interop",
+ "compile:test": "mkdir -p build && sucrase ./test -d ./build/test --exclude-dirs files,rules --transforms typescript,imports --enable-legacy-typescript-module-interop && sucrase ./src -d ./build/src --transforms typescript,imports --enable-legacy-typescript-module-interop",
"lint": "npm-run-all -p lint:global lint:from-bin",
"lint:global": "tslint --project test/tsconfig.json --format stylish # test includes 'src' too",
"lint:from-bin": "node bin/tslint --project test/tsconfig.json --format stylish",
Expand Down Expand Up @@ -73,3 +73,27 @@ index 4f1b0bf7..049d4f31 100644
-})(Lint.RuleWalker);
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..60dc8db4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "importHelpers": true,
+ "declaration": true,
+ "sourceMap": false,
+ "target": "es2017",
+ "lib": ["es6"],
+ "outDir": "../lib"
+ }
+}
5 changes: 4 additions & 1 deletion example-runner/example-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,20 @@ async function runProject(project: string, shouldSave: boolean): Promise<boolean
if (!await exists(revPath) || !await exists(patchPath) || shouldSave) {
console.log(`Generating metadata for ${project}`);
await run(`git rev-parse HEAD > ${revPath}`);
await run(`git diff > ${patchPath}`);
await run(`git diff HEAD > ${patchPath}`);
}

try {
await run(`cat ${revPath} | xargs git reset --hard`);
await run(`git clean -f`);
} catch (e) {
await run("git fetch");
await run(`cat ${revPath} | xargs git reset --hard`);
await run(`git clean -f`);
}
if ((await readFile(patchPath)).length) {
await run(`cat ${patchPath} | git apply`);
await run(`git add -A`);
}

await run("yarn");
Expand Down
1 change: 1 addition & 0 deletions register/ts-legacy-module-interop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("../dist/src/register").registerTSLegacyModuleInterop();
1 change: 1 addition & 0 deletions register/tsx-legacy-module-interop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("../dist/src/register").registerTSXLegacyModuleInterop();
10 changes: 5 additions & 5 deletions src/ImportProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export default class ImportProcessor {
constructor(
readonly nameManager: NameManager,
readonly tokens: TokenProcessor,
readonly isTypeScript: boolean,
readonly enableLegacyTypeScriptModuleInterop: boolean,
) {}

getPrefixCode(): string {
if (this.isTypeScript) {
if (this.enableLegacyTypeScriptModuleInterop) {
return "";
}
let prefix = "";
Expand Down Expand Up @@ -70,7 +70,7 @@ export default class ImportProcessor {
}

preprocessTokens(): void {
if (!this.isTypeScript) {
if (!this.enableLegacyTypeScriptModuleInterop) {
this.interopRequireWildcardName = this.nameManager.claimFreeName("_interopRequireWildcard");
this.interopRequireDefaultName = this.nameManager.claimFreeName("_interopRequireDefault");
}
Expand Down Expand Up @@ -167,7 +167,7 @@ export default class ImportProcessor {

const primaryImportName = this.getFreeIdentifierForPath(path);
let secondaryImportName;
if (this.isTypeScript) {
if (this.enableLegacyTypeScriptModuleInterop) {
secondaryImportName = primaryImportName;
} else {
secondaryImportName =
Expand All @@ -176,7 +176,7 @@ export default class ImportProcessor {
let requireCode = `var ${primaryImportName} = require('${path}');`;
if (wildcardNames.length > 0) {
for (const wildcardName of wildcardNames) {
const moduleExpr = this.isTypeScript
const moduleExpr = this.enableLegacyTypeScriptModuleInterop
? primaryImportName
: `${this.interopRequireWildcardName}(${primaryImportName})`;
requireCode += ` var ${wildcardName} = ${moduleExpr};`;
Expand Down
34 changes: 20 additions & 14 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable no-console */
import * as commander from "commander";
import commander from "commander";
import {exists, mkdir, readdir, readFile, stat, writeFile} from "mz/fs";
import {join} from "path";

import {Transform, transform} from "./index";
import {Options, Transform, transform} from "./index";

export default function run(): void {
commander
Expand All @@ -15,6 +15,11 @@ export default function run(): void {
)
.option("--exclude-dirs <paths>", "Names of directories that should not be traversed.")
.option("-t, --transforms <transforms>", "Comma-separated list of transforms to run.")
.option(
"--enable-legacy-typescript-module-interop",
"Use default TypeScript ESM/CJS interop strategy.",
)
.option("--enable-legacy-babel5-module-interop", "Use Babel 5 ESM/CJS interop strategy.")
.parse(process.argv);

if (!commander.outDir) {
Expand All @@ -33,11 +38,16 @@ export default function run(): void {
}

const outDir = commander.outDir;
const transforms = commander.transforms.split(",");
const srcDir = commander.args[0];
const excludeDirs = commander.excludeDirs ? commander.excludeDirs.split(",") : [];

buildDirectory(srcDir, outDir, excludeDirs, transforms).catch((e) => {
const options: Options = {
transforms: commander.transforms.split(","),
enableLegacyTypeScriptModuleInterop: commander.enableLegacyTypescriptModuleInterop,
enableLegacyBabel5ModuleInterop: commander.enableLegacyBabel5ModuleInterop,
};

buildDirectory(srcDir, outDir, excludeDirs, options).catch((e) => {
process.exitCode = 1;
console.error(e);
});
Expand All @@ -47,9 +57,9 @@ async function buildDirectory(
srcDirPath: string,
outDirPath: string,
excludeDirs: Array<string>,
transforms: Array<Transform>,
options: Options,
): Promise<void> {
const extension = transforms.includes("typescript") ? ".ts" : ".js";
const extension = options.transforms.includes("typescript") ? ".ts" : ".js";
if (!(await exists(outDirPath))) {
await mkdir(outDirPath);
}
Expand All @@ -60,21 +70,17 @@ async function buildDirectory(
const srcChildPath = join(srcDirPath, child);
const outChildPath = join(outDirPath, child);
if ((await stat(srcChildPath)).isDirectory()) {
await buildDirectory(srcChildPath, outChildPath, excludeDirs, transforms);
await buildDirectory(srcChildPath, outChildPath, excludeDirs, options);
} else if (srcChildPath.endsWith(extension)) {
const outPath = `${outChildPath.substr(0, outChildPath.length - 3)}.js`;
await buildFile(srcChildPath, outPath, transforms);
await buildFile(srcChildPath, outPath, options);
}
}
}

async function buildFile(
srcPath: string,
outPath: string,
transforms: Array<Transform>,
): Promise<void> {
async function buildFile(srcPath: string, outPath: string, options: Options): Promise<void> {
console.log(`${srcPath} -> ${outPath}`);
const code = (await readFile(srcPath)).toString();
const transformedCode = transform(code, {transforms, filePath: srcPath});
const transformedCode = transform(code, {...options, filePath: srcPath});
await writeFile(outPath, transformedCode);
}
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import TokenProcessor from "./TokenProcessor";
import RootTransformer from "./transformers/RootTransformer";
import formatTokens from "./util/formatTokens";

export type Transform = "jsx" | "imports" | "flow" | "typescript" | "add-module-exports";
export type Transform = "jsx" | "typescript" | "flow" | "imports";

export type Options = {
transforms: Array<Transform>;
enableLegacyTypeScriptModuleInterop?: boolean;
enableLegacyBabel5ModuleInterop?: boolean;
filePath?: string;
};

Expand All @@ -32,6 +34,7 @@ export function transform(code: string, options: Options): string {
return new RootTransformer(
sucraseContext,
options.transforms,
Boolean(options.enableLegacyBabel5ModuleInterop),
options.filePath || null,
).transform();
} catch (e) {
Expand Down Expand Up @@ -71,10 +74,14 @@ function getSucraseContext(code: string, options: Options): SucraseContext {
const tokenProcessor = new TokenProcessor(code, tokens);
const nameManager = new NameManager(tokenProcessor);
nameManager.preprocessNames();
const isTypeScript = options.transforms.includes("typescript");
const importProcessor = new ImportProcessor(nameManager, tokenProcessor, isTypeScript);
const enableLegacyTypeScriptModuleInterop = Boolean(options.enableLegacyTypeScriptModuleInterop);
const importProcessor = new ImportProcessor(
nameManager,
tokenProcessor,
enableLegacyTypeScriptModuleInterop,
);
importProcessor.preprocessTokens();
if (isTypeScript) {
if (options.transforms.includes("typescript")) {
importProcessor.pruneTypeOnlyImports();
}
identifyShadowedGlobals(tokenProcessor, scopes, importProcessor.getGlobalNames());
Expand Down
28 changes: 21 additions & 7 deletions src/register.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
// @ts-ignore: no types available.
import * as pirates from "pirates";
import {Transform, transform} from "./index";
import {Options, Transform, transform} from "./index";

export function addHook(extension: string, transforms: Array<Transform>): void {
export function addHook(extension: string, options: Options): void {
pirates.addHook(
(code: string, filePath: string): string => transform(code, {filePath, transforms}),
(code: string, filePath: string): string => transform(code, {...options, filePath}),
{exts: [extension]},
);
}

export function registerJS(): void {
addHook(".js", ["imports", "flow", "jsx"]);
addHook(".js", {transforms: ["imports", "flow", "jsx"]});
}

export function registerJSX(): void {
addHook(".jsx", ["imports", "flow", "jsx"]);
addHook(".jsx", {transforms: ["imports", "flow", "jsx"]});
}

export function registerTS(): void {
addHook(".ts", ["imports", "typescript"]);
addHook(".ts", {transforms: ["imports", "typescript"]});
}

export function registerTSX(): void {
addHook(".tsx", ["imports", "typescript", "jsx"]);
addHook(".tsx", {transforms: ["imports", "typescript", "jsx"]});
}

export function registerTSLegacyModuleInterop(): void {
addHook(".ts", {
transforms: ["imports", "typescript"],
enableLegacyTypeScriptModuleInterop: true,
});
}

export function registerTSXLegacyModuleInterop(): void {
addHook(".tsx", {
transforms: ["imports", "typescript", "jsx"],
enableLegacyTypeScriptModuleInterop: true,
});
}

export function registerAll(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/transformers/ImportTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class ImportTransformer extends Transformer {
readonly rootTransformer: RootTransformer,
readonly tokens: TokenProcessor,
readonly importProcessor: ImportProcessor,
readonly shouldAddModuleExports: boolean,
readonly enableLegacyBabel5ModuleInterop: boolean,
) {
super();
}
Expand All @@ -28,7 +28,7 @@ export default class ImportTransformer extends Transformer {
}

getSuffixCode(): string {
if (this.shouldAddModuleExports && this.hadDefaultExport && !this.hadNamedExport) {
if (this.enableLegacyBabel5ModuleInterop && this.hadDefaultExport && !this.hadNamedExport) {
return "\nmodule.exports = exports.default;\n";
}
return "";
Expand Down
9 changes: 7 additions & 2 deletions src/transformers/RootTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class RootTransformer {
constructor(
sucraseContext: SucraseContext,
transforms: Array<Transform>,
enableLegacyBabel5ModuleInterop: boolean,
filePath: string | null,
) {
this.nameManager = sucraseContext.nameManager;
Expand All @@ -39,9 +40,13 @@ export default class RootTransformer {
}

if (transforms.includes("imports")) {
const shouldAddModuleExports = transforms.includes("add-module-exports");
this.transformers.push(
new ImportTransformer(this, tokenProcessor, importProcessor, shouldAddModuleExports),
new ImportTransformer(
this,
tokenProcessor,
importProcessor,
enableLegacyBabel5ModuleInterop,
),
);
}

Expand Down
2 changes: 1 addition & 1 deletion test/flow-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {IMPORT_PREFIX} from "./prefixes";
import {assertResult} from "./util";

function assertFlowResult(code: string, expectedResult: string): void {
assertResult(code, expectedResult, ["jsx", "imports", "flow"]);
assertResult(code, expectedResult, {transforms: ["jsx", "imports", "flow"]});
}

describe("transform flow", () => {
Expand Down