diff --git a/.prettierignore b/.prettierignore index 492a816c..b830eb82 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,11 @@ !/scripts !/src !/tests +!/website +/website/.docusaurus +/website/docs +/website/readme-sources +/website/static tests/main-realpath/symlink/tsconfig.json tests/throw error.ts tests/throw error react tsx.tsx diff --git a/README.md b/README.md index 90fb9f54..5eaea70a 100644 --- a/README.md +++ b/README.md @@ -153,11 +153,14 @@ ts-node-transpile-only script.ts # Equivalent to ts-node --cwdMode ts-node-cwd script.ts + +# Equivalent to ts-node --esm +ts-node-esm script.ts ``` ## Shebang -```typescript +```typescript twoslash #!/usr/bin/env ts-node console.log("Hello, world!") @@ -165,7 +168,7 @@ console.log("Hello, world!") Passing options via shebang requires the [`env -S` flag](https://manpages.debian.org/bullseye/coreutils/env.1.en.html#S), which is available on recent versions of `env`. ([compatibility](https://github.com/TypeStrong/ts-node/pull/1448#issuecomment-913895766)) -```typescript +```typescript twoslash #!/usr/bin/env -S ts-node --files // This shebang works on Mac and Linux with newer versions of env // Technically, Mac allows omitting `-S`, but Linux requires it @@ -173,7 +176,7 @@ Passing options via shebang requires the [`env -S` flag](https://manpages.debian To write scripts with maximum portability, [specify all options in your `tsconfig.json`](#via-tsconfigjson-recommended) and omit them from the shebang. -```typescript +```typescript twoslash #!/usr/bin/env ts-node // This shebang works everywhere ``` @@ -302,6 +305,7 @@ All command-line flags support both `--camelCase` and `--hyphen-case`. * `-e, --eval` Evaluate code * `-p, --print` Print result of `--eval` * `-i, --interactive` Opens the REPL even if stdin does not appear to be a terminal +* `--esm` Bootstrap with the ESM loader, enabling full ESM support ## TSConfig @@ -361,7 +365,7 @@ Here is a brief comparison of the two. | Write native `import` syntax | Write native `import` syntax | | Transforms `import` into `require()` | Does not transform `import` | | Node executes scripts using the classic [CommonJS loader](https://nodejs.org/dist/latest-v16.x/docs/api/modules.html) | Node executes scripts using the new [ESM loader](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html) | -| Use any of:
ts-node CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | +| Use any of:
`ts-node`
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Use any of:
`ts-node --esm`
`ts-node-esm`
Set `"esm": true` in `tsconfig.json`
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | ## CommonJS @@ -415,10 +419,36 @@ You must set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_ { "compilerOptions": { "module": "ESNext" // or ES2015, ES2020 + }, + "ts-node": { + // Tell ts-node CLI to install the --loader automatically, explained below + "esm": true } } ``` +You must also ensure node is passed `--loader`. The ts-node CLI will do this automatically with our `esm` option. + +> Note: `--esm` must spawn a child process to pass it `--loader`. This may change if node adds the ability to install loader hooks +> into the current process. + +```shell +# pass the flag +ts-node --esm +# Use the convenience binary +ts-node-esm +# or add `"esm": true` to your tsconfig.json to make it automatic +ts-node +``` + +If you are not using our CLI, pass the loader flag to node. + +```shell +node --loader ts-node/esm ./index.ts +# Or via environment variable +NODE_OPTIONS="--loader ts-node/esm" node ./index.ts +``` + # Troubleshooting ## Understanding configuration @@ -490,7 +520,7 @@ the [tsconfig `"target"` option](https://www.typescriptlang.org/tsconfig#target) For example, `node` 12 does not understand the `?.` optional chaining operator. If you use `"target": "esnext"`, then the following TypeScript syntax: -```typescript +```typescript twoslash const bar: string | undefined = foo?.bar; ``` @@ -606,7 +636,7 @@ Example project structure: Example module declaration file: -```typescript +```typescript twoslash declare module '' { // module definitions go here } @@ -614,7 +644,7 @@ declare module '' { For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): -```jsonc +```jsonc title="tsconfig.json" { "compilerOptions": { "baseUrl": ".", @@ -627,9 +657,11 @@ For module definitions, you can use [`paths`](https://www.typescriptlang.org/doc An alternative approach for definitions of third-party libraries are [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your TypeScript `compilerOptions` or structure your custom type definitions when using `typeRoots`. Below is an example of the triple-slash directive as a relative path within your project: -```typescript -/// -import UntypedJsLib from "untyped_js_lib" +```typescript twoslash +/// +import {Greeter} from "untyped_js_lib" +const g = new Greeter(); +g.sayHello(); ``` **Tip:** If you *must* use `files`, `include`, or `exclude`, enable `--files` flags or set `TS_NODE_FILES=true`. @@ -737,7 +769,7 @@ CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file ex The following example tells ts-node to execute a webpack config as CommonJS: -```jsonc title=tsconfig.json +```jsonc title="tsconfig.json" { "ts-node": { "transpileOnly": true, @@ -783,7 +815,7 @@ Assuming you are configuring AVA via your `package.json`, add one of the followi Use this configuration if your `package.json` does not have `"type": "module"`. -```jsonc title"package.json" +```jsonc title="package.json" { "ava": { "extensions": [ @@ -800,7 +832,7 @@ Use this configuration if your `package.json` does not have `"type": "module"`. This configuration is necessary if your `package.json` has `"type": "module"`. -```jsonc title"package.json" +```jsonc title="package.json" { "ava": { "extensions": { diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 8f125fb4..acde0be5 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { BaseError } from 'make-error'; import type * as _ts from 'typescript'; @@ -24,6 +26,7 @@ export interface CreateOptions { // @deprecated dir?: string; emit?: boolean; + esm?: boolean; experimentalReplAwait?: boolean; // (undocumented) fileExists?: (path: string) => boolean; @@ -32,6 +35,7 @@ export interface CreateOptions { ignoreDiagnostics?: Array; logError?: boolean; moduleTypes?: ModuleTypes; + preferTsExts?: boolean; pretty?: boolean; project?: string; projectSearchDir?: string; @@ -164,7 +168,6 @@ export const REGISTER_INSTANCE: unique symbol; // @public export interface RegisterOptions extends CreateOptions { experimentalResolverFeatures?: boolean; - preferTsExts?: boolean; } // @public (undocumented) @@ -345,7 +348,6 @@ export interface TypeInfo { // @public export const VERSION: any; - // (No @packageDocumentation comment for this package) ``` diff --git a/ava.config.cjs b/ava.config.cjs new file mode 100644 index 00000000..aa04b33b --- /dev/null +++ b/ava.config.cjs @@ -0,0 +1,40 @@ +const expect = require('expect'); +const { createRequire } = require('module'); + +module.exports = { + files: ['dist/test/**/*.spec.js'], + failWithoutAssertions: false, + environmentVariables: { + ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, + // Force jest expect() errors to generate colorized strings, makes output more readable. + // Delete the env var within ava processes via `require` option below. + // This avoids passing it to spawned processes under test, which would negatively affect + // their behavior. + FORCE_COLOR: '3', + }, + require: ['./src/test/remove-env-var-force-color.js'], + timeout: '300s', + concurrency: 1, +}; + +{ + /* + * Tests *must* install and use our most recent ts-node tarball. + * We must prevent them from accidentally require-ing a different version of + * ts-node, from either node_modules or tests/node_modules + */ + + const { existsSync } = require('fs'); + const rimraf = require('rimraf'); + const { resolve } = require('path'); + + remove(resolve(__dirname, 'node_modules/ts-node')); + remove(resolve(__dirname, 'tests/node_modules/ts-node')); + + // Prove that we did it correctly + expect(() => {createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node')}).toThrow(); + + function remove(p) { + if(existsSync(p)) rimraf.sync(p, {recursive: true}) + } +} diff --git a/ava.config.js b/ava.config.js deleted file mode 100644 index 6181565d..00000000 --- a/ava.config.js +++ /dev/null @@ -1,14 +0,0 @@ -export default { - files: ['dist/test/**/*.spec.js'], - failWithoutAssertions: false, - environmentVariables: { - ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, - // Force jest expect() errors to generate colorized strings, makes output more readable. - // Delete the env var within ava processes via `require` option below. - // This avoids passing it to spawned processes under test, which would negatively affect - // their behavior. - FORCE_COLOR: '3', - }, - require: ['./src/test/remove-env-var-force-color.js'], - timeout: '300s', -}; diff --git a/child-loader.mjs b/child-loader.mjs new file mode 100644 index 00000000..3a96eeea --- /dev/null +++ b/child-loader.mjs @@ -0,0 +1,7 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +const require = createRequire(fileURLToPath(import.meta.url)); + +/** @type {import('./dist/child-loader')} */ +const childLoader = require('./dist/child/child-loader'); +export const { resolve, load, getFormat, transformSource } = childLoader; diff --git a/dist-raw/node-esm-default-get-format.js b/dist-raw/node-esm-default-get-format.js index d8af956f..65b0bf6d 100644 --- a/dist-raw/node-esm-default-get-format.js +++ b/dist-raw/node-esm-default-get-format.js @@ -11,7 +11,11 @@ const { const { extname } = require('path'); const { getOptionValue } = require('./node-options'); -const experimentalJsonModules = getOptionValue('--experimental-json-modules'); +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(s => parseInt(s, 10)); +const experimentalJsonModules = + nodeMajor > 17 + || (nodeMajor === 17 && nodeMinor >= 5) + || getOptionValue('--experimental-json-modules'); const experimentalSpeciferResolution = getOptionValue('--experimental-specifier-resolution'); const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index ae3b8b91..21d8cfd1 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -1,7 +1,6 @@ module.exports = { ArrayFrom: Array.from, ArrayIsArray: Array.isArray, - ArrayPrototypeJoin: (obj, separator) => Array.prototype.join.call(obj, separator), ArrayPrototypeShift: (obj) => Array.prototype.shift.call(obj), ArrayPrototypeForEach: (arr, ...rest) => Array.prototype.forEach.apply(arr, rest), ArrayPrototypeIncludes: (arr, ...rest) => Array.prototype.includes.apply(arr, rest), diff --git a/package-lock.json b/package-lock.json index 3ad590ff..19319b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.6.0", + "version": "10.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -402,40 +402,25 @@ } }, "@microsoft/api-extractor": { - "version": "7.15.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.15.2.tgz", - "integrity": "sha512-/Y/n+QOc1vM6Vg3OAUByT/wXdZciE7jV3ay33+vxl3aKva5cNsuOauL14T7XQWUiLko3ilPwrcnFcEjzXpLsuA==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.4.tgz", + "integrity": "sha512-iehC6YA3DGJvxTUaK7HUtQmP6hAQU07+Q/OR8TG4dVR6KpqCi9UPEVk8AgCvQkiK+6FbVEFQTx0qLuYk4EeuHg==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.13.2", + "@microsoft/api-extractor-model": "7.15.3", "@microsoft/tsdoc": "0.13.2", "@microsoft/tsdoc-config": "~0.15.2", - "@rushstack/node-core-library": "3.38.0", - "@rushstack/rig-package": "0.2.12", - "@rushstack/ts-command-line": "4.7.10", + "@rushstack/node-core-library": "3.45.0", + "@rushstack/rig-package": "0.3.7", + "@rushstack/ts-command-line": "4.10.6", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.2.4" + "typescript": "~4.5.2" }, "dependencies": { - "colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", - "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", @@ -453,30 +438,18 @@ "requires": { "lru-cache": "^6.0.0" } - }, - "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, "@microsoft/api-extractor-model": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.2.tgz", - "integrity": "sha512-gA9Q8q5TPM2YYk7rLinAv9KqcodrmRC13BVmNzLswjtFxpz13lRh0BmrqD01/sddGpGMIuWFYlfUM4VSWxnggA==", + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.15.3.tgz", + "integrity": "sha512-NkSjolmSI7NGvbdz0Y7kjQfdpD+j9E5CwXTxEyjDqxd10MI7GXV8DnAsQ57GFJcgHKgTjf2aUnYfMJ9w3aMicw==", "dev": true, "requires": { "@microsoft/tsdoc": "0.13.2", "@microsoft/tsdoc-config": "~0.15.2", - "@rushstack/node-core-library": "3.38.0" + "@rushstack/node-core-library": "3.45.0" } }, "@microsoft/tsdoc": { @@ -551,12 +524,12 @@ } }, "@rushstack/node-core-library": { - "version": "3.38.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.38.0.tgz", - "integrity": "sha512-cmvl0yQx8sSmbuXwiRYJi8TO+jpTtrLJQ8UmFHhKvgPVJAW8cV8dnpD1Xx/BvTGrJZ2XtRAIkAhBS9okBnap4w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.0.tgz", + "integrity": "sha512-YMuIJl19vQT1+g/OU9mLY6T5ZBT9uDlmeXExDQACpGuxTJW+LHNbk/lRX+eCApQI2eLBlaL4U68r3kZlqwbdmw==", "dev": true, "requires": { - "@types/node": "10.17.13", + "@types/node": "12.20.24", "colors": "~1.2.1", "fs-extra": "~7.0.1", "import-lazy": "~4.0.0", @@ -564,56 +537,21 @@ "resolve": "~1.17.0", "semver": "~7.3.0", "timsort": "~0.3.0", - "z-schema": "~3.18.3" + "z-schema": "~5.0.2" }, "dependencies": { "@types/node": { - "version": "10.17.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", - "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "version": "12.20.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz", + "integrity": "sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==", "dev": true }, - "colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", - "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", - "dev": true - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, "import-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", @@ -631,25 +569,13 @@ "requires": { "lru-cache": "^6.0.0" } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, "@rushstack/rig-package": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.12.tgz", - "integrity": "sha512-nbePcvF8hQwv0ql9aeQxcaMPK/h1OLAC00W7fWCRWIvD2MchZOE8jumIIr66HGrfG2X1sw++m/ZYI4D+BM5ovQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.7.tgz", + "integrity": "sha512-pzMsTSeTC8IiZ6EJLr53gGMvhT4oLWH+hxD7907cHyWuIUlEXFtu/2pK25vUQT13nKp5DJCWxXyYoGRk/h6rtA==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -674,23 +600,15 @@ } }, "@rushstack/ts-command-line": { - "version": "4.7.10", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.10.tgz", - "integrity": "sha512-8t042g8eerypNOEcdpxwRA3uCmz0duMo21rG4Z2mdz7JxJeylDmzjlU3wDdef2t3P1Z61JCdZB6fbm1Mh0zi7w==", + "version": "4.10.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.6.tgz", + "integrity": "sha512-Y3GkUag39sTIlukDg9mUp8MCHrrlJ27POrBNRQGc/uF+VVgX8M7zMzHch5zP6O1QVquWgD7Engdpn2piPYaS/g==", "dev": true, "requires": { "@types/argparse": "1.0.38", "argparse": "~1.0.9", "colors": "~1.2.1", "string-argv": "~0.3.1" - }, - "dependencies": { - "colors": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", - "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", - "dev": true - } } }, "@sindresorhus/is": { @@ -1860,6 +1778,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2376,6 +2300,17 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3031,6 +2966,15 @@ "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", "dev": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -3131,6 +3075,15 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -4767,6 +4720,12 @@ "crypto-random-string": "^2.0.0" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "update-notifier": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", @@ -4873,9 +4832,9 @@ } }, "validator": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", - "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", "dev": true }, "vscode-oniguruma": { @@ -5021,6 +4980,12 @@ "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms=", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yargs": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", @@ -5056,15 +5021,15 @@ "integrity": "sha1-HodAGgnXZ8HV6rJqbkwYUYLS61A=" }, "z-schema": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", - "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", "dev": true, "requires": { "commander": "^2.7.1", - "lodash.get": "^4.0.0", - "lodash.isequal": "^4.0.0", - "validator": "^8.0.0" + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" } } } diff --git a/package.json b/package.json index d303ae90..1f158f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.6.0", + "version": "10.7.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { @@ -15,6 +15,8 @@ "./dist/bin-script.js": "./dist/bin-script.js", "./dist/bin-cwd": "./dist/bin-cwd.js", "./dist/bin-cwd.js": "./dist/bin-cwd.js", + "./dist/bin-esm": "./dist/bin-esm.js", + "./dist/bin-esm.js": "./dist/bin-esm.js", "./register": "./register/index.js", "./register/files": "./register/files.js", "./register/transpile-only": "./register/transpile-only.js", @@ -23,6 +25,7 @@ "./esm.mjs": "./esm.mjs", "./esm/transpile-only": "./esm/transpile-only.mjs", "./esm/transpile-only.mjs": "./esm/transpile-only.mjs", + "./child-loader.mjs": "./child-loader.mjs", "./transpilers/swc": "./transpilers/swc.js", "./transpilers/swc-experimental": "./transpilers/swc-experimental.js", "./node10/tsconfig.json": "./node10/tsconfig.json", @@ -33,10 +36,11 @@ "types": "dist/index.d.ts", "bin": { "ts-node": "dist/bin.js", - "ts-script": "dist/bin-script-deprecated.js", - "ts-node-script": "dist/bin-script.js", "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-transpile-only": "dist/bin-transpile.js" + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" }, "files": [ "/transpilers/", @@ -46,6 +50,7 @@ "/register/", "/esm/", "/esm.mjs", + "/child-loader.mjs", "/LICENSE", "/tsconfig.schema.json", "/tsconfig.schemastore-schema.json", @@ -105,7 +110,7 @@ }, "homepage": "https://typestrong.org/ts-node", "devDependencies": { - "@microsoft/api-extractor": "^7.15.2", + "@microsoft/api-extractor": "^7.19.4", "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/chai": "^4.0.4", @@ -171,7 +176,7 @@ "singleQuote": true }, "volta": { - "node": "16.9.1", + "node": "17.5.0", "npm": "6.14.15" } } diff --git a/src/bin-esm.ts b/src/bin-esm.ts new file mode 100644 index 00000000..3bc6bbbd --- /dev/null +++ b/src/bin-esm.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from './bin'; + +main(undefined, { '--esm': true }); diff --git a/src/bin.ts b/src/bin.ts index 13907f39..3e972dd9 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -3,7 +3,7 @@ import { join, resolve, dirname, parse as parsePath, relative } from 'path'; import { inspect } from 'util'; import Module = require('module'); -import arg = require('arg'); +let arg: typeof import('arg'); import { parse, createRequire, hasOwnProperty } from './util'; import { EVAL_FILENAME, @@ -17,17 +17,76 @@ import { STDIN_NAME, REPL_FILENAME, } from './repl'; -import { VERSION, TSError, register, versionGteLt } from './index'; +import { + VERSION, + TSError, + register, + versionGteLt, + createEsmHooks, + createFromPreloadedConfig, + DEFAULTS, +} from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; +import { callInChild } from './child/spawn-child'; +import { findAndReadConfig } from './configuration'; /** * Main `bin` functionality. + * + * This file is split into a chain of functions (phases), each one adding to a shared state object. + * This is done so that the next function can either be invoked in-process or, if necessary, invoked in a child process. + * + * The functions are intentionally given uncreative names and left in the same order as the original code, to make a + * smaller git diff. */ export function main( argv: string[] = process.argv.slice(2), entrypointArgs: Record = {} ) { + const args = parseArgv(argv, entrypointArgs); + const state: BootstrapState = { + shouldUseChildProcess: false, + isInChildProcess: false, + entrypoint: __filename, + parseArgvResult: args, + }; + return bootstrap(state); +} + +/** + * @internal + * Describes state of CLI bootstrapping. + * Can be marshalled when necessary to resume bootstrapping in a child process. + */ +export interface BootstrapState { + isInChildProcess: boolean; + shouldUseChildProcess: boolean; + entrypoint: string; + parseArgvResult: ReturnType; + phase2Result?: ReturnType; + phase3Result?: ReturnType; +} + +/** @internal */ +export function bootstrap(state: BootstrapState) { + if (!state.phase2Result) { + state.phase2Result = phase2(state); + if (state.shouldUseChildProcess && !state.isInChildProcess) { + return callInChild(state); + } + } + if (!state.phase3Result) { + state.phase3Result = phase3(state); + if (state.shouldUseChildProcess && !state.isInChildProcess) { + return callInChild(state); + } + } + return phase4(state); +} + +function parseArgv(argv: string[], entrypointArgs: Record) { + arg ??= require('arg'); // HACK: technically, this function is not marked @internal so it's possible // that libraries in the wild are doing `require('ts-node/dist/bin').main({'--transpile-only': true})` // We can mark this function @internal in next major release. @@ -58,6 +117,7 @@ export function main( '--scriptMode': Boolean, '--version': arg.COUNT, '--showConfig': Boolean, + '--esm': Boolean, // Project options. '--cwd': String, @@ -156,7 +216,51 @@ export function main( '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, + '--esm': esm, + _: restArgs, } = args; + return { + // Note: argv and restArgs may be overwritten by child process + argv: process.argv, + restArgs, + + cwdArg, + help, + scriptMode, + cwdMode, + version, + showConfig, + argsRequire, + code, + print, + interactive, + files, + compiler, + compilerOptions, + project, + ignoreDiagnostics, + ignore, + transpileOnly, + typeCheck, + transpiler, + swc, + compilerHost, + pretty, + skipProject, + skipIgnore, + preferTsExts, + logError, + emit, + scope, + scopeDir, + noExperimentalReplAwait, + esm, + }; +} + +function phase2(payload: BootstrapState) { + const { help, version, code, interactive, cwdArg, restArgs, esm } = + payload.parseArgvResult; if (help) { console.log(` @@ -169,13 +273,14 @@ Options: -r, --require [path] Require a node module before execution -i, --interactive Opens the REPL even if stdin does not appear to be a terminal + --esm Bootstrap with the ESM loader, enabling full ESM support + --swc Use the faster swc transpiler + -h, --help Print CLI usage - -v, --version Print module version information - --cwdMode Use current directory instead of for config resolution + -v, --version Print module version information. -vvv to print additional information --showConfig Print resolved configuration and exit -T, --transpileOnly Use TypeScript's faster \`transpileModule\` or a third-party transpiler - --swc Use the swc transpiler -H, --compilerHost Use TypeScript's compiler host API -I, --ignore [pattern] Override the path patterns to skip compilation -P, --project [path] Path to TypeScript JSON project file @@ -187,6 +292,7 @@ Options: --cwd Behave as if invoked within this working directory. --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup --pretty Use pretty diagnostic formatter (usually enabled by default) + --cwdMode Use current directory instead of for config resolution --skipProject Skip reading \`tsconfig.json\` --skipIgnore Skip \`--ignore\` checks --emit Emit output files into \`.ts-node\` directory @@ -209,8 +315,8 @@ Options: // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint // This is complicated because node's behavior is complicated // `node -e code -i ./script.js` ignores -e - const executeEval = code != null && !(interactive && args._.length); - const executeEntrypoint = !executeEval && args._.length > 0; + const executeEval = code != null && !(interactive && restArgs.length); + const executeEntrypoint = !executeEval && restArgs.length > 0; const executeRepl = !executeEntrypoint && (interactive || (process.stdin.isTTY && !executeEval)); @@ -218,8 +324,90 @@ Options: const cwd = cwdArg || process.cwd(); /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ - const scriptPath = executeEntrypoint ? resolve(cwd, args._[0]) : undefined; + const scriptPath = executeEntrypoint ? resolve(cwd, restArgs[0]) : undefined; + + if (esm) payload.shouldUseChildProcess = true; + return { + executeEval, + executeEntrypoint, + executeRepl, + executeStdin, + cwd, + scriptPath, + }; +} +function phase3(payload: BootstrapState) { + const { + emit, + files, + pretty, + transpileOnly, + transpiler, + noExperimentalReplAwait, + typeCheck, + swc, + compilerHost, + ignore, + preferTsExts, + logError, + scriptMode, + cwdMode, + project, + skipProject, + skipIgnore, + compiler, + ignoreDiagnostics, + compilerOptions, + argsRequire, + scope, + scopeDir, + } = payload.parseArgvResult; + const { cwd, scriptPath } = payload.phase2Result!; + + const preloadedConfig = findAndReadConfig({ + cwd, + emit, + files, + pretty, + transpileOnly: transpileOnly ?? transpiler != null ? true : undefined, + experimentalReplAwait: noExperimentalReplAwait ? false : undefined, + typeCheck, + transpiler, + swc, + compilerHost, + ignore, + logError, + projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), + project, + skipProject, + skipIgnore, + compiler, + ignoreDiagnostics, + compilerOptions, + require: argsRequire, + scope, + scopeDir, + preferTsExts, + }); + + if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; + return { preloadedConfig }; +} + +function phase4(payload: BootstrapState) { + const { isInChildProcess, entrypoint } = payload; + const { version, showConfig, restArgs, code, print, argv } = + payload.parseArgvResult; + const { + executeEval, + cwd, + executeStdin, + executeRepl, + executeEntrypoint, + scriptPath, + } = payload.phase2Result!; + const { preloadedConfig } = payload.phase3Result!; /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL * service to handle eval-ing of code. @@ -278,33 +466,22 @@ Options: } // Register the TypeScript compiler instance. - const service = register({ - cwd, - emit, - files, - pretty, - transpileOnly: transpileOnly ?? transpiler != null ? true : undefined, - experimentalReplAwait: noExperimentalReplAwait ? false : undefined, - typeCheck, - transpiler, - swc, - compilerHost, - ignore, - preferTsExts, - logError, - projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), - project, - skipProject, - skipIgnore, - compiler, - ignoreDiagnostics, - compilerOptions, - require: argsRequire, - readFile: evalAwarePartialHost?.readFile ?? undefined, - fileExists: evalAwarePartialHost?.fileExists ?? undefined, - scope, - scopeDir, + const service = createFromPreloadedConfig({ + // Since this struct may have been marshalled across thread or process boundaries, we must restore + // un-marshall-able values. + ...preloadedConfig, + options: { + ...preloadedConfig.options, + readFile: evalAwarePartialHost?.readFile ?? undefined, + fileExists: evalAwarePartialHost?.fileExists ?? undefined, + tsTrace: DEFAULTS.tsTrace, + }, }); + register(service); + if (isInChildProcess) + ( + require('./child/child-loader') as typeof import('./child/child-loader') + ).lateBindHooks(createEsmHooks(service)); // Bind REPL service to ts-node compiler service (chicken-and-egg problem) replStuff?.repl.setService(service); @@ -377,12 +554,13 @@ Options: // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( - __filename, - ...process.argv.slice(2, process.argv.length - args._.length) + entrypoint, + ...argv.slice(2, argv.length - restArgs.length) ); + // TODO this comes from BoostrapState process.argv = [process.argv[1]] .concat(executeEntrypoint ? ([scriptPath] as string[]) : []) - .concat(args._.slice(executeEntrypoint ? 1 : 0)); + .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); // Execute the main contents (either eval, script or piped). if (executeEntrypoint) { diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts new file mode 100644 index 00000000..03a02d2e --- /dev/null +++ b/src/child/child-entrypoint.ts @@ -0,0 +1,16 @@ +import { BootstrapState, bootstrap } from '../bin'; +import { brotliDecompressSync } from 'zlib'; + +const base64ConfigArg = process.argv[2]; +const argPrefix = '--brotli-base64-config='; +if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv'); +const base64Payload = base64ConfigArg.slice(argPrefix.length); +const payload = JSON.parse( + brotliDecompressSync(Buffer.from(base64Payload, 'base64')).toString() +) as BootstrapState; +payload.isInChildProcess = true; +payload.entrypoint = __filename; +payload.parseArgvResult.argv = process.argv; +payload.parseArgvResult.restArgs = process.argv.slice(3); + +bootstrap(payload); diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts new file mode 100644 index 00000000..0ac01813 --- /dev/null +++ b/src/child/child-loader.ts @@ -0,0 +1,34 @@ +// TODO same version check as ESM loader, but export stubs +// Also export a binder function that allows re-binding where the stubs +// delegate. + +import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from '..'; +import { filterHooksByAPIVersion } from '../esm'; + +let hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; + +/** @internal */ +export function lateBindHooks( + _hooks: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 +) { + hooks = _hooks as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; +} + +const proxy: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 = { + resolve(...args: Parameters) { + return (hooks?.resolve ?? args[2])(...args); + }, + load(...args: Parameters) { + return (hooks?.load ?? args[2])(...args); + }, + getFormat(...args: Parameters) { + return (hooks?.getFormat ?? args[2])(...args); + }, + transformSource(...args: Parameters) { + return (hooks?.transformSource ?? args[2])(...args); + }, +}; + +/** @internal */ +export const { resolve, load, getFormat, transformSource } = + filterHooksByAPIVersion(proxy) as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; diff --git a/src/child/child-require.ts b/src/child/child-require.ts new file mode 100644 index 00000000..2ee15522 --- /dev/null +++ b/src/child/child-require.ts @@ -0,0 +1,27 @@ +interface EventEmitterInternals { + _events: Record>; +} +const _process = process as any as EventEmitterInternals; + +// Not shown here: Additional logic to correctly interact with process's events, either using this direct manipulation, or via the API + +let originalOnWarning: Function | undefined; +if (Array.isArray(_process._events.warning)) { + originalOnWarning = _process._events.warning[0]; + _process._events.warning[0] = onWarning; +} else { + originalOnWarning = _process._events.warning; + _process._events.warning = onWarning; +} + +const messageMatch = /--(?:experimental-)?loader\b/; +function onWarning(this: any, warning: Error, ...rest: any[]) { + // Suppress warning about how `--loader` is experimental + if ( + warning?.name === 'ExperimentalWarning' && + messageMatch.test(warning?.message) + ) + return; + // Will be undefined if `--no-warnings` + return originalOnWarning?.call(this, warning, ...rest); +} diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts new file mode 100644 index 00000000..74bf4017 --- /dev/null +++ b/src/child/spawn-child.ts @@ -0,0 +1,51 @@ +import type { BootstrapState } from '../bin'; +import { spawn } from 'child_process'; +import { brotliCompressSync } from 'zlib'; +import { pathToFileURL } from 'url'; +import { versionGteLt } from '..'; + +const argPrefix = '--brotli-base64-config='; + +/** @internal */ +export function callInChild(state: BootstrapState) { + if (!versionGteLt(process.versions.node, '12.17.0')) { + throw new Error( + '`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.' + ); + } + const child = spawn( + process.execPath, + [ + '--require', + require.resolve('./child-require.js'), + '--loader', + // Node on Windows doesn't like `c:\` absolute paths here; must be `file:///c:/` + pathToFileURL(require.resolve('../../child-loader.mjs')).toString(), + require.resolve('./child-entrypoint.js'), + `${argPrefix}${brotliCompressSync( + Buffer.from(JSON.stringify(state), 'utf8') + ).toString('base64')}`, + ...state.parseArgvResult.restArgs, + ], + { + stdio: 'inherit', + argv0: process.argv0, + } + ); + child.on('error', (error) => { + console.error(error); + process.exit(1); + }); + child.on('exit', (code) => { + child.removeAllListeners(); + process.off('SIGINT', sendSignalToChild); + process.off('SIGTERM', sendSignalToChild); + process.exitCode = code === null ? 1 : code; + }); + // Ignore sigint and sigterm in parent; pass them to child + process.on('SIGINT', sendSignalToChild); + process.on('SIGTERM', sendSignalToChild); + function sendSignalToChild(signal: string) { + process.kill(child.pid, signal); + } +} diff --git a/src/configuration.ts b/src/configuration.ts index ff38ddd4..13b7ad28 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -4,13 +4,19 @@ import { CreateOptions, DEFAULTS, OptionBasePaths, + RegisterOptions, TSCommon, TsConfigOptions, } from './index'; import type { TSInternal } from './ts-compiler-types'; import { createTsInternals } from './ts-internals'; import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; -import { assign, createProjectLocalResolveHelper } from './util'; +import { + assign, + attemptRequireWithV8CompileCache, + createProjectLocalResolveHelper, + getBasePathForProjectLocalDependencyResolution, +} from './util'; /** * TypeScript compiler option values required by `ts-node` which cannot be overridden. @@ -49,6 +55,68 @@ function fixConfig(ts: TSCommon, config: _ts.ParsedCommandLine) { return config; } +/** @internal */ +export function findAndReadConfig(rawOptions: CreateOptions) { + const cwd = resolve( + rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd() + ); + const compilerName = rawOptions.compiler ?? DEFAULTS.compiler; + + // Compute minimum options to read the config file. + let projectLocalResolveDir = getBasePathForProjectLocalDependencyResolution( + undefined, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ); + let { compiler, ts } = resolveAndLoadCompiler( + compilerName, + projectLocalResolveDir + ); + + // Read config file and merge new options between env and CLI options. + const { configFilePath, config, tsNodeOptionsFromTsconfig, optionBasePaths } = + readConfig(cwd, ts, rawOptions); + + const options = assign( + {}, + DEFAULTS, + tsNodeOptionsFromTsconfig || {}, + { optionBasePaths }, + rawOptions + ); + options.require = [ + ...(tsNodeOptionsFromTsconfig.require || []), + ...(rawOptions.require || []), + ]; + + // Re-resolve the compiler in case it has changed. + // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a + // different compiler than we did above, even if the name has not changed. + if (configFilePath) { + projectLocalResolveDir = getBasePathForProjectLocalDependencyResolution( + configFilePath, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ); + ({ compiler } = resolveCompiler( + options.compiler, + optionBasePaths.compiler ?? projectLocalResolveDir + )); + } + + return { + options, + config, + projectLocalResolveDir, + optionBasePaths, + configFilePath, + cwd, + compiler, + }; +} + /** * Load TypeScript configuration. Returns the parsed TypeScript config and * any `ts-node` options specified in the config file. @@ -193,6 +261,9 @@ export function readConfig( if (options.compiler != null) { optionBasePaths.compiler = basePath; } + if (options.swc != null) { + optionBasePaths.swc = basePath; + } assign(tsNodeOptionsFromTsconfig, options); } @@ -255,6 +326,32 @@ export function readConfig( }; } +/** + * Load the typescript compiler. It is required to load the tsconfig but might + * be changed by the tsconfig, so we have to do this twice. + * @internal + */ +export function resolveAndLoadCompiler( + name: string | undefined, + relativeToPath: string +) { + const { compiler } = resolveCompiler(name, relativeToPath); + const ts = loadCompiler(compiler); + return { compiler, ts }; +} + +function resolveCompiler(name: string | undefined, relativeToPath: string) { + const projectLocalResolveHelper = + createProjectLocalResolveHelper(relativeToPath); + const compiler = projectLocalResolveHelper(name || 'typescript', true); + return { compiler }; +} + +/** @internal */ +export function loadCompiler(compiler: string): TSCommon { + return attemptRequireWithV8CompileCache(require, compiler); +} + /** * Given the raw "ts-node" sub-object from a tsconfig, return an object with only the properties * recognized by "ts-node" @@ -286,6 +383,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { experimentalReplAwait, swc, experimentalResolverFeatures, + esm, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -310,6 +408,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { moduleTypes, swc, experimentalResolverFeatures, + esm, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = diff --git a/src/esm.ts b/src/esm.ts index b42af64b..38f4f0d5 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -95,6 +95,31 @@ export type NodeLoaderHooksFormat = | 'module' | 'wasm'; +export type NodeImportConditions = unknown; +export interface NodeImportAssertions { + type?: 'json'; +} + +// The hooks API changed in node version X so we need to check for backwards compatibility. +// TODO: When the new API is backported to v12, v14, update these version checks accordingly. +const newHooksAPI = + versionGteLt(process.versions.node, '17.0.0') || + versionGteLt(process.versions.node, '16.12.0', '17.0.0') || + versionGteLt(process.versions.node, '14.999.999', '15.0.0') || + versionGteLt(process.versions.node, '12.999.999', '13.0.0'); + +/** @internal */ +export function filterHooksByAPIVersion( + hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 +): NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 { + const { getFormat, load, resolve, transformSource } = hooks; + // Explicit return type to avoid TS's non-ideal inferred type + const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI + ? { resolve, load, getFormat: undefined, transformSource: undefined } + : { resolve, getFormat, transformSource, load: undefined }; + return hooksAPI; +} + /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` @@ -112,18 +137,12 @@ export function createEsmHooks(tsNodeService: Service) { preferTsExts: tsNodeService.options.preferTsExts, }); - // The hooks API changed in node version X so we need to check for backwards compatibility. - // TODO: When the new API is backported to v12, v14, update these version checks accordingly. - const newHooksAPI = - versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.12.0', '17.0.0') || - versionGteLt(process.versions.node, '14.999.999', '15.0.0') || - versionGteLt(process.versions.node, '12.999.999', '13.0.0'); - - // Explicit return type to avoid TS's non-ideal inferred type - const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI - ? { resolve, load, getFormat: undefined, transformSource: undefined } - : { resolve, getFormat, transformSource, load: undefined }; + const hooksAPI = filterHooksByAPIVersion({ + resolve, + load, + getFormat, + transformSource, + }); function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` diff --git a/src/index.ts b/src/index.ts index ffb85810..996556a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,23 @@ -import { relative, basename, extname, resolve, dirname, join } from 'path'; +import { relative, basename, extname, dirname, join } from 'path'; import { Module } from 'module'; import * as util from 'util'; import { fileURLToPath } from 'url'; -import sourceMapSupport = require('@cspotcode/source-map-support'); +import type * as _sourceMapSupport from '@cspotcode/source-map-support'; import { BaseError } from 'make-error'; import type * as _ts from 'typescript'; import type { Transpiler, TranspilerFactory } from './transpilers/types'; import { - assign, - attemptRequireWithV8CompileCache, cachedLookup, createProjectLocalResolveHelper, - getBasePathForProjectLocalDependencyResolution, normalizeSlashes, parse, ProjectLocalResolveHelper, split, yn, } from './util'; -import { readConfig } from './configuration'; +import { findAndReadConfig, loadCompiler } from './configuration'; import type { TSCommon, TSInternal } from './ts-compiler-types'; import { createModuleTypeClassifier, @@ -369,6 +366,18 @@ export interface CreateOptions { * @default console.log */ tsTrace?: (str: string) => void; + /** + * TODO DOCS YAY + */ + esm?: boolean; + /** + * Re-order file extensions so that TypeScript imports are preferred. + * + * For example, when both `index.js` and `index.ts` exist, enabling this option causes `require('./index')` to resolve to `index.ts` instead of `index.js` + * + * @default false + */ + preferTsExts?: boolean; } export type ModuleTypes = Record; @@ -378,21 +387,13 @@ export interface OptionBasePaths { moduleTypes?: string; transpiler?: string; compiler?: string; + swc?: string; } /** * Options for registering a TypeScript compiler instance globally. */ export interface RegisterOptions extends CreateOptions { - /** - * Re-order file extensions so that TypeScript imports are preferred. - * - * For example, when both `index.js` and `index.ts` exist, enabling this option causes `require('./index')` to resolve to `index.ts` instead of `index.js` - * - * @default false - */ - preferTsExts?: boolean; - /** * Enable experimental features that re-map imports and require calls to support: * `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, @@ -592,63 +593,29 @@ export function register( * Create TypeScript compiler instance. */ export function create(rawOptions: CreateOptions = {}): Service { - const cwd = resolve( - rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd() - ); - const compilerName = rawOptions.compiler ?? DEFAULTS.compiler; - - /** - * Load the typescript compiler. It is required to load the tsconfig but might - * be changed by the tsconfig, so we have to do this twice. - */ - function loadCompiler(name: string | undefined, relativeToPath: string) { - const projectLocalResolveHelper = - createProjectLocalResolveHelper(relativeToPath); - const compiler = projectLocalResolveHelper(name || 'typescript', true); - const ts: TSCommon = attemptRequireWithV8CompileCache(require, compiler); - return { compiler, ts, projectLocalResolveHelper }; - } + const foundConfigResult = findAndReadConfig(rawOptions); + return createFromPreloadedConfig(foundConfigResult); +} - // Compute minimum options to read the config file. - let { compiler, ts, projectLocalResolveHelper } = loadCompiler( - compilerName, - getBasePathForProjectLocalDependencyResolution( - undefined, - rawOptions.projectSearchDir, - rawOptions.project, - cwd - ) - ); +/** @internal */ +export function createFromPreloadedConfig( + foundConfigResult: ReturnType +): Service { + const { + configFilePath, + cwd, + options, + config, + compiler, + projectLocalResolveDir, + optionBasePaths, + } = foundConfigResult; - // Read config file and merge new options between env and CLI options. - const { configFilePath, config, tsNodeOptionsFromTsconfig, optionBasePaths } = - readConfig(cwd, ts, rawOptions); - const options = assign( - {}, - DEFAULTS, - tsNodeOptionsFromTsconfig || {}, - { optionBasePaths }, - rawOptions + const projectLocalResolveHelper = createProjectLocalResolveHelper( + projectLocalResolveDir ); - options.require = [ - ...(tsNodeOptionsFromTsconfig.require || []), - ...(rawOptions.require || []), - ]; - // Re-load the compiler in case it has changed. - // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a - // different compiler than we did above, even if the name has not changed. - if (configFilePath) { - ({ compiler, ts, projectLocalResolveHelper } = loadCompiler( - options.compiler, - getBasePathForProjectLocalDependencyResolution( - configFilePath, - rawOptions.projectSearchDir, - rawOptions.project, - cwd - ) - )); - } + const ts = loadCompiler(compiler); // Experimental REPL await is not compatible targets lower than ES2018 const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018; @@ -692,11 +659,15 @@ export function create(rawOptions: CreateOptions = {}): Service { const transpileOnly = (options.transpileOnly === true || options.swc === true) && options.typeCheck !== true; - const transpiler = options.transpiler - ? options.transpiler - : options.swc - ? require.resolve('./transpilers/swc.js') - : undefined; + let transpiler: RegisterOptions['transpiler'] | undefined = undefined; + let transpilerBasePath: string | undefined = undefined; + if (options.transpiler) { + transpiler = options.transpiler; + transpilerBasePath = optionBasePaths.transpiler; + } else if (options.swc) { + transpiler = require.resolve('./transpilers/swc.js'); + transpilerBasePath = optionBasePaths.swc; + } const transformers = options.transformers || undefined; const diagnosticFilters: Array = [ { @@ -763,7 +734,13 @@ export function create(rawOptions: CreateOptions = {}): Service { typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - const transpilerPath = projectLocalResolveHelper(transpilerName, true); + const transpilerConfigLocalResolveHelper = transpilerBasePath + ? createProjectLocalResolveHelper(transpilerBasePath) + : projectLocalResolveHelper; + const transpilerPath = transpilerConfigLocalResolveHelper( + transpilerName, + true + ); const transpilerFactory = require(transpilerPath) .create as TranspilerFactory; createTranspiler = function (compilerOptions) { @@ -776,6 +753,7 @@ export function create(rawOptions: CreateOptions = {}): Service { }, projectLocalResolveHelper, }, + transpilerConfigLocalResolveHelper, ...transpilerOptions, }); }; @@ -793,6 +771,8 @@ export function create(rawOptions: CreateOptions = {}): Service { // Install source map support and read from memory cache. installSourceMapSupport(); function installSourceMapSupport() { + const sourceMapSupport = + require('@cspotcode/source-map-support') as typeof _sourceMapSupport; sourceMapSupport.install({ environment: 'node', retrieveFile(pathOrUrl: string) { diff --git a/src/repl.ts b/src/repl.ts index 41776e12..c6371bdb 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -1,4 +1,4 @@ -import { diffLines } from 'diff'; +import type * as _diff from 'diff'; import { homedir } from 'os'; import { join } from 'path'; import { @@ -26,6 +26,13 @@ function getProcessTopLevelAwait() { } return _processTopLevelAwait; } +let diff: typeof _diff; +function getDiffLines() { + if (diff === undefined) { + diff = require('diff'); + } + return diff.diffLines; +} /** @internal */ export const EVAL_FILENAME = `[eval].ts`; @@ -544,7 +551,7 @@ function appendCompileAndEvalInput(options: { ); // Use `diff` to check for new JavaScript to execute. - const changes = diffLines( + const changes = getDiffLines()( oldOutputWithoutSourcemapComment, outputWithoutSourcemapComment ); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index d4a94379..cc673e54 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -5,18 +5,22 @@ import { context } from './testlib'; import semver = require('semver'); import { + BIN_ESM_PATH, BIN_PATH, + BIN_PATH_JS, CMD_ESM_LOADER_WITHOUT_PROJECT, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, contextTsNodeUnderTest, + delay, EXPERIMENTAL_MODULES_FLAG, nodeSupportsEsmHooks, nodeSupportsImportAssertions, + nodeSupportsSpawningChildProcess, nodeUsesNewHooksApi, resetNodeEnvironment, TEST_DIR, } from './helpers'; -import { createExec } from './exec-helpers'; +import { createExec, createSpawn, ExecReturn } from './exec-helpers'; import { join, resolve } from 'path'; import * as expect from 'expect'; import type { NodeLoaderHooksAPI2 } from '../'; @@ -27,6 +31,9 @@ const test = context(contextTsNodeUnderTest); const exec = createExec({ cwd: TEST_DIR, }); +const spawn = createSpawn({ + cwd: TEST_DIR, +}); test.suite('esm', (test) => { test.suite('when node supports loader hooks', (test) => { @@ -261,17 +268,38 @@ test.suite('esm', (test) => { test.suite('supports import assertions', (test) => { test.runIf(nodeSupportsImportAssertions); - test('Can import JSON using the appropriate flag and assertion', async (t) => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-json-modules ./importJson.ts`, - { - cwd: resolve(TEST_DIR, 'esm-import-assertions'), - } - ); - expect(err).toBe(null); - expect(stdout.trim()).toBe( - 'A fuchsia car has 2 seats and the doors are open.\nDone!' - ); + test.suite('node >=17.5.0', (test) => { + test.runIf(semver.gte(process.version, '17.5.0')); + + test('Can import JSON modules with appropriate assertion', async (t) => { + const { err, stdout } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./importJson.ts`, + { + cwd: resolve(TEST_DIR, 'esm-import-assertions'), + } + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe( + 'A fuchsia car has 2 seats and the doors are open.\nDone!' + ); + }); + }); + + test.suite('node <17.5.0', (test) => { + test.runIf(semver.lt(process.version, '17.5.0')); + + test('Can import JSON using the appropriate flag and assertion', async (t) => { + const { err, stdout } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-json-modules ./importJson.ts`, + { + cwd: resolve(TEST_DIR, 'esm-import-assertions'), + } + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe( + 'A fuchsia car has 2 seats and the doors are open.\nDone!' + ); + }); }); }); @@ -301,6 +329,81 @@ test.suite('esm', (test) => { }); } ); + + test.suite('spawns child process', async (test) => { + test.runIf(nodeSupportsSpawningChildProcess); + + basic('ts-node-esm executable', () => + exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts foo bar`) + ); + basic('ts-node --esm flag', () => + exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts foo bar`) + ); + basic('ts-node w/tsconfig esm:true', () => + exec( + `${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts foo bar` + ) + ); + + function basic(title: string, cb: () => ExecReturn) { + test(title, async (t) => { + const { err, stdout, stderr } = await cb(); + expect(err).toBe(null); + expect(stdout.trim()).toBe('CLI args: foo bar'); + expect(stderr).toBe(''); + }); + } + + test.suite('parent passes signals to child', (test) => { + test.runSerially(); + + signalTest('SIGINT'); + signalTest('SIGTERM'); + + function signalTest(signal: string) { + test(signal, async (t) => { + const childP = spawn([ + // exec lets us run the shims on windows; spawn does not + process.execPath, + BIN_PATH_JS, + `./esm-child-process/via-tsconfig/sleep.ts`, + ]); + let code: number | null | undefined = undefined; + childP.child.on('exit', (_code) => (code = _code)); + await delay(6e3); + const codeAfter6Seconds = code; + process.kill(childP.child.pid, signal); + await delay(2e3); + const codeAfter8Seconds = code; + const { stdoutP, stderrP } = await childP; + const stdout = await stdoutP; + const stderr = await stderrP; + t.log({ + stdout, + stderr, + codeAfter6Seconds, + codeAfter8Seconds, + code, + }); + expect(codeAfter6Seconds).toBeUndefined(); + if (process.platform === 'win32') { + // Windows doesn't have signals, and node attempts an imperfect facsimile. + // In Windows, SIGINT and SIGTERM kill the process immediately with exit + // code 1, and the process can't catch or prevent this. + expect(codeAfter8Seconds).toBe(1); + expect(code).toBe(1); + } else { + expect(codeAfter8Seconds).toBe(undefined); + expect(code).toBe(123); + expect(stdout.trim()).toBe( + `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` + ); + } + expect(stderr).toBe(''); + }); + } + }); + }); }); test.suite('node >= 12.x.x', (test) => { diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index fc70f0e3..bf076647 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -1,5 +1,14 @@ -import type { ChildProcess, ExecException, ExecOptions } from 'child_process'; -import { exec as childProcessExec } from 'child_process'; +import type { + ChildProcess, + ExecException, + ExecOptions, + SpawnOptions, +} from 'child_process'; +import { + exec as childProcessExec, + spawn as childProcessSpawn, +} from 'child_process'; +import { getStream } from './helpers'; import { expect } from './testlib'; export type ExecReturn = Promise & { child: ChildProcess }; @@ -44,6 +53,52 @@ export function createExec>( }; } +export type SpawnReturn = Promise & { child: ChildProcess }; +export interface SpawnResult { + stdoutP: Promise; + stderrP: Promise; + code: number | null; + child: ChildProcess; +} + +export function createSpawn>( + preBoundOptions?: T +) { + /** + * Helper to spawn a child process. + * Returns a Promise and a reference to the child process to suite multiple situations. + * + * Should almost always avoid this helper, and instead use `createExec` / `exec`. `spawn` + * may be necessary if you need to avoid `exec`'s intermediate shell. + */ + return function spawn( + cmd: string[], + opts?: Pick> & + Partial> + ) { + let child!: ChildProcess; + return Object.assign( + new Promise((resolve, reject) => { + child = childProcessSpawn(cmd[0], cmd.slice(1), { + ...preBoundOptions, + ...opts, + }); + const stdoutP = getStream(child.stdout!); + const stderrP = getStream(child.stderr!); + child.on('exit', (code) => { + resolve({ stdoutP, stderrP, code, child }); + }); + child.on('error', (error) => { + reject(error); + }); + }), + { + child, + } + ); + }; +} + const defaultExec = createExec(); export interface ExecTesterOptions { diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 5327459b..cddf7575 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -17,13 +17,7 @@ import semver = require('semver'); const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; -export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); -export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); -export const nodeSupportsImportAssertions = semver.gte( - process.version, - '17.1.0' -); - +//#region Paths export const ROOT_DIR = resolve(__dirname, '../..'); export const DIST_DIR = resolve(__dirname, '..'); export const TEST_DIR = join(__dirname, '../../tests'); @@ -35,6 +29,10 @@ export const BIN_SCRIPT_PATH = join( 'node_modules/.bin/ts-node-script' ); export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); +export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); +//#endregion + +//#region command lines /** Default `ts-node --project` invocation */ export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; /** Default `ts-node` invocation without `--project` */ @@ -43,12 +41,33 @@ export const EXPERIMENTAL_MODULES_FLAG = semver.gte(process.version, '12.17.0') ? '' : '--experimental-modules'; export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} --loader ts-node/esm`; +//#endregion // `createRequire` does not exist on older node versions export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); export const ts = testsDirRequire('typescript'); +//#region version checks +export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); +export const nodeSupportsSpawningChildProcess = semver.gte( + process.version, + '12.17.0' +); +export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); +export const nodeSupportsImportAssertions = semver.gte( + process.version, + '17.1.0' +); +/** Supports tsconfig "extends" >= v3.2.0 */ +export const tsSupportsTsconfigInheritanceViaNodePackages = semver.gte( + ts.version, + '3.2.0' +); +/** Supports --showConfig: >= v3.2.0 */ +export const tsSupportsShowConfig = semver.gte(ts.version, '3.2.0'); +//#endregion + export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ @@ -60,6 +79,7 @@ export const contextTsNodeUnderTest = once(async () => { }; }); +//#region install ts-node tarball const ts_node_install_lock = process.env.ts_node_install_lock as string; const lockPath = join(__dirname, ts_node_install_lock); @@ -128,6 +148,7 @@ async function lockedMemoizedOperation( releaseLock(); } } +//#endregion /** * Get a stream into a string. @@ -165,6 +186,8 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { } } +//#region Reset node environment + const defaultRequireExtensions = captureObjectState(require.extensions); const defaultProcess = captureObjectState(process); const defaultModule = captureObjectState(require('module')); @@ -224,3 +247,7 @@ function resetObject( // Reset descriptors Object.defineProperties(object, state.descriptors); } + +//#endregion + +export const delay = promisify(setTimeout); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 5487a7b6..72faf86e 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -3,7 +3,13 @@ import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; import { tmpdir } from 'os'; import semver = require('semver'); -import { BIN_PATH_JS, nodeSupportsEsmHooks, ts } from './helpers'; +import { + BIN_PATH_JS, + nodeSupportsEsmHooks, + ts, + tsSupportsShowConfig, + tsSupportsTsconfigInheritanceViaNodePackages, +} from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; @@ -167,31 +173,29 @@ test.suite('ts-node', (test) => { expect(stdout).toBe('object\n'); }); - if (semver.gte(ts.version, '1.8.0')) { - test('should allow js', async () => { - const { err, stdout } = await exec( - [ - CMD_TS_NODE_WITH_PROJECT_FLAG, - '-O "{\\"allowJs\\":true}"', - '-pe "import { main } from \'./allow-js/run\';main()"', - ].join(' ') - ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); - }); + test('should allow js', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"allowJs\\":true}"', + '-pe "import { main } from \'./allow-js/run\';main()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); - test('should include jsx when `allow-js` true', async () => { - const { err, stdout } = await exec( - [ - CMD_TS_NODE_WITH_PROJECT_FLAG, - '-O "{\\"allowJs\\":true}"', - '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', - ].join(' ') - ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); - }); - } + test('should include jsx when `allow-js` true', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"allowJs\\":true}"', + '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); test('should eval code', async () => { const { err, stdout } = await exec( @@ -501,21 +505,16 @@ test.suite('ts-node', (test) => { }); test.suite('issue #884', (test) => { + // TODO disabled because it consistently fails on Windows on TS 2.7 + test.skipIf( + process.platform === 'win32' && semver.satisfies(ts.version, '2.7') + ); test('should compile', async (t) => { - // TODO disabled because it consistently fails on Windows on TS 2.7 - if ( - process.platform === 'win32' && - semver.satisfies(ts.version, '2.7') - ) { - t.log('Skipping'); - return; - } else { - const { err, stdout } = await exec( - `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` - ); - expect(err).toBe(null); - expect(stdout).toBe(''); - } + const { err, stdout } = await exec( + `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` + ); + expect(err).toBe(null); + expect(stdout).toBe(''); }); }); @@ -706,7 +705,7 @@ test.suite('ts-node', (test) => { ]); }); - if (semver.gte(ts.version, '3.2.0')) { + if (tsSupportsTsconfigInheritanceViaNodePackages) { test('should pull ts-node options from extended `tsconfig.json`', async () => { const { err, stdout } = await exec( `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` @@ -810,33 +809,33 @@ test.suite('ts-node', (test) => { } ); - if (semver.gte(ts.version, '3.2.0')) { - test.suite( - 'should bundle @tsconfig/bases to be used in your own tsconfigs', - (test) => { - const macro = test.macro((nodeVersion: string) => async (t) => { - const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --showConfig -e 10n`, - { - cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), - } - ); - expect(err).toBe(null); - t.like(JSON.parse(stdout), { - compilerOptions: { - target: config.compilerOptions.target, - lib: config.compilerOptions.lib, - }, - }); + test.suite( + 'should bundle @tsconfig/bases to be used in your own tsconfigs', + (test) => { + test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); + + const macro = test.macro((nodeVersion: string) => async (t) => { + const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --showConfig -e 10n`, + { + cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), + } + ); + expect(err).toBe(null); + t.like(JSON.parse(stdout), { + compilerOptions: { + target: config.compilerOptions.target, + lib: config.compilerOptions.lib, + }, }); - test(`ts-node/node10/tsconfig.json`, macro, 'node10'); - test(`ts-node/node12/tsconfig.json`, macro, 'node12'); - test(`ts-node/node14/tsconfig.json`, macro, 'node14'); - test(`ts-node/node16/tsconfig.json`, macro, 'node16'); - } - ); - } + }); + test(`ts-node/node10/tsconfig.json`, macro, 'node10'); + test(`ts-node/node12/tsconfig.json`, macro, 'node12'); + test(`ts-node/node14/tsconfig.json`, macro, 'node14'); + test(`ts-node/node16/tsconfig.json`, macro, 'node16'); + } + ); test.suite('compiler host', (test) => { test('should execute cli', async () => { @@ -896,7 +895,7 @@ test.suite('ts-node', (test) => { }); }); - if (semver.gte(ts.version, '3.2.0')) { + if (tsSupportsShowConfig) { test('--showConfig should log resolved configuration', async (t) => { function native(path: string) { return path.replace(/\/|\\/g, pathSep); diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts new file mode 100644 index 00000000..95504351 --- /dev/null +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -0,0 +1,98 @@ +import { context } from './testlib'; +import { + contextTsNodeUnderTest, + resetNodeEnvironment, + tsSupportsTsconfigInheritanceViaNodePackages, +} from './helpers'; +import * as expect from 'expect'; +import { resolve } from 'path'; + +const test = context(contextTsNodeUnderTest); + +test.suite( + 'Pluggable dependency (compiler, transpiler, swc backend) is require()d relative to the tsconfig file that declares it', + (test) => { + test.runSerially(); + + // The use-case we want to support: + // + // User shares their tsconfig across multiple projects as an npm module named "shared-config", similar to @tsconfig/bases + // In their npm module + // They have tsconfig.json with `swc: true` or `compiler: "ts-patch"` or something like that + // The module declares a dependency on a known working version of @swc/core, or ts-patch, or something like that. + // They use this reusable config via `npm install shared-config` and `"extends": "shared-config/tsconfig.json"` + // + // ts-node should resolve ts-patch or @swc/core relative to the extended tsconfig + // to ensure we use the known working versions. + + const macro = _macro.bind(null, test); + + macro('tsconfig-custom-compiler.json', 'root custom compiler'); + macro('tsconfig-custom-transpiler.json', 'root custom transpiler'); + macro('tsconfig-swc-custom-backend.json', 'root custom swc backend'); + macro('tsconfig-swc-core.json', 'root @swc/core'); + macro('tsconfig-swc-wasm.json', 'root @swc/wasm'); + macro('tsconfig-swc.json', 'root @swc/core'); + + macro( + 'node_modules/shared-config/tsconfig-custom-compiler.json', + 'shared-config custom compiler' + ); + macro( + 'node_modules/shared-config/tsconfig-custom-transpiler.json', + 'shared-config custom transpiler' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-custom-backend.json', + 'shared-config custom swc backend' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-core.json', + 'shared-config @swc/core' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-wasm.json', + 'shared-config @swc/wasm' + ); + macro( + 'node_modules/shared-config/tsconfig-swc.json', + 'shared-config @swc/core' + ); + + test.suite('"extends"', (test) => { + test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); + + const macro = _macro.bind(null, test); + + macro( + 'tsconfig-extend-custom-compiler.json', + 'shared-config custom compiler' + ); + macro( + 'tsconfig-extend-custom-transpiler.json', + 'shared-config custom transpiler' + ); + macro( + 'tsconfig-extend-swc-custom-backend.json', + 'shared-config custom swc backend' + ); + macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); + macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); + macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); + }); + + function _macro(_test: typeof test, config: string, expected: string) { + _test(`${config} uses ${expected}`, async (t) => { + t.teardown(resetNodeEnvironment); + + const output = t.context.tsNodeUnderTest + .create({ + project: resolve('tests/pluggable-dep-resolution', config), + }) + .compile('', 'index.ts'); + + expect(output).toContain(`emit from ${expected}\n`); + }); + } + } +); diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 377d93ef..6304164b 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -19,6 +19,19 @@ export { ExecutionContext, expect }; // each .spec file in its own process, so actual concurrency is higher. const concurrencyLimiter = throat(16); +function errorPostprocessor(fn: T): T { + return async function (this: any) { + try { + return await fn.call(this, arguments); + } catch (error: any) { + delete error?.matcherResult; + // delete error?.matcherResult?.message; + if (error?.message) error.message = `\n${error.message}\n`; + throw error; + } + } as any; +} + function once(func: T): T { let run = false; let ret: any = undefined; @@ -35,7 +48,8 @@ export const test = createTestInterface({ mustDoSerial: false, automaticallyDoSerial: false, automaticallySkip: false, - separator: ' > ', + // The little right chevron used by ava + separator: ' \u203a ', titlePrefix: undefined, }); // In case someone wants to `const test = _test.context()` @@ -101,6 +115,8 @@ export interface TestInterface< skipUnless(conditional: boolean): void; /** If conditional is true, run tests, otherwise skip them */ runIf(conditional: boolean): void; + /** If conditional is false, skip tests */ + skipIf(conditional: boolean): void; // TODO add teardownEach } @@ -167,14 +183,16 @@ function createTestInterface(opts: { ) { const wrappedMacros = macros.map((macro) => { return async function (t: ExecutionContext, ...args: any[]) { - return concurrencyLimiter(async () => { - let i = 0; - for (const func of beforeEachFunctions) { - await func(t); - i++; - } - return macro(t, ...args); - }); + return concurrencyLimiter( + errorPostprocessor(async () => { + let i = 0; + for (const func of beforeEachFunctions) { + await func(t); + i++; + } + return macro(t, ...args); + }) + ); }; }); const computedTitle = computeTitle(title); @@ -270,5 +288,8 @@ function createTestInterface(opts: { assertOrderingForDeclaringSkipUnless(); automaticallySkip = automaticallySkip || !runIfTrue; }; + test.skipIf = function (skipIfTrue: boolean) { + test.runIf(!skipIfTrue); + }; return test as any; } diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 23949595..1d8d1c44 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -16,22 +16,23 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { const { swc, service: { config, projectLocalResolveHelper }, + transpilerConfigLocalResolveHelper, } = createOptions; // Load swc compiler let swcInstance: typeof swcWasm; if (typeof swc === 'string') { - swcInstance = require(projectLocalResolveHelper( + swcInstance = require(transpilerConfigLocalResolveHelper( swc, true )) as typeof swcWasm; } else if (swc == null) { let swcResolved; try { - swcResolved = projectLocalResolveHelper('@swc/core', true); + swcResolved = transpilerConfigLocalResolveHelper('@swc/core', true); } catch (e) { try { - swcResolved = projectLocalResolveHelper('@swc/wasm', true); + swcResolved = transpilerConfigLocalResolveHelper('@swc/wasm', true); } catch (e) { throw new Error( 'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers' diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index ab524cbd..641deb4c 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -1,5 +1,6 @@ import type * as ts from 'typescript'; import type { Service } from '../index'; +import type { ProjectLocalResolveHelper } from '../util'; /** * Third-party transpilers are implemented as a CommonJS module with a @@ -21,6 +22,13 @@ export interface CreateTranspilerOptions { Service, Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service> >; + /** + * If `"transpiler"` option is declared in an "extends" tsconfig, this path might be different than + * the `projectLocalResolveHelper` + * + * @internal + */ + transpilerConfigLocalResolveHelper: ProjectLocalResolveHelper; } export interface Transpiler { // TODOs diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts new file mode 100644 index 00000000..939272be --- /dev/null +++ b/tests/esm-child-process/via-flag/index.ts @@ -0,0 +1,3 @@ +import { strictEqual } from 'assert'; +strictEqual(import.meta.url.includes('index.ts'), true); +console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); diff --git a/tests/esm-child-process/via-flag/package.json b/tests/esm-child-process/via-flag/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/tests/esm-child-process/via-flag/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/via-flag/tsconfig.json b/tests/esm-child-process/via-flag/tsconfig.json new file mode 100644 index 00000000..25a7642a --- /dev/null +++ b/tests/esm-child-process/via-flag/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ESNext", + "esModuleInterop": true + }, + "ts-node": { + "swc": true + } +} diff --git a/tests/esm-child-process/via-tsconfig/index.ts b/tests/esm-child-process/via-tsconfig/index.ts new file mode 100644 index 00000000..939272be --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/index.ts @@ -0,0 +1,3 @@ +import { strictEqual } from 'assert'; +strictEqual(import.meta.url.includes('index.ts'), true); +console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); diff --git a/tests/esm-child-process/via-tsconfig/package.json b/tests/esm-child-process/via-tsconfig/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/via-tsconfig/sleep.ts b/tests/esm-child-process/via-tsconfig/sleep.ts new file mode 100644 index 00000000..f45b9dad --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/sleep.ts @@ -0,0 +1,13 @@ +setTimeout(function () { + console.log('Slept 30 seconds'); +}, 30e3); +process.on('SIGTERM', onSignal); +process.on('SIGINT', onSignal); +console.log('child registered signal handlers'); +function onSignal(signal: string) { + console.log(`child received signal: ${signal}`); + setTimeout(() => { + console.log(`child exiting`); + process.exit(123); + }, 5e3); +} diff --git a/tests/esm-child-process/via-tsconfig/tsconfig.json b/tests/esm-child-process/via-tsconfig/tsconfig.json new file mode 100644 index 00000000..31f702b8 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "ESNext", + "esModuleInterop": true + }, + "ts-node": { + "esm": true, + "swc": true + } +} diff --git a/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js b/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js new file mode 100644 index 00000000..b9924d5f --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root @swc/core', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js b/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js new file mode 100644 index 00000000..f149018f --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root @swc/wasm', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js new file mode 100644 index 00000000..806376ab --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js @@ -0,0 +1,9 @@ +module.exports = { + ...require('typescript'), + transpileModule() { + return { + outputText: 'emit from root custom compiler', + sourceMapText: '{}', + }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js b/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js new file mode 100644 index 00000000..e2390743 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root custom swc backend', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js b/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js new file mode 100644 index 00000000..ed3d1cb2 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js @@ -0,0 +1,10 @@ +module.exports.create = function () { + return { + transpile() { + return { + outputText: 'emit from root custom transpiler', + sourceMapText: '{}', + }; + }, + }; + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js new file mode 100644 index 00000000..ee65ccdd --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config @swc/core', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js new file mode 100644 index 00000000..7b4a479e --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config @swc/wasm', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js new file mode 100644 index 00000000..b1a45e62 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js @@ -0,0 +1,9 @@ +module.exports = { + ...require('typescript'), + transpileModule() { + return { + outputText: 'emit from shared-config custom compiler', + sourceMapText: '{}', + }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js new file mode 100644 index 00000000..9d69e702 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config custom swc backend', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js new file mode 100644 index 00000000..d8ca0d3f --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js @@ -0,0 +1,10 @@ +module.exports.create = function () { + return { + transpile() { + return { + outputText: 'emit from shared-config custom transpiler', + sourceMapText: '{}', + }; + }, + }; + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json new file mode 100644 index 00000000..926d5498 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"compiler":"custom-compiler"}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json new file mode 100644 index 00000000..bb64bd1f --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":"custom-transpiler"}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json new file mode 100644 index 00000000..c4191aec --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"@swc/core"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json new file mode 100644 index 00000000..c23cd162 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"custom-swc"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json new file mode 100644 index 00000000..94d91973 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"@swc/wasm"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json new file mode 100644 index 00000000..430482e8 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json @@ -0,0 +1 @@ +{"ts-node":{"swc":true}} diff --git a/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json b/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json new file mode 100644 index 00000000..12f1bfe6 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json @@ -0,0 +1 @@ +{ "ts-node": { "transpileOnly": true, "compiler": "custom-compiler" } } diff --git a/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json b/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json new file mode 100644 index 00000000..c2339a1e --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json @@ -0,0 +1 @@ +{ "ts-node": { "transpileOnly": true, "transpiler": "custom-transpiler" } } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json b/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json new file mode 100644 index 00000000..674b908e --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-custom-compiler.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json b/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json new file mode 100644 index 00000000..afe9b5d7 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-custom-transpiler.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json new file mode 100644 index 00000000..4ad6e1a8 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-core.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json new file mode 100644 index 00000000..c28b49a1 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-custom-backend.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json new file mode 100644 index 00000000..8acee239 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-wasm.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc.json new file mode 100644 index 00000000..29827a78 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-core.json b/tests/pluggable-dep-resolution/tsconfig-swc-core.json new file mode 100644 index 00000000..8e33432e --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-core.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "@swc/core" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json b/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json new file mode 100644 index 00000000..7a3d2442 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "custom-swc" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json b/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json new file mode 100644 index 00000000..bfa5a0eb --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "@swc/wasm" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc.json b/tests/pluggable-dep-resolution/tsconfig-swc.json new file mode 100644 index 00000000..9f129531 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc.json @@ -0,0 +1 @@ +{ "ts-node": { "swc": true } } diff --git a/website/docs/imports.md b/website/docs/imports.md index 6b04f776..4a0ea5c7 100644 --- a/website/docs/imports.md +++ b/website/docs/imports.md @@ -11,7 +11,7 @@ Here is a brief comparison of the two. | Write native `import` syntax | Write native `import` syntax | | Transforms `import` into `require()` | Does not transform `import` | | Node executes scripts using the classic [CommonJS loader](https://nodejs.org/dist/latest-v16.x/docs/api/modules.html) | Node executes scripts using the new [ESM loader](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html) | -| Use any of:
ts-node CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | +| Use any of:
`ts-node`
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Use any of:
`ts-node --esm`
`ts-node-esm`
Set `"esm": true` in `tsconfig.json`
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | ## CommonJS @@ -65,6 +65,32 @@ You must set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_ { "compilerOptions": { "module": "ESNext" // or ES2015, ES2020 + }, + "ts-node": { + // Tell ts-node CLI to install the --loader automatically, explained below + "esm": true } } ``` + +You must also ensure node is passed `--loader`. The ts-node CLI will do this automatically with our `esm` option. + +> Note: `--esm` must spawn a child process to pass it `--loader`. This may change if node adds the ability to install loader hooks +into the current process. + +```shell +# pass the flag +ts-node --esm +# Use the convenience binary +ts-node-esm +# or add `"esm": true` to your tsconfig.json to make it automatic +ts-node +``` + +If you are not using our CLI, pass the loader flag to node. + +```shell +node --loader ts-node/esm ./index.ts +# Or via environment variable +NODE_OPTIONS="--loader ts-node/esm" node ./index.ts +``` diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index 43ab19f5..05222ce2 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -17,7 +17,7 @@ CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file ex The following example tells ts-node to execute a webpack config as CommonJS: -```json title=tsconfig.json +```json title="tsconfig.json" { "ts-node": { "transpileOnly": true, diff --git a/website/docs/options.md b/website/docs/options.md index aadf36da..96e49c1e 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -15,6 +15,7 @@ _Environment variables, where available, are in `ALL_CAPS`_ - `-e, --eval` Evaluate code - `-p, --print` Print result of `--eval` - `-i, --interactive` Opens the REPL even if stdin does not appear to be a terminal +- `--esm` Bootstrap with the ESM loader, enabling full ESM support ## TSConfig diff --git a/website/docs/recipes/ava.md b/website/docs/recipes/ava.md index 945008ec..83eda744 100644 --- a/website/docs/recipes/ava.md +++ b/website/docs/recipes/ava.md @@ -8,7 +8,7 @@ Assuming you are configuring AVA via your `package.json`, add one of the followi Use this configuration if your `package.json` does not have `"type": "module"`. -```json title"package.json" +```json title="package.json" { "ava": { "extensions": [ @@ -25,7 +25,7 @@ Use this configuration if your `package.json` does not have `"type": "module"`. This configuration is necessary if your `package.json` has `"type": "module"`. -```json title"package.json" +```json title="package.json" { "ava": { "extensions": { diff --git a/website/docs/troubleshooting.md b/website/docs/troubleshooting.md index 2e2125c1..d007bcac 100644 --- a/website/docs/troubleshooting.md +++ b/website/docs/troubleshooting.md @@ -71,7 +71,10 @@ the [tsconfig `"target"` option](https://www.typescriptlang.org/tsconfig#target) For example, `node` 12 does not understand the `?.` optional chaining operator. If you use `"target": "esnext"`, then the following TypeScript syntax: -```typescript +```typescript twoslash +export {}; +var foo: {bar: string} | undefined; +// ---cut--- const bar: string | undefined = foo?.bar; ``` diff --git a/website/docs/types.md b/website/docs/types.md index eaf425a9..cb1df0be 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -28,7 +28,7 @@ Example project structure: Example module declaration file: -```typescript +```typescript twoslash declare module '' { // module definitions go here } @@ -36,7 +36,7 @@ declare module '' { For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): -```json +```json title="tsconfig.json" { "compilerOptions": { "baseUrl": ".", @@ -49,9 +49,11 @@ For module definitions, you can use [`paths`](https://www.typescriptlang.org/doc An alternative approach for definitions of third-party libraries are [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your TypeScript `compilerOptions` or structure your custom type definitions when using `typeRoots`. Below is an example of the triple-slash directive as a relative path within your project: -```typescript -/// -import UntypedJsLib from "untyped_js_lib" +```typescript twoslash +/// +import {Greeter} from "untyped_js_lib" +const g = new Greeter(); +g.sayHello(); ``` **Tip:** If you _must_ use `files`, `include`, or `exclude`, enable `--files` flags or set `TS_NODE_FILES=true`. diff --git a/website/docs/usage.md b/website/docs/usage.md index d988e606..9b62841f 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -25,11 +25,14 @@ ts-node-transpile-only script.ts # Equivalent to ts-node --cwdMode ts-node-cwd script.ts + +# Equivalent to ts-node --esm +ts-node-esm script.ts ``` ## Shebang -```typescript +```typescript twoslash #!/usr/bin/env ts-node console.log("Hello, world!") @@ -37,7 +40,7 @@ console.log("Hello, world!") Passing options via shebang requires the [`env -S` flag](https://manpages.debian.org/bullseye/coreutils/env.1.en.html#S), which is available on recent versions of `env`. ([compatibility](https://github.com/TypeStrong/ts-node/pull/1448#issuecomment-913895766)) -```typescript +```typescript twoslash #!/usr/bin/env -S ts-node --files // This shebang works on Mac and Linux with newer versions of env // Technically, Mac allows omitting `-S`, but Linux requires it @@ -45,7 +48,7 @@ Passing options via shebang requires the [`env -S` flag](https://manpages.debian To write scripts with maximum portability, [specify all options in your `tsconfig.json`](./configuration#via-tsconfigjson-recommended) and omit them from the shebang. -```typescript +```typescript twoslash #!/usr/bin/env ts-node // This shebang works everywhere ``` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 109116d2..8c33ffcb 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -19,7 +19,7 @@ module.exports = { // //isCloseable: false, // Defaults to `true`. // }, colorMode: { - respectPrefersColorScheme: true + respectPrefersColorScheme: true, }, navbar: { title: 'ts-node', @@ -61,6 +61,20 @@ module.exports = { }, ], }, + metadata: [ + { + name: 'msapplication-TileColor', + content: '#2b5797', + }, + { + name: 'msapplication-config', + content: '/ts-node/img/favicon/browserconfig.xml', + }, + { + name: 'theme-color', + content: '#ffffff', + }, + ], // footer: { // style: 'dark', // links: [ @@ -99,23 +113,14 @@ module.exports = { // // copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`, // }, prism: { - // for syntax highlighting - // additionalLanguages: ['powershell'], + // Note: these themes are ignored due to using shiki-twoslash + theme: require('prism-react-renderer/themes/vsLight'), + darkTheme: require('prism-react-renderer/themes/vsDark'), }, algolia: { - apiKey: 'c882a0a136ef4e15aa99db604280caa6', + appId: 'BYGNLKSCOV', + apiKey: '74ac2b781b0cf603c2f1b5e4f44e1c69', indexName: 'ts-node', - - // Optional: see doc section below - // contextualSearch: true, - - // Optional: see doc section below - // appId: 'YOUR_APP_ID', - - // Optional: Algolia search parameters - // searchParameters: {}, - - //... other Algolia params }, }, presets: [ @@ -124,19 +129,67 @@ module.exports = { { docs: { sidebarPath: require.resolve('./sidebars.js'), - editUrl: - 'https://github.com/TypeStrong/ts-node/edit/docs/website/', + editUrl: 'https://github.com/TypeStrong/ts-node/edit/docs/website/', }, - // blog: { - // showReadingTime: true, - // // Please change this to your repo. - // editUrl: - // 'https://github.com/facebook/docusaurus/edit/master/website/blog/', - // }, + blog: false, theme: { customCss: require.resolve('./src/css/custom.css'), }, }, ], + [ + 'docusaurus-preset-shiki-twoslash', + { + // https://github.com/shikijs/twoslash/blob/main/packages/shiki-twoslash/README.md#user-settings + + // langs: ["shell", "typescript", "javascript", "ts", "js", "tsx", "jsx", "json", "jsonc"], + includeJSDocInHover: true, + + themes: ['github-light', 'nord'], + + // VSCode default + // themes: ["light-plus", "dark-plus"], + + // Other options + // themes: ["min-light", "nord"], + // themes: ["min-light", "min-dark"], + // themes: ["github-light", "github-dark"], + // themes: ["solarized-light", "solarized-dark"], + }, + ], + ], + // Misleading API that probably will be refactored in Docusaurus, but this is + // simply a list of tags + stylesheets: [ + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/ts-node/img/favicon/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/ts-node/img/favicon/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/ts-node/img/favicon/favicon-16x16.png', + }, + { + rel: 'manifest', + href: '/ts-node/img/favicon/site.webmanifest', + }, + { + rel: 'mask-icon', + href: '/ts-node/img/favicon/safari-pinned-tab.svg', + color: '#5bbad5', + }, + { + rel: 'shortcut icon', + href: '/ts-node/img/favicon/favicon.ico', + }, ], }; diff --git a/website/package.json b/website/package.json index 6c3851cd..c3545ea8 100644 --- a/website/package.json +++ b/website/package.json @@ -13,15 +13,15 @@ "build-readme": "./scripts/build-readme.mjs" }, "dependencies": { - "@docusaurus/core": "2.0.0-alpha.75", - "@docusaurus/preset-classic": "2.0.0-alpha.75", - "@docusaurus/theme-search-algolia": "^2.0.0-alpha.75", - "@mdx-js/react": "^1.6.21", + "@docusaurus/core": "2.0.0-beta.17", + "@docusaurus/preset-classic": "2.0.0-beta.17", + "@mdx-js/react": "^1.6.22", "@types/js-yaml": "^4.0.0", "clsx": "^1.1.1", + "docusaurus-preset-shiki-twoslash": "^1.1.36", "js-yaml": "^4.0.0", - "react": "^16.8.4", - "react-dom": "^16.8.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", "remark": "^13.0.0", "remark-behead": "^2.3.3", "remark-frontmatter": "^3.0.0", @@ -42,5 +42,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "packageManager": "yarn@1.22.17", + "volta": { + "extends": "../package.json", + "yarn": "1.22.17" } } diff --git a/website/scripts/build-readme.mjs b/website/scripts/build-readme.mjs index b7cb22c4..50842776 100755 --- a/website/scripts/build-readme.mjs +++ b/website/scripts/build-readme.mjs @@ -25,7 +25,7 @@ const readmePath = Path.resolve(__root, 'README.md'); const generateReadmeHeadersForCategories = { General: false, Advanced: true, - Recipes: true + Recipes: true, }; import sidebars from '../sidebars.js'; @@ -35,42 +35,50 @@ async function main() { await appendMarkdownFileToReadmeAst({ path: 'readme-sources/prefix.md', - headerLevel: 1 + headerLevel: 1, }); const sidebar = sidebars.primarySidebar; - for(const category of sidebar) { - const generateReadmeHeader = generateReadmeHeadersForCategories[category.label]; - if(generateReadmeHeader) { + for (const category of sidebar) { + const generateReadmeHeader = + generateReadmeHeadersForCategories[category.label]; + if (generateReadmeHeader) { readmeNodes.push(headerNode(1, category.label)); - } else if(generateReadmeHeader == null) { - throw new Error(`Update ${ import.meta.url } to include all sidebar categories`); + } else if (generateReadmeHeader == null) { + throw new Error( + `Update ${import.meta.url} to include all sidebar categories` + ); } - for(const page of category.items) { + for (const page of category.items) { await appendMarkdownFileToReadmeAst({ - path: `docs/${ page }.md`, - headerLevel: 1 + !!generateReadmeHeader + path: `docs/${page}.md`, + headerLevel: 1 + !!generateReadmeHeader, }); } } appendMarkdownFileToReadmeAst({ path: 'readme-sources/license.md', - headerLevel: 1 + headerLevel: 1, }); - async function appendMarkdownFileToReadmeAst({path, headerLevel}) { + async function appendMarkdownFileToReadmeAst({ path, headerLevel }) { const absPath = Path.resolve(__websiteRoot, path); - console.log(`Appending ${ path } at header level ${ headerLevel }`); + console.log(`Appending ${path} at header level ${headerLevel}`); const markdownSource = fs.readFileSync(absPath, 'utf8'); await remark() .use(remarkFrontmatter, ['yaml']) .use(parseFrontmatter) .use(remarkBehead, { after: '', depth: headerLevel - 1 }) .use(() => (ast) => { - const {frontmatter} = ast; - if(frontmatter && !frontmatter.omitHeaderOnMerge) { - readmeNodes.push(headerNode(headerLevel, frontmatter && frontmatter.title || Path.basename(absPath))); + const { frontmatter } = ast; + if (frontmatter && !frontmatter.omitHeaderOnMerge) { + readmeNodes.push( + headerNode( + headerLevel, + (frontmatter && frontmatter.title) || Path.basename(absPath) + ) + ); } readmeNodes.push(...ast.children); }) @@ -84,73 +92,93 @@ async function main() { .use(codeLanguageJsonToJsonc) .use(rewritePageLinksToAnchorLinks) .use(rewriteImgTargets) - .use(remarkToc, {tight: true}) - .process(vfile({ - path: readmePath, - contents: '' - })); + .use(trimCutFromTwoslashCode) + .use(remarkToc, { tight: true }) + .process( + vfile({ + path: readmePath, + contents: '', + }) + ); console.error(vfileReporter(renderedReadme)); - if(renderedReadme.messages.length) throw new Error('Aborting on diagnostics.'); + if (renderedReadme.messages.length) + throw new Error('Aborting on diagnostics.'); const lintResults = await remark() .use(remarkValidateLinks) .use(remarkRecommended) .process(renderedReadme); console.error(vfileReporter(lintResults)); - if(lintResults.messages.length) throw new Error('Aborting on diagnostics.'); + if (lintResults.messages.length) throw new Error('Aborting on diagnostics.'); fs.writeFileSync(readmePath, renderedReadme.contents); } function parseFrontmatter() { return (ast) => { - if(ast.children[0].type === 'yaml') { + if (ast.children[0].type === 'yaml') { ast.frontmatter = jsYaml.load(ast.children[0].value); ast.children.splice(0, 1); } - } + }; } function codeLanguageJsonToJsonc() { return (ast) => { - visit(ast, 'code', node => { - if(node.lang === 'json') node.lang = 'jsonc'; - }) - } + visit(ast, 'code', (node) => { + if (node.lang === 'json') node.lang = 'jsonc'; + }); + }; } function rewritePageLinksToAnchorLinks() { return (ast) => { - visit(ast, 'link', node => { - if(node.url?.match?.(/^https?\:\/\//)) return; + visit(ast, 'link', (node) => { + if (node.url?.match?.(/^https?\:\/\//)) return; // TODO take page title into account node.url = node.url.replace(/^[\.\/]*(?:([^#]+)|.*#(.*))$/, '#$1$2'); node.url = node.url.replace(/\.md$/, ''); }); - } + }; } function rewriteImgTargets() { return (ast) => { - visit(ast, 'image', node => { + visit(ast, 'image', (node) => { node.url = node.url.replace(/^\//, 'website/static/'); }); - } + }; +} + +function trimCutFromTwoslashCode() { + return (ast) => { + // Strip everything above // ---cut--- in twoslash code blocks + const lookingFor = '\n// ---cut---\n'; + visit(ast, 'code', (node) => { + if (node.meta?.includes('twoslash') && node.value.includes(lookingFor)) { + node.value = node.value.slice( + node.value.lastIndexOf(lookingFor) + lookingFor.length + ); + } + }); + }; } function headerNode(depth, value) { return { type: 'heading', depth, - children: [{ - type: 'text', - value, - children: [] - }] + children: [ + { + type: 'text', + value, + children: [], + }, + ], }; } try { await main(); -} catch(e) { +} catch (e) { console.error(e.message); process.exitCode = 1; } diff --git a/website/sidebars.js b/website/sidebars.js index 4f1d70e0..4e3dbd9f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -1,51 +1,55 @@ module.exports = { - primarySidebar: [{ - type: 'category', - label: 'General', - collapsed: false, - items: [ - 'overview', - 'installation', - 'usage', - 'configuration', - 'options', - 'imports', - 'troubleshooting', - 'performance', - ] - }, { - type: 'category', - label: 'Advanced', - collapsed: false, - items: [ - 'how-it-works', - 'paths', - 'types', - 'compilers', - 'transpilers', - 'module-type-overrides' - ], - }, { - type: 'category', - label: 'Recipes', - collapsed: false, - items: [ - 'recipes/watching-and-restarting', - 'recipes/ava', - 'recipes/gulp', - 'recipes/intellij', - 'recipes/mocha', - 'recipes/tape', - 'recipes/visual-studio-code', - 'recipes/other' - ] - }], - hiddenSidebar: [{ - type: 'category', - label: 'Hidden pages', - collapsed: false, - items: [ - 'options-table', - ] - }], + primarySidebar: [ + { + type: 'category', + label: 'General', + collapsed: false, + items: [ + 'overview', + 'installation', + 'usage', + 'configuration', + 'options', + 'imports', + 'troubleshooting', + 'performance', + ], + }, + { + type: 'category', + label: 'Advanced', + collapsed: false, + items: [ + 'how-it-works', + 'paths', + 'types', + 'compilers', + 'transpilers', + 'module-type-overrides', + ], + }, + { + type: 'category', + label: 'Recipes', + collapsed: false, + items: [ + 'recipes/watching-and-restarting', + 'recipes/ava', + 'recipes/gulp', + 'recipes/intellij', + 'recipes/mocha', + 'recipes/tape', + 'recipes/visual-studio-code', + 'recipes/other', + ], + }, + ], + hiddenSidebar: [ + { + type: 'category', + label: 'Hidden pages', + collapsed: false, + items: ['options-table'], + }, + ], }; diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 9b7e4d80..49c27214 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -33,3 +33,24 @@ margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); } + +/* Hide the external link icon */ +.navbar__item svg { + display: none; +} + +/* + * Shiki-twoslash + * See: https://github.com/shikijs/twoslash/tree/main/packages/docusaurus-preset-shiki-twoslash + */ +[data-theme='light'] .shiki.nord, +[data-theme='light'] .shiki.min-dark, +[data-theme='light'] .shiki.github-dark, +[data-theme='light'] .shiki.dark-plus, +[data-theme='light'] .shiki.solarized-dark, +[data-theme='dark'] .shiki.min-light, +[data-theme='dark'] .shiki.github-light, +[data-theme='dark'] .shiki.light-plus, +[data-theme='dark'] .shiki.solarized-light { + display: none; +} diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 9a50c6e3..dcb410ad 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -2,12 +2,11 @@ import React from 'react'; import clsx from 'clsx'; import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; -import Head from '@docusaurus/Head'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useBaseUrl from '@docusaurus/useBaseUrl'; import styles from './styles.module.css'; -function Feature({imageUrl, title, description}) { +function Feature({ imageUrl, title, description }) { const imgUrl = useBaseUrl(imageUrl); return (
@@ -22,38 +21,28 @@ function Feature({imageUrl, title, description}) { function Home() { const context = useDocusaurusContext(); - const {siteConfig = {}} = context; + const { siteConfig = {} } = context; return ( - - - - - - - - - - - - +

{siteConfig.title}

{siteConfig.tagline}

+ to={useBaseUrl('docs/')} + > Get Started