diff --git a/.eslintignore b/.eslintignore index 38b0d069..066d77f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ benchmark/sample/* benchmark/node_modules/* example-runner/example-repos +integration-test/test-cases spec-compliance-tests/babel-tests/babel-tests-checkout spec-compliance-tests/test262/test262-checkout diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e10962f..b4a6a11b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,7 @@ jobs: - run: yarn build - run: yarn lint - run: yarn test-with-coverage && yarn report-coverage + - run: yarn integration-test - run: yarn test262 - run: yarn check-babel-tests test-older-node: diff --git a/.npmignore b/.npmignore index 230bf779..e3dfd707 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,4 @@ !bin/** !dist/** !register/** +!ts-node-plugin/** diff --git a/README.md b/README.md index 1ff7a2a4..82faa7b2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,23 @@ [![MIT License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](LICENSE) [![Join the chat at https://gitter.im/sucrasejs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/sucrasejs/Lobby) -### [Try it out](https://sucrase.io) +## [Try it out](https://sucrase.io) + +## Quick usage + +```bash +yarn add --dev sucrase # Or npm install --save-dev sucrase +node -r sucrase/register main.ts +``` + +Using the [ts-node](https://github.com/TypeStrong/ts-node) integration: + +```bash +yarn add --dev sucrase ts-node typescript +./node_modules/.bin/ts-node --transpiler sucrase/ts-node-plugin main.ts +``` + +## Project overview Sucrase is an alternative to Babel that allows super-fast development builds. Instead of compiling a large range of JS features to be able to work in Internet @@ -150,41 +166,40 @@ Two legacy modes can be used with the `imports` transform: ## Usage -Installation: +### Tool integrations -```bash -yarn add --dev sucrase # Or npm install --save-dev sucrase -``` +* [Webpack](https://github.com/alangpierce/sucrase/tree/main/integrations/webpack-loader) +* [Gulp](https://github.com/alangpierce/sucrase/tree/main/integrations/gulp-plugin) +* [Jest](https://github.com/alangpierce/sucrase/tree/main/integrations/jest-plugin) +* [Rollup](https://github.com/rollup/plugins/tree/master/packages/sucrase) +* [Broccoli](https://github.com/stefanpenner/broccoli-sucrase) -Often, you'll want to use one of the build tool integrations: -[Webpack](https://github.com/alangpierce/sucrase/tree/main/integrations/webpack-loader), -[Gulp](https://github.com/alangpierce/sucrase/tree/main/integrations/gulp-plugin), -[Jest](https://github.com/alangpierce/sucrase/tree/main/integrations/jest-plugin), -[Rollup](https://github.com/rollup/plugins/tree/master/packages/sucrase), -[Broccoli](https://github.com/stefanpenner/broccoli-sucrase). +### Usage in Node -Compile on-the-fly via a require hook with some [reasonable defaults](src/register.ts): - -```js -// Register just one extension. -require("sucrase/register/ts"); -// Or register all at once. -require("sucrase/register"); +The most robust way is to use the Sucrase plugin for [ts-node](https://github.com/TypeStrong/ts-node), +which has various Node integrations and configures Sucrase via `tsconfig.json`: +```bash +ts-node --transpiler sucrase/ts-node-plugin ``` -Compile on-the-fly via a drop-in replacement for node: +For projects that don't target ESM, Sucrase also has a require hook with some +reasonable defaults that can be accessed in a few ways: -```bash -sucrase-node index.ts -``` +* From code: `require("sucrase/register");` +* When invoking Node: `node -r sucrase/register main.ts` +* As a separate binary: `sucrase-node main.ts` -Run on a directory: +### Compiling a project to JS +For simple use cases, Sucrase comes with a `sucrase` CLI that mirrors your +directory structure to an output directory: ```bash sucrase ./srcDir -d ./outDir --transforms typescript,imports ``` -Call from JS directly: +### Usage from code + +For any advanced use cases, Sucrase can be called from JS directly: ```js import {transform} from "sucrase"; diff --git a/integration-test/integration-tests.ts b/integration-test/integration-tests.ts new file mode 100644 index 00000000..819810b8 --- /dev/null +++ b/integration-test/integration-tests.ts @@ -0,0 +1,42 @@ +import {join} from "path"; +import {readdir, stat} from "fs/promises"; +import {exec} from "child_process"; +import {promisify} from "util"; +const execPromise = promisify(exec); + +describe("ts-node tests", async () => { + /** + * Find all integration tests in the test-cases directory. + * + * Each test has a file starting with "main" (e.g. main.ts, main.tsx, + * main.mts, etc) that's used as the entry point. The test should be written + * in such a way that the execution throws an exception if the test fails. + */ + async function* discoverTests(dir: string): AsyncIterable { + for (const child of await readdir(dir)) { + const childPath = join(dir, child); + if ((await stat(childPath)).isDirectory()) { + yield* discoverTests(childPath); + } else if (child.startsWith("main")) { + yield childPath; + } + } + } + + for await (const testPath of discoverTests("test-cases")) { + it(testPath, async () => { + // To help confirm that the behavior is in sync with the default ts-node + // behavior, first run ts-node without the plugin to make sure it works, + // then run it with the plugin. + await execPromise(`npx ts-node --esm --transpile-only ${testPath}`); + await execPromise( + `npx ts-node --esm --transpiler ${__dirname}/../ts-node-plugin ${testPath}`, + ); + }); + } + + // Currently, mocha needs to be run with --delay to allow async test + // generation like this, and that also requires explicitly invoking this run + // callback. + run(); +}); diff --git a/integration-test/package.json b/integration-test/package.json new file mode 100644 index 00000000..bb69067a --- /dev/null +++ b/integration-test/package.json @@ -0,0 +1,8 @@ +{ + "name": "sucrase-integration-tests", + "version": "1.0.0", + "private": true, + "devDependencies": { + "ts-node": "^10.9.1" + } +} diff --git a/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/file.js b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/file.js new file mode 100644 index 00000000..c9066203 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/file.js @@ -0,0 +1 @@ +export const x = 4; diff --git a/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/main.js b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/main.js new file mode 100644 index 00000000..0a0e2211 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/main.js @@ -0,0 +1,13 @@ +import {x} from './file'; + +if (x !== 4) { + throw new Error(); +} + +const a = 1; +// This snippet confirms that we're running in JS, not TS. In TS, it is parsed +// as a function call, and in JS, it is parsed as comparison operators. +const comparisonResult = a<2>(3); +if (comparisonResult !== false) { + throw new Error(); +} diff --git a/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/tsconfig.json b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/tsconfig.json new file mode 100644 index 00000000..fc4ec1b7 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/allows-js-files-with-import-syntax/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "esModuleInterop": true, + "allowJs": true + } +} diff --git a/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/file.ts b/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/file.ts new file mode 100644 index 00000000..a4a9e753 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/file.ts @@ -0,0 +1 @@ +export const x = 7; diff --git a/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/main.ts b/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/main.ts new file mode 100644 index 00000000..48279209 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/dynamic-import-is-transpiled/main.ts @@ -0,0 +1,14 @@ +async function foo() { + let calledFakeRequire = false; + const require = () => { + calledFakeRequire = true; + } + // Import should become require, which will end up calling our shadowed + // declaration of require. This is different behavior from nodenext, where + // import should be a true ESM import. + const OtherFile = await import('./file'); + if (!calledFakeRequire) { + throw new Error(); + } +} +foo(); diff --git a/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/file.js b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/file.js new file mode 100644 index 00000000..baee1d57 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/file.js @@ -0,0 +1,3 @@ +module.exports = function twelve() { + return 12; +} diff --git a/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/main.ts b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/main.ts new file mode 100644 index 00000000..4ccb77e1 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/main.ts @@ -0,0 +1,5 @@ +import * as twelve from './file'; + +if (twelve() !== 12) { + throw new Error(); +} diff --git a/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/tsconfig.json b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/tsconfig.json new file mode 100644 index 00000000..23f60f7f --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/esmoduleinterop-false-is-respected/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "esModuleInterop": false + }, +} diff --git a/integration-test/test-cases/commonjs-cases/package.json b/integration-test/test-cases/commonjs-cases/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/integration-test/test-cases/commonjs-cases/tsconfig.json b/integration-test/test-cases/commonjs-cases/tsconfig.json new file mode 100644 index 00000000..ad75d5e0 --- /dev/null +++ b/integration-test/test-cases/commonjs-cases/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "esModuleInterop": true + }, +} diff --git a/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/main.tsx b/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/main.tsx new file mode 100644 index 00000000..10c06a90 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/main.tsx @@ -0,0 +1,16 @@ +let wasCalled: boolean = false; +function require(path) { + if (path !== "react/jsx-dev-runtime") { + throw new Error(); + } + return { + jsxDEV: () => { + wasCalled = true; + } + }; +} + +const elem =
; +if (!wasCalled) { + throw new Error(); +} diff --git a/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/tsconfig.json b/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/tsconfig.json new file mode 100644 index 00000000..33597d69 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/allows-automatic-dev-transform/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react-jsxdev", + }, +} diff --git a/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/main.tsx b/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/main.tsx new file mode 100644 index 00000000..44743d33 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/main.tsx @@ -0,0 +1,16 @@ +let wasCalled: boolean = false; +function require(path) { + if (path !== "react/jsx-runtime") { + throw new Error(); + } + return { + jsx: () => { + wasCalled = true; + } + }; +} + +const elem =
; +if (!wasCalled) { + throw new Error(); +} diff --git a/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/tsconfig.json b/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/tsconfig.json new file mode 100644 index 00000000..e2fe044f --- /dev/null +++ b/integration-test/test-cases/jsx-cases/allows-automatic-prod-transform/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react-jsx", + }, +} diff --git a/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/main.tsx b/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/main.tsx new file mode 100644 index 00000000..e644d408 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/main.tsx @@ -0,0 +1,20 @@ +let hWasCalledWithDiv = false; +let hWasCalledWithFragment = false; +const Fragment = {}; + +function h(tag) { + if (tag === 'div') { + hWasCalledWithDiv = true; + } else if (tag === Fragment) { + hWasCalledWithFragment = true; + } +} +const elem1 =
; +if (!hWasCalledWithDiv) { + throw new Error(); +} + +const elem2 = <>hello; +if (!hWasCalledWithFragment) { + throw new Error(); +} diff --git a/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/tsconfig.json b/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/tsconfig.json new file mode 100644 index 00000000..6981030e --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-factory-config-is-respected/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/main.jsx b/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/main.jsx new file mode 100644 index 00000000..2193d08c --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/main.jsx @@ -0,0 +1,19 @@ +let wasCalled = false; +const React = { + createElement() { + wasCalled = true; + } +} +const elem =
; +if (!wasCalled) { + throw new Error(); +} + +const a = 1; +// This snippet confirms that we're running in JS, not TS. In TS, it is parsed +// as a function call, and in JS, it is parsed as comparison operators. +const comparisonResult = a<2>(3); +if (comparisonResult !== false) { + throw new Error(); +} +{} diff --git a/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/tsconfig.json b/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/tsconfig.json new file mode 100644 index 00000000..a177e316 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-files-are-transpiled/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react", + "allowJs": true + } +} diff --git a/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/main.tsx b/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/main.tsx new file mode 100644 index 00000000..ef7730dc --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/main.tsx @@ -0,0 +1,16 @@ +let wasCalled: boolean = false; +function require(path) { + if (path !== "my-library/jsx-runtime") { + throw new Error(); + } + return { + jsx: () => { + wasCalled = true; + } + }; +} + +const elem =
; +if (!wasCalled) { + throw new Error(); +} diff --git a/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/tsconfig.json b/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/tsconfig.json new file mode 100644 index 00000000..d266dc63 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/jsx-import-source-is-respected/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "my-library" + } +} diff --git a/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/main.tsx b/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/main.tsx new file mode 100644 index 00000000..74a25746 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/main.tsx @@ -0,0 +1,10 @@ +let wasCalled: boolean = false; +const React = { + createElement(): void { + wasCalled = true; + } +} +const elem =
; +if (!wasCalled) { + throw new Error(); +} diff --git a/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/tsconfig.json b/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/tsconfig.json new file mode 100644 index 00000000..83a88505 --- /dev/null +++ b/integration-test/test-cases/jsx-cases/tsx-files-allow-jsx-syntax/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react", + }, +} diff --git a/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/file.mts b/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/file.mts new file mode 100644 index 00000000..7dc80b84 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/file.mts @@ -0,0 +1,3 @@ +// Crashes if run as CJS +console.log(import.meta.url); +export const x = 3; diff --git a/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/main.cts b/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/main.cts new file mode 100644 index 00000000..06294adb --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-can-dynamic-import-mts/main.cts @@ -0,0 +1,9 @@ +async function foo() { + // Dynamic import is expected to be preserved so that it's possible to import + // mjs files. + const result = await import('./file.mjs'); + if (result.x !== 3) { + throw new Error(); + } +} +foo(); diff --git a/integration-test/test-cases/nodenext-cases/cts-can-import-cts/file.cts b/integration-test/test-cases/nodenext-cases/cts-can-import-cts/file.cts new file mode 100644 index 00000000..a983fc86 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-can-import-cts/file.cts @@ -0,0 +1,2 @@ +export const two = 2; +export default 1; diff --git a/integration-test/test-cases/nodenext-cases/cts-can-import-cts/main.cts b/integration-test/test-cases/nodenext-cases/cts-can-import-cts/main.cts new file mode 100644 index 00000000..15b7ed4f --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-can-import-cts/main.cts @@ -0,0 +1,5 @@ +import one, {two} from './file.cjs' + +if (one + two !== 3) { + throw new Error(); +} diff --git a/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/main.cts b/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/main.cts new file mode 100644 index 00000000..c22aac9c --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/main.cts @@ -0,0 +1,2 @@ +// Crashes if run as ESM +console.log(__dirname); diff --git a/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/package.json b/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/cts-runs-as-cjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file2.cts b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file2.cts new file mode 100644 index 00000000..a4f80b8e --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file2.cts @@ -0,0 +1 @@ +export = 2; diff --git a/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file3.cjs b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file3.cjs new file mode 100644 index 00000000..690aad34 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/file3.cjs @@ -0,0 +1 @@ +module.exports = 3; diff --git a/integration-test/test-cases/nodenext-cases/mts-can-import-cts/main.mts b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/main.mts new file mode 100644 index 00000000..677050d1 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-can-import-cts/main.mts @@ -0,0 +1,8 @@ +import File2 from './file2.cjs'; +import File3 = require("./file3.cjs"); +if (File2 !== 2) { + throw new Error(); +} +if (File3 !== 3) { + throw new Error(); +} diff --git a/integration-test/test-cases/nodenext-cases/mts-can-import-mts/main.mts b/integration-test/test-cases/nodenext-cases/mts-can-import-mts/main.mts new file mode 100644 index 00000000..a338db38 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-can-import-mts/main.mts @@ -0,0 +1,5 @@ +import {x} from './other-file.mjs'; + +if (x !== 3) { + throw new Error(); +} diff --git a/integration-test/test-cases/nodenext-cases/mts-can-import-mts/other-file.mts b/integration-test/test-cases/nodenext-cases/mts-can-import-mts/other-file.mts new file mode 100644 index 00000000..fe01c7d7 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-can-import-mts/other-file.mts @@ -0,0 +1 @@ +export const x = 3; diff --git a/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/main.mts b/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/main.mts new file mode 100644 index 00000000..bb2f8817 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/main.mts @@ -0,0 +1,2 @@ +// Crashes if run as CJS +console.log(import.meta.url); diff --git a/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/package.json b/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/mts-runs-as-esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/main.ts b/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/main.ts new file mode 100644 index 00000000..c22aac9c --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/main.ts @@ -0,0 +1,2 @@ +// Crashes if run as ESM +console.log(__dirname); diff --git a/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/package.json b/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/main.ts b/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/main.ts new file mode 100644 index 00000000..bb2f8817 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/main.ts @@ -0,0 +1,2 @@ +// Crashes if run as CJS +console.log(import.meta.url); diff --git a/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/package.json b/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/ts-infers-to-esm-from-package-json/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/integration-test/test-cases/nodenext-cases/tsconfig.json b/integration-test/test-cases/nodenext-cases/tsconfig.json new file mode 100644 index 00000000..454d91a1 --- /dev/null +++ b/integration-test/test-cases/nodenext-cases/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "esModuleInterop": true + }, + "ts-node": { + "experimentalResolver": true + } +} diff --git a/integration-test/yarn.lock b/integration-test/yarn.lock new file mode 100644 index 00000000..89b4263a --- /dev/null +++ b/integration-test/yarn.lock @@ -0,0 +1,107 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/package.json b/package.json index aae4421f..205ab65f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "run-examples": "sucrase-node example-runner/example-runner.ts", "test": "yarn lint && yarn test-only", "test-only": "mocha './test/**/*.ts'", + "integration-test": "cd integration-test && yarn && mocha --delay --timeout 10000 ./integration-tests.ts", "test262": "sucrase-node spec-compliance-tests/test262/run-test262.ts", "check-babel-tests": "sucrase-node spec-compliance-tests/babel-tests/check-babel-tests.ts", "test-with-coverage": "nyc mocha './test/**/*.ts'", diff --git a/ts-node-plugin/index.js b/ts-node-plugin/index.js new file mode 100644 index 00000000..d072c673 --- /dev/null +++ b/ts-node-plugin/index.js @@ -0,0 +1,76 @@ +const {transform} = require("../dist"); + +// Enum constants taken from the TypeScript codebase. +const ModuleKindCommonJS = 1; + +const JsxEmitReactJSX = 4; +const JsxEmitReactJSXDev = 5; + +/** + * ts-node transpiler plugin + * + * This plugin hooks into ts-node so that Sucrase can handle all TS-to-JS + * conversion while ts-node handles the ESM loader, require hook, REPL + * integration, etc. ts-node automatically discovers the relevant tsconfig file, + * so the main logic in this integration is translating tsconfig options to the + * corresponding Sucrase options. + * + * Any tsconfig options relevant to Sucrase are translated, but some config + * options outside the scope of Sucrase are ignored. For example, we assume the + * isolatedModules option, and we ignore target because Sucrase doesn't provide + * JS syntax downleveling (at least not in a way that is useful for Node). + * + * One notable caveat is that importsNotUsedAsValues and preserveValueImports + * are ignored right now, and Sucrase uses TypeScript's default behavior of + * eliding imports only used as types. This usually makes no difference when + * running the code, so for now we ignore these options without a warning. + */ +function create(createOptions) { + const {nodeModuleEmitKind} = createOptions; + const {module, jsx, jsxFactory, jsxFragmentFactory, jsxImportSource, esModuleInterop} = + createOptions.service.config.options; + + return { + transpile(input, transpileOptions) { + const {fileName} = transpileOptions; + const transforms = []; + // Detect JS rather than TS so we bias toward including the typescript + // transform, since almost always it doesn't hurt to include. + const isJS = + fileName.endsWith(".js") || + fileName.endsWith(".jsx") || + fileName.endsWith(".mjs") || + fileName.endsWith(".cjs"); + if (!isJS) { + transforms.push("typescript"); + } + if (module === ModuleKindCommonJS || nodeModuleEmitKind === "nodecjs") { + transforms.push("imports"); + } + if (fileName.endsWith(".tsx") || fileName.endsWith(".jsx")) { + transforms.push("jsx"); + } + + const {code, sourceMap} = transform(input, { + transforms, + disableESTransforms: true, + jsxRuntime: jsx === JsxEmitReactJSX || jsx === JsxEmitReactJSXDev ? "automatic" : "classic", + production: jsx === JsxEmitReactJSX, + jsxImportSource, + jsxPragma: jsxFactory, + jsxFragmentPragma: jsxFragmentFactory, + preserveDynamicImport: nodeModuleEmitKind === "nodecjs", + injectCreateRequireForImportRequire: nodeModuleEmitKind === "nodeesm", + enableLegacyTypeScriptModuleInterop: !esModuleInterop, + sourceMapOptions: {compiledFilename: fileName}, + filePath: fileName, + }); + return { + outputText: code, + sourceMapText: JSON.stringify(sourceMap), + }; + }, + }; +} + +exports.create = create; diff --git a/tsconfig.json b/tsconfig.json index 92b5fa33..3e9422e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "exclude": [ "benchmark/sample", "example-runner/example-repos", + "integration-test/test-cases", "spec-compliance-tests/test262/test262-checkout", "spec-compliance-tests/babel-tests/babel-tests-checkout" ]