diff --git a/.gitignore b/.gitignore index 93103040..cde25441 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist dist-self-build dist-types example-runner/example-repos +integration-test/test-cases/**/dist-actual spec-compliance-tests/test262/test262-checkout spec-compliance-tests/babel-tests/babel-tests-checkout integrations/gulp-plugin/dist diff --git a/integration-test/integration-tests.ts b/integration-test/integration-tests.ts index 23912302..49685b8e 100644 --- a/integration-test/integration-tests.ts +++ b/integration-test/integration-tests.ts @@ -1,11 +1,16 @@ import assert from "assert"; import {exec} from "child_process"; import {readdirSync, statSync} from "fs"; -import {writeFile} from "fs/promises"; -import {join, dirname} from "path"; +import {rm, writeFile} from "fs/promises"; +import {join, dirname, resolve} from "path"; import {promisify} from "util"; -import {readFileContents, readJSONFileContentsIfExists} from "../script/util/readFileContents"; +import { + readFileContents, + readJSONFileContents, + readJSONFileContentsIfExists, +} from "../script/util/readFileContents"; +import assertDirectoriesEqual from "./util/assertDirectoriesEqual"; const execPromise = promisify(exec); @@ -101,4 +106,24 @@ describe("integration tests", () => { ); }); } + + /** + * Find CLI integration tests. + * + * Each test must have a test.json file and directories "src" and + * "dist-expected". The Sucrase CLI is invoked with cliOptions and the result + * is expected to exactly match dist-expected. + */ + for (const testFile of discoverTests("test-cases/cli-cases", "test.json")) { + const testDir = dirname(testFile); + it(testDir, async () => { + process.chdir(testDir); + const testConfig = await readJSONFileContents("./test.json"); + await rm("./dist-actual", {recursive: true, force: true}); + await execPromise( + `${__dirname}/../bin/sucrase ./src --out-dir ./dist-actual ${testConfig.cliOptions}`, + ); + await assertDirectoriesEqual(resolve("./dist-actual"), resolve("./dist-expected")); + }); + } }); diff --git a/integration-test/test-cases/cli-cases/respects-disable-es-transforms/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/dist-expected/main.js new file mode 100644 index 00000000..86ec743e --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/dist-expected/main.js @@ -0,0 +1,4 @@ +class A { + x = 1; + constructor( y) {;this.y = y;} +} diff --git a/integration-test/test-cases/cli-cases/respects-disable-es-transforms/src/main.ts b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/src/main.ts new file mode 100644 index 00000000..f78edb07 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/src/main.ts @@ -0,0 +1,4 @@ +class A { + x: number = 1; + constructor(readonly y: string) {} +} diff --git a/integration-test/test-cases/cli-cases/respects-disable-es-transforms/test.json b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/test.json new file mode 100644 index 00000000..9699e567 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-disable-es-transforms/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms typescript --disable-es-transforms" +} diff --git a/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/dist-expected/main.js new file mode 100644 index 00000000..bbda1a51 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/dist-expected/main.js @@ -0,0 +1,6 @@ + import {createRequire as _createRequire} from "module"; const _require = _createRequire(import.meta.url); +const A = _require('../A'); + +function foo() { + console.log(A); +} diff --git a/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/src/main.ts b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/src/main.ts new file mode 100644 index 00000000..cdad96fc --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/src/main.ts @@ -0,0 +1,6 @@ + +import A = require('../A'); + +function foo(): void { + console.log(A); +} diff --git a/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/test.json b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/test.json new file mode 100644 index 00000000..59896e97 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms typescript --inject-create-require-for-import-require" +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-defaults/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-jsx-defaults/dist-expected/main.js new file mode 100644 index 00000000..65e582b2 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-defaults/dist-expected/main.js @@ -0,0 +1,5 @@ +const _jsxFileName = "src/main.jsx";import React from 'react'; + +function Foo() { + return React.createElement(React.Fragment, null, React.createElement('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} )); +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-defaults/src/main.jsx b/integration-test/test-cases/cli-cases/respects-jsx-defaults/src/main.jsx new file mode 100644 index 00000000..a7272f21 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-defaults/src/main.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +function Foo() { + return <>
; +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-defaults/test.json b/integration-test/test-cases/cli-cases/respects-jsx-defaults/test.json new file mode 100644 index 00000000..d3e49c02 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-defaults/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms jsx" +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/dist-expected/main.js new file mode 100644 index 00000000..5cd5f076 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/dist-expected/main.js @@ -0,0 +1,4 @@ +const _jsxFileName = "src/main.tsx";import {jsxDEV as _jsxDEV} from "preact/jsx-dev-runtime"; +function Foo() { + return _jsxDEV('div', {}, void 0, false, {fileName: _jsxFileName, lineNumber: 3}, this ); +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/src/main.tsx b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/src/main.tsx new file mode 100644 index 00000000..da3f8440 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/src/main.tsx @@ -0,0 +1,4 @@ + +function Foo(): JSX.Element { + return
; +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/test.json b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/test.json new file mode 100644 index 00000000..4490aad6 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms jsx,typescript --jsx-runtime automatic --jsx-import-source preact" +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/dist-expected/main.js new file mode 100644 index 00000000..77c4f777 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/dist-expected/main.js @@ -0,0 +1,4 @@ + +function Foo() { + return h(React.Fragment, null, h('div', null )); +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/src/main.jsx b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/src/main.jsx new file mode 100644 index 00000000..f50392b1 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/src/main.jsx @@ -0,0 +1,4 @@ + +function Foo() { + return <>
; +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/test.json b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/test.json new file mode 100644 index 00000000..5c78c501 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms jsx --production --jsx-pragma h jsx-fragment-pragma Frag" +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/dist-expected/main.js new file mode 100644 index 00000000..e49be6e0 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/dist-expected/main.js @@ -0,0 +1,3 @@ +function Foo() { + return
; +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/src/main.tsx b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/src/main.tsx new file mode 100644 index 00000000..a7265e6d --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/src/main.tsx @@ -0,0 +1,3 @@ +function Foo(): JSX.Element { + return
; +} diff --git a/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/test.json b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/test.json new file mode 100644 index 00000000..6ac9442e --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms jsx,typescript --jsx-runtime preserve" +} diff --git a/integration-test/test-cases/cli-cases/respects-keep-unused-imports/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/dist-expected/main.js new file mode 100644 index 00000000..d5244311 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/dist-expected/main.js @@ -0,0 +1,4 @@ +import A from '../A'; +import B from '../B'; + +console.log(A); diff --git a/integration-test/test-cases/cli-cases/respects-keep-unused-imports/src/main.ts b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/src/main.ts new file mode 100644 index 00000000..d5244311 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/src/main.ts @@ -0,0 +1,4 @@ +import A from '../A'; +import B from '../B'; + +console.log(A); diff --git a/integration-test/test-cases/cli-cases/respects-keep-unused-imports/test.json b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/test.json new file mode 100644 index 00000000..e0789440 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-keep-unused-imports/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms typescript --keep-unused-imports" +} diff --git a/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/dist-expected/main.js b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/dist-expected/main.js new file mode 100644 index 00000000..d7377b6b --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/dist-expected/main.js @@ -0,0 +1,6 @@ +"use strict"; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _A = require('../A'); var _A2 = _interopRequireDefault(_A); + +async function foo() { + const B = (await import("../B")).default; + console.log(_A2.default + B); +} diff --git a/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/src/main.js b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/src/main.js new file mode 100644 index 00000000..55cfef29 --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/src/main.js @@ -0,0 +1,6 @@ +import A from "../A"; + +async function foo() { + const B = (await import("../B")).default; + console.log(A + B); +} diff --git a/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/test.json b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/test.json new file mode 100644 index 00000000..81c6576f --- /dev/null +++ b/integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/test.json @@ -0,0 +1,3 @@ +{ + "cliOptions": "--transforms imports --preserve-dynamic-import" +} diff --git a/integration-test/util/assertDirectoriesEqual.ts b/integration-test/util/assertDirectoriesEqual.ts new file mode 100644 index 00000000..d8c53721 --- /dev/null +++ b/integration-test/util/assertDirectoriesEqual.ts @@ -0,0 +1,27 @@ +import * as assert from "assert"; +import {readdir, readFile, stat} from "fs/promises"; +import {join} from "path"; + +export default async function assertDirectoriesEqual(dir1: string, dir2: string): Promise { + const dir1Files = (await readdir(dir1)).sort(); + const dir2Files = (await readdir(dir2)).sort(); + assert.strictEqual( + dir1Files.join(", "), + dir2Files.join(", "), + `Unexpected different lists of files for directories:\n${dir1}\n${dir2}`, + ); + for (const filename of dir1Files) { + const path1 = join(dir1, filename); + const path2 = join(dir2, filename); + const isDir1 = (await stat(path1)).isDirectory(); + const isDir2 = (await stat(path2)).isDirectory(); + assert.strictEqual(isDir1, isDir2, `Paths are different types:\n${path1}\n${path2}`); + if (isDir1) { + await assertDirectoriesEqual(path1, path2); + } else { + const contents1 = (await readFile(path1)).toString(); + const contents2 = (await readFile(path2)).toString(); + assert.strictEqual(contents1, contents2, `File contents differed:\n${path1}\n${path2}`); + } + } +} diff --git a/src/cli.ts b/src/cli.ts index 9035c1ea..d612f5f2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,16 +33,34 @@ export default function run(): void { ) .option("--out-extension ", "File extension to use for all output files.", "js") .option("--exclude-dirs ", "Names of directories that should not be traversed.") - .option("-t, --transforms ", "Comma-separated list of transforms to run.") .option("-q, --quiet", "Don't print the names of converted files.") + .option("-t, --transforms ", "Comma-separated list of transforms to run.") + .option("--disable-es-transforms", "Opt out of all ES syntax transforms.") + .option("--jsx-runtime ", "Transformation mode for the JSX transform.") + .option("--production", "Disable debugging information from JSX in output.") + .option( + "--jsx-import-source ", + "Automatic JSX transform import path prefix, defaults to `React.Fragment`.", + ) + .option( + "--jsx-pragma ", + "Classic JSX transform element creation function, defaults to `React.createElement`.", + ) + .option( + "--jsx-fragment-pragma ", + "Classic JSX transform fragment component, defaults to `React.Fragment`.", + ) + .option("--keep-unused-imports", "Disable automatic removal of type-only imports/exports.") + .option("--preserve-dynamic-import", "Don't transpile dynamic import() to require.") + .option( + "--inject-create-require-for-import-require", + "Use `createRequire` when transpiling TS `import = require` to ESM.", + ) .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.") - .option("--jsx-pragma ", "Element creation function, defaults to `React.createElement`") - .option("--jsx-fragment-pragma ", "Fragment component, defaults to `React.Fragment`") - .option("--production", "Disable debugging information from JSX in output.") .parse(process.argv); if (commander.project) { @@ -84,11 +102,17 @@ export default function run(): void { quiet: commander.quiet, sucraseOptions: { transforms: commander.transforms ? commander.transforms.split(",") : [], - enableLegacyTypeScriptModuleInterop: commander.enableLegacyTypescriptModuleInterop, - enableLegacyBabel5ModuleInterop: commander.enableLegacyBabel5ModuleInterop, + disableESTransforms: commander.disableEsTransforms, + jsxRuntime: commander.jsxRuntime, + production: commander.production, + jsxImportSource: commander.jsxImportSource, jsxPragma: commander.jsxPragma || "React.createElement", jsxFragmentPragma: commander.jsxFragmentPragma || "React.Fragment", - production: commander.production, + keepUnusedImports: commander.keepUnusedImports, + preserveDynamicImport: commander.preserveDynamicImport, + injectCreateRequireForImportRequire: commander.injectCreateRequireForImportRequire, + enableLegacyTypeScriptModuleInterop: commander.enableLegacyTypescriptModuleInterop, + enableLegacyBabel5ModuleInterop: commander.enableLegacyBabel5ModuleInterop, }, };