diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e279668f4..4bd5898c5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -48,7 +48,7 @@ jobs: matrix: os: [ubuntu, windows] # Don't forget to add all new flavors to this list! - flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] include: # Node 12.15 # TODO Add comments about why we test 12.15; I think git blame says it's because of an ESM behavioral change that happened at 12.16 @@ -94,32 +94,68 @@ jobs: typescript: next typescriptFlag: next # Node 16 + # Node 16.11.1 + # Earliest version that supports old ESM Loader Hooks API: https://github.com/TypeStrong/ts-node/pull/1522 - flavor: 8 + node: 16.11.1 + nodeFlag: 16_11_1 + typescript: latest + typescriptFlag: latest + - flavor: 9 node: 16 nodeFlag: 16 typescript: latest typescriptFlag: latest downgradeNpm: true - - flavor: 9 + - flavor: 10 node: 16 nodeFlag: 16 typescript: 2.7 typescriptFlag: 2_7 downgradeNpm: true - - flavor: 10 + - flavor: 11 node: 16 nodeFlag: 16 typescript: next typescriptFlag: next downgradeNpm: true + # Node nightly + - flavor: 12 + node: nightly + nodeFlag: nightly + typescript: latest + typescriptFlag: latest + downgradeNpm: true steps: # checkout code - uses: actions/checkout@v2 # install node - name: Use Node.js ${{ matrix.node }} + if: matrix.node != 'nightly' uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} + - name: Use Node.js 16, will be subsequently overridden by download of nightly + if: matrix.node == 'nightly' + uses: actions/setup-node@v1 + with: + node-version: 16 + - name: Download Node.js nightly + if: matrix.node == 'nightly' && matrix.os == 'ubuntu' + run: | + export N_PREFIX=$(pwd)/n + npm install -g n + n nightly + sudo cp "${N_PREFIX}/bin/node" "$(which node)" + node --version + - name: Download Node.js nightly + if: matrix.node == 'nightly' && matrix.os == 'windows' + run: | + $version = (Invoke-WebRequest https://nodejs.org/download/nightly/index.json | ConvertFrom-json)[0].version + $url = "https://nodejs.org/download/nightly/$version/win-x64/node.exe" + $targetPath = (Get-Command node.exe).Source + Invoke-WebRequest -Uri $url -OutFile $targetPath + node --version # lint, build, test # Downgrade from npm 7 to 6 because 7 still seems buggy to me - if: ${{ matrix.downgradeNpm }} diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 6e907f718..940088257 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -17,8 +17,7 @@ jobs: with: node-version: 14 # Render typedoc - # Using custom branch to workaround: https://github.com/TypeStrong/typedoc/issues/1585 - - run: npm install && git clone --depth 1 https://github.com/cspotcode/typedoc --branch patch-2 && pushd typedoc && npm install && npm run build || true && popd && ./typedoc/bin/typedoc + - run: npm install && npx typedoc # Render docusaurus and deploy website - run: | set -euo pipefail diff --git a/.prettierignore b/.prettierignore index 4b463a062..e0158cb5c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,4 @@ tests/main-realpath/symlink/tsconfig.json tests/throw error.ts tests/throw error react tsx.tsx tests/esm/throw error.ts +tests/legacy-source-map-support-interop/index.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..8b1ffa3f4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Debug AVA test file", + "type": "node", + "request": "launch", + "preLaunchTask": "npm: pre-debug", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", + "program": "${file}", + "outputCapture": "std", + "skipFiles": [ + "/**/*.js" + ], + } + ], +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..b39ad703a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "tasks": [ + { + "type": "npm", + "script": "pre-debug", + "problemMatcher": [ + "$tsc" + ], + "label": "npm: pre-debug", + "detail": "npm run build-tsc && npm run build-pack" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1f0363f9..2131146ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,32 @@ compiled code in `dist`. `dist-raw` is for larger chunks of code which are not compiled nor linted because they have been copy-pasted from `node`'s source code. +## Tests + +Test cases are declared in `src/test/*.spec.ts`, and test fixtures live in `./tests`. They can be run with `npm run test-local`. + +Tests are run with AVA, but using a custom wrapper API to enable some TS-friendly features and grouped test suites. + +The tests `npm pack` ts-node into a tarball and `npm install` it into `./tests/node_modules`. This makes `./tests` a better testing environment +because it more closely matches the end-user's environment. Complex `require()` / `import` / `--require` / `--loader` invocations behave +the way they would in a users's project. + +Historically, it has been difficult to test ts-node in-process because it mutates the node environment: installing require hooks, stack trace formatters, etc. +`nyc`, `ava`, and `ts-node` all mutate the node environment, so it is tricky to setup and teardown individual tests in isolation, because ts-node's hooks need to be +reset without disturbing `nyc` or `ava` hooks. For this reason, many tests are integration style, spawning ts-node's CLI in an external process, asking it to +execute one of the fixture projects in `./tests`. + +Over time, I've gradually added setup and teardown logic so that more components can be tested in-process. + +We have a debug configuration for VSCode. + +1. Open a `*.spec.ts` so it is the active/focused file. +2. (optional) set breakpoints. +3. Invoke debugger with F5. + +Note that some tests might misbehave in the debugger. REPL tests in particular. I'm not sure why, but I think it is related to how `console` does not write to +stdout when in a debug session. + ## Documentation Documentation is written in markdown in `website/docs` and rendered into a website by Docusaurus. The README is also generated from these markdown files. diff --git a/README.md b/README.md index 6a550beff..d55198566 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,10 @@ You can build the readme with this command: [![Build status](https://img.shields.io/github/workflow/status/TypeStrong/ts-node/Continuous%20Integration)](https://github.com/TypeStrong/ts-node/actions?query=workflow%3A%22Continuous+Integration%22) [![Test coverage](https://codecov.io/gh/TypeStrong/ts-node/branch/main/graph/badge.svg)](https://codecov.io/gh/TypeStrong/ts-node) -> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.7`**. +> TypeScript execution and REPL for node.js, with source map and native ESM support. The latest documentation can also be found on our website: -### *Experimental ESM support* - -Native ESM support is currently experimental. For usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). - # Table of Contents * [Overview](#overview) @@ -66,7 +62,7 @@ Native ESM support is currently experimental. For usage, limitations, and to pro * [Skipping `node_modules`](#skipping-node_modules) * [paths and baseUrl ](#paths-and-baseurl) - * [Why is this not built-in to `ts-node`?](#why-is-this-not-built-in-to-ts-node) + * [Why is this not built-in to ts-node?](#why-is-this-not-built-in-to-ts-node) * [Help! My Types Are Missing!](#help-my-types-are-missing) * [Third-party compilers](#third-party-compilers) * [Third-party transpilers](#third-party-transpilers) @@ -91,7 +87,7 @@ Native ESM support is currently experimental. For usage, limitations, and to pro # Overview -`ts-node` is a TypeScript execution engine and REPL for Node.js. +ts-node is a TypeScript execution engine and REPL for Node.js. It JIT transforms TypeScript into JavaScript, enabling you to directly execute TypeScript on Node.js without precompiling. This is accomplished by hooking node's module loading APIs, enabling it to be used seamlessly alongside other Node.js @@ -128,7 +124,7 @@ npm install -g ts-node npm install -D tslib @types/node ``` -**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. TS Node will always resolve the compiler from `cwd` before checking relative to its own installation. +**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. ts-node will always resolve the compiler from `cwd` before checking relative to its own installation. # Usage @@ -170,27 +166,27 @@ Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, #!/usr/bin/env ts-node --files // This shebang is not portable. It only works on Mac -Instead, specify all `ts-node` options in your `tsconfig.json`. +Instead, specify all ts-node options in your `tsconfig.json`. ## Programmatic -You can require `ts-node` and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. +You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. -**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of the `ts-node` CLI. +**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of ts-node's CLI. ### Developers -`ts-node` exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. +ts-node exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. # Configuration -`ts-node` supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. +ts-node supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. For a complete list, see [Options](#options). ## CLI flags -`ts-node` CLI flags must come *before* the entrypoint script. For example: +ts-node CLI flags must come *before* the entrypoint script. For example: ```shell $ ts-node --project tsconfig-dev.json say-hello.ts Ronald @@ -199,7 +195,7 @@ Hello, Ronald! ## Via tsconfig.json (recommended) -`ts-node` automatically finds and loads `tsconfig.json`. Most `ts-node` options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. +ts-node automatically finds and loads `tsconfig.json`. Most ts-node options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. Use `--skip-project` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json`. @@ -237,7 +233,7 @@ Our bundled [JSON schema](https://unpkg.com/browse/ts-node@latest/tsconfig.schem ### @tsconfig/bases [@tsconfig/bases](https://github.com/tsconfig/bases) maintains recommended configurations for several node versions. -As a convenience, these are bundled with `ts-node`. +As a convenience, these are bundled with ts-node. ```jsonc title="tsconfig.json" { @@ -250,7 +246,7 @@ As a convenience, these are bundled with `ts-node`. ### Default config -If no `tsconfig.json` is loaded from disk, `ts-node` will use the newest recommended defaults from +If no `tsconfig.json` is loaded from disk, ts-node will use the newest recommended defaults from [@tsconfig/bases](https://github.com/tsconfig/bases/) compatible with your `node` and `typescript` versions. With the latest `node` and `typescript`, this is [`@tsconfig/node16`](https://github.com/tsconfig/bases/blob/master/bases/node16.json). @@ -260,7 +256,7 @@ When in doubt, `ts-node --show-config` will log the configuration being used, an ## `node` flags -[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the `ts-node` binary nor can they be specified in `tsconfig.json` +[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the ts-node binary nor can they be specified in `tsconfig.json` We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node_options_options) environment variable to pass options to `node`. @@ -268,7 +264,7 @@ We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node NODE_OPTIONS='--trace-deprecation --abort-on-uncaught-exception' ts-node ./index.ts ``` -Alternatively, you can invoke `node` directly and install `ts-node` via `--require`/`-r` +Alternatively, you can invoke `node` directly and install ts-node via `--require`/`-r` ```shell node --trace-deprecation --abort-on-uncaught-exception -r ts-node/register ./index.ts @@ -335,7 +331,7 @@ The API includes [additional options](https://typestrong.org/ts-node/api/interfa # CommonJS vs native ECMAScript modules -TypeScript is almost always written using modern `import` syntax, but you can choose to either transform to CommonJS or use node's native ESM support. Configuration is different for each. +TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. Here is a brief comparison of the two. @@ -344,7 +340,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 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` | ## CommonJS @@ -365,7 +361,7 @@ Transforming to CommonJS is typically simpler and more widely supported because } ``` -If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for `ts-node`. +If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for ts-node. ```jsonc title="tsconfig.json" { @@ -382,8 +378,7 @@ If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, ## Native ECMAScript modules -[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. `ts-node`'s ESM support is also experimental. They may have -breaking changes in minor and patch releases and are not recommended for production. +[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can *and will* break in new versions of node. Thus it is not recommended for production. For complete usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). @@ -407,11 +402,11 @@ You must set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_ ## Understanding configuration -`ts-node` uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you +ts-node uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you have one. If you are unsure which configuration is used, you can log it with `ts-node --show-config`. This is similar to `tsc --showConfig` but includes `"ts-node"` options as well. -`ts-node` also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed +ts-node also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed `typescript`. If you are unsure which versions are used, `ts-node -vv` will log them. ```shell @@ -457,7 +452,7 @@ $ ts-node --show-config ## Understanding Errors -It is important to differentiate between errors from `ts-node`, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. +It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. ### `TSError` @@ -465,7 +460,7 @@ Type errors from the compiler are thrown as a `TSError`. These are the same as ### `SyntaxError` -Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or `ts-node`. These are bugs in your code or configuration. +Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or ts-node. These are bugs in your code or configuration. #### Unsupported JavaScript syntax @@ -488,11 +483,11 @@ When you try to run this code, node 12 will throw a `SyntaxError`. To fix this, # Make it fast -These tricks will make `ts-node` faster. +These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, `ts-node` can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. * Enable [`transpileOnly`](#options) to skip typechecking * Use our [`swc` integration](#bundled-swc-integration) @@ -510,7 +505,7 @@ It is often better to use `tsc --noEmit` to typecheck once before your tests run ## How It Works -`ts-node` works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. +ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. @@ -520,7 +515,7 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook > **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. -> **Warning:** When `ts-node` is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. +> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. ### Skipping `node_modules` @@ -532,7 +527,7 @@ By default, **TypeScript Node** avoids compiling files in `/node_modules/` for t ## paths and baseUrl -You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. +You can use ts-node together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. ```jsonc title="tsconfig.json" { @@ -543,7 +538,7 @@ You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/packa } ``` -### Why is this not built-in to `ts-node`? +### Why is this not built-in to ts-node? The official TypeScript Handbook explains the intended purpose for `"paths"` in ["Additional module resolution flags"](https://www.typescriptlang.org/docs/handbook/module-resolution.html#additional-module-resolution-flags). @@ -552,11 +547,11 @@ The official TypeScript Handbook explains the intended purpose for `"paths"` in > It is important to note that the compiler will not perform any of these transformations; it just uses these pieces of information to guide the process of resolving a module import to its definition file. This means `"paths"` are intended to describe mappings that the build tool or runtime *already* performs, not to tell the build tool or -runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, `ts-node` does not modify `node`'s module resolution behavior to implement `"paths"` mappings. +runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, ts-node does not modify `node`'s module resolution behavior to implement `"paths"` mappings. ## Help! My Types Are Missing! -**TypeScript Node** does *not* use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, `ts-node` starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. +ts-node does *not* use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). @@ -637,7 +632,7 @@ For example, to use `ttypescript` and `ts-transformer-keys`, add this to your `t In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -`ts-node`'s automatic `tsconfig.json` discovery, sourcemap support, and global `ts-node` CLI. Integrations +ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project boilerplate. @@ -653,10 +648,10 @@ We have bundled an experimental `swc` integration. [`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than `transpileModule`. -To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. +To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. ```shell -npm i -D @swc/core @swc/helpers +npm i -D @swc/core @swc/helpers regenerator-runtime ``` Then add the following to your `tsconfig.json`. @@ -681,7 +676,7 @@ Integrations are `require()`d, so they can be published to npm. The module must ## Module type overrides -When deciding between CommonJS and native ECMAScript modules, `ts-node` defaults to matching vanilla `node` and `tsc` +When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed according to node's rules for the `package.json` `"type"` field. @@ -694,7 +689,7 @@ In these situations, our `moduleTypes` option lets you override certain files, f CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. `moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. -The following example tells `ts-node` to execute a webpack config as CommonJS: +The following example tells ts-node to execute a webpack config as CommonJS: ```jsonc title=tsconfig.json { @@ -836,20 +831,22 @@ ts-node node_modules/tape/bin/tape [...args] ## Visual Studio Code -Create a new node.js configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). +Create a new Node.js debug configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). -```jsonc +```jsonc title=".vscode/launch.json" { - "type": "node", - "request": "launch", - "name": "Launch Program", - "runtimeArgs": [ - "-r", - "ts-node/register" - ], - "args": [ - "${workspaceFolder}/index.ts" - ] + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "${workspaceFolder}/src/index.ts" + ] + }], } ``` diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 8801e708d..6c6e8ee6d 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -10,6 +10,11 @@ import type * as _ts from 'typescript'; // @public export function create(rawOptions?: CreateOptions): Service; +// Warning: (ae-forgotten-export) The symbol "createEsmHooks" needs to be exported by the entry point index.d.ts +// +// @public +export const createEsmHooks: typeof createEsmHooks_2; + // @public export interface CreateOptions { compiler?: string; @@ -73,12 +78,70 @@ export interface CreateTranspilerOptions { // @public export type EvalAwarePartialHost = Pick; +// @public (undocumented) +export interface NodeLoaderHooksAPI1 { + // (undocumented) + getFormat: NodeLoaderHooksAPI1.GetFormatHook; + // (undocumented) + resolve: NodeLoaderHooksAPI1.ResolveHook; + // (undocumented) + transformSource: NodeLoaderHooksAPI1.TransformSourceHook; +} + +// @public (undocumented) +export namespace NodeLoaderHooksAPI1 { + // (undocumented) + export type GetFormatHook = (url: string, context: {}, defaultGetFormat: GetFormatHook) => Promise<{ + format: NodeLoaderHooksFormat; + }>; + // (undocumented) + export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook; + // (undocumented) + export type TransformSourceHook = (source: string | Buffer, context: { + url: string; + format: NodeLoaderHooksFormat; + }, defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook) => Promise<{ + source: string | Buffer; + }>; +} + +// @public (undocumented) +export interface NodeLoaderHooksAPI2 { + // (undocumented) + load: NodeLoaderHooksAPI2.LoadHook; + // (undocumented) + resolve: NodeLoaderHooksAPI2.ResolveHook; +} + +// @public (undocumented) +export namespace NodeLoaderHooksAPI2 { + // (undocumented) + export type LoadHook = (url: string, context: { + format: NodeLoaderHooksFormat | null | undefined; + }, defaultLoad: NodeLoaderHooksAPI2['load']) => Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }>; + // (undocumented) + export type ResolveHook = (specifier: string, context: { + parentURL: string; + }, defaultResolve: ResolveHook) => Promise<{ + url: string; + }>; +} + +// @public (undocumented) +export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; + // @public @deprecated export type Register = Service; // @public export function register(opts?: RegisterOptions): Service; +// @public +export function register(service: Service): Service; + // @public export const REGISTER_INSTANCE: unique symbol; @@ -213,7 +276,7 @@ export interface TSCommon { } // @public -export interface TsConfigOptions extends Omit { +export interface TsConfigOptions extends Omit { } // @public diff --git a/development-docs/release-template.md b/development-docs/release-template.md new file mode 100644 index 000000000..e1f32c582 --- /dev/null +++ b/development-docs/release-template.md @@ -0,0 +1,56 @@ +## Template to be copy-pasted as a template for new release notes. + +--- + + +Questions about this release? Ask in the official discussion thread: #TODO + + + +*Breaking changes are prefixed with **[BREAKING]*** + +**Added** + +- Adds description of thing added (#Issue, #PR, #etc) @contributor-name + + + - Optionally add details ([docs](link to docs)) + - Or multiple docs links ([CLI docs](link to docusaurus page), [API docs](link to typedoc page)) + +**Changed** + +- **[BREAKING]** Make yadda yadda... + +**Deprecated** + +**Removed** + +**Fixed** + +- Fix #TODO: Description of fix (#Issue, #PR, #etc) + +**Docs** + +- In the past I've documented major improvements to docs, new docsite, new docs sections about confusing bits +- In general should avoid changelog entries that do not affect ts-node consumers + +**Misc** + +- In the past I've documented improvements to testing, codecov, etc. +- In general should avoid changelog entries that do not affect ts-node consumers + +https://github.com/TypeStrong/ts-node/compare/vTODO...vTODO +https://github.com/TypeStrong/ts-node/milestone/TODO + +--- + +## Discussion thread template + +--- + +Discussion thread for the vTODO release. + +[Release notes](https://github.com/TypeStrong/ts-node/releases/tag/vTODO) diff --git a/dist-raw/node-esm-default-get-format.js b/dist-raw/node-esm-default-get-format.js new file mode 100644 index 000000000..d8af956f3 --- /dev/null +++ b/dist-raw/node-esm-default-get-format.js @@ -0,0 +1,83 @@ +// Copied from https://raw.githubusercontent.com/nodejs/node/v15.3.0/lib/internal/modules/esm/get_format.js +// Then modified to suite our needs. +// Formatting is intentionally bad to keep the diff as small as possible, to make it easier to merge +// upstream changes and understand our modifications. + +'use strict'; +const { + RegExpPrototypeExec, + StringPrototypeStartsWith, +} = require('./node-primordials'); +const { extname } = require('path'); +const { getOptionValue } = require('./node-options'); + +const experimentalJsonModules = getOptionValue('--experimental-json-modules'); +const experimentalSpeciferResolution = + getOptionValue('--experimental-specifier-resolution'); +const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); +const { getPackageType } = require('./node-esm-resolve-implementation.js').createResolve({tsExtensions: [], jsExtensions: []}); +const { URL, fileURLToPath } = require('url'); +const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-errors').codes; + +const extensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'module', + '.mjs': 'module' +}; + +const legacyExtensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'commonjs', + '.json': 'commonjs', + '.mjs': 'module', + '.node': 'commonjs' +}; + +if (experimentalWasmModules) + extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm'; + +if (experimentalJsonModules) + extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; + +function defaultGetFormat(url, context, defaultGetFormatUnused) { + if (StringPrototypeStartsWith(url, 'node:')) { + return { format: 'builtin' }; + } + const parsed = new URL(url); + if (parsed.protocol === 'data:') { + const [ , mime ] = RegExpPrototypeExec( + /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + parsed.pathname, + ) || [ null, null, null ]; + const format = ({ + '__proto__': null, + 'text/javascript': 'module', + 'application/json': experimentalJsonModules ? 'json' : null, + 'application/wasm': experimentalWasmModules ? 'wasm' : null + })[mime] || null; + return { format }; + } else if (parsed.protocol === 'file:') { + const ext = extname(parsed.pathname); + let format; + if (ext === '.js') { + format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs'; + } else { + format = extensionFormatMap[ext]; + } + if (!format) { + if (experimentalSpeciferResolution === 'node') { + process.emitWarning( + 'The Node.js specifier resolution in ESM is experimental.', + 'ExperimentalWarning'); + format = legacyExtensionFormatMap[ext]; + } else { + throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url)); + } + } + return { format: format || null }; + } + return { format: null }; +} +exports.defaultGetFormat = defaultGetFormat; diff --git a/dist-raw/node-package-json-reader.js b/dist-raw/node-package-json-reader.js index 1c36501cd..e9f82c6f4 100644 --- a/dist-raw/node-package-json-reader.js +++ b/dist-raw/node-package-json-reader.js @@ -5,6 +5,7 @@ const { SafeMap } = require('./node-primordials'); const { internalModuleReadJSON } = require('./node-internal-fs'); const { pathToFileURL } = require('url'); const { toNamespacedPath } = require('path'); +// const { getOptionValue } = require('./node-options'); const cache = new SafeMap(); @@ -23,7 +24,6 @@ function read(jsonPath) { toNamespacedPath(jsonPath) ); const result = { string, containsKeys }; - const { getOptionValue } = require('./node-options'); if (string !== undefined) { if (manifest === undefined) { // manifest = getOptionValue('--experimental-policy') ? diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index ec8083460..ae3b8b911 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -16,6 +16,7 @@ module.exports = { ObjectGetOwnPropertyNames: Object.getOwnPropertyNames, ObjectDefineProperty: Object.defineProperty, ObjectPrototypeHasOwnProperty: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop), + RegExpPrototypeExec: (obj, string) => RegExp.prototype.exec.call(obj, string), RegExpPrototypeTest: (obj, string) => RegExp.prototype.test.call(obj, string), RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest), SafeMap: Map, diff --git a/esm.mjs b/esm.mjs index 2a11ac36e..4d404070d 100644 --- a/esm.mjs +++ b/esm.mjs @@ -6,6 +6,7 @@ const require = createRequire(fileURLToPath(import.meta.url)); const esm = require('./dist/esm'); export const { resolve, + load, getFormat, transformSource, } = esm.registerAndCreateEsmHooks(); diff --git a/esm/transpile-only.mjs b/esm/transpile-only.mjs index c19132284..07b2c7ae6 100644 --- a/esm/transpile-only.mjs +++ b/esm/transpile-only.mjs @@ -6,6 +6,7 @@ const require = createRequire(fileURLToPath(import.meta.url)); const esm = require('../dist/esm'); export const { resolve, + load, getFormat, transformSource, } = esm.registerAndCreateEsmHooks({ transpileOnly: true }); diff --git a/package-lock.json b/package-lock.json index 3f8ca56be..f075aa828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.2.1", + "version": "10.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -317,9 +317,9 @@ "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" }, "@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "requires": { "@cspotcode/source-map-consumer": "0.8.0" } @@ -510,27 +510,18 @@ } }, "@napi-rs/triples": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.0.2.tgz", - "integrity": "sha512-EL3SiX43m9poFSnhDx4d4fn9SSaqyO2rHsCNhETi9bWPmjXK3uPJ0QpPFtx39FEdHcz1vJmsiW41kqc0AgvtzQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.0.3.tgz", + "integrity": "sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==", "dev": true }, "@node-rs/helper": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.1.0.tgz", - "integrity": "sha512-r43YnnrY5JNzDuXJdW3sBJrKzvejvFmFWbiItUEoBJsaPzOIWFMhXB7i5j4c9EMXcFfxveF4l7hT+rLmwtjrVQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", + "integrity": "sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==", "dev": true, "requires": { - "@napi-rs/triples": "^1.0.2", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - } + "@napi-rs/triples": "^1.0.3" } }, "@nodelib/fs.scandir": { @@ -709,83 +700,107 @@ "dev": true }, "@swc/core": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.58.tgz", - "integrity": "sha512-u/vaon34x4ISDDdgZLaxacPB4Ly8SqqmFkBKPp2VtUDbD12VqKzb6EoLDC3A5EULFQDgIdIMHuVlBB+mc8dq0w==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.106.tgz", + "integrity": "sha512-9uw8gqU+lsk7KROAcSNhsrnBgNiC5H4MIaps5LlnnEevJmKu/o1ws22tXc2qjJg+F4/V1ynUbh8E0rYlmo1XGw==", "dev": true, "requires": { "@node-rs/helper": "^1.0.0", - "@swc/core-android-arm64": "^1.2.58", - "@swc/core-darwin-arm64": "^1.2.58", - "@swc/core-darwin-x64": "^1.2.58", - "@swc/core-linux-arm-gnueabihf": "^1.2.58", - "@swc/core-linux-arm64-gnu": "^1.2.58", - "@swc/core-linux-x64-gnu": "^1.2.58", - "@swc/core-linux-x64-musl": "^1.2.58", - "@swc/core-win32-ia32-msvc": "^1.2.58", - "@swc/core-win32-x64-msvc": "^1.2.58" + "@swc/core-android-arm64": "^1.2.106", + "@swc/core-darwin-arm64": "^1.2.106", + "@swc/core-darwin-x64": "^1.2.106", + "@swc/core-freebsd-x64": "^1.2.106", + "@swc/core-linux-arm-gnueabihf": "^1.2.106", + "@swc/core-linux-arm64-gnu": "^1.2.106", + "@swc/core-linux-arm64-musl": "^1.2.106", + "@swc/core-linux-x64-gnu": "^1.2.106", + "@swc/core-linux-x64-musl": "^1.2.106", + "@swc/core-win32-arm64-msvc": "^1.2.106", + "@swc/core-win32-ia32-msvc": "^1.2.106", + "@swc/core-win32-x64-msvc": "^1.2.106" } }, "@swc/core-android-arm64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.58.tgz", - "integrity": "sha512-eSNNt/KiAbseOZ/lbaHnXClWOOeEPBRJBjxIBDX6U4oXaHLBCwgwU+qhWziVV4Lq6gX0zqcw6JY7Pxz9r2Pxzw==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.106.tgz", + "integrity": "sha512-F5T6kP3yV9S0/oXyco305QaAyE6rLNsNSdR0QI4CtACwKadiPwTOptwNIDCiTNLNgWlWLQmIRkPpxg+G4doT6Q==", "dev": true, "optional": true }, "@swc/core-darwin-arm64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.58.tgz", - "integrity": "sha512-PHZm9kYi4KjWgac86fhr1238elI7M1K8Zh634eDCJCZbU7LHWUWOyeTpT9G8dxOuAUTOUZDaCHNW/+63N5XWPA==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.106.tgz", + "integrity": "sha512-bgKzzYLFnc+mv2mDS/DLwzBvx5DCC9ZCKYY46b4dAnBfasr+SMHj+v/WI84HtilbjLBMUfYZ2hgYKls3CebIIQ==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.58.tgz", - "integrity": "sha512-jKJNNxBbt/ckp49QUP28P+YEGDS3baruCBRbVkgJQY5Nj5GKw5kay6prVf6ajhoegmtjLr+1p3By7S5XOgIc8g==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.106.tgz", + "integrity": "sha512-I5Uhit5RqbXaMIV2+v9jL+MIQeR3lT1DqVwzxZs1bTARclAheFZQpTmg+h6QmichjCiUT74SXQb6Apc/vqYKog==", + "dev": true, + "optional": true + }, + "@swc/core-freebsd-x64": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.106.tgz", + "integrity": "sha512-ZSK3vgzbA2Pkpw2LgHlAkUdx4okIpdXXTbLXuc5jkZMw1KhRWpeQaDlwbrN7XVynAYjkj2qgGQ7wv1tD43vQig==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.58.tgz", - "integrity": "sha512-dnZcurOjTEr2IkSdWakyoVlE6ay3QQSSTv/9IsBH3eI7CI2+W8m9AtQ+KyN5BKPBSK5NjswF59xA3gocbsUpng==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.106.tgz", + "integrity": "sha512-WZh6XV8cQ9Fh3IQNX9z87Tv68+sLtfnT51ghMQxceRhfvc5gIaYW+PCppezDDdlPJnWXhybGWNPAl5SHppWb2g==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.58.tgz", - "integrity": "sha512-lSOd73EqFLx0I0f9UJq2wbwjQc+tbXbLznJp89tEZeLOljuMJkF3O22l2Nv6Vet6NBPbTQYiKy6ouibFvOqMag==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.106.tgz", + "integrity": "sha512-OSI9VUWPsRrCbUlRQ4KdYqdwV63VYBC5ahSNq+72DXhtRwVbLSFuF7MNsnXgUSMHidxbc0No3/bPPamshqHdsQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.106.tgz", + "integrity": "sha512-de8AAUOP8D2/tZIpQ399xw+pGGKlR1+l5Jmy4lW7ixarEI4xKkBSF4bS9eXtC1jckmenzrLPiK/5sSbQSf6BWQ==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.58.tgz", - "integrity": "sha512-bDU2LiURs4MKXWNNUKxVU1KKCO6lp1ILszkLPYuRAHbbQCtoQUe5JCbFlCqniFOxZOm2NBtZF4a+z5bGFpb0QA==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.106.tgz", + "integrity": "sha512-QzFC7+lBSuVBmX5tS2pdM+74voiJcGgIMJ+x9pcjUu3GkDl3ow6WC6ta2WUzlgGopCGNp6IdZaFemKRzjLr3lw==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.58.tgz", - "integrity": "sha512-wdF8nHrlMI4PUL13PQFo4BMfCr9HL3kNWftiA8i+mJhfp8Z2xfyarOnVkeXmYmQYGPoqSDCsskui6n5PvBLePw==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.106.tgz", + "integrity": "sha512-QZ1gFqNiCJefkNMihbmYc7nr5stERyjoQpWgAIN6dzrgMUzRHXHGDRl/p1qsXW2VKos+okSdLwPFEmRT94H+1A==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.106.tgz", + "integrity": "sha512-MbuQwk+s43bfBNnAZTKnoQlfo4UPSOsy6t9F15yU4P3rVUuFtcxdZg6CpDnUqNPbojILXujp8z4SSigRYh5cgg==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.58.tgz", - "integrity": "sha512-Qrox0Kz3KQSYnMwAH55DYXzOG+L0PPYQHaQnJCh5rywKDUx2n/Ar5zKkVkEhRf0ehPgKajt0h2BYHsTpqNA9/w==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.106.tgz", + "integrity": "sha512-BFxWpcPxsG2LLQZ+8K8ma45rbTckjpPbnvOOhybQ0hEhLgoVzMVPp3RIUGmC+RMZe6DkGSaEQf/Rjn2cbMdQhw==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.58.tgz", - "integrity": "sha512-HPmxovhC7DbNcXLJe5nUmo+4o6Ea2d7oFdli3IvTgDri0IynQaRlfVWIuNnZmEsN7Gl1kW7PUK5WZXPUosMn8A==", + "version": "1.2.106", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.106.tgz", + "integrity": "sha512-Emn5akqApGXzPsA7ntSXEohL0AH0WjQMHy6mT3MS9Yil42yTJ96dJGf68ejKVptxwg7Iz798mT+J9r1JbAFBgg==", "dev": true, "optional": true }, @@ -4668,6 +4683,17 @@ "diff": "^4.0.1", "make-error": "^1.1.1", "yn": "3.1.1" + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + } } }, "tslib": { diff --git a/package.json b/package.json index e440ef3b0..8b9d0daab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.2.1", + "version": "10.4.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { @@ -23,6 +23,7 @@ "./esm.mjs": "./esm.mjs", "./esm/transpile-only": "./esm/transpile-only.mjs", "./esm/transpile-only.mjs": "./esm/transpile-only.mjs", + "./transpilers/swc": "./transpilers/swc.js", "./transpilers/swc-experimental": "./transpilers/swc-experimental.js", "./node10/tsconfig.json": "./node10/tsconfig.json", "./node12/tsconfig.json": "./node12/tsconfig.json", @@ -67,12 +68,12 @@ "test-cov": "nyc ava", "test": "npm run build && npm run lint && npm run test-cov --", "test-local": "npm run lint-fix && npm run build-tsc && npm run build-pack && npm run test-spec --", + "pre-debug": "npm run build-tsc && npm run build-pack", "coverage-report": "nyc report --reporter=lcov", "prepare": "npm run clean && npm run build-nopack", - "api-extractor": "api-extractor run --local --verbose" - }, - "engines": { - "node": ">=12.0.0" + "api-extractor": "api-extractor run --local --verbose", + "esm-usage-example": "npm run build-tsc && cd esm-usage-example && node --experimental-specifier-resolution node --loader ../esm.mjs ./index", + "esm-usage-example2": "npm run build-tsc && cd tests && TS_NODE_PROJECT=./module-types/override-to-cjs/tsconfig.json node --loader ../esm.mjs ./module-types/override-to-cjs/test.cjs" }, "repository": { "type": "git", @@ -152,7 +153,7 @@ } }, "dependencies": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -167,5 +168,9 @@ }, "prettier": { "singleQuote": true + }, + "volta": { + "node": "16.9.1", + "npm": "6.14.15" } } diff --git a/src/bin.ts b/src/bin.ts index 900383d71..d97261a3b 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -55,6 +55,7 @@ export function main( '--ignore': [String], '--transpile-only': Boolean, '--transpiler': String, + '--swc': Boolean, '--type-check': Boolean, '--compiler-host': Boolean, '--pretty': Boolean, @@ -116,6 +117,7 @@ export function main( '--transpile-only': transpileOnly, '--type-check': typeCheck, '--transpiler': transpiler, + '--swc': swc, '--compiler-host': compilerHost, '--pretty': pretty, '--skip-project': skipProject, @@ -145,6 +147,7 @@ export function main( --show-config Print resolved configuration and exit -T, --transpile-only Use TypeScript's faster \`transpileModule\` or a third-party transpiler + --swc Use the swc transpiler -H, --compiler-host 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 @@ -256,6 +259,7 @@ export function main( experimentalReplAwait: noExperimentalReplAwait ? false : undefined, typeCheck, transpiler, + swc, compilerHost, ignore, preferTsExts, @@ -299,7 +303,6 @@ export function main( ['ts-node']: { ...service.options, optionBasePaths: undefined, - experimentalEsmLoader: undefined, compilerOptions: undefined, project: service.configFilePath ?? service.options.project, }, diff --git a/src/configuration.ts b/src/configuration.ts index 6b4cfb607..fb1591b92 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -251,7 +251,10 @@ export function readConfig( */ function filterRecognizedTsConfigTsNodeOptions( jsonObject: any -): { recognized: TsConfigOptions; unrecognized: any } { +): { + recognized: TsConfigOptions; + unrecognized: any; +} { if (jsonObject == null) return { recognized: {}, unrecognized: {} }; const { compiler, @@ -274,6 +277,7 @@ function filterRecognizedTsConfigTsNodeOptions( moduleTypes, experimentalReplAwait, trace, + swc, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -297,6 +301,7 @@ function filterRecognizedTsConfigTsNodeOptions( scopeDir, moduleTypes, trace, + swc, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = (null as any) as keyof typeof filteredTsConfigOptions; diff --git a/src/esm.ts b/src/esm.ts index 53e14fd71..c83fd22c4 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -1,4 +1,10 @@ -import { register, getExtensions, RegisterOptions } from './index'; +import { + register, + getExtensions, + RegisterOptions, + Service, + versionGteLt, +} from './index'; import { parse as parseUrl, format as formatUrl, @@ -12,23 +18,101 @@ import { normalizeSlashes } from './util'; const { createResolve, } = require('../dist-raw/node-esm-resolve-implementation'); +const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format'); // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts +// NOTE ABOUT MULTIPLE EXPERIMENTAL LOADER APIS +// +// At the time of writing, this file implements 2x different loader APIs. +// Node made a breaking change to the loader API in https://github.com/nodejs/node/pull/37468 +// +// We check the node version number and export either the *old* or the *new* API. +// +// Today, we are implementing the *new* API on top of our implementation of the *old* API, +// which relies on copy-pasted code from the *old* hooks implementation in node. +// +// In the future, we will likely invert this: we will copy-paste the *new* API implementation +// from node, build our implementation of the *new* API on top of it, and implement the *old* +// hooks API as a shim to the *new* API. + +export interface NodeLoaderHooksAPI1 { + resolve: NodeLoaderHooksAPI1.ResolveHook; + getFormat: NodeLoaderHooksAPI1.GetFormatHook; + transformSource: NodeLoaderHooksAPI1.TransformSourceHook; +} +export namespace NodeLoaderHooksAPI1 { + export type ResolveHook = NodeLoaderHooksAPI2.ResolveHook; + export type GetFormatHook = ( + url: string, + context: {}, + defaultGetFormat: GetFormatHook + ) => Promise<{ format: NodeLoaderHooksFormat }>; + export type TransformSourceHook = ( + source: string | Buffer, + context: { url: string; format: NodeLoaderHooksFormat }, + defaultTransformSource: NodeLoaderHooksAPI1.TransformSourceHook + ) => Promise<{ source: string | Buffer }>; +} + +export interface NodeLoaderHooksAPI2 { + resolve: NodeLoaderHooksAPI2.ResolveHook; + load: NodeLoaderHooksAPI2.LoadHook; +} +export namespace NodeLoaderHooksAPI2 { + export type ResolveHook = ( + specifier: string, + context: { parentURL: string }, + defaultResolve: ResolveHook + ) => Promise<{ url: string }>; + export type LoadHook = ( + url: string, + context: { format: NodeLoaderHooksFormat | null | undefined }, + defaultLoad: NodeLoaderHooksAPI2['load'] + ) => Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }>; +} + +export type NodeLoaderHooksFormat = + | 'builtin' + | 'commonjs' + | 'dynamic' + | 'json' + | 'module' + | 'wasm'; + +/** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` - const tsNodeInstance = register({ - ...opts, - experimentalEsmLoader: true, - }); + const tsNodeInstance = register(opts); + + return createEsmHooks(tsNodeInstance); +} + +export function createEsmHooks(tsNodeService: Service) { + tsNodeService.enableExperimentalEsmLoaderInterop(); // Custom implementation that considers additional file extensions and automatically adds file extensions const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeInstance.config), - preferTsExts: tsNodeInstance.options.preferTsExts, + ...getExtensions(tsNodeService.config), + preferTsExts: tsNodeService.options.preferTsExts, }); - return { resolve, getFormat, transformSource }; + // 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 }; + return hooksAPI; function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` @@ -72,12 +156,60 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { ); } - type Format = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; + // `load` from new loader hook API (See description at the top of this file) + async function load( + url: string, + context: { format: NodeLoaderHooksFormat | null | undefined }, + defaultLoad: typeof load + ): Promise<{ + format: NodeLoaderHooksFormat; + source: string | Buffer | undefined; + }> { + // If we get a format hint from resolve() on the context then use it + // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node + const format = + context.format ?? + (await getFormat(url, context, defaultGetFormat)).format; + + let source = undefined; + if (format !== 'builtin' && format !== 'commonjs') { + // Call the new defaultLoad() to get the source + const { source: rawSource } = await defaultLoad( + url, + { format }, + defaultLoad + ); + + if (rawSource === undefined || rawSource === null) { + throw new Error( + `Failed to load raw source: Format was '${format}' and url was '${url}''.` + ); + } + + // Emulate node's built-in old defaultTransformSource() so we can re-use the old transformSource() hook + const defaultTransformSource: typeof transformSource = async ( + source, + _context, + _defaultTransformSource + ) => ({ source }); + + // Call the old hook + const { source: transformedSource } = await transformSource( + rawSource, + { url, format }, + defaultTransformSource + ); + source = transformedSource; + } + + return { format, source }; + } + async function getFormat( url: string, context: {}, defaultGetFormat: typeof getFormat - ): Promise<{ format: Format }> { + ): Promise<{ format: NodeLoaderHooksFormat }> { const defer = (overrideUrl: string = url) => defaultGetFormat(overrideUrl, context, defaultGetFormat); @@ -97,18 +229,18 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js const ext = extname(nativePath); - let nodeSays: { format: Format }; - if (ext !== '.js' && !tsNodeInstance.ignored(nativePath)) { + let nodeSays: { format: NodeLoaderHooksFormat }; + if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js'))); } else { nodeSays = await defer(); } // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification if ( - !tsNodeInstance.ignored(nativePath) && + !tsNodeService.ignored(nativePath) && (nodeSays.format === 'commonjs' || nodeSays.format === 'module') ) { - const { moduleType } = tsNodeInstance.moduleTypeClassifier.classifyModule( + const { moduleType } = tsNodeService.moduleTypeClassifier.classifyModule( normalizeSlashes(nativePath) ); if (moduleType === 'cjs') { @@ -122,9 +254,13 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { async function transformSource( source: string | Buffer, - context: { url: string; format: Format }, + context: { url: string; format: NodeLoaderHooksFormat }, defaultTransformSource: typeof transformSource ): Promise<{ source: string | Buffer }> { + if (source === null || source === undefined) { + throw new Error('No source'); + } + const defer = () => defaultTransformSource(source, context, defaultTransformSource); @@ -139,11 +275,11 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { } const nativePath = fileURLToPath(url); - if (tsNodeInstance.ignored(nativePath)) { + if (tsNodeService.ignored(nativePath)) { return defer(); } - const emittedJs = tsNodeInstance.compile(sourceAsString, nativePath); + const emittedJs = tsNodeService.compile(sourceAsString, nativePath); return { source: emittedJs }; } diff --git a/src/index.ts b/src/index.ts index a2f313f9b..ec2122337 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { ModuleTypeClassifier, } from './module-type-classifier'; import { createResolverFunctions } from './resolver-functions'; +import type { createEsmHooks as createEsmHooksFn } from './esm'; export { TSCommon }; export { @@ -39,6 +40,11 @@ export type { TranspileOptions, Transpiler, } from './transpilers/types'; +export type { + NodeLoaderHooksAPI1, + NodeLoaderHooksAPI2, + NodeLoaderHooksFormat, +} from './esm'; /** * Does this version of node obey the package.json "type" field @@ -47,18 +53,31 @@ export type { const engineSupportsPackageTypeField = parseInt(process.versions.node.split('.')[0], 10) >= 12; -function versionGte(version: string, requirement: string) { - const [major, minor, patch, extra] = version - .split(/[\.-]/) - .map((s) => parseInt(s, 10)); - const [reqMajor, reqMinor, reqPatch] = requirement - .split('.') - .map((s) => parseInt(s, 10)); - return ( - major > reqMajor || - (major === reqMajor && - (minor > reqMinor || (minor === reqMinor && patch >= reqPatch))) - ); +/** @internal */ +export function versionGteLt( + version: string, + gteRequirement: string, + ltRequirement?: string +) { + const [major, minor, patch, extra] = parse(version); + const [gteMajor, gteMinor, gtePatch] = parse(gteRequirement); + const isGte = + major > gteMajor || + (major === gteMajor && + (minor > gteMinor || (minor === gteMinor && patch >= gtePatch))); + let isLt = true; + if (ltRequirement) { + const [ltMajor, ltMinor, ltPatch] = parse(ltRequirement); + isLt = + major < ltMajor || + (major === ltMajor && + (minor < ltMinor || (minor === ltMinor && patch < ltPatch))); + } + return isGte && isLt; + + function parse(requirement: string) { + return requirement.split(/[\.-]/).map((s) => parseInt(s, 10)); + } } /** @@ -237,6 +256,12 @@ export interface CreateOptions { * Specify a custom transpiler for use with transpileOnly */ transpiler?: string | [string, object]; + /** + * Transpile with swc instead of the TypeScript compiler, and skip typechecking. + * + * Equivalent to setting both `transpileOnly: true` and `transpiler: 'ts-node/transpilers/swc'` + */ + swc?: boolean; /** * Paths which should not be compiled. * @@ -294,12 +319,6 @@ export interface CreateOptions { transformers?: | _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers); - /** - * True if require() hooks should interop with experimental ESM loader. - * Enabled explicitly via a flag since it is a breaking change. - * @internal - */ - experimentalEsmLoader?: boolean; /** * Allows the usage of top level await in REPL. * @@ -375,7 +394,6 @@ export interface TsConfigOptions | 'dir' | 'cwd' | 'projectSearchDir' - | 'experimentalEsmLoader' | 'optionBasePaths' > {} @@ -411,7 +429,6 @@ export const DEFAULTS: RegisterOptions = { typeCheck: yn(env.TS_NODE_TYPE_CHECK), compilerHost: yn(env.TS_NODE_COMPILER_HOST), logError: yn(env.TS_NODE_LOG_ERROR), - experimentalEsmLoader: false, experimentalReplAwait: yn(env.TS_NODE_EXPERIMENTAL_REPL_AWAIT) ?? undefined, trace: console.log.bind(console), }; @@ -434,10 +451,14 @@ export class TSError extends BaseError { } } +const TS_NODE_SERVICE_BRAND = Symbol('TS_NODE_SERVICE_BRAND'); + /** * Primary ts-node service, which wraps the TypeScript API and can compile TypeScript to JavaScript */ export interface Service { + /** @internal */ + [TS_NODE_SERVICE_BRAND]: true; ts: TSCommon; config: _ts.ParsedCommandLine; options: RegisterOptions; @@ -453,6 +474,10 @@ export interface Service { readonly shouldReplAwait: boolean; /** @internal */ addDiagnosticFilter(filter: DiagnosticFilter): void; + /** @internal */ + installSourceMapSupport(): void; + /** @internal */ + enableExperimentalEsmLoaderInterop(): void; } /** @@ -484,12 +509,25 @@ export function getExtensions(config: _ts.ParsedCommandLine) { return { tsExtensions, jsExtensions }; } +/** + * Create a new TypeScript compiler instance and register it onto node.js + */ +export function register(opts?: RegisterOptions): Service; /** * Register TypeScript compiler instance onto node.js */ -export function register(opts: RegisterOptions = {}): Service { +export function register(service: Service): Service; +export function register( + serviceOrOpts: Service | RegisterOptions | undefined +): Service { + // Is this a Service or a RegisterOptions? + let service = serviceOrOpts as Service; + if (!(serviceOrOpts as Service)?.[TS_NODE_SERVICE_BRAND]) { + // Not a service; is options + service = create((serviceOrOpts ?? {}) as RegisterOptions); + } + const originalJsHandler = require.extensions['.js']; - const service = create(opts); const { tsExtensions, jsExtensions } = getExtensions(service.config); const extensions = [...tsExtensions, ...jsExtensions]; @@ -564,7 +602,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } // Top-level await was added in TS 3.8 - const tsVersionSupportsTla = versionGte(ts.version, '3.8.0'); + const tsVersionSupportsTla = versionGteLt(ts.version, '3.8.0'); if (options.experimentalReplAwait === true && !tsVersionSupportsTla) { throw new Error( 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' @@ -583,11 +621,33 @@ export function create(rawOptions: CreateOptions = {}): Service { ({ compiler, ts } = loadCompiler(options.compiler, configFilePath)); } + // swc implies two other options + // typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line + // So we should allow using typeCheck to override swc + if (options.swc && !options.typeCheck) { + if (options.transpileOnly === false) { + throw new Error( + "Cannot enable 'swc' option with 'transpileOnly: false'. 'swc' implies 'transpileOnly'." + ); + } + if (options.transpiler) { + throw new Error( + "Cannot specify both 'swc' and 'transpiler' options. 'swc' uses the built-in swc transpiler." + ); + } + } + const readFile = options.readFile || ts.sys.readFile; const fileExists = options.fileExists || ts.sys.fileExists; // typeCheck can override transpileOnly, useful for CLI flag to override config file const transpileOnly = - options.transpileOnly === true && options.typeCheck !== true; + (options.transpileOnly === true || options.swc === true) && + options.typeCheck !== true; + const transpiler = options.transpiler + ? options.transpiler + : options.swc + ? require.resolve('./transpilers/swc.js') + : undefined; const transformers = options.transformers || undefined; const diagnosticFilters: Array = [ { @@ -643,17 +703,15 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } let customTranspiler: Transpiler | undefined = undefined; - if (options.transpiler) { + if (transpiler) { if (!transpileOnly) throw new Error( 'Custom transpiler can only be used when transpileOnly is enabled.' ); const transpilerName = - typeof options.transpiler === 'string' - ? options.transpiler - : options.transpiler[0]; + typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = - typeof options.transpiler === 'string' ? {} : options.transpiler[1] ?? {}; + typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; // TODO mimic fixed resolution logic from loadCompiler main // TODO refactor into a more generic "resolve dep relative to project" helper const transpilerPath = require.resolve(transpilerName, { @@ -666,25 +724,51 @@ export function create(rawOptions: CreateOptions = {}): Service { }); } + /** + * True if require() hooks should interop with experimental ESM loader. + * Enabled explicitly via a flag since it is a breaking change. + */ + let experimentalEsmLoader = false; + function enableExperimentalEsmLoaderInterop() { + experimentalEsmLoader = true; + } + // Install source map support and read from memory cache. - sourceMapSupport.install({ - environment: 'node', - retrieveFile(pathOrUrl: string) { - let path = pathOrUrl; - // If it's a file URL, convert to local path - // Note: fileURLToPath does not exist on early node v10 - // I could not find a way to handle non-URLs except to swallow an error - if (options.experimentalEsmLoader && path.startsWith('file://')) { - try { - path = fileURLToPath(path); - } catch (e) { - /* swallow error */ + installSourceMapSupport(); + function installSourceMapSupport() { + sourceMapSupport.install({ + environment: 'node', + retrieveFile(pathOrUrl: string) { + let path = pathOrUrl; + // If it's a file URL, convert to local path + // Note: fileURLToPath does not exist on early node v10 + // I could not find a way to handle non-URLs except to swallow an error + if (experimentalEsmLoader && path.startsWith('file://')) { + try { + path = fileURLToPath(path); + } catch (e) { + /* swallow error */ + } } - } - path = normalizeSlashes(path); - return outputCache.get(path)?.content || ''; - }, - }); + path = normalizeSlashes(path); + return outputCache.get(path)?.content || ''; + }, + redirectConflictingLibrary: true, + onConflictingLibraryRedirect( + request, + parent, + isMain, + options, + redirectedRequest + ) { + debug( + `Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${ + (parent as NodeJS.Module).filename + }" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".` + ); + }, + }); + } const shouldHavePrettyErrors = options.pretty === undefined ? process.stdout.isTTY : options.pretty; @@ -1233,6 +1317,7 @@ export function create(rawOptions: CreateOptions = {}): Service { } return { + [TS_NODE_SERVICE_BRAND]: true, ts, config, compile, @@ -1244,6 +1329,8 @@ export function create(rawOptions: CreateOptions = {}): Service { moduleTypeClassifier, shouldReplAwait, addDiagnosticFilter, + installSourceMapSupport, + enableExperimentalEsmLoaderInterop, }; } @@ -1438,3 +1525,17 @@ function getTokenAtPosition( return current; } } + +/** + * Create an implementation of node's ESM loader hooks. + * + * This may be useful if you + * want to wrap or compose the loader hooks to add additional functionality or + * combine with another loader. + * + * Node changed the hooks API, so there are two possible APIs. This function + * detects your node version and returns the appropriate API. + */ +export const createEsmHooks: typeof createEsmHooksFn = ( + tsNodeService: Service +) => (require('./esm') as typeof import('./esm')).createEsmHooks(tsNodeService); diff --git a/src/repl.ts b/src/repl.ts index 03d4d97d5..ad399b850 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -14,6 +14,7 @@ import { Console } from 'console'; import * as assert from 'assert'; import type * as tty from 'tty'; import type * as Module from 'module'; +import { builtinModules } from 'module'; // Lazy-loaded. let _processTopLevelAwait: (src: string) => string | null; @@ -329,6 +330,11 @@ export function createRepl(options: CreateReplOptions = {}) { } } + // In case the typescript compiler hasn't compiled anything yet, + // make it run though compilation at least one time before + // the REPL starts for a snappier user experience on startup. + service?.compile('', state.path); + const repl = nodeReplStart({ prompt: '> ', input: replService.stdin, @@ -356,6 +362,22 @@ export function createRepl(options: CreateReplOptions = {}) { if (forceToBeModule) { state.input += 'export {};void 0;\n'; } + + // Declare node builtins. + // Skip the same builtins as `addBuiltinLibsToObject`: + // those starting with _ + // those containing / + // those that already exist as globals + // Intentionally suppress type errors in case @types/node does not declare any of them. + state.input += `// @ts-ignore\n${builtinModules + .filter( + (name) => + !name.startsWith('_') && + !name.includes('/') && + !['console', 'module', 'process'].includes(name) + ) + .map((name) => `declare import ${name} = require('${name}')`) + .join(';')}\n`; } reset(); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts new file mode 100644 index 000000000..6c3c1c51a --- /dev/null +++ b/src/test/esm-loader.spec.ts @@ -0,0 +1,73 @@ +// ESM loader hook tests +// TODO: at the time of writing, other ESM loader hook tests have not been moved into this file. +// Should consolidate them here. + +import { context } from './testlib'; +import semver = require('semver'); +import { + contextTsNodeUnderTest, + EXPERIMENTAL_MODULES_FLAG, + resetNodeEnvironment, + TEST_DIR, +} from './helpers'; +import { createExec } from './exec-helpers'; +import { join } from 'path'; +import * as expect from 'expect'; +import type { NodeLoaderHooksAPI2 } from '../'; + +const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); + +const test = context(contextTsNodeUnderTest); + +const exec = createExec({ + cwd: TEST_DIR, +}); + +test.suite('createEsmHooks', (test) => { + if (semver.gte(process.version, '12.16.0')) { + test('should create proper hooks with provided instance', async () => { + const { err } = await exec( + `node ${EXPERIMENTAL_MODULES_FLAG} --loader ./loader.mjs index.ts`, + { + cwd: join(TEST_DIR, './esm-custom-loader'), + } + ); + + if (err === null) { + throw new Error('Command was expected to fail, but it succeeded.'); + } + + expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); + }); + } +}); + +test.suite('hooks', (_test) => { + const test = _test.context(async (t) => { + const service = t.context.tsNodeUnderTest.create({ + cwd: TEST_DIR, + }); + t.teardown(() => { + resetNodeEnvironment(); + }); + return { + service, + hooks: t.context.tsNodeUnderTest.createEsmHooks(service), + }; + }); + + if (nodeUsesNewHooksApi) { + test('Correctly determines format of data URIs', async (t) => { + const { hooks } = t.context; + const url = 'data:text/javascript,console.log("hello world");'; + const result = await (hooks as NodeLoaderHooksAPI2).load( + url, + { format: undefined }, + async (url, context, _ignored) => { + return { format: context.format!, source: '' }; + } + ); + expect(result.format).toBe('module'); + }); + } +}); diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 6683b0558..245e0a924 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -12,9 +12,9 @@ import type { Readable } from 'stream'; */ import type * as tsNodeTypes from '../index'; import type _createRequire from 'create-require'; -import { once } from 'lodash'; +import { has, once } from 'lodash'; import semver = require('semver'); -import { isConstructSignatureDeclaration } from 'typescript'; +import * as expect from 'expect'; const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; @@ -40,12 +40,14 @@ export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} // `createRequire` does not exist on older node versions export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); +export const ts = testsDirRequire('typescript'); + export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ export const contextTsNodeUnderTest = once(async () => { await installTsNode(); - const tsNodeUnderTest = testsDirRequire('ts-node'); + const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node'); return { tsNodeUnderTest, }; @@ -155,3 +157,63 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { combinedString = combinedBuffer.toString('utf8'); } } + +const defaultRequireExtensions = captureObjectState(require.extensions); +const defaultProcess = captureObjectState(process); +const defaultModule = captureObjectState(require('module')); +const defaultError = captureObjectState(Error); +const defaultGlobal = captureObjectState(global); + +/** + * Undo all of ts-node & co's installed hooks, resetting the node environment to default + * so we can run multiple test cases which `.register()` ts-node. + * + * Must also play nice with `nyc`'s environmental mutations. + */ +export function resetNodeEnvironment() { + // We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test. + require('@cspotcode/source-map-support').uninstall(); + + // Modified by ts-node hooks + resetObject(require.extensions, defaultRequireExtensions); + + // ts-node attaches a property when it registers an instance + // source-map-support monkey-patches the emit function + resetObject(process, defaultProcess); + + // source-map-support swaps out the prepareStackTrace function + resetObject(Error, defaultError); + + // _resolveFilename is modified by tsconfig-paths, future versions of source-map-support, and maybe future versions of ts-node + resetObject(require('module'), defaultModule); + + // May be modified by REPL tests, since the REPL sets globals. + resetObject(global, defaultGlobal); +} + +function captureObjectState(object: any) { + return { + descriptors: Object.getOwnPropertyDescriptors(object), + values: { ...object }, + }; +} +// Redefine all property descriptors and delete any new properties +function resetObject( + object: any, + state: ReturnType +) { + const currentDescriptors = Object.getOwnPropertyDescriptors(object); + for (const key of Object.keys(currentDescriptors)) { + if (!has(state.descriptors, key)) { + delete object[key]; + } + } + // Trigger nyc's setter functions + for (const [key, value] of Object.entries(state.values)) { + try { + object[key] = value; + } catch {} + } + // Reset descriptors + Object.defineProperties(object, state.descriptors); +} diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 31ece4d71..3b128afdc 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -3,7 +3,7 @@ import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; import { tmpdir } from 'os'; import semver = require('semver'); -import ts = require('typescript'); +import { ts } from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; @@ -70,6 +70,7 @@ test.suite('ts-node', (test) => { testsDirRequire.resolve('ts-node/esm/transpile-only'); testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); + testsDirRequire.resolve('ts-node/transpilers/swc'); testsDirRequire.resolve('ts-node/transpilers/swc-experimental'); testsDirRequire.resolve('ts-node/node10/tsconfig.json'); @@ -304,21 +305,31 @@ test.suite('ts-node', (test) => { expect(err.message).toMatch('error TS1003: Identifier expected'); }); - test('should support third-party transpilers via --transpiler', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc` - ); - expect(err).toBe(null); - expect(stdout).toMatch('Hello World!'); - }); - - test('should support third-party transpilers via tsconfig', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} transpile-only-swc-via-tsconfig` - ); - expect(err).toBe(null); - expect(stdout).toMatch('Hello World!'); - }); + for (const flavor of [ + '--transpiler ts-node/transpilers/swc transpile-only-swc', + '--transpiler ts-node/transpilers/swc-experimental transpile-only-swc', + '--swc transpile-only-swc', + 'transpile-only-swc-via-tsconfig', + 'transpile-only-swc-shorthand-via-tsconfig', + ]) { + test(`should support swc and third-party transpilers: ${flavor}`, async () => { + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ${flavor}`, + { + env: { + ...process.env, + NODE_OPTIONS: `${ + process.env.NODE_OPTIONS || '' + } --require ${require.resolve('../../tests/spy-swc-transpiler')}`, + }, + } + ); + expect(err).toBe(null); + expect(stdout).toMatch( + 'Hello World! swc transpiler invocation count: 1\n' + ); + }); + } test.suite('should support `traceResolution` compiler option', (test) => { test('prints traces before running code when enabled', async () => { diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts index 5708b9c37..db04b2846 100644 --- a/src/test/register.spec.ts +++ b/src/test/register.spec.ts @@ -1,45 +1,72 @@ import { once } from 'lodash'; import { - installTsNode, + contextTsNodeUnderTest, PROJECT, - testsDirRequire, + resetNodeEnvironment, TEST_DIR, tsNodeTypes, } from './helpers'; -import { test } from './testlib'; +import { context } from './testlib'; import { expect } from 'chai'; -import { join } from 'path'; +import * as exp from 'expect'; +import { join, resolve } from 'path'; import proxyquire = require('proxyquire'); -import type * as Module from 'module'; const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; -// Set after ts-node is installed locally -let { register }: typeof tsNodeTypes = {} as any; -test.beforeAll(async () => { - await installTsNode(); - ({ register } = testsDirRequire('ts-node')); +const createOptions: tsNodeTypes.CreateOptions = { + project: PROJECT, + compilerOptions: { + jsx: 'preserve', + }, +}; + +const test = context(contextTsNodeUnderTest).context( + once(async (t) => { + return { + moduleTestPath: resolve(__dirname, '../../tests/module.ts'), + service: t.context.tsNodeUnderTest.create(createOptions), + }; + }) +); +test.beforeEach(async (t) => { + // Un-install all hook and remove our test module from cache + resetNodeEnvironment(); + delete require.cache[t.context.moduleTestPath]; + // Paranoid check that we are truly uninstalled + exp(() => require(t.context.moduleTestPath)).toThrow( + "Unexpected token 'export'" + ); }); - -test.suite('register', (_test) => { - const test = _test.context( - once(async () => { - return { - registered: register({ - project: PROJECT, - compilerOptions: { - jsx: 'preserve', - }, - }), - moduleTestPath: require.resolve('../../tests/module'), - }; - }) +test.runSerially(); + +test('create() does not register()', async (t) => { + // nyc sets its own `require.extensions` hooks; to truly detect if we're + // installed we must attempt to load a TS file + t.context.tsNodeUnderTest.create(createOptions); + // This error indicates node attempted to run the code as .js + exp(() => require(t.context.moduleTestPath)).toThrow( + "Unexpected token 'export'" ); - test.beforeEach(async ({ context: { registered } }) => { +}); + +test('register(options) is shorthand for register(create(options))', (t) => { + t.context.tsNodeUnderTest.register(createOptions); + require(t.context.moduleTestPath); +}); + +test('register(service) registers a previously-created service', (t) => { + t.context.tsNodeUnderTest.register(t.context.service); + require(t.context.moduleTestPath); +}); + +test.suite('register(create(options))', (test) => { + test.beforeEach(async (t) => { // Re-enable project for every test. - registered.enabled(true); + t.context.service.enabled(true); + t.context.tsNodeUnderTest.register(t.context.service); + t.context.service.installSourceMapSupport(); }); - test.runSerially(); test('should be able to require typescript', ({ context: { moduleTestPath }, @@ -50,75 +77,29 @@ test.suite('register', (_test) => { }); test('should support dynamically disabling', ({ - context: { registered, moduleTestPath }, + context: { service, moduleTestPath }, }) => { delete require.cache[moduleTestPath]; - expect(registered.enabled(false)).to.equal(false); + expect(service.enabled(false)).to.equal(false); expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(registered.enabled()).to.equal(false); + expect(service.enabled()).to.equal(false); expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(registered.enabled(true)).to.equal(true); + expect(service.enabled(true)).to.equal(true); expect(() => require(moduleTestPath)).to.not.throw(); delete require.cache[moduleTestPath]; - expect(registered.enabled()).to.equal(true); + expect(service.enabled()).to.equal(true); expect(() => require(moduleTestPath)).to.not.throw(); }); - test('should support compiler scopes', ({ - context: { registered, moduleTestPath }, - }) => { - const calls: string[] = []; - - registered.enabled(false); - - const compilers = [ - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/a'), - scope: true, - }), - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/b'), - scope: true, - }), - ]; - - compilers.forEach((c) => { - const old = c.compile; - c.compile = (code, fileName, lineOffset) => { - calls.push(fileName); - - return old(code, fileName, lineOffset); - }; - }); - - try { - expect(require('../../tests/scope/a').ext).to.equal('.ts'); - expect(require('../../tests/scope/b').ext).to.equal('.ts'); - } finally { - compilers.forEach((c) => c.enabled(false)); - } - - expect(calls).to.deep.equal([ - join(TEST_DIR, 'scope/a/index.ts'), - join(TEST_DIR, 'scope/b/index.ts'), - ]); - - delete require.cache[moduleTestPath]; - - expect(() => require(moduleTestPath)).to.throw(); - }); - test('should compile through js and ts', () => { const m = require('../../tests/complex'); @@ -143,7 +124,7 @@ test.suite('register', (_test) => { try { require('../../tests/throw error'); } catch (error: any) { - expect(error.stack).to.contain( + exp(error.stack).toMatch( [ 'Error: this is a demo', ` at Foo.bar (${join(TEST_DIR, './throw error.ts')}:100:17)`, @@ -153,12 +134,10 @@ test.suite('register', (_test) => { }); test.suite('JSX preserve', (test) => { - let old: (m: Module, filename: string) => any; let compiled: string; - test.runSerially(); test.beforeAll(async () => { - old = require.extensions['.tsx']!; + const old = require.extensions['.tsx']!; require.extensions['.tsx'] = (m: any, fileName) => { const _compile = m._compile; @@ -172,9 +151,6 @@ test.suite('register', (_test) => { }); test('should use source maps', async (t) => { - t.teardown(() => { - require.extensions['.tsx'] = old; - }); try { require('../../tests/with-jsx.tsx'); } catch (error: any) { @@ -185,3 +161,46 @@ test.suite('register', (_test) => { }); }); }); + +test('should support compiler scopes w/multiple registered compiler services at once', (t) => { + const { moduleTestPath, tsNodeUnderTest } = t.context; + const calls: string[] = []; + + const compilers = [ + tsNodeUnderTest.register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/a'), + scope: true, + }), + tsNodeUnderTest.register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/b'), + scope: true, + }), + ]; + + compilers.forEach((c) => { + const old = c.compile; + c.compile = (code, fileName, lineOffset) => { + calls.push(fileName); + + return old(code, fileName, lineOffset); + }; + }); + + try { + expect(require('../../tests/scope/a').ext).to.equal('.ts'); + expect(require('../../tests/scope/b').ext).to.equal('.ts'); + } finally { + compilers.forEach((c) => c.enabled(false)); + } + + expect(calls).to.deep.equal([ + join(TEST_DIR, 'scope/a/index.ts'), + join(TEST_DIR, 'scope/b/index.ts'), + ]); + + delete require.cache[moduleTestPath]; + + expect(() => require(moduleTestPath)).to.throw(); +}); diff --git a/src/test/regression.spec.ts b/src/test/regression.spec.ts new file mode 100644 index 000000000..db3c44649 --- /dev/null +++ b/src/test/regression.spec.ts @@ -0,0 +1,39 @@ +// Misc regression tests go here if they do not have a better home + +import * as exp from 'expect'; +import { join } from 'path'; +import { createExec, createExecTester } from './exec-helpers'; +import { + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + contextTsNodeUnderTest, + TEST_DIR, +} from './helpers'; +import { test as _test } from './testlib'; + +const test = _test.context(contextTsNodeUnderTest); +const exec = createExecTester({ + exec: createExec({ + cwd: TEST_DIR, + }), +}); + +test('#1488 regression test', async () => { + // Scenario that caused the bug: + // `allowJs` turned on + // `skipIgnore` turned on so that ts-node tries to compile itself (not ideal but theoretically we should be ok with this) + // Attempt to `require()` a `.js` file + // `assertScriptCanLoadAsCJS` is triggered within `require()` + // `./package.json` needs to be fetched into cache via `assertScriptCanLoadAsCJS` which caused a recursive `require()` call + // Circular dependency warning is emitted by node + + const { stdout, stderr } = await exec({ + exec: createExec({ + cwd: join(TEST_DIR, '1488'), + }), + cmd: `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.js`, + }); + + // Assert that we do *not* get `Warning: Accessing non-existent property 'getOptionValue' of module exports inside circular dependency` + exp(stdout).toBe('foo\n'); // prove that it ran + exp(stderr).toBe(''); // prove that no warnings +}); diff --git a/src/test/repl/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts index 210d22ec0..616237926 100644 --- a/src/test/repl/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import type { Key } from 'readline'; import { Stream } from 'stream'; import semver = require('semver'); -import ts = require('typescript'); +import { ts } from '../helpers'; import type { ContextWithTsNodeUnderTest } from './helpers'; interface SharedObjects extends ContextWithTsNodeUnderTest { diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts index 4becc6f10..9071688c6 100644 --- a/src/test/repl/repl-environment.spec.ts +++ b/src/test/repl/repl-environment.spec.ts @@ -94,7 +94,7 @@ test.suite( } ); - const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any, fs: any;`; + const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any;`; function setReportGlobal(type: 'repl' | 'stdin' | 'eval') { return ` ${declareGlobals} @@ -107,7 +107,7 @@ test.suite( modulePaths: typeof module !== 'undefined' && [...module.paths], exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null, stackTest: new Error().stack!.split('\\n')[1], - moduleAccessorsTest: typeof fs === 'undefined' ? null : fs === require('fs'), + moduleAccessorsTest: eval('typeof fs') === 'undefined' ? null : eval('fs') === require('fs'), argv: [...process.argv] }; `.replace(/\n/g, ''); @@ -203,7 +203,7 @@ test.suite( exportsTest: true, // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` + ` at ${join(TEST_DIR, '.ts')}:4:` ), moduleAccessorsTest: true, argv: [tsNodeExe], @@ -356,7 +356,7 @@ test.suite( exportsTest: true, // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` + ` at ${join(TEST_DIR, '.ts')}:4:` ), moduleAccessorsTest: true, argv: [tsNodeExe], diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 05162d246..8e1ceab04 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -1,4 +1,4 @@ -import ts = require('typescript'); +import { ts } from '../helpers'; import semver = require('semver'); import * as expect from 'expect'; import { @@ -214,7 +214,7 @@ test.suite('top level await', (_test) => { expect(stdout).toBe('> > '); expect(stderr.replace(/\r\n/g, '\n')).toBe( - '.ts(2,7): error TS2322: ' + + '.ts(4,7): error TS2322: ' + (semver.gte(ts.version, '4.0.0') ? `Type 'number' is not assignable to type 'string'.\n` : `Type '1' is not assignable to type 'string'.\n`) + @@ -454,3 +454,30 @@ test.suite('REPL works with traceResolution', (test) => { } ); }); + +test.serial('REPL declares types for node built-ins within REPL', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `util.promisify(setTimeout)("should not be a string" as string) + type Duplex = stream.Duplex + const s = stream + 'done'`, + { + registerHooks: true, + waitPattern: `done`, + startInternalOptions: { + useGlobal: false, + }, + } + ); + + // Assert that we receive a typechecking error about improperly using + // `util.promisify` but *not* an error about the absence of `util` + expect(stderr).not.toMatch("Cannot find name 'util'"); + expect(stderr).toMatch( + "Argument of type 'string' is not assignable to parameter of type 'number'" + ); + // Assert that both types and values can be used without error + expect(stderr).not.toMatch("Cannot find namespace 'stream'"); + expect(stderr).not.toMatch("Cannot find name 'stream'"); + expect(stdout).toMatch(`done`); +}); diff --git a/src/test/sourcemaps.spec.ts b/src/test/sourcemaps.spec.ts new file mode 100644 index 000000000..505107c88 --- /dev/null +++ b/src/test/sourcemaps.spec.ts @@ -0,0 +1,30 @@ +import * as expect from 'expect'; +import { createExec, createExecTester } from './exec-helpers'; +import { + CMD_TS_NODE_WITH_PROJECT_FLAG, + contextTsNodeUnderTest, + TEST_DIR, +} from './helpers'; +import { test as _test } from './testlib'; +const test = _test.context(contextTsNodeUnderTest); + +const exec = createExecTester({ + cmd: CMD_TS_NODE_WITH_PROJECT_FLAG, + exec: createExec({ + cwd: TEST_DIR, + }), +}); + +test('Redirects source-map-support to @cspotcode/source-map-support so that third-party libraries get correct source-mapped locations', async () => { + const { stdout } = await exec({ + flags: `./legacy-source-map-support-interop/index.ts`, + }); + expect(stdout.split('\n')).toMatchObject([ + expect.stringContaining('.ts:2 '), + 'true', + 'true', + expect.stringContaining('.ts:100:'), + expect.stringContaining('.ts:101 '), + '', + ]); +}); diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 5d090d7d7..ce97a07a3 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -37,6 +37,8 @@ export const test = createTestInterface({ }); // In case someone wants to `const test = _test.context()` export { test as _test }; +// Or import `context` +export const context = test.context; export interface TestInterface< Context @@ -85,7 +87,7 @@ export interface TestInterface< beforeAll(cb: (t: ExecutionContext) => Promise): void; beforeEach(cb: (t: ExecutionContext) => Promise): void; - context( + context( cb: (t: ExecutionContext) => Promise ): TestInterface; suite(title: string, cb: (test: TestInterface) => void): void; diff --git a/src/test/transpilers.spec.ts b/src/test/transpilers.spec.ts new file mode 100644 index 000000000..f1d30172a --- /dev/null +++ b/src/test/transpilers.spec.ts @@ -0,0 +1,47 @@ +// third-party transpiler and swc transpiler tests +// TODO: at the time of writing, other transpiler tests have not been moved into this file. +// Should consolidate them here. + +import { context } from './testlib'; +import { contextTsNodeUnderTest, testsDirRequire } from './helpers'; +import * as expect from 'expect'; + +const test = context(contextTsNodeUnderTest); + +test.suite('swc', (test) => { + test('verify that TS->SWC target mappings suppport all possible values from both TS and SWC', async (t) => { + const swcTranspiler = testsDirRequire( + 'ts-node/transpilers/swc-experimental' + ) as typeof import('../transpilers/swc'); + + // Detect when mapping is missing any ts.ScriptTargets + const ts = testsDirRequire('typescript') as typeof import('typescript'); + for (const key of Object.keys(ts.ScriptTarget)) { + if (/^\d+$/.test(key)) continue; + if (key === 'JSON') continue; + expect( + swcTranspiler.targetMapping.has(ts.ScriptTarget[key as any] as any) + ).toBe(true); + } + + // Detect when mapping is missing any swc targets + // Assuming that tests/package.json declares @swc/core: latest + const swc = testsDirRequire('@swc/core'); + let msg: string | undefined = undefined; + try { + swc.transformSync('', { jsc: { target: 'invalid' } }); + } catch (e) { + msg = (e as Error).message; + } + expect(msg).toBeDefined(); + // Error looks like: + // unknown variant `invalid`, expected one of `es3`, `es5`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020`, `es2021` at line 1 column 28 + const match = msg!.match(/unknown variant.*, expected one of (.*) at line/); + expect(match).toBeDefined(); + const targets = match![1].split(', ').map((v: string) => v.slice(1, -1)); + + for (const target of targets) { + expect([...swcTranspiler.targetMapping.values()]).toContain(target); + } + }); +}); diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 9b7361a08..3111bfc16 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -52,12 +52,29 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { module, jsxFactory, jsxFragmentFactory, + strict, + alwaysStrict, + noImplicitUseStrict, } = compilerOptions; const nonTsxOptions = createSwcOptions(false); const tsxOptions = createSwcOptions(true); function createSwcOptions(isTsx: boolean): swcTypes.Options { - const swcTarget = targetMapping.get(target!) ?? 'es3'; + let swcTarget = targetMapping.get(target!) ?? 'es3'; + // Downgrade to lower target if swc does not support the selected target. + // Perhaps project has an older version of swc. + // TODO cache the results of this; slightly faster + let swcTargetIndex = swcTargets.indexOf(swcTarget); + for (; swcTargetIndex >= 0; swcTargetIndex--) { + try { + swcInstance.transformSync('', { + jsc: { target: swcTargets[swcTargetIndex] }, + }); + break; + } catch (e) {} + } + swcTarget = swcTargets[swcTargetIndex]; const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; + // swc only supports these 4x module options const moduleType = module === ModuleKind.CommonJS ? 'commonjs' @@ -65,7 +82,23 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? 'amd' : module === ModuleKind.UMD ? 'umd' - : undefined; + : 'es6'; + // In swc: + // strictMode means `"use strict"` is *always* emitted for non-ES module, *never* for ES module where it is assumed it can be omitted. + // (this assumption is invalid, but that's the way swc behaves) + // tsc is a bit more complex: + // alwaysStrict will force emitting it always unless `import`/`export` syntax is emitted which implies it per the JS spec. + // if not alwaysStrict, will emit implicitly whenever module target is non-ES *and* transformed module syntax is emitted. + // For node, best option is to assume that all scripts are modules (commonjs or esm) and thus should get tsc's implicit strict behavior. + + // Always set strictMode, *unless* alwaysStrict is disabled and noImplicitUseStrict is enabled + const strictMode = + // if `alwaysStrict` is disabled, remembering that `strict` defaults `alwaysStrict` to true + (alwaysStrict === false || (alwaysStrict !== true && strict !== true)) && + // if noImplicitUseStrict is enabled + noImplicitUseStrict === true + ? false + : true; return { sourceMaps: sourceMap, // isModule: true, @@ -73,6 +106,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? ({ noInterop: !esModuleInterop, type: moduleType, + strictMode, } as swcTypes.ModuleConfig) : undefined, swcrc: false, @@ -119,7 +153,8 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { }; } -const targetMapping = new Map(); +/** @internal */ +export const targetMapping = new Map(); targetMapping.set(/* ts.ScriptTarget.ES3 */ 0, 'es3'); targetMapping.set(/* ts.ScriptTarget.ES5 */ 1, 'es5'); targetMapping.set(/* ts.ScriptTarget.ES2015 */ 2, 'es2015'); @@ -127,8 +162,28 @@ targetMapping.set(/* ts.ScriptTarget.ES2016 */ 3, 'es2016'); targetMapping.set(/* ts.ScriptTarget.ES2017 */ 4, 'es2017'); targetMapping.set(/* ts.ScriptTarget.ES2018 */ 5, 'es2018'); targetMapping.set(/* ts.ScriptTarget.ES2019 */ 6, 'es2019'); -targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2019'); -targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2019'); +targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2020'); +targetMapping.set(/* ts.ScriptTarget.ES2021 */ 8, 'es2021'); +targetMapping.set(/* ts.ScriptTarget.ES2022 */ 9, 'es2022'); +targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2022'); + +type SwcTarget = typeof swcTargets[number]; +/** + * @internal + * We use this list to downgrade to a prior target when we probe swc to detect if it supports a particular target + */ +const swcTargets = [ + 'es3', + 'es5', + 'es2015', + 'es2016', + 'es2017', + 'es2018', + 'es2019', + 'es2020', + 'es2021', + 'es2022', +] as const; const ModuleKind = { None: 0, diff --git a/tests/1488/index.js b/tests/1488/index.js new file mode 100644 index 000000000..81afa3157 --- /dev/null +++ b/tests/1488/index.js @@ -0,0 +1 @@ +console.log('foo'); diff --git a/tests/1488/package.json b/tests/1488/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/1488/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/1488/tsconfig.json b/tests/1488/tsconfig.json new file mode 100644 index 000000000..7a37a1fad --- /dev/null +++ b/tests/1488/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "allowJs": true + }, + "ts-node": { + "skipIgnore": true + } +} diff --git a/tests/esm-custom-loader/index.ts b/tests/esm-custom-loader/index.ts new file mode 100755 index 000000000..89efb1cf9 --- /dev/null +++ b/tests/esm-custom-loader/index.ts @@ -0,0 +1,4 @@ +export function abc() { + let unusedVar: string; + return true; +} diff --git a/tests/esm-custom-loader/loader.mjs b/tests/esm-custom-loader/loader.mjs new file mode 100755 index 000000000..bf82e766b --- /dev/null +++ b/tests/esm-custom-loader/loader.mjs @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +const require = createRequire(fileURLToPath(import.meta.url)); + +/** @type {import('../../dist')} **/ +const { createEsmHooks, register } = require('ts-node'); + +const tsNodeInstance = register({ + compilerOptions: { + noUnusedLocals: true, + }, +}); + +export const { resolve, getFormat, transformSource, load } = createEsmHooks( + tsNodeInstance +); diff --git a/tests/esm-custom-loader/package.json b/tests/esm-custom-loader/package.json new file mode 100755 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-custom-loader/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-custom-loader/tsconfig.json b/tests/esm-custom-loader/tsconfig.json new file mode 100755 index 000000000..ad01eee33 --- /dev/null +++ b/tests/esm-custom-loader/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "noUnusedLocals": false + } +} diff --git a/tests/legacy-source-map-support-interop/index.ts b/tests/legacy-source-map-support-interop/index.ts new file mode 100644 index 000000000..a4ae13c92 --- /dev/null +++ b/tests/legacy-source-map-support-interop/index.ts @@ -0,0 +1,101 @@ +import { Logger } from 'tslog'; +new Logger().info('hi'); +console.log(require.resolve('source-map-support') === require.resolve('@cspotcode/source-map-support')); +console.log(require.resolve('source-map-support/register') === require.resolve('@cspotcode/source-map-support/register')); +/* +tslog uses `require('source-map-support').wrapCallSite` directly. +Without redirection to @cspotcode/source-map-support it does not have access to the sourcemap information we provide. +*/ +interface Foo { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} +console.log(new Error().stack!.split('\n')[1]); +new Logger().info('hi'); diff --git a/tests/package.json b/tests/package.json index 1b91c8b50..371b90084 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@swc/core": "latest", - "ts-node": "file:ts-node-packed.tgz" + "ts-node": "file:ts-node-packed.tgz", + "tslog": "3.2.2" } } diff --git a/tests/spy-swc-transpiler.js b/tests/spy-swc-transpiler.js new file mode 100644 index 000000000..99e77acc3 --- /dev/null +++ b/tests/spy-swc-transpiler.js @@ -0,0 +1,16 @@ +// Spy on the swc transpiler so that tests can prove it was used rather than +// TypeScript's `transpileModule`. +const swcTranspiler = require('ts-node/transpilers/swc'); + +global.swcTranspilerCalls = 0; + +const wrappedCreate = swcTranspiler.create; +swcTranspiler.create = function (...args) { + const transpiler = wrappedCreate(...args); + const wrappedTranspile = transpiler.transpile; + transpiler.transpile = function (...args) { + global.swcTranspilerCalls++; + return wrappedTranspile.call(this, ...args); + }; + return transpiler; +}; diff --git a/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts b/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts new file mode 100644 index 000000000..909fde693 --- /dev/null +++ b/tests/transpile-only-swc-shorthand-via-tsconfig/index.ts @@ -0,0 +1,13 @@ +// Test for #1343 +const Decorator = function () {}; +@Decorator +class World {} + +// intentional type errors to check transpile-only ESM loader skips type checking +parseInt(1101, 2); +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; +console.log(x); + +// test module type emit +import { readFileSync } from 'fs'; +readFileSync; diff --git a/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json b/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json new file mode 100644 index 000000000..9dba8805a --- /dev/null +++ b/tests/transpile-only-swc-shorthand-via-tsconfig/tsconfig.json @@ -0,0 +1,12 @@ +{ + "ts-node": { + "swc": true + }, + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "allowJs": true, + "jsx": "react", + "experimentalDecorators": true + } +} diff --git a/tests/transpile-only-swc-via-tsconfig/index.ts b/tests/transpile-only-swc-via-tsconfig/index.ts index b7dfa09cd..909fde693 100644 --- a/tests/transpile-only-swc-via-tsconfig/index.ts +++ b/tests/transpile-only-swc-via-tsconfig/index.ts @@ -5,7 +5,7 @@ class World {} // intentional type errors to check transpile-only ESM loader skips type checking parseInt(1101, 2); -const x: number = `Hello ${World.name}!`; +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; console.log(x); // test module type emit diff --git a/tests/transpile-only-swc-via-tsconfig/tsconfig.json b/tests/transpile-only-swc-via-tsconfig/tsconfig.json index 5097d63ac..863b6c4f3 100644 --- a/tests/transpile-only-swc-via-tsconfig/tsconfig.json +++ b/tests/transpile-only-swc-via-tsconfig/tsconfig.json @@ -1,7 +1,7 @@ { "ts-node": { "transpileOnly": true, - "transpiler": "ts-node/transpilers/swc-experimental" + "transpiler": "ts-node/transpilers/swc" }, "compilerOptions": { "target": "ES2018", diff --git a/tests/transpile-only-swc/index.ts b/tests/transpile-only-swc/index.ts index b7dfa09cd..909fde693 100644 --- a/tests/transpile-only-swc/index.ts +++ b/tests/transpile-only-swc/index.ts @@ -5,7 +5,7 @@ class World {} // intentional type errors to check transpile-only ESM loader skips type checking parseInt(1101, 2); -const x: number = `Hello ${World.name}!`; +const x: number = `Hello ${World.name}! swc transpiler invocation count: ${global.swcTranspilerCalls}`; console.log(x); // test module type emit diff --git a/transpilers/swc.js b/transpilers/swc.js new file mode 100644 index 000000000..7cf79b13c --- /dev/null +++ b/transpilers/swc.js @@ -0,0 +1 @@ +module.exports = require('../dist/transpilers/swc') diff --git a/website/docs/configuration.md b/website/docs/configuration.md index ad9998328..5a1d43bea 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -2,13 +2,13 @@ title: Configuration --- -`ts-node` supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. +ts-node supports a variety of options which can be specified via `tsconfig.json`, as CLI flags, as environment variables, or programmatically. For a complete list, see [Options](./options.md). ## CLI flags -`ts-node` CLI flags must come *before* the entrypoint script. For example: +ts-node CLI flags must come *before* the entrypoint script. For example: ```shell $ ts-node --project tsconfig-dev.json say-hello.ts Ronald @@ -17,7 +17,7 @@ Hello, Ronald! ## Via tsconfig.json (recommended) -`ts-node` automatically finds and loads `tsconfig.json`. Most `ts-node` options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. +ts-node automatically finds and loads `tsconfig.json`. Most ts-node options can be specified in a `"ts-node"` object using their programmatic, camelCase names. We recommend this because it works even when you cannot pass CLI flags, such as `node --require ts-node/register` and when using shebangs. Use `--skip-project` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json`. @@ -55,7 +55,7 @@ Our bundled [JSON schema](https://unpkg.com/browse/ts-node@latest/tsconfig.schem ### @tsconfig/bases [@tsconfig/bases](https://github.com/tsconfig/bases) maintains recommended configurations for several node versions. -As a convenience, these are bundled with `ts-node`. +As a convenience, these are bundled with ts-node. ```json title="tsconfig.json" { @@ -68,7 +68,7 @@ As a convenience, these are bundled with `ts-node`. ### Default config -If no `tsconfig.json` is loaded from disk, `ts-node` will use the newest recommended defaults from +If no `tsconfig.json` is loaded from disk, ts-node will use the newest recommended defaults from [@tsconfig/bases](https://github.com/tsconfig/bases/) compatible with your `node` and `typescript` versions. With the latest `node` and `typescript`, this is [`@tsconfig/node16`](https://github.com/tsconfig/bases/blob/master/bases/node16.json). @@ -78,7 +78,7 @@ When in doubt, `ts-node --show-config` will log the configuration being used, an ## `node` flags -[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the `ts-node` binary nor can they be specified in `tsconfig.json` +[`node` flags](https://nodejs.org/api/cli.html) must be passed directly to `node`; they cannot be passed to the ts-node binary nor can they be specified in `tsconfig.json` We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node_options_options) environment variable to pass options to `node`. @@ -86,7 +86,7 @@ We recommend using the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node NODE_OPTIONS='--trace-deprecation --abort-on-uncaught-exception' ts-node ./index.ts ``` -Alternatively, you can invoke `node` directly and install `ts-node` via `--require`/`-r` +Alternatively, you can invoke `node` directly and install ts-node via `--require`/`-r` ```shell node --trace-deprecation --abort-on-uncaught-exception -r ts-node/register ./index.ts diff --git a/website/docs/how-it-works.md b/website/docs/how-it-works.md index 28dec08aa..923c0592c 100644 --- a/website/docs/how-it-works.md +++ b/website/docs/how-it-works.md @@ -2,7 +2,7 @@ title: How It Works --- -`ts-node` works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. +ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. @@ -12,7 +12,7 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook > **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. -> **Warning:** When `ts-node` is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. +> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. ## Skipping `node_modules` diff --git a/website/docs/imports.md b/website/docs/imports.md index 9f759d207..6b04f776f 100644 --- a/website/docs/imports.md +++ b/website/docs/imports.md @@ -2,7 +2,7 @@ title: "CommonJS vs native ECMAScript modules" --- -TypeScript is almost always written using modern `import` syntax, but you can choose to either transform to CommonJS or use node's native ESM support. Configuration is different for each. +TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. Here is a brief comparison of the two. @@ -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 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` | ## CommonJS @@ -32,7 +32,7 @@ Transforming to CommonJS is typically simpler and more widely supported because } ``` -If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for `ts-node`. +If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, you can set an override for ts-node. ```json title="tsconfig.json" { @@ -49,8 +49,7 @@ If you must keep `"module": "ESNext"` for `tsc`, webpack, or another build tool, ## Native ECMAScript modules -[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. `ts-node`'s ESM support is also experimental. They may have -breaking changes in minor and patch releases and are not recommended for production. +[Node's ESM loader hooks](https://nodejs.org/api/esm.html#esm_experimental_loaders) are [**experimental**](https://nodejs.org/api/documentation.html#documentation_stability_index) and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can *and will* break in new versions of node. Thus it is not recommended for production. For complete usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). diff --git a/website/docs/installation.md b/website/docs/installation.md index 4b22d54d3..541434b49 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -15,4 +15,4 @@ npm install -g ts-node npm install -D tslib @types/node ``` -**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. TS Node will always resolve the compiler from `cwd` before checking relative to its own installation. +**Tip:** Installing modules locally allows you to control and share the versions through `package.json`. ts-node will always resolve the compiler from `cwd` before checking relative to its own installation. diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index 1656d9629..43ab19f55 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -2,7 +2,7 @@ title: Module type overrides --- -When deciding between CommonJS and native ECMAScript modules, `ts-node` defaults to matching vanilla `node` and `tsc` +When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed according to node's rules for the `package.json` `"type"` field. @@ -15,7 +15,7 @@ In these situations, our `moduleTypes` option lets you override certain files, f CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. `moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. -The following example tells `ts-node` to execute a webpack config as CommonJS: +The following example tells ts-node to execute a webpack config as CommonJS: ```json title=tsconfig.json { diff --git a/website/docs/options.md b/website/docs/options.md index 9139ea85c..90878e4f9 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -35,6 +35,7 @@ _Environment variables, where available, are in `ALL_CAPS`_ - `-I, --ignore [pattern]` Override the path patterns to skip compilation
*Default:* `/node_modules/`
*Environment:* `TS_NODE_IGNORE` - `--skip-ignore` Skip ignore checks
*Default:* `false`
*Environment:* `TS_NODE_SKIP_IGNORE` - `-C, --compiler [name]` Specify a custom TypeScript compiler
*Default:* `typescript`
*Environment:* `TS_NODE_COMPILER` +- `--swc` Transpile with [swc](./transpilers.md#swc). Implies `--transpile-only`
*Default:* `false` - `--transpiler [name]` Specify a third-party, non-typechecking transpiler - `--prefer-ts-exts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` diff --git a/website/docs/overview.md b/website/docs/overview.md index 9835306f9..4afde82d2 100644 --- a/website/docs/overview.md +++ b/website/docs/overview.md @@ -3,7 +3,7 @@ title: Overview slug: / --- -`ts-node` is a TypeScript execution engine and REPL for Node.js. +ts-node is a TypeScript execution engine and REPL for Node.js. It JIT transforms TypeScript into JavaScript, enabling you to directly execute TypeScript on Node.js without precompiling. This is accomplished by hooking node's module loading APIs, enabling it to be used seamlessly alongside other Node.js diff --git a/website/docs/paths.md b/website/docs/paths.md index 6bc60fecc..f514e8043 100644 --- a/website/docs/paths.md +++ b/website/docs/paths.md @@ -3,7 +3,7 @@ title: | paths and baseUrl --- -You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. +You can use ts-node together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. ```json title="tsconfig.json" { @@ -14,7 +14,7 @@ You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/packa } ``` -## Why is this not built-in to `ts-node`? +## Why is this not built-in to ts-node? The official TypeScript Handbook explains the intended purpose for `"paths"` in ["Additional module resolution flags"](https://www.typescriptlang.org/docs/handbook/module-resolution.html#additional-module-resolution-flags). @@ -23,4 +23,4 @@ The official TypeScript Handbook explains the intended purpose for `"paths"` in > It is important to note that the compiler will not perform any of these transformations; it just uses these pieces of information to guide the process of resolving a module import to its definition file. This means `"paths"` are intended to describe mappings that the build tool or runtime *already* performs, not to tell the build tool or -runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, `ts-node` does not modify `node`'s module resolution behavior to implement `"paths"` mappings. +runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, ts-node does not modify `node`'s module resolution behavior to implement `"paths"` mappings. diff --git a/website/docs/performance.md b/website/docs/performance.md index 625e1cd96..69b2c6cba 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -2,14 +2,14 @@ title: Make it fast --- -These tricks will make `ts-node` faster. +These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, `ts-node` can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. * Enable [`transpileOnly`](./options.md) to skip typechecking -* Use our [`swc` integration](./transpilers.md#bundled-swc-integration) +* Use our [`swc` integration](./transpilers.md#swc) * This is by far the fastest option ## With typechecking diff --git a/website/docs/recipes/visual-studio-code.md b/website/docs/recipes/visual-studio-code.md index 1d8d44b7f..e3327c03b 100644 --- a/website/docs/recipes/visual-studio-code.md +++ b/website/docs/recipes/visual-studio-code.md @@ -2,20 +2,22 @@ title: Visual Studio Code --- -Create a new node.js configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). +Create a new Node.js debug configuration, add `-r ts-node/register` to node args and move the `program` to the `args` list (so VS Code doesn't look for `outFiles`). -```json +```json title=".vscode/launch.json" { - "type": "node", - "request": "launch", - "name": "Launch Program", - "runtimeArgs": [ - "-r", - "ts-node/register" - ], - "args": [ - "${workspaceFolder}/index.ts" - ] + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "${workspaceFolder}/src/index.ts" + ] + }], } ``` diff --git a/website/docs/transpilers.md b/website/docs/transpilers.md index 2df5fa2e8..6d6f37095 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -1,10 +1,10 @@ --- -title: Third-party transpilers +title: Transpilers --- In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -`ts-node`'s automatic `tsconfig.json` discovery, sourcemap support, and global `ts-node` CLI. Integrations +ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project boilerplate. @@ -13,17 +13,16 @@ boilerplate. > For our purposes, a compiler implements TypeScript's API and can perform typechecking. > A third-party transpiler does not. Both transform TypeScript into JavaScript. -## Bundled `swc` integration +## swc -We have bundled an experimental `swc` integration. +swc support is built-in via the `--swc` flag or `"swc": true` tsconfig option. -[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster -than `transpileModule`. +[`swc`](https://swc.rs) is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than vanilla `transpileOnly`. -To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. +To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. ```shell -npm i -D @swc/core @swc/helpers +npm i -D @swc/core @swc/helpers regenerator-runtime ``` Then add the following to your `tsconfig.json`. @@ -31,17 +30,35 @@ Then add the following to your `tsconfig.json`. ```json title="tsconfig.json" { "ts-node": { - "transpileOnly": true, - "transpiler": "ts-node/transpilers/swc-experimental" + "swc": true } } ``` > `swc` uses `@swc/helpers` instead of `tslib`. If you have enabled `importHelpers`, you must also install `@swc/helpers`. +## Third-party transpilers + +The `transpiler` option allows using third-party transpiler integrations with ts-node. `transpiler` must be given the +name of a module which can be `require()`d. The built-in `swc` integration is exposed as `ts-node/transpilers/swc`. + +For example, to use a hypothetical "speedy-ts-compiler", first install it into your project: `npm install speedy-ts-compiler` + +Then add the following to your tsconfig: + +```json title="tsconfig.json" +{ + "ts-node": { + "transpileOnly": true, + "transpiler": "speedy-ts-compiler" + } +} +``` + ## Writing your own integration To write your own transpiler integration, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). -Integrations are `require()`d, so they can be published to npm. The module must export a `create` function matching the -[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface. +Integrations are `require()`d by ts-node, so they can be published to npm for convenience. The module must export a `create` function described by our +[`TranspilerModule`](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html) interface. `create` is invoked by ts-node +at startup to create the transpiler. The transpiler is used repeatedly to transform TypeScript into JavaScript. diff --git a/website/docs/troubleshooting.md b/website/docs/troubleshooting.md index 4ddac6910..2ee75225d 100644 --- a/website/docs/troubleshooting.md +++ b/website/docs/troubleshooting.md @@ -4,11 +4,11 @@ title: Troubleshooting ## Understanding configuration -`ts-node` uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you +ts-node uses sensible default configurations to reduce boilerplate while still respecting `tsconfig.json` if you have one. If you are unsure which configuration is used, you can log it with `ts-node --show-config`. This is similar to `tsc --showConfig` but includes `"ts-node"` options as well. -`ts-node` also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed +ts-node also respects your locally-installed `typescript` version, but global installations fallback to the globally-installed `typescript`. If you are unsure which versions are used, `ts-node -vv` will log them. ```shell @@ -54,7 +54,7 @@ $ ts-node --show-config ## Understanding Errors -It is important to differentiate between errors from `ts-node`, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. +It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. ### `TSError` @@ -62,7 +62,7 @@ Type errors from the compiler are thrown as a `TSError`. These are the same as ### `SyntaxError` -Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or `ts-node`. These are bugs in your code or configuration. +Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cannot be fixed by TypeScript or ts-node. These are bugs in your code or configuration. #### Unsupported JavaScript syntax diff --git a/website/docs/types.md b/website/docs/types.md index 168c5c6a7..eaf425a93 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -2,7 +2,7 @@ title: "Help! My Types Are Missing!" --- -**TypeScript Node** does _not_ use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, `ts-node` starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. +ts-node does _not_ use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). diff --git a/website/docs/usage.md b/website/docs/usage.md index 76048706c..ecd6721bc 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -42,14 +42,14 @@ Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, // This shebang is not portable. It only works on Mac ``` -Instead, specify all `ts-node` options in your `tsconfig.json`. +Instead, specify all ts-node options in your `tsconfig.json`. ## Programmatic -You can require `ts-node` and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. +You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. -**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of the `ts-node` CLI. +**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of ts-node's CLI. ### Developers -`ts-node` exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. +ts-node exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. diff --git a/website/readme-sources/prefix.md b/website/readme-sources/prefix.md index 2b5fe41ab..a24738ff9 100644 --- a/website/readme-sources/prefix.md +++ b/website/readme-sources/prefix.md @@ -21,12 +21,8 @@ You can build the readme with this command: [![Build status](https://img.shields.io/github/workflow/status/TypeStrong/ts-node/Continuous%20Integration)](https://github.com/TypeStrong/ts-node/actions?query=workflow%3A%22Continuous+Integration%22) [![Test coverage](https://codecov.io/gh/TypeStrong/ts-node/branch/main/graph/badge.svg)](https://codecov.io/gh/TypeStrong/ts-node) -> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.7`**. +> TypeScript execution and REPL for node.js, with source map and native ESM support. The latest documentation can also be found on our website: [https://typestrong.org/ts-node](https://typestrong.org/ts-node) -### *Experimental ESM support* - -Native ESM support is currently experimental. For usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007). - # Table of Contents