diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4bd5898c..3d2a5417 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, 11, 12] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] 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 @@ -119,8 +119,15 @@ jobs: typescript: next typescriptFlag: next downgradeNpm: true - # Node nightly + # Node 17 - flavor: 12 + node: 17 + nodeFlag: 17 + typescript: latest + typescriptFlag: latest + downgradeNpm: true + # Node nightly + - flavor: 13 node: nightly nodeFlag: nightly typescript: latest @@ -154,7 +161,7 @@ jobs: $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 + Invoke-WebRequest -Uri $url -OutFile $targetPath -UserAgent ([Microsoft.PowerShell.Commands.PSUserAgent]::Chrome) node --version # lint, build, test # Downgrade from npm 7 to 6 because 7 still seems buggy to me diff --git a/.prettierignore b/.prettierignore index e0158cb5..492a816c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,4 @@ tests/throw error.ts tests/throw error react tsx.tsx tests/esm/throw error.ts tests/legacy-source-map-support-interop/index.ts +tests/main-realpath/symlink/symlink.tsx diff --git a/README.md b/README.md index d5519856..90fb9f54 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ You can build the readme with this command: cd website && yarn build-readme --> -# ![TypeScript Node](logo.svg?sanitize=true) +# [![TypeScript Node](logo.svg?sanitize=true)](https://typestrong.org/ts-node) [![NPM version](https://img.shields.io/npm/v/ts-node.svg?style=flat)](https://npmjs.org/package/ts-node) [![NPM downloads](https://img.shields.io/npm/dm/ts-node.svg?style=flat)](https://npmjs.org/package/ts-node) @@ -60,13 +60,15 @@ The latest documentation can also be found on our website: *Environment:* `TS_NODE_PROJECT` -* `--skip-project` Skip project config resolution and loading
*Default:* `false`
*Environment:* `TS_NODE_SKIP_PROJECT` -* `-c, --cwd-mode` Resolve config relative to the current directory instead of the directory of the entrypoint script -* `-O, --compiler-options [opts]` JSON object to merge with compiler options
*Environment:* `TS_NODE_COMPILER_OPTIONS` -* `--show-config` Print resolved `tsconfig.json`, including `ts-node` options, and exit +* `--skipProject` Skip project config resolution and loading
*Default:* `false`
*Environment:* `TS_NODE_SKIP_PROJECT` +* `-c, --cwdMode` Resolve config relative to the current directory instead of the directory of the entrypoint script +* `-O, --compilerOptions [opts]` JSON object to merge with compiler options
*Environment:* `TS_NODE_COMPILER_OPTIONS` +* `--showConfig` Print resolved `tsconfig.json`, including `ts-node` options, and exit ## Typechecking -* `-T, --transpile-only` Use TypeScript's faster `transpileModule`
*Default:* `false`
*Environment:* `TS_NODE_TRANSPILE_ONLY` -* `--type-check` Opposite of `--transpile-only`
*Default:* `true`
*Environment:* `TS_NODE_TYPE_CHECK` -* `-H, --compiler-host` Use TypeScript's compiler host API
*Default:* `false`
*Environment:* `TS_NODE_COMPILER_HOST` +* `-T, --transpileOnly` Use TypeScript's faster `transpileModule`
*Default:* `false`
*Environment:* `TS_NODE_TRANSPILE_ONLY` +* `--typeCheck` Opposite of `--transpileOnly`
*Default:* `true`
*Environment:* `TS_NODE_TYPE_CHECK` +* `-H, --compilerHost` Use TypeScript's compiler host API
*Default:* `false`
*Environment:* `TS_NODE_COMPILER_HOST` * `--files` Load `files`, `include` and `exclude` from `tsconfig.json` on startup
*Default:* `false`
*Environment:* `TS_NODE_FILES` -* `-D, --ignore-diagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` +* `-D, --ignoreDiagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` ## Transpilation * `-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` +* `--skipIgnore` 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](#swc). Implies `--transpileOnly`
*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` +* `--preferTsExts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` ## Diagnostics -* `--log-error` Logs TypeScript errors to stderr instead of throwing exceptions
*Default:* `false`
*Environment:* `TS_NODE_LOG_ERROR` +* `--logError` Logs TypeScript errors to stderr instead of throwing exceptions
*Default:* `false`
*Environment:* `TS_NODE_LOG_ERROR` * `--pretty` Use pretty diagnostic formatter
*Default:* `false`
*Environment:* `TS_NODE_PRETTY` * `TS_NODE_DEBUG` Enable debug logging
@@ -321,9 +341,10 @@ node --trace-deprecation --abort-on-uncaught-exception -r ts-node/register ./ind * `--emit` Emit output files into `.ts-node` directory
*Default:* `false`
*Environment:* `TS_NODE_EMIT` * `--scope` Scope compiler to files within `scopeDir`. Anything outside this directory is ignored.
\*Default: `false`
*Environment:* `TS_NODE_SCOPE` * `--scopeDir` Directory within which compiler is limited when `scope` is enabled.
*Default:* First of: `tsconfig.json` "rootDir" if specified, directory containing `tsconfig.json`, or cwd if no `tsconfig.json` is loaded.
*Environment:* `TS_NODE_SCOPE_DIR` -* `moduleType` Override the module type of certain files, ignoring the `package.json` `"type"` field. See [Module type overrides](#module-type-overrides) for details.
*Default:* obeys `package.json` `"type"` and `tsconfig.json` `"module"`
*Can only be specified via `tsconfig.json` or API.* +* `moduleTypes` Override the module type of certain files, ignoring the `package.json` `"type"` field. See [Module type overrides](#module-type-overrides) for details.
*Default:* obeys `package.json` `"type"` and `tsconfig.json` `"module"`
*Can only be specified via `tsconfig.json` or API.* * `TS_NODE_HISTORY` Path to history file for REPL
*Default:* `~/.ts_node_repl_history`
-* `--no-experimental-repl-await` Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl-await`](https://nodejs.org/api/cli.html#cli_no_experimental_repl_await)
*Default:* Enabled if TypeScript version is 3.8 or higher and target is ES2018 or higher.
*Environment:* `TS_NODE_EXPERIMENTAL_REPL_AWAIT` set `false` to disable +* `--noExperimentalReplAwait` Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl-await`](https://nodejs.org/api/cli.html#cli_no_experimental_repl_await)
*Default:* Enabled if TypeScript version is 3.8 or higher and target is ES2018 or higher.
*Environment:* `TS_NODE_EXPERIMENTAL_REPL_AWAIT` set `false` to disable +* `experimentalResolverFeatures` Enable experimental features that re-map imports and require calls to support: `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, `outDir` to `rootDir` mappings for composite projects and monorepos. For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514)
*Default:* `false`
*Can only be specified via `tsconfig.json` or API.* ## API @@ -403,7 +424,7 @@ 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 -have one. If you are unsure which configuration is used, you can log it with `ts-node --show-config`. This is similar to +have one. If you are unsure which configuration is used, you can log it with `ts-node --showConfig`. 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 @@ -415,7 +436,7 @@ ts-node v10.0.0 node v16.1.0 compiler v4.2.2 -$ ts-node --show-config +$ ts-node --showConfig { "compilerOptions": { "target": "es6", @@ -490,7 +511,7 @@ These tricks will make ts-node faster. 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) +* Use our [`swc` integration](#swc) * This is by far the fastest option ## With typechecking @@ -519,12 +540,20 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook ### Skipping `node_modules` -By default, **TypeScript Node** avoids compiling files in `/node_modules/` for three reasons: +By default, ts-node avoids compiling files in `/node_modules/` for three reasons: 1. Modules should always be published in a format node.js can consume 2. Transpiling the entire dependency tree will make your project slower 3. Differing behaviours between TypeScript and node.js (e.g. ES2015 modules) can result in a project that works until you decide to support a feature natively from node.js +If you need to import uncompiled TypeScript in `node_modules`, use [`--skipIgnore`](#transpilation) or [`TS_NODE_SKIP_IGNORE`](#transpilation) to bypass this restriction. + +### Skipping pre-compiled TypeScript + +If a compiled JavaScript file with the same name as a TypeScript file already exists, the TypeScript file will be ignored. ts-node will import the pre-compiled JavaScript. + +To force ts-node to import the TypeScript source, not the precompiled JavaScript, use [`--preferTsExts`](#transpilation). + ## 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`. @@ -628,7 +657,7 @@ For example, to use `ttypescript` and `ts-transformer-keys`, add this to your `t } ``` -## Third-party transpilers +## 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 @@ -641,12 +670,11 @@ 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`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. @@ -659,20 +687,38 @@ Then add the following to your `tsconfig.json`. ```jsonc 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: + +```jsonc 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. ## Module type overrides diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 6c6e8ee6..67f04f2b 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -31,7 +31,7 @@ export interface CreateOptions { ignore?: string[]; ignoreDiagnostics?: Array; logError?: boolean; - moduleTypes?: Record; + moduleTypes?: ModuleTypes; pretty?: boolean; project?: string; projectSearchDir?: string; @@ -43,10 +43,12 @@ export interface CreateOptions { scopeDir?: string; skipIgnore?: boolean; skipProject?: boolean; + swc?: boolean; // (undocumented) transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers); transpileOnly?: boolean; transpiler?: string | [string, object]; + tsTrace?: (str: string) => void; typeCheck?: boolean; } @@ -72,12 +74,15 @@ export interface CreateReplOptions { // @public (undocumented) export interface CreateTranspilerOptions { // (undocumented) - service: Pick; + service: Pick>; } // @public export type EvalAwarePartialHost = Pick; +// @public (undocumented) +export type ModuleTypes = Record; + // @public (undocumented) export interface NodeLoaderHooksAPI1 { // (undocumented) @@ -118,12 +123,22 @@ export namespace NodeLoaderHooksAPI2 { // (undocumented) export type LoadHook = (url: string, context: { format: NodeLoaderHooksFormat | null | undefined; + importAssertions?: NodeImportAssertions; }, defaultLoad: NodeLoaderHooksAPI2['load']) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined; }>; // (undocumented) + export interface NodeImportAssertions { + // (undocumented) + type?: 'json'; + } + // (undocumented) + export type NodeImportConditions = unknown; + // (undocumented) export type ResolveHook = (specifier: string, context: { + conditions?: NodeImportConditions; + importAssertions?: NodeImportAssertions; parentURL: string; }, defaultResolve: ResolveHook) => Promise<{ url: string; @@ -147,6 +162,7 @@ export const REGISTER_INSTANCE: unique symbol; // @public export interface RegisterOptions extends CreateOptions { + experimentalResolverFeatures?: boolean; preferTsExts?: boolean; } @@ -276,7 +292,7 @@ export interface TSCommon { } // @public -export interface TsConfigOptions extends Omit { +export interface TsConfigOptions extends Omit { } // @public diff --git a/ava.config.js b/ava.config.js index 9cf877b8..6181565d 100644 --- a/ava.config.js +++ b/ava.config.js @@ -3,6 +3,12 @@ export default { failWithoutAssertions: false, environmentVariables: { ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, + // Force jest expect() errors to generate colorized strings, makes output more readable. + // Delete the env var within ava processes via `require` option below. + // This avoids passing it to spawned processes under test, which would negatively affect + // their behavior. + FORCE_COLOR: '3', }, + require: ['./src/test/remove-env-var-force-color.js'], timeout: '300s', }; diff --git a/dist-raw/node-errors.js b/dist-raw/node-errors.js index f7c6edcf..73255a7d 100644 --- a/dist-raw/node-errors.js +++ b/dist-raw/node-errors.js @@ -12,6 +12,7 @@ exports.codes = { ERR_PACKAGE_PATH_NOT_EXPORTED: createErrorCtor(joinArgs('ERR_PACKAGE_PATH_NOT_EXPORTED')), ERR_UNSUPPORTED_DIR_IMPORT: createErrorCtor(joinArgs('ERR_UNSUPPORTED_DIR_IMPORT')), ERR_UNSUPPORTED_ESM_URL_SCHEME: createErrorCtor(joinArgs('ERR_UNSUPPORTED_ESM_URL_SCHEME')), + ERR_UNKNOWN_FILE_EXTENSION: createErrorCtor(joinArgs('ERR_UNKNOWN_FILE_EXTENSION')), } function joinArgs(name) { diff --git a/dist-raw/node-options.js b/dist-raw/node-options.js index 0602a476..22722755 100644 --- a/dist-raw/node-options.js +++ b/dist-raw/node-options.js @@ -34,7 +34,9 @@ function parseArgv(argv) { '--es-module-specifier-resolution': '--experimental-specifier-resolution', '--experimental-policy': String, '--conditions': [String], - '--pending-deprecation': Boolean + '--pending-deprecation': Boolean, + '--experimental-json-modules': Boolean, + '--experimental-wasm-modules': Boolean, }, { argv, permissive: true diff --git a/esm.mjs b/esm.mjs index 4d404070..0843cf34 100644 --- a/esm.mjs +++ b/esm.mjs @@ -4,9 +4,5 @@ const require = createRequire(fileURLToPath(import.meta.url)); /** @type {import('./dist/esm')} */ const esm = require('./dist/esm'); -export const { - resolve, - load, - getFormat, - transformSource, -} = esm.registerAndCreateEsmHooks(); +export const { resolve, load, getFormat, transformSource } = + esm.registerAndCreateEsmHooks(); diff --git a/esm/transpile-only.mjs b/esm/transpile-only.mjs index 07b2c7ae..2e154da0 100644 --- a/esm/transpile-only.mjs +++ b/esm/transpile-only.mjs @@ -4,9 +4,5 @@ const require = createRequire(fileURLToPath(import.meta.url)); /** @type {import('../dist/esm')} */ const esm = require('../dist/esm'); -export const { - resolve, - load, - getFormat, - transformSource, -} = esm.registerAndCreateEsmHooks({ transpileOnly: true }); +export const { resolve, load, getFormat, transformSource } = + esm.registerAndCreateEsmHooks({ transpileOnly: true }); diff --git a/package-lock.json b/package-lock.json index db9178e3..632b7c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.4.0", + "version": "10.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -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 }, @@ -3131,15 +3146,6 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, "lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -3178,9 +3184,9 @@ } }, "marked": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.4.tgz", - "integrity": "sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz", + "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==", "dev": true }, "matcher": { @@ -3595,15 +3601,6 @@ "mimic-fn": "^2.1.0" } }, - "onigasm": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", - "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", - "dev": true, - "requires": { - "lru-cache": "^5.1.1" - } - }, "ora": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", @@ -3876,9 +3873,9 @@ "dev": true }, "prettier": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", - "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", "dev": true }, "pretty-format": { @@ -4271,13 +4268,13 @@ "dev": true }, "shiki": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.11.tgz", - "integrity": "sha512-tjruNTLFhU0hruCPoJP0y+B9LKOmcqUhTpxn7pcJB3fa+04gFChuEmxmrUfOJ7ZO6Jd+HwMnDHgY3lv3Tqonuw==", + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.15.tgz", + "integrity": "sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==", "dev": true, "requires": { "jsonc-parser": "^3.0.0", - "onigasm": "^2.2.5", + "vscode-oniguruma": "^1.6.1", "vscode-textmate": "5.2.0" } }, @@ -4709,16 +4706,16 @@ } }, "typedoc": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.4.tgz", - "integrity": "sha512-M/a8NnPxq3/iZNNVjzFCK5gu4m//HTJIPbSS0JQVbkHJPP9wyepR12agylWTSqeVZe0xsbidVtO26+PP7iD/jw==", + "version": "0.22.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.10.tgz", + "integrity": "sha512-hQYZ4WtoMZ61wDC6w10kxA42+jclWngdmztNZsDvIz7BMJg7F2xnT+uYsUa7OluyKossdFj9E9Ye4QOZKTy8SA==", "dev": true, "requires": { - "glob": "^7.1.7", + "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^3.0.4", + "marked": "^3.0.8", "minimatch": "^3.0.4", - "shiki": "^0.9.11" + "shiki": "^0.9.12" }, "dependencies": { "glob": { @@ -4738,9 +4735,9 @@ } }, "typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, "typescript-json-schema": { @@ -4885,6 +4882,11 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -4901,6 +4903,12 @@ "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", "dev": true }, + "vscode-oniguruma": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz", + "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==", + "dev": true + }, "vscode-textmate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", @@ -5038,12 +5046,6 @@ "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms=", "dev": true }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "yargs": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", diff --git a/package.json b/package.json index 060a78c4..5e6323f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.4.0", + "version": "10.5.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", @@ -125,15 +126,15 @@ "lodash": "^4.17.15", "ntypescript": "^1.201507091536.1", "nyc": "^15.0.1", - "prettier": "^2.2.1", + "prettier": "^2.5.1", "proper-lockfile": "^4.1.2", "proxyquire": "^2.0.0", "react": "^16.14.0", "rimraf": "^3.0.0", "semver": "^7.1.3", "throat": "^6.0.1", - "typedoc": "^0.22.4", - "typescript": "4.4.3", + "typedoc": "^0.22.10", + "typescript": "4.5.2", "typescript-json-schema": "^0.51.0", "util.promisify": "^1.0.1" }, @@ -163,6 +164,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", "yn": "3.1.1" }, "prettier": { diff --git a/scripts/create-merged-schema.ts b/scripts/create-merged-schema.ts index 4016fd33..c380deee 100755 --- a/scripts/create-merged-schema.ts +++ b/scripts/create-merged-schema.ts @@ -15,30 +15,53 @@ async function main() { const schemastoreSchema = await getSchemastoreSchema(); /** ts-node schema auto-generated from ts-node source code */ - const typescriptNodeSchema = require('../tsconfig.schema.json'); + const originalTsNodeSchema = require('../tsconfig.schema.json'); + // Apply this prefix to the names of all ts-node-generated definitions + const tsnodeDefinitionPrefix = 'tsNode'; + let tsNodeSchema: any = JSON.parse( + JSON.stringify(originalTsNodeSchema).replace( + /#\/definitions\//g, + `#/definitions/${tsnodeDefinitionPrefix}` + ) + ); + tsNodeSchema.definitions = Object.fromEntries( + Object.entries(tsNodeSchema.definitions).map(([key, value]) => [ + `${tsnodeDefinitionPrefix}${key}`, + value, + ]) + ); + // console.dir(tsNodeSchema, { + // depth: Infinity + // }); /** Patch ts-node stuff into the schemastore definition. */ const mergedSchema = { ...schemastoreSchema, definitions: { - ...schemastoreSchema.definitions, + ...Object.fromEntries( + Object.entries(schemastoreSchema.definitions).filter( + ([key]) => !key.startsWith(tsnodeDefinitionPrefix) + ) + ), + ...tsNodeSchema.definitions, + tsNodeTsConfigOptions: undefined, + tsNodeTsConfigSchema: undefined, tsNodeDefinition: { properties: { 'ts-node': { - ...typescriptNodeSchema.definitions.TsConfigOptions, + ...tsNodeSchema.definitions.tsNodeTsConfigOptions, description: - typescriptNodeSchema.definitions.TsConfigSchema.properties[ + tsNodeSchema.definitions.tsNodeTsConfigSchema.properties[ 'ts-node' ].description, properties: { - ...typescriptNodeSchema.definitions.TsConfigOptions.properties, + ...tsNodeSchema.definitions.tsNodeTsConfigOptions.properties, compilerOptions: { - ...typescriptNodeSchema.definitions.TsConfigOptions.properties + ...tsNodeSchema.definitions.tsNodeTsConfigOptions.properties .compilerOptions, allOf: [ { - $ref: - '#/definitions/compilerOptionsDefinition/properties/compilerOptions', + $ref: '#/definitions/compilerOptionsDefinition/properties/compilerOptions', }, ], }, @@ -47,14 +70,16 @@ async function main() { }, }, }, - allOf: [ - // Splice into the allOf array at a spot that looks good. Does not affect - // behavior of the schema, but looks nicer if we want to submit as a PR to schemastore. - ...schemastoreSchema.allOf.slice(0, 4), - { $ref: '#/definitions/tsNodeDefinition' }, - ...schemastoreSchema.allOf.slice(4), - ], }; + // Splice into the allOf array at a spot that looks good. Does not affect + // behavior of the schema, but looks nicer if we want to submit as a PR to schemastore. + mergedSchema.allOf = mergedSchema.allOf.filter( + (item: any) => !item.$ref?.includes('tsNode') + ); + mergedSchema.allOf.splice(mergedSchema.allOf.length - 1, 0, { + $ref: '#/definitions/tsNodeDefinition', + }); + writeFileSync( resolve(__dirname, '../tsconfig.schemastore-schema.json'), JSON.stringify(mergedSchema, null, 2) @@ -62,9 +87,7 @@ async function main() { } export async function getSchemastoreSchema() { - const { - data: schemastoreSchema, - } = await axios.get( + const { data: schemastoreSchema } = await axios.get( 'https://schemastore.azurewebsites.net/schemas/json/tsconfig.json', { responseType: 'json' } ); diff --git a/src/bin-cwd.ts b/src/bin-cwd.ts index bd2f4483..3a669765 100644 --- a/src/bin-cwd.ts +++ b/src/bin-cwd.ts @@ -2,4 +2,4 @@ import { main } from './bin'; -main(undefined, { '--cwd-mode': true }); +main(undefined, { '--cwdMode': true }); diff --git a/src/bin-script-deprecated.ts b/src/bin-script-deprecated.ts index 07112d85..a4dcdb91 100644 --- a/src/bin-script-deprecated.ts +++ b/src/bin-script-deprecated.ts @@ -7,4 +7,4 @@ console.warn( 'Please use ts-node-script instead' ); -main(undefined, { '--script-mode': true }); +main(undefined, { '--scriptMode': true }); diff --git a/src/bin-script.ts b/src/bin-script.ts index 78f9ab32..7c28aa1f 100644 --- a/src/bin-script.ts +++ b/src/bin-script.ts @@ -2,4 +2,4 @@ import { main } from './bin'; -main(undefined, { '--script-mode': true }); +main(undefined, { '--scriptMode': true }); diff --git a/src/bin-transpile.ts b/src/bin-transpile.ts index 8eb81814..4dce851c 100644 --- a/src/bin-transpile.ts +++ b/src/bin-transpile.ts @@ -2,4 +2,4 @@ import { main } from './bin'; -main(undefined, { '--transpile-only': true }); +main(undefined, { '--transpileOnly': true }); diff --git a/src/bin.ts b/src/bin.ts index 95c7f932..0194f3a3 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { join, resolve, dirname, parse as parsePath } from 'path'; +import { join, resolve, dirname, parse as parsePath, relative } from 'path'; import { inspect } from 'util'; import Module = require('module'); import arg = require('arg'); @@ -17,7 +17,7 @@ import { STDIN_NAME, REPL_FILENAME, } from './repl'; -import { VERSION, TSError, register } from './index'; +import { VERSION, TSError, register, versionGteLt } from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; @@ -28,6 +28,20 @@ export function main( argv: string[] = process.argv.slice(2), entrypointArgs: Record = {} ) { + // HACK: technically, this function is not marked @internal so it's possible + // that libraries in the wild are doing `require('ts-node/dist/bin').main({'--transpile-only': true})` + // We can mark this function @internal in next major release. + // For now, rewrite args to avoid a breaking change. + entrypointArgs = { ...entrypointArgs }; + for (const key of Object.keys(entrypointArgs)) { + entrypointArgs[ + key.replace( + /([a-z])-([a-z])/g, + (_$0, $1, $2: string) => `${$1}${$2.toUpperCase()}` + ) + ] = entrypointArgs[key]; + } + const args = { ...entrypointArgs, ...arg( @@ -40,32 +54,33 @@ export function main( // CLI options. '--help': Boolean, - '--cwd-mode': Boolean, - '--script-mode': Boolean, + '--cwdMode': Boolean, + '--scriptMode': Boolean, '--version': arg.COUNT, - '--show-config': Boolean, + '--showConfig': Boolean, // Project options. '--cwd': String, '--files': Boolean, '--compiler': String, - '--compiler-options': parse, + '--compilerOptions': parse, '--project': String, - '--ignore-diagnostics': [String], + '--ignoreDiagnostics': [String], '--ignore': [String], - '--transpile-only': Boolean, + '--transpileOnly': Boolean, '--transpiler': String, - '--type-check': Boolean, - '--compiler-host': Boolean, + '--swc': Boolean, + '--typeCheck': Boolean, + '--compilerHost': Boolean, '--pretty': Boolean, - '--skip-project': Boolean, - '--skip-ignore': Boolean, - '--prefer-ts-exts': Boolean, - '--log-error': Boolean, + '--skipProject': Boolean, + '--skipIgnore': Boolean, + '--preferTsExts': Boolean, + '--logError': Boolean, '--emit': Boolean, '--scope': Boolean, - '--scope-dir': String, - '--no-experimental-repl-await': Boolean, + '--scopeDir': String, + '--noExperimentalReplAwait': Boolean, // Aliases. '-e': '--eval', @@ -75,16 +90,30 @@ export function main( '-h': '--help', '-s': '--script-mode', '-v': '--version', - '-T': '--transpile-only', - '-H': '--compiler-host', + '-T': '--transpileOnly', + '-H': '--compilerHost', '-I': '--ignore', '-P': '--project', '-C': '--compiler', - '-D': '--ignore-diagnostics', - '-O': '--compiler-options', + '-D': '--ignoreDiagnostics', + '-O': '--compilerOptions', '--dir': '--cwd', - '--showConfig': '--show-config', - '--scopeDir': '--scope-dir', + + // Support both tsc-style camelCase and node-style hypen-case for *all* flags + '--cwd-mode': '--cwdMode', + '--script-mode': '--scriptMode', + '--show-config': '--showConfig', + '--compiler-options': '--compilerOptions', + '--ignore-diagnostics': '--ignoreDiagnostics', + '--transpile-only': '--transpileOnly', + '--type-check': '--typeCheck', + '--compiler-host': '--compilerHost', + '--skip-project': '--skipProject', + '--skip-ignore': '--skipIgnore', + '--prefer-ts-exts': '--preferTsExts', + '--log-error': '--logError', + '--scope-dir': '--scopeDir', + '--no-experimental-repl-await': '--noExperimentalReplAwait', }, { argv, @@ -99,72 +128,74 @@ export function main( const { '--cwd': cwdArg, '--help': help = false, - '--script-mode': scriptMode, - '--cwd-mode': cwdMode, + '--scriptMode': scriptMode, + '--cwdMode': cwdMode, '--version': version = 0, - '--show-config': showConfig, + '--showConfig': showConfig, '--require': argsRequire = [], '--eval': code = undefined, '--print': print = false, '--interactive': interactive = false, '--files': files, '--compiler': compiler, - '--compiler-options': compilerOptions, + '--compilerOptions': compilerOptions, '--project': project, - '--ignore-diagnostics': ignoreDiagnostics, + '--ignoreDiagnostics': ignoreDiagnostics, '--ignore': ignore, - '--transpile-only': transpileOnly, - '--type-check': typeCheck, + '--transpileOnly': transpileOnly, + '--typeCheck': typeCheck, '--transpiler': transpiler, - '--compiler-host': compilerHost, + '--swc': swc, + '--compilerHost': compilerHost, '--pretty': pretty, - '--skip-project': skipProject, - '--skip-ignore': skipIgnore, - '--prefer-ts-exts': preferTsExts, - '--log-error': logError, + '--skipProject': skipProject, + '--skipIgnore': skipIgnore, + '--preferTsExts': preferTsExts, + '--logError': logError, '--emit': emit, '--scope': scope = undefined, - '--scope-dir': scopeDir = undefined, - '--no-experimental-repl-await': noExperimentalReplAwait, + '--scopeDir': scopeDir = undefined, + '--noExperimentalReplAwait': noExperimentalReplAwait, } = args; if (help) { console.log(` - Usage: ts-node [options] [ -e script | script.ts ] [arguments] - - Options: - - -e, --eval [code] Evaluate code - -p, --print Print result of \`--eval\` - -r, --require [path] Require a node module before execution - -i, --interactive Opens the REPL even if stdin does not appear to be a terminal - - -h, --help Print CLI usage - -v, --version Print module version information - --cwd-mode Use current directory instead of for config resolution - --show-config Print resolved configuration and exit - - -T, --transpile-only Use TypeScript's faster \`transpileModule\` or a third-party 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 - -C, --compiler [name] Specify a custom TypeScript compiler - --transpiler [name] Specify a third-party, non-typechecking transpiler - -D, --ignore-diagnostics [code] Ignore TypeScript warnings by diagnostic code - -O, --compiler-options [opts] JSON object to merge with compiler options - - --cwd Behave as if invoked within this working directory. - --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup - --pretty Use pretty diagnostic formatter (usually enabled by default) - --skip-project Skip reading \`tsconfig.json\` - --skip-ignore Skip \`--ignore\` checks - --emit Emit output files into \`.ts-node\` directory - --scope Scope compiler to files within \`scopeDir\`. Anything outside this directory is ignored. - --scope-dir Directory for \`--scope\` - --prefer-ts-exts Prefer importing TypeScript files over JavaScript files - --log-error Logs TypeScript errors to stderr instead of throwing exceptions - --no-experimental-repl-await Disable top-level await in REPL. Equivalent to node's --no-experimental-repl-await - `); +Usage: ts-node [options] [ -e script | script.ts ] [arguments] + +Options: + + -e, --eval [code] Evaluate code + -p, --print Print result of \`--eval\` + -r, --require [path] Require a node module before execution + -i, --interactive Opens the REPL even if stdin does not appear to be a terminal + + -h, --help Print CLI usage + -v, --version Print module version information + --cwdMode Use current directory instead of for config resolution + --showConfig Print resolved configuration and exit + + -T, --transpileOnly Use TypeScript's faster \`transpileModule\` or a third-party transpiler + --swc Use the swc transpiler + -H, --compilerHost Use TypeScript's compiler host API + -I, --ignore [pattern] Override the path patterns to skip compilation + -P, --project [path] Path to TypeScript JSON project file + -C, --compiler [name] Specify a custom TypeScript compiler + --transpiler [name] Specify a third-party, non-typechecking transpiler + -D, --ignoreDiagnostics [code] Ignore TypeScript warnings by diagnostic code + -O, --compilerOptions [opts] JSON object to merge with compiler options + + --cwd Behave as if invoked within this working directory. + --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup + --pretty Use pretty diagnostic formatter (usually enabled by default) + --skipProject Skip reading \`tsconfig.json\` + --skipIgnore Skip \`--ignore\` checks + --emit Emit output files into \`.ts-node\` directory + --scope Scope compiler to files within \`scopeDir\`. Anything outside this directory is ignored. + --scopeDir Directory for \`--scope\` + --preferTsExts Prefer importing TypeScript files over JavaScript files + --logError Logs TypeScript errors to stderr instead of throwing exceptions + --noExperimentalReplAwait Disable top-level await in REPL. Equivalent to node's --no-experimental-repl-await +`); process.exit(0); } @@ -256,6 +287,7 @@ export function main( experimentalReplAwait: noExperimentalReplAwait ? false : undefined, typeCheck, transpiler, + swc, compilerHost, ignore, preferTsExts, @@ -280,24 +312,50 @@ export function main( stdinStuff?.repl.setService(service); // Output project information. - if (version >= 2) { + if (version === 2) { console.log(`ts-node v${VERSION}`); console.log(`node ${process.version}`); console.log(`compiler v${service.ts.version}`); process.exit(0); } + if (version >= 3) { + console.log(`ts-node v${VERSION} ${dirname(__dirname)}`); + console.log(`node ${process.version}`); + console.log( + `compiler v${service.ts.version} ${service.compilerPath ?? ''}` + ); + process.exit(0); + } if (showConfig) { - const ts = (service.ts as any) as TSInternal; + const ts = service.ts as any as TSInternal; if (typeof ts.convertToTSConfig !== 'function') { console.error( - 'Error: --show-config requires a typescript versions >=3.2 that support --showConfig' + 'Error: --showConfig requires a typescript versions >=3.2 that support --showConfig' ); process.exit(1); } + let moduleTypes = undefined; + if (service.options.moduleTypes) { + // Assumption: this codepath requires CLI invocation, so moduleTypes must have come from a tsconfig, not API. + const showRelativeTo = dirname(service.configFilePath!); + moduleTypes = {} as Record; + for (const [key, value] of Object.entries(service.options.moduleTypes)) { + moduleTypes[ + relative( + showRelativeTo, + resolve(service.options.optionBasePaths?.moduleTypes!, key) + ) + ] = value; + } + } const json = { ['ts-node']: { ...service.options, + require: service.options.require?.length + ? service.options.require + : undefined, + moduleTypes, optionBasePaths: undefined, compilerOptions: undefined, project: service.configFilePath ?? service.options.project, @@ -423,12 +481,11 @@ let guaranteedNonexistentDirectorySuffix = 0; * https://stackoverflow.com/questions/59865584/how-to-invalidate-cached-require-resolve-results */ function requireResolveNonCached(absoluteModuleSpecifier: string) { - // node 10 and 11 fallback: The trick below triggers a node 10 & 11 bug - // On those node versions, pollute the require cache instead. This is a deliberate - // ts-node limitation that will *rarely* manifest, and will not matter once node 10 - // is end-of-life'd on 2021-04-30 - const isSupportedNodeVersion = - parseInt(process.versions.node.split('.')[0], 10) >= 12; + // node <= 12.1.x fallback: The trick below triggers a node bug on old versions. + // On these old versions, pollute the require cache instead. This is a deliberate + // ts-node limitation that will *rarely* manifest, and will not matter once node 12 + // is end-of-life'd on 2022-04-30 + const isSupportedNodeVersion = versionGteLt(process.versions.node, '12.2.0'); if (!isSupportedNodeVersion) return require.resolve(absoluteModuleSpecifier); const { dir, base } = parsePath(absoluteModuleSpecifier); diff --git a/src/cjs-resolve-filename-hook.ts b/src/cjs-resolve-filename-hook.ts new file mode 100644 index 00000000..9c6f66b0 --- /dev/null +++ b/src/cjs-resolve-filename-hook.ts @@ -0,0 +1,59 @@ +import type Module = require('module'); +import type { Service } from '.'; + +/** @internal */ +export type ModuleConstructorWithInternals = typeof Module & { + _resolveFilename( + request: string, + parent?: Module, + isMain?: boolean, + options?: ModuleResolveFilenameOptions, + ...rest: any[] + ): string; + _preloadModules(requests?: string[]): void; +}; + +interface ModuleResolveFilenameOptions { + paths?: Array; +} + +/** + * @internal + */ +export function installCommonjsResolveHookIfNecessary(tsNodeService: Service) { + const Module = require('module') as ModuleConstructorWithInternals; + const originalResolveFilename = Module._resolveFilename; + const shouldInstallHook = tsNodeService.options.experimentalResolverFeatures; + if (shouldInstallHook) { + Module._resolveFilename = _resolveFilename; + } + function _resolveFilename( + this: any, + request: string, + parent?: Module, + isMain?: boolean, + options?: ModuleResolveFilenameOptions, + ...rest: any[] + ): string { + if (!tsNodeService.enabled()) + return originalResolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + + // This is a stub to support other pull requests that will be merged in the near future + // Right now, it does nothing. + return originalResolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + } +} diff --git a/src/configuration.ts b/src/configuration.ts index a970b49c..ff38ddd4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -10,7 +10,7 @@ import { import type { TSInternal } from './ts-compiler-types'; import { createTsInternals } from './ts-internals'; import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; -import { assign, createRequire, trace } from './util'; +import { assign, createProjectLocalResolveHelper } from './util'; /** * TypeScript compiler option values required by `ts-node` which cannot be overridden. @@ -94,6 +94,7 @@ export function readConfig( readFile = ts.sys.readFile, skipProject = DEFAULTS.skipProject, project = DEFAULTS.project, + tsTrace = DEFAULTS.tsTrace, } = rawApiOptions; // Read project configuration when available. @@ -137,11 +138,11 @@ export function readConfig( readDirectory: ts.sys.readDirectory, readFile, useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, - trace, + trace: tsTrace, }, bp, errors, - ((ts as unknown) as TSInternal).createCompilerDiagnostic + (ts as unknown as TSInternal).createCompilerDiagnostic ); if (errors.length) { return { @@ -164,15 +165,18 @@ export function readConfig( const optionBasePaths: OptionBasePaths = {}; for (let i = configChain.length - 1; i >= 0; i--) { const { config, basePath, configPath } = configChain[i]; - const options = filterRecognizedTsConfigTsNodeOptions(config['ts-node']) - .recognized; + const options = filterRecognizedTsConfigTsNodeOptions( + config['ts-node'] + ).recognized; // Some options are relative to the config file, so must be converted to absolute paths here if (options.require) { // Modules are found relative to the tsconfig file, not the `dir` option - const tsconfigRelativeRequire = createRequire(configPath); + const tsconfigRelativeResolver = createProjectLocalResolveHelper( + dirname(configPath) + ); options.require = options.require.map((path: string) => - tsconfigRelativeRequire.resolve(path) + tsconfigRelativeResolver(path, false) ); } if (options.scopeDir) { @@ -183,6 +187,12 @@ export function readConfig( if (options.moduleTypes) { optionBasePaths.moduleTypes = basePath; } + if (options.transpiler != null) { + optionBasePaths.transpiler = basePath; + } + if (options.compiler != null) { + optionBasePaths.compiler = basePath; + } assign(tsNodeOptionsFromTsconfig, options); } @@ -249,9 +259,7 @@ export function readConfig( * Given the raw "ts-node" sub-object from a tsconfig, return an object with only the properties * recognized by "ts-node" */ -function filterRecognizedTsConfigTsNodeOptions( - jsonObject: any -): { +function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { recognized: TsConfigOptions; unrecognized: any; } { @@ -276,6 +284,8 @@ function filterRecognizedTsConfigTsNodeOptions( scopeDir, moduleTypes, experimentalReplAwait, + swc, + experimentalResolverFeatures, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -298,9 +308,13 @@ function filterRecognizedTsConfigTsNodeOptions( scope, scopeDir, moduleTypes, + swc, + experimentalResolverFeatures, }; // 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; - const catchMissingProps: keyof typeof filteredTsConfigOptions = (null as any) as keyof TsConfigOptions; + const catchExtraneousProps: keyof TsConfigOptions = + null as any as keyof typeof filteredTsConfigOptions; + const catchMissingProps: keyof typeof filteredTsConfigOptions = + null as any as keyof TsConfigOptions; return { recognized: filteredTsConfigOptions, unrecognized }; } diff --git a/src/esm.ts b/src/esm.ts index c83fd22c..b27be4a0 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -62,17 +62,28 @@ export interface NodeLoaderHooksAPI2 { export namespace NodeLoaderHooksAPI2 { export type ResolveHook = ( specifier: string, - context: { parentURL: string }, + context: { + conditions?: NodeImportConditions; + importAssertions?: NodeImportAssertions; + parentURL: string; + }, defaultResolve: ResolveHook ) => Promise<{ url: string }>; export type LoadHook = ( url: string, - context: { format: NodeLoaderHooksFormat | null | undefined }, + context: { + format: NodeLoaderHooksFormat | null | undefined; + importAssertions?: NodeImportAssertions; + }, defaultLoad: NodeLoaderHooksAPI2['load'] ) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined; }>; + export type NodeImportConditions = unknown; + export interface NodeImportAssertions { + type?: 'json'; + } } export type NodeLoaderHooksFormat = @@ -159,7 +170,10 @@ export function createEsmHooks(tsNodeService: Service) { // `load` from new loader hook API (See description at the top of this file) async function load( url: string, - context: { format: NodeLoaderHooksFormat | null | undefined }, + context: { + format: NodeLoaderHooksFormat | null | undefined; + importAssertions?: NodeLoaderHooksAPI2.NodeImportAssertions; + }, defaultLoad: typeof load ): Promise<{ format: NodeLoaderHooksFormat; @@ -176,7 +190,10 @@ export function createEsmHooks(tsNodeService: Service) { // Call the new defaultLoad() to get the source const { source: rawSource } = await defaultLoad( url, - { format }, + { + ...context, + format, + }, defaultLoad ); diff --git a/src/index.ts b/src/index.ts index 977922ea..0ffeea3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,13 @@ import type * as _ts from 'typescript'; import type { Transpiler, TranspilerFactory } from './transpilers/types'; import { assign, + attemptRequireWithV8CompileCache, cachedLookup, + createProjectLocalResolveHelper, + getBasePathForProjectLocalDependencyResolution, normalizeSlashes, parse, + ProjectLocalResolveHelper, split, yn, } from './util'; @@ -24,6 +28,10 @@ import { } from './module-type-classifier'; import { createResolverFunctions } from './resolver-functions'; import type { createEsmHooks as createEsmHooksFn } from './esm'; +import { + installCommonjsResolveHookIfNecessary, + ModuleConstructorWithInternals, +} from './cjs-resolve-filename-hook'; export { TSCommon }; export { @@ -256,6 +264,14 @@ 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'` + * + * For complete instructions: https://typestrong.org/ts-node/docs/transpilers + */ + swc?: boolean; /** * Paths which should not be compiled. * @@ -338,7 +354,7 @@ export interface CreateOptions { * `package` overrides either of the above to default behavior, which obeys package.json "type" and * tsconfig.json "module" options. */ - moduleTypes?: Record; + moduleTypes?: ModuleTypes; /** * @internal * Set by our configuration loader whenever a config file contains options that @@ -347,11 +363,21 @@ export interface CreateOptions { * the configuration loader, so it is *not* necessary for their source to be set here. */ optionBasePaths?: OptionBasePaths; + /** + * A function to collect trace messages from the TypeScript compiler, for example when `traceResolution` is enabled. + * + * @default console.log + */ + tsTrace?: (str: string) => void; } +export type ModuleTypes = Record; + /** @internal */ export interface OptionBasePaths { moduleTypes?: string; + transpiler?: string; + compiler?: string; } /** @@ -366,6 +392,15 @@ export interface RegisterOptions extends CreateOptions { * @default false */ preferTsExts?: boolean; + + /** + * Enable experimental features that re-map imports and require calls to support: + * `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, + * `outDir` to `rootDir` mappings for composite projects and monorepos. + * + * For details, see https://github.com/TypeStrong/ts-node/issues/1514 + */ + experimentalResolverFeatures?: boolean; } /** @@ -383,6 +418,7 @@ export interface TsConfigOptions | 'cwd' | 'projectSearchDir' | 'optionBasePaths' + | 'tsTrace' > {} /** @@ -418,6 +454,7 @@ export const DEFAULTS: RegisterOptions = { compilerHost: yn(env.TS_NODE_COMPILER_HOST), logError: yn(env.TS_NODE_LOG_ERROR), experimentalReplAwait: yn(env.TS_NODE_EXPERIMENTAL_REPL_AWAIT) ?? undefined, + tsTrace: console.log.bind(console), }; /** @@ -425,9 +462,15 @@ export const DEFAULTS: RegisterOptions = { */ export class TSError extends BaseError { name = 'TSError'; + diagnosticText!: string; - constructor(public diagnosticText: string, public diagnosticCodes: number[]) { + constructor(diagnosticText: string, public diagnosticCodes: number[]) { super(`⨯ Unable to compile TypeScript:\n${diagnosticText}`); + Object.defineProperty(this, 'diagnosticText', { + configurable: true, + writable: true, + value: diagnosticText, + }); } /** @@ -447,6 +490,8 @@ export interface Service { /** @internal */ [TS_NODE_SERVICE_BRAND]: true; ts: TSCommon; + /** @internal */ + compilerPath: string; config: _ts.ParsedCommandLine; options: RegisterOptions; enabled(enabled?: boolean): boolean; @@ -465,6 +510,10 @@ export interface Service { installSourceMapSupport(): void; /** @internal */ enableExperimentalEsmLoaderInterop(): void; + /** @internal */ + transpileOnly: boolean; + /** @internal */ + projectLocalResolveHelper: ProjectLocalResolveHelper; } /** @@ -529,8 +578,12 @@ export function register( originalJsHandler ); + installCommonjsResolveHookIfNecessary(service); + // Require specified modules before start-up. - (Module as any)._preloadModules(service.options.require); + (Module as ModuleConstructorWithInternals)._preloadModules( + service.options.require + ); return service; } @@ -549,26 +602,27 @@ export function create(rawOptions: CreateOptions = {}): Service { * be changed by the tsconfig, so we have to do this twice. */ function loadCompiler(name: string | undefined, relativeToPath: string) { - const compiler = require.resolve(name || 'typescript', { - paths: [relativeToPath, __dirname], - }); - const ts: typeof _ts = require(compiler); - return { compiler, ts }; + const projectLocalResolveHelper = + createProjectLocalResolveHelper(relativeToPath); + const compiler = projectLocalResolveHelper(name || 'typescript', true); + const ts: typeof _ts = attemptRequireWithV8CompileCache(require, compiler); + return { compiler, ts, projectLocalResolveHelper }; } // Compute minimum options to read the config file. - let { compiler, ts } = loadCompiler( + let { compiler, ts, projectLocalResolveHelper } = loadCompiler( compilerName, - rawOptions.projectSearchDir ?? rawOptions.project ?? cwd + getBasePathForProjectLocalDependencyResolution( + undefined, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ) ); // Read config file and merge new options between env and CLI options. - const { - configFilePath, - config, - tsNodeOptionsFromTsconfig, - optionBasePaths, - } = readConfig(cwd, ts, rawOptions); + const { configFilePath, config, tsNodeOptionsFromTsconfig, optionBasePaths } = + readConfig(cwd, ts, rawOptions); const options = assign( {}, DEFAULTS, @@ -581,6 +635,21 @@ export function create(rawOptions: CreateOptions = {}): Service { ...(rawOptions.require || []), ]; + // Re-load the compiler in case it has changed. + // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a + // different compiler than we did above, even if the name has not changed. + if (configFilePath) { + ({ compiler, ts, projectLocalResolveHelper } = loadCompiler( + options.compiler, + getBasePathForProjectLocalDependencyResolution( + configFilePath, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ) + )); + } + // Experimental REPL await is not compatible targets lower than ES2018 const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018; if (options.experimentalReplAwait === true && !targetSupportsTla) { @@ -601,18 +670,33 @@ export function create(rawOptions: CreateOptions = {}): Service { tsVersionSupportsTla && targetSupportsTla; - // Re-load the compiler in case it has changed. - // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a - // different compiler than we did above, even if the name has not changed. - if (configFilePath) { - ({ compiler, ts } = 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 = [ { @@ -668,25 +752,19 @@ 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] ?? {}; - // 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, { - paths: [cwd, __dirname], - }); + typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; + const transpilerPath = projectLocalResolveHelper(transpilerName, true); const transpilerFactory: TranspilerFactory = require(transpilerPath).create; customTranspiler = transpilerFactory({ - service: { options, config }, + service: { options, config, projectLocalResolveHelper }, ...transpilerOptions, }); } @@ -783,9 +861,9 @@ export function create(rawOptions: CreateOptions = {}): Service { _position: number ) => TypeInfo; - const getCanonicalFileName = ((ts as unknown) as TSInternal).createGetCanonicalFileName( - ts.sys.useCaseSensitiveFileNames - ); + const getCanonicalFileName = ( + ts as unknown as TSInternal + ).createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames); const moduleTypeClassifier = createModuleTypeClassifier({ basePath: options.optionBasePaths?.moduleTypes, @@ -857,6 +935,7 @@ export function create(rawOptions: CreateOptions = {}): Service { getCompilationSettings: () => config.options, getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options), getCustomTransformers: getCustomTransformers, + trace: options.tsTrace, }; const { resolveModuleNames, @@ -865,16 +944,18 @@ export function create(rawOptions: CreateOptions = {}): Service { isFileKnownToBeInternal, markBucketOfFilenameInternal, } = createResolverFunctions({ - serviceHost, + host: serviceHost, getCanonicalFileName, ts, cwd, config, - configFilePath, + projectLocalResolveHelper, }); serviceHost.resolveModuleNames = resolveModuleNames; - serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache = getResolvedModuleWithFailedLookupLocationsFromCache; - serviceHost.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; + serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache = + getResolvedModuleWithFailedLookupLocationsFromCache; + serviceHost.resolveTypeReferenceDirectives = + resolveTypeReferenceDirectives; const registry = ts.createDocumentRegistry( ts.sys.useCaseSensitiveFileNames, @@ -941,7 +1022,7 @@ export function create(rawOptions: CreateOptions = {}): Service { if (diagnosticList.length) reportTSError(diagnosticList); if (output.emitSkipped) { - throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`); + return [undefined, undefined, true]; } // Throw an error when requiring `.d.ts` files. @@ -954,7 +1035,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } - return [output.outputFiles[1].text, output.outputFiles[0].text]; + return [output.outputFiles[1].text, output.outputFiles[0].text, false]; }; getTypeInfo = (code: string, fileName: string, position: number) => { @@ -1010,18 +1091,19 @@ export function create(rawOptions: CreateOptions = {}): Service { ), useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, }; + host.trace = options.tsTrace; const { resolveModuleNames, resolveTypeReferenceDirectives, isFileKnownToBeInternal, markBucketOfFilenameInternal, } = createResolverFunctions({ - serviceHost: host, + host, cwd, - configFilePath, config, ts, getCanonicalFileName, + projectLocalResolveHelper, }); host.resolveModuleNames = resolveModuleNames; host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; @@ -1031,7 +1113,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ? ts.createIncrementalProgram({ rootNames: Array.from(rootFileNames), options: config.options, - host: host, + host, configFileParsingDiagnostics: config.errors, projectReferences: config.projectReferences, }) @@ -1083,7 +1165,8 @@ export function create(rawOptions: CreateOptions = {}): Service { }; getOutput = (code: string, fileName: string) => { - const output: [string, string] = ['', '']; + let outText = ''; + let outMap = ''; updateMemoryCache(code, fileName); @@ -1103,9 +1186,9 @@ export function create(rawOptions: CreateOptions = {}): Service { sourceFile, (path, file, writeByteOrderMark) => { if (path.endsWith('.map')) { - output[1] = file; + outMap = file; } else { - output[0] = file; + outText = file; } if (options.emit) sys.writeFile(path, file, writeByteOrderMark); @@ -1116,11 +1199,11 @@ export function create(rawOptions: CreateOptions = {}): Service { ); if (result.emitSkipped) { - throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`); + return [undefined, undefined, true]; } // Throw an error when requiring files that cannot be compiled. - if (output[0] === '') { + if (outText === '') { if (program.isSourceFileFromExternalLibrary(sourceFile)) { throw new TypeError( `Unable to compile file from external library: ${relative( @@ -1138,7 +1221,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ); } - return output; + return [outText, outMap, false]; }; getTypeInfo = (code: string, fileName: string, position: number) => { @@ -1215,7 +1298,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ); if (diagnosticList.length) reportTSError(diagnosticList); - return [result.outputText, result.sourceMapText as string]; + return [result.outputText, result.sourceMapText as string, false]; }; } @@ -1232,25 +1315,27 @@ export function create(rawOptions: CreateOptions = {}): Service { : createTranspileOnlyGetOutputFunction( ts.ModuleKind.ES2020 || ts.ModuleKind.ES2015 ); + const getOutputTranspileOnly = createTranspileOnlyGetOutputFunction(); // Create a simple TypeScript compiler proxy. function compile(code: string, fileName: string, lineOffset = 0) { const normalizedFileName = normalizeSlashes(fileName); - const classification = moduleTypeClassifier.classifyModule( - normalizedFileName - ); + const classification = + moduleTypeClassifier.classifyModule(normalizedFileName); // Must always call normal getOutput to throw typechecking errors - let [value, sourceMap] = getOutput(code, normalizedFileName); + let [value, sourceMap, emitSkipped] = getOutput(code, normalizedFileName); // If module classification contradicts the above, call the relevant transpiler if (classification.moduleType === 'cjs' && getOutputForceCommonJS) { [value, sourceMap] = getOutputForceCommonJS(code, normalizedFileName); } else if (classification.moduleType === 'esm' && getOutputForceESM) { [value, sourceMap] = getOutputForceESM(code, normalizedFileName); + } else if (emitSkipped) { + [value, sourceMap] = getOutputTranspileOnly(code, normalizedFileName); } const output = updateOutput( - value, + value!, normalizedFileName, - sourceMap, + sourceMap!, getExtension ); outputCache.set(normalizedFileName, { content: output }); @@ -1285,6 +1370,7 @@ export function create(rawOptions: CreateOptions = {}): Service { return { [TS_NODE_SERVICE_BRAND]: true, ts, + compilerPath: compiler, config, compile, getTypeInfo, @@ -1297,6 +1383,8 @@ export function create(rawOptions: CreateOptions = {}): Service { addDiagnosticFilter, installSourceMapSupport, enableExperimentalEsmLoaderInterop, + transpileOnly, + projectLocalResolveHelper, }; } @@ -1378,7 +1466,7 @@ function registerExtension( /** * Internal source output. */ -type SourceOutput = [string, string]; +type SourceOutput = [string, string, false] | [undefined, undefined, true]; /** * Update the output remapping the source map. diff --git a/src/repl.ts b/src/repl.ts index 0ef51017..41776e12 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -330,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, @@ -363,16 +368,20 @@ export function createRepl(options: CreateReplOptions = {}) { // 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`; + // Intentionally suppress type errors in case @types/node does not declare any of them, and because + // `declare import` is technically invalid syntax. + // Avoid this when in transpileOnly, because third-party transpilers may not handle `declare import`. + if (!service?.transpileOnly) { + 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(); @@ -475,6 +484,8 @@ export function createEvalAwarePartialHost( return { readFile, fileExists }; } +const sourcemapCommentRe = /\/\/# ?sourceMappingURL=\S+[\s\r\n]*$/; + type AppendCompileAndEvalInputResult = | { containsTopLevelAwait: true; valuePromise: Promise } | { containsTopLevelAwait: false; value: any }; @@ -520,8 +531,23 @@ function appendCompileAndEvalInput(options: { output = adjustUseStrict(output); + // Note: REPL does not respect sourcemaps! + // To properly do that, we'd need to prefix the code we eval -- which comes + // from `diffLines` -- with newlines so that it's at the proper line numbers. + // Then we'd need to ensure each bit of eval-ed code, if there are multiples, + // has the sourcemap appended to it. + // We might also need to integrate with our sourcemap hooks' cache; I'm not sure. + const outputWithoutSourcemapComment = output.replace(sourcemapCommentRe, ''); + const oldOutputWithoutSourcemapComment = state.output.replace( + sourcemapCommentRe, + '' + ); + // Use `diff` to check for new JavaScript to execute. - const changes = diffLines(state.output, output); + const changes = diffLines( + oldOutputWithoutSourcemapComment, + outputWithoutSourcemapComment + ); if (isCompletion) { undo(); diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index e7652815..f032bf0a 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -1,5 +1,6 @@ import { resolve } from 'path'; import type * as _ts from 'typescript'; +import type { ProjectLocalResolveHelper } from './util'; /** * @internal @@ -7,19 +8,19 @@ import type * as _ts from 'typescript'; */ export function createResolverFunctions(kwargs: { ts: typeof _ts; - serviceHost: _ts.ModuleResolutionHost; + host: _ts.ModuleResolutionHost; cwd: string; getCanonicalFileName: (filename: string) => string; config: _ts.ParsedCommandLine; - configFilePath: string | undefined; + projectLocalResolveHelper: ProjectLocalResolveHelper; }) { const { - serviceHost, + host, ts, config, cwd, getCanonicalFileName, - configFilePath, + projectLocalResolveHelper, } = kwargs; const moduleResolutionCache = ts.createModuleResolutionCache( cwd, @@ -93,7 +94,7 @@ export function createResolverFunctions(kwargs: { moduleName, containingFile, config.options, - serviceHost, + host, moduleResolutionCache, redirectedReference ); @@ -105,69 +106,69 @@ export function createResolverFunctions(kwargs: { }; // language service never calls this, but TS docs recommend that we implement it - const getResolvedModuleWithFailedLookupLocationsFromCache: _ts.LanguageServiceHost['getResolvedModuleWithFailedLookupLocationsFromCache'] = ( - moduleName, - containingFile - ): _ts.ResolvedModuleWithFailedLookupLocations | undefined => { - const ret = ts.resolveModuleNameFromCache( + const getResolvedModuleWithFailedLookupLocationsFromCache: _ts.LanguageServiceHost['getResolvedModuleWithFailedLookupLocationsFromCache'] = + ( moduleName, - containingFile, - moduleResolutionCache - ); - if (ret && ret.resolvedModule) { - fixupResolvedModule(ret.resolvedModule); - } - return ret; - }; - - const resolveTypeReferenceDirectives: _ts.LanguageServiceHost['resolveTypeReferenceDirectives'] = ( - typeDirectiveNames: string[], - containingFile: string, - redirectedReference: _ts.ResolvedProjectReference | undefined, - options: _ts.CompilerOptions - ): (_ts.ResolvedTypeReferenceDirective | undefined)[] => { - // Note: seems to be called with empty typeDirectiveNames array for all files. - return typeDirectiveNames.map((typeDirectiveName) => { - let { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective( - typeDirectiveName, + containingFile + ): _ts.ResolvedModuleWithFailedLookupLocations | undefined => { + const ret = ts.resolveModuleNameFromCache( + moduleName, containingFile, - config.options, - serviceHost, - redirectedReference + moduleResolutionCache ); - if (typeDirectiveName === 'node' && !resolvedTypeReferenceDirective) { - // Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function) - let typesNodePackageJsonPath: string | undefined; - try { - typesNodePackageJsonPath = require.resolve( - '@types/node/package.json', - { - paths: [configFilePath ?? cwd, __dirname], - } - ); - } catch {} // gracefully do nothing when @types/node is not installed for any reason - if (typesNodePackageJsonPath) { - const typeRoots = [resolve(typesNodePackageJsonPath, '../..')]; - ({ - resolvedTypeReferenceDirective, - } = ts.resolveTypeReferenceDirective( + if (ret && ret.resolvedModule) { + fixupResolvedModule(ret.resolvedModule); + } + return ret; + }; + + const resolveTypeReferenceDirectives: _ts.LanguageServiceHost['resolveTypeReferenceDirectives'] = + ( + typeDirectiveNames: string[], + containingFile: string, + redirectedReference: _ts.ResolvedProjectReference | undefined, + options: _ts.CompilerOptions + ): (_ts.ResolvedTypeReferenceDirective | undefined)[] => { + // Note: seems to be called with empty typeDirectiveNames array for all files. + return typeDirectiveNames.map((typeDirectiveName) => { + let { resolvedTypeReferenceDirective } = + ts.resolveTypeReferenceDirective( typeDirectiveName, containingFile, - { - ...config.options, - typeRoots, - }, - serviceHost, + config.options, + host, redirectedReference - )); + ); + if (typeDirectiveName === 'node' && !resolvedTypeReferenceDirective) { + // Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function) + let typesNodePackageJsonPath: string | undefined; + try { + typesNodePackageJsonPath = projectLocalResolveHelper( + '@types/node/package.json', + true + ); + } catch {} // gracefully do nothing when @types/node is not installed for any reason + if (typesNodePackageJsonPath) { + const typeRoots = [resolve(typesNodePackageJsonPath, '../..')]; + ({ resolvedTypeReferenceDirective } = + ts.resolveTypeReferenceDirective( + typeDirectiveName, + containingFile, + { + ...config.options, + typeRoots, + }, + host, + redirectedReference + )); + } } - } - if (resolvedTypeReferenceDirective) { - fixupResolvedModule(resolvedTypeReferenceDirective); - } - return resolvedTypeReferenceDirective; - }); - }; + if (resolvedTypeReferenceDirective) { + fixupResolvedModule(resolvedTypeReferenceDirective); + } + return resolvedTypeReferenceDirective; + }); + }; return { resolveModuleNames, diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 6c3c1c51..0de0482c 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -5,17 +5,19 @@ import { context } from './testlib'; import semver = require('semver'); import { + CMD_ESM_LOADER_WITHOUT_PROJECT, contextTsNodeUnderTest, EXPERIMENTAL_MODULES_FLAG, resetNodeEnvironment, TEST_DIR, } from './helpers'; import { createExec } from './exec-helpers'; -import { join } from 'path'; +import { join, resolve } from 'path'; import * as expect from 'expect'; import type { NodeLoaderHooksAPI2 } from '../'; const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); +const nodeSupportsImportAssertions = semver.gte(process.version, '17.1.0'); const test = context(contextTsNodeUnderTest); @@ -71,3 +73,55 @@ test.suite('hooks', (_test) => { }); } }); + +if (nodeSupportsImportAssertions) { + test.suite('Supports import assertions', (test) => { + test('Can import JSON using the appropriate flag and assertion', async (t) => { + const { err, stdout } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-json-modules ./importJson.ts`, + { + cwd: resolve(TEST_DIR, 'esm-import-assertions'), + } + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe( + 'A fuchsia car has 2 seats and the doors are open.\nDone!' + ); + }); + }); + + test.suite("Catch unexpected changes to node's loader context", (test) => { + /* + * This does not test ts-node. + * Rather, it is meant to alert us to potentially breaking changes in node's + * loader API. If node starts returning more or less properties on `context` + * objects, we want to know, because it may indicate that our loader code + * should be updated to accomodate the new properties, either by proxying them, + * modifying them, or suppressing them. + */ + test('Ensure context passed to loader by node has only expected properties', async (t) => { + const { stdout, stderr } = await exec( + `node --loader ./esm-loader-context/loader.mjs --experimental-json-modules ./esm-loader-context/index.mjs` + ); + const rows = stdout.split('\n').filter((v) => v[0] === '{'); + expect(rows.length).toBe(14); + rows.forEach((row) => { + const json = JSON.parse(row) as { + resolveContextKeys?: string[]; + loadContextKeys?: string; + }; + if (json.resolveContextKeys) { + expect(json.resolveContextKeys).toEqual([ + 'conditions', + 'importAssertions', + 'parentURL', + ]); + } else if (json.loadContextKeys) { + expect(json.loadContextKeys).toEqual(['format', 'importAssertions']); + } else { + throw new Error('Unexpected stdout in test.'); + } + }); + }); + }); +} diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index 87a79787..fc70f0e3 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -1,6 +1,6 @@ import type { ChildProcess, ExecException, ExecOptions } from 'child_process'; import { exec as childProcessExec } from 'child_process'; -import * as expect from 'expect'; +import { expect } from './testlib'; export type ExecReturn = Promise & { child: ChildProcess }; export interface ExecResult { diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 245e0a92..83d45d3f 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -14,7 +14,6 @@ import type * as tsNodeTypes from '../index'; import type _createRequire from 'create-require'; import { has, once } from 'lodash'; import semver = require('semver'); -import * as expect from 'expect'; const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 730d1a3b..8eef6bc0 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -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'); @@ -78,6 +79,14 @@ test.suite('ts-node', (test) => { testsDirRequire.resolve('ts-node/node16/tsconfig.json'); }); + test('should not load typescript outside of loadConfig', async () => { + const { err, stdout } = await exec( + `node -e "require('ts-node'); console.dir(Object.keys(require.cache).filter(k => k.includes('node_modules/typescript')).length)"` + ); + expect(err).toBe(null); + expect(stdout).toBe('0\n'); + }); + test.suite('cli', (test) => { test('should execute cli', async () => { const { err, stdout } = await exec( @@ -304,20 +313,50 @@ 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!'); - }); + 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('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!'); + test.suite('should support `traceResolution` compiler option', (test) => { + test('prints traces before running code when enabled', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} --compiler-options="{ \\"traceResolution\\": true }" -e "console.log('ok')"` + ); + expect(err).toBeNull(); + expect(stdout).toContain('======== Resolving module'); + expect(stdout.endsWith('ok\n')).toBe(true); + }); + + test('does NOT print traces when not enabled', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} -e "console.log('ok')"` + ); + expect(err).toBeNull(); + expect(stdout).not.toContain('======== Resolving module'); + expect(stdout.endsWith('ok\n')).toBe(true); + }); }); if (semver.gte(process.version, '12.16.0')) { @@ -858,7 +897,7 @@ test.suite('ts-node', (test) => { }); if (semver.gte(ts.version, '3.2.0')) { - test('--show-config should log resolved configuration', async (t) => { + test('--showConfig should log resolved configuration', async (t) => { function native(path: string) { return path.replace(/\/|\\/g, pathSep); } @@ -877,7 +916,6 @@ test.suite('ts-node', (test) => { cwd: native(`${ROOT_DIR}/tests`), projectSearchDir: native(`${ROOT_DIR}/tests`), project: native(`${ROOT_DIR}/tests/tsconfig.json`), - require: [], }, compilerOptions: { target: 'es6', @@ -907,7 +945,7 @@ test.suite('ts-node', (test) => { `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` ); expect(err).not.toBe(null); - expect(stderr).toMatch('Error: --show-config requires'); + expect(stderr).toMatch('Error: --showConfig requires'); }); } @@ -1055,10 +1093,7 @@ test.suite('ts-node', (test) => { test.suite('supports experimental-specifier-resolution=node', (test) => { test('via --experimental-specifier-resolution', async () => { - const { - err, - stdout, - } = await exec( + const { err, stdout } = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-specifier-resolution=node index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver') } ); @@ -1066,10 +1101,7 @@ test.suite('ts-node', (test) => { expect(stdout).toBe('foo bar baz biff libfoo\n'); }); test('via --es-module-specifier-resolution alias', async () => { - const { - err, - stdout, - } = await exec( + const { err, stdout } = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ${EXPERIMENTAL_MODULES_FLAG} --es-module-specifier-resolution=node index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver') } ); @@ -1115,7 +1147,7 @@ test.suite('ts-node', (test) => { expect(err).not.toBe(null); // expect error from node's default resolver expect(stderr).toMatch( - /Error \[ERR_UNSUPPORTED_ESM_URL_SCHEME\]:.*(?:\n.*){0,1}\n *at defaultResolve/ + /Error \[ERR_UNSUPPORTED_ESM_URL_SCHEME\]:.*(?:\n.*){0,2}\n *at defaultResolve/ ); }); @@ -1224,3 +1256,14 @@ test.suite('ts-node', (test) => { } }); }); + +test('Falls back to transpileOnly when ts compiler returns emitSkipped', async () => { + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project tsconfig.json ./outside-rootDir/foo.js`, + { + cwd: join(TEST_DIR, 'emit-skipped-fallback'), + } + ); + expect(err).toBe(null); + expect(stdout).toBe('foo\n'); +}); diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts index db04b284..97769283 100644 --- a/src/test/register.spec.ts +++ b/src/test/register.spec.ts @@ -12,7 +12,8 @@ import * as exp from 'expect'; import { join, resolve } from 'path'; import proxyquire = require('proxyquire'); -const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; +const SOURCE_MAP_REGEXP = + /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; const createOptions: tsNodeTypes.CreateOptions = { project: PROJECT, diff --git a/src/test/remove-env-var-force-color.js b/src/test/remove-env-var-force-color.js new file mode 100644 index 00000000..ebbb349b --- /dev/null +++ b/src/test/remove-env-var-force-color.js @@ -0,0 +1 @@ +delete process.env.FORCE_COLOR; diff --git a/src/test/repl/helpers.ts b/src/test/repl/helpers.ts index cc4edfcf..a085a266 100644 --- a/src/test/repl/helpers.ts +++ b/src/test/repl/helpers.ts @@ -39,12 +39,13 @@ export async function contextReplHelpers( stderr, ...createReplOpts, }); - const service = (registerHooks - ? tsNodeUnderTest.register - : tsNodeUnderTest.create)({ + const service = ( + registerHooks ? tsNodeUnderTest.register : tsNodeUnderTest.create + )({ ...replService.evalAwarePartialHost, project: `${TEST_DIR}/tsconfig.json`, ...createServiceOpts, + tsTrace: replService.console.log.bind(replService.console), }); replService.setService(service); t.teardown(async () => { diff --git a/src/test/repl/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts index 61623792..5c4962e7 100644 --- a/src/test/repl/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -39,9 +39,11 @@ export async function upstreamTopLevelAwaitTests({ }, }); replService.setService(service); - (replService.stdout as NodeJS.WritableStream & { - isTTY: boolean; - }).isTTY = true; + ( + replService.stdout as NodeJS.WritableStream & { + isTTY: boolean; + } + ).isTTY = true; const replServer = replService.startInternal({ prompt: PROMPT, terminal: true, diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts index 9071688c..f54d2030 100644 --- a/src/test/repl/repl-environment.spec.ts +++ b/src/test/repl/repl-environment.spec.ts @@ -3,8 +3,7 @@ * globals, __filename, builtin module accessors. */ -import { test as _test } from '../testlib'; -import * as expect from 'expect'; +import { test as _test, expect } from '../testlib'; import * as promisify from 'util.promisify'; import * as getStream from 'get-stream'; import { @@ -45,53 +44,50 @@ test.suite( const globalInRepl = global as GlobalInRepl; const programmaticTest = test.macro( ( - { - evalCodeBefore, - stdinCode, - waitFor, - }: { - evalCodeBefore: string | null; - stdinCode: string; - waitFor?: () => boolean; - }, - assertions: (stdout: string) => Promise | void - ) => async (t) => { - delete globalInRepl.testReport; - delete globalInRepl.replReport; - delete globalInRepl.stdinReport; - delete globalInRepl.evalReport; - delete globalInRepl.module; - delete globalInRepl.exports; - delete globalInRepl.fs; - delete globalInRepl.__filename; - delete globalInRepl.__dirname; - const { - stdin, - stderr, - stdout, - replService, - } = t.context.createReplViaApi({ registerHooks: true }); - if (typeof evalCodeBefore === 'string') { - replService.evalCode(evalCodeBefore); + { + evalCodeBefore, + stdinCode, + waitFor, + }: { + evalCodeBefore: string | null; + stdinCode: string; + waitFor?: () => boolean; + }, + assertions: (stdout: string) => Promise | void + ) => + async (t) => { + delete globalInRepl.testReport; + delete globalInRepl.replReport; + delete globalInRepl.stdinReport; + delete globalInRepl.evalReport; + delete globalInRepl.module; + delete globalInRepl.exports; + delete globalInRepl.fs; + delete globalInRepl.__filename; + delete globalInRepl.__dirname; + const { stdin, stderr, stdout, replService } = + t.context.createReplViaApi({ registerHooks: true }); + if (typeof evalCodeBefore === 'string') { + replService.evalCode(evalCodeBefore); + } + replService.start(); + stdin.write(stdinCode); + stdin.end(); + let done = false; + await Promise.race([ + promisify(setTimeout)(20e3), + (async () => { + while (!done && !waitFor?.()) { + await promisify(setTimeout)(1e3); + } + })(), + ]); + done = true; + stdout.end(); + stderr.end(); + expect(await getStream(stderr)).toBe(''); + await assertions(await getStream(stdout)); } - replService.start(); - stdin.write(stdinCode); - stdin.end(); - let done = false; - await Promise.race([ - promisify(setTimeout)(20e3), - (async () => { - while (!done && !waitFor?.()) { - await promisify(setTimeout)(1e3); - } - })(), - ]); - done = true; - stdout.end(); - stderr.end(); - expect(await getStream(stderr)).toBe(''); - await assertions(await getStream(stdout)); - } ); const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any;`; diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 2ef41d15..3a106a38 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -1,15 +1,16 @@ +import { _test, expect } from '../testlib'; import { ts } from '../helpers'; import semver = require('semver'); -import * as expect from 'expect'; import { CMD_TS_NODE_WITH_PROJECT_FLAG, contextTsNodeUnderTest, + getStream, TEST_DIR, } from '../helpers'; import { createExec, createExecTester } from '../exec-helpers'; import { upstreamTopLevelAwaitTests } from './node-repl-tla'; -import { _test } from '../testlib'; import { contextReplHelpers } from './helpers'; +import { promisify } from 'util'; const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); @@ -30,6 +31,16 @@ test('should run REPL when --interactive passed and stdin is not a TTY', async ( expect(stdout).toBe('> 123\n' + 'undefined\n' + '> '); }); +test('should echo a value when using the swc transpiler', async () => { + const execPromise = exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive --transpiler ts-node/transpilers/swc-experimental` + ); + execPromise.child.stdin!.end('400\n401\n'); + const { err, stdout } = await execPromise; + expect(err).toBe(null); + expect(stdout).toBe('> 400\n> 401\n> '); +}); + test('REPL has command to get type information', async () => { const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); execPromise.child.stdin!.end('\nconst a = 123\n.type a'); @@ -44,16 +55,20 @@ test('REPL has command to get type information', async () => { test.serial('REPL can be configured on `start`', async (t) => { const prompt = '#> '; - const { stdout, stderr } = await t.context.executeInRepl('const x = 3', { - registerHooks: true, - startInternalOptions: { - prompt, - ignoreUndefined: true, - }, - }); + const { stdout, stderr } = await t.context.executeInRepl( + `const x = 3\n'done'`, + { + waitPattern: "'done'", + registerHooks: true, + startInternalOptions: { + prompt, + ignoreUndefined: true, + }, + } + ); expect(stderr).toBe(''); - expect(stdout).toBe(`${prompt}${prompt}`); + expect(stdout).toBe(`${prompt}${prompt}'done'\n`); }); // Serial because it's timing-sensitive @@ -412,29 +427,95 @@ test.suite( } ); -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, - }, +test.suite('REPL works with traceResolution', (test) => { + test.serial( + 'startup traces should print before the prompt appears when traceResolution is enabled', + async (t) => { + const repl = t.context.createReplViaApi({ + registerHooks: false as true, + createServiceOpts: { + compilerOptions: { + traceResolution: true, + }, + }, + }); + + repl.replService.start(); + + repl.stdin.end(); + + await promisify(setTimeout)(3e3); + + repl.stdout.end(); + const stdout = await getStream(repl.stdout); + + expect(stdout).toContain('======== Resolving module'); + expect(stdout.endsWith('> ')).toBe(true); } ); - // 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'" + test.serial( + 'traces should NOT appear when traceResolution is not enabled', + async (t) => { + const { stdout, stderr } = await t.context.executeInRepl('1', { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: '1\n>', + }); + expect(stderr).toBe(''); + expect(stdout).not.toContain('======== Resolving module'); + } ); - // 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`); +}); + +test.suite('REPL declares types for node built-ins within REPL', (test) => { + test.runSerially(); + test('enabled when typechecking', 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`); + }); + + test('disabled in transpile-only mode, to avoid breaking third-party SWC transpiler which rejects `declare import` syntax', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `type Duplex = stream.Duplex + const s = stream + 'done'`, + { + createServiceOpts: { + swc: true, + }, + registerHooks: true, + waitPattern: `done`, + startInternalOptions: { + useGlobal: false, + }, + } + ); + + // Assert that we do not get errors about `declare import` syntax from swc + expect(stdout).toBe("> undefined\n> undefined\n> 'done'\n"); + expect(stderr).toBe(''); + }); }); diff --git a/src/test/testlib.ts b/src/test/testlib.ts index ce97a07a..4ce806dd 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -11,7 +11,9 @@ import avaTest, { } from 'ava'; import * as assert from 'assert'; import throat from 'throat'; -export { ExecutionContext }; +import * as expect from 'expect'; + +export { ExecutionContext, expect }; // NOTE: this limits concurrency within a single process, but AVA launches // each .spec file in its own process, so actual concurrency is higher. @@ -20,12 +22,12 @@ const concurrencyLimiter = throat(16); function once(func: T): T { let run = false; let ret: any = undefined; - return (function (...args: any[]) { + return function (...args: any[]) { if (run) return ret; run = true; ret = func(...args); return ret; - } as any) as T; + } as any as T; } export const test = createTestInterface({ diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 6955bc90..fedc6a3a 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -15,23 +15,26 @@ export interface SwcTranspilerOptions extends CreateTranspilerOptions { export function create(createOptions: SwcTranspilerOptions): Transpiler { const { swc, - service: { config }, + service: { config, projectLocalResolveHelper }, } = createOptions; // Load swc compiler let swcInstance: typeof swcWasm; if (typeof swc === 'string') { - swcInstance = require(swc) as typeof swcWasm; + swcInstance = require(projectLocalResolveHelper( + swc, + true + )) as typeof swcWasm; } else if (swc == null) { let swcResolved; try { - swcResolved = require.resolve('@swc/core'); + swcResolved = projectLocalResolveHelper('@swc/core', true); } catch (e) { try { - swcResolved = require.resolve('@swc/wasm'); + swcResolved = projectLocalResolveHelper('@swc/wasm', true); } catch (e) { throw new Error( - 'swc compiler requires either @swc/core or @swc/wasm to be installed as dependencies' + 'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers' ); } } @@ -52,6 +55,9 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { module, jsxFactory, jsxFragmentFactory, + strict, + alwaysStrict, + noImplicitUseStrict, } = compilerOptions; const nonTsxOptions = createSwcOptions(false); const tsxOptions = createSwcOptions(true); @@ -71,6 +77,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } swcTarget = swcTargets[swcTargetIndex]; const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; + // swc only supports these 4x module options const moduleType = module === ModuleKind.CommonJS ? 'commonjs' @@ -78,7 +85,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, @@ -86,6 +109,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? ({ noInterop: !esModuleInterop, type: moduleType, + strictMode, } as swcTypes.ModuleConfig) : undefined, swcrc: false, @@ -143,7 +167,8 @@ targetMapping.set(/* ts.ScriptTarget.ES2018 */ 5, 'es2018'); targetMapping.set(/* ts.ScriptTarget.ES2019 */ 6, 'es2019'); targetMapping.set(/* ts.ScriptTarget.ES2020 */ 7, 'es2020'); targetMapping.set(/* ts.ScriptTarget.ES2021 */ 8, 'es2021'); -targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2021'); +targetMapping.set(/* ts.ScriptTarget.ES2022 */ 9, 'es2022'); +targetMapping.set(/* ts.ScriptTarget.ESNext */ 99, 'es2022'); type SwcTarget = typeof swcTargets[number]; /** @@ -160,6 +185,7 @@ const swcTargets = [ 'es2019', 'es2020', 'es2021', + 'es2022', ] as const; const ModuleKind = { diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index 3c1e7afc..ab524cbd 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -16,7 +16,11 @@ export type TranspilerFactory = ( ) => Transpiler; export interface CreateTranspilerOptions { // TODO this is confusing because its only a partial Service. Rename? - service: Pick; + // Careful: must avoid stripInternal breakage by guarding with Extract<> + service: Pick< + Service, + Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service> + >; } export interface Transpiler { // TODOs diff --git a/src/tsconfigs.ts b/src/tsconfigs.ts index 74a92744..cc104fd7 100644 --- a/src/tsconfigs.ts +++ b/src/tsconfigs.ts @@ -7,7 +7,7 @@ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10); * @internal */ export function getDefaultTsconfigJsonForNodeVersion(ts: TSCommon): any { - const tsInternal = (ts as any) as TSInternal; + const tsInternal = ts as any as TSInternal; if (nodeMajor >= 16) { const config = require('@tsconfig/node16/tsconfig.json'); if (configCompatible(config)) return config; diff --git a/src/util.ts b/src/util.ts index 23e19bba..adc5eaca 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,6 +4,7 @@ import { } from 'module'; import type _createRequire from 'create-require'; import * as ynModule from 'yn'; +import { dirname } from 'path'; /** @internal */ export const createRequire = @@ -92,7 +93,66 @@ export function cachedLookup(fn: (arg: T) => R): (arg: T) => R { } /** - * We do not support ts's `trace` option yet. In the meantime, rather than omit - * `trace` options in hosts, I am using this placeholder. + * @internal + * Require something with v8-compile-cache, which should make subsequent requires faster. + * Do lots of error-handling so that, worst case, we require without the cache, and users are not blocked. + */ +export function attemptRequireWithV8CompileCache( + requireFn: typeof require, + specifier: string +) { + try { + const v8CC = ( + require('v8-compile-cache-lib') as typeof import('v8-compile-cache-lib') + ).install(); + try { + return requireFn(specifier); + } finally { + v8CC?.uninstall(); + } + } catch (e) { + return requireFn(specifier); + } +} + +/** + * Helper to discover dependencies relative to a user's project, optionally + * falling back to relative to ts-node. This supports global installations of + * ts-node, for example where someone does `#!/usr/bin/env -S ts-node --swc` and + * we need to fallback to a global install of @swc/core + * @internal */ -export function trace(s: string): void {} +export function createProjectLocalResolveHelper(localDirectory: string) { + return function projectLocalResolveHelper( + specifier: string, + fallbackToTsNodeRelative: boolean + ) { + return require.resolve(specifier, { + paths: fallbackToTsNodeRelative + ? [localDirectory, __dirname] + : [localDirectory], + }); + }; +} +/** @internal */ +export type ProjectLocalResolveHelper = ReturnType< + typeof createProjectLocalResolveHelper +>; + +/** + * Used as a reminder of all the factors we must consider when finding project-local dependencies and when a config file + * on disk may or may not exist. + * @internal + */ +export function getBasePathForProjectLocalDependencyResolution( + configFilePath: string | undefined, + projectSearchDirOption: string | undefined, + projectOption: string | undefined, + cwdOption: string +) { + if (configFilePath != null) return dirname(configFilePath); + return projectSearchDirOption ?? projectOption ?? cwdOption; + // TODO technically breaks if projectOption is path to a file, not a directory, + // and we attempt to resolve relative specifiers. By the time we resolve relative specifiers, + // should have configFilePath, so not reach this codepath. +} diff --git a/tests/emit-skipped-fallback/outside-rootDir/foo.js b/tests/emit-skipped-fallback/outside-rootDir/foo.js new file mode 100644 index 00000000..ad12b2b9 --- /dev/null +++ b/tests/emit-skipped-fallback/outside-rootDir/foo.js @@ -0,0 +1,15 @@ +// This file causes TS to return emitSkipped because it's outside of rootDir and +// it's .js. I assume this happens because the emit path is the same as the +// input path, and perhaps also because the file is classified "external" + +const decorator = () => {}; + +class Foo { + // Using a decorator to prove this .js file is getting compiled + @decorator + method() { + return 'foo'; + } +} + +console.log(new Foo().method()); diff --git a/tests/emit-skipped-fallback/tsconfig.json b/tests/emit-skipped-fallback/tsconfig.json new file mode 100644 index 00000000..aae99723 --- /dev/null +++ b/tests/emit-skipped-fallback/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "allowJs": true, + "experimentalDecorators": true, + "rootDir": "rootDir" + } +} diff --git a/tests/esm-custom-loader/loader.mjs b/tests/esm-custom-loader/loader.mjs index bf82e766..1e3113ac 100755 --- a/tests/esm-custom-loader/loader.mjs +++ b/tests/esm-custom-loader/loader.mjs @@ -11,6 +11,5 @@ const tsNodeInstance = register({ }, }); -export const { resolve, getFormat, transformSource, load } = createEsmHooks( - tsNodeInstance -); +export const { resolve, getFormat, transformSource, load } = + createEsmHooks(tsNodeInstance); diff --git a/tests/esm-import-assertions/car.json b/tests/esm-import-assertions/car.json new file mode 100644 index 00000000..a63dfe91 --- /dev/null +++ b/tests/esm-import-assertions/car.json @@ -0,0 +1,5 @@ +{ + "color": "fuchsia", + "doors": "open", + "seats": 2 +} diff --git a/tests/esm-import-assertions/importJson.ts b/tests/esm-import-assertions/importJson.ts new file mode 100644 index 00000000..b4446aa7 --- /dev/null +++ b/tests/esm-import-assertions/importJson.ts @@ -0,0 +1,28 @@ +import carData from './car.json' assert { type: 'json' }; + +if (carData.color !== 'fuchsia') throw new Error('failed to import json'); + +const { default: dynamicCarData } = await import('./car.json', { + assert: { type: 'json' }, +}); + +if (dynamicCarData.doors !== 'open') + throw new Error('failed to dynamically import json'); + +console.log( + `A ${carData.color} car has ${carData.seats} seats and the doors are ${dynamicCarData.doors}.` +); + +// Test that omitting the assertion causes node to throw an error +await import('./car.json').then( + () => { + throw new Error('should have thrown'); + }, + (error: any) => { + if (error.code !== 'ERR_IMPORT_ASSERTION_TYPE_MISSING') { + throw error; + } + /* error is expected */ + } +); +console.log('Done!'); diff --git a/tests/esm-import-assertions/package.json b/tests/esm-import-assertions/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/tests/esm-import-assertions/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-import-assertions/tsconfig.json b/tests/esm-import-assertions/tsconfig.json new file mode 100644 index 00000000..d626b927 --- /dev/null +++ b/tests/esm-import-assertions/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/tests/esm-loader-context/index.mjs b/tests/esm-loader-context/index.mjs new file mode 100644 index 00000000..3def3324 --- /dev/null +++ b/tests/esm-loader-context/index.mjs @@ -0,0 +1,7 @@ +import * as moduleA from './moduleA.mjs'; +import * as moduleB from './moduleB.mjs' assert { foo: 'bar' }; +import * as jsonModule from './jsonModuleA.json' assert { type: 'json' }; + +await import('./moduleC.mjs'); +await import('./moduleD.mjs', { foo: 'bar' }); +await import('./jsonModuleB.json', { assert: { type: 'json' } }); diff --git a/tests/esm-loader-context/jsonModuleA.json b/tests/esm-loader-context/jsonModuleA.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/esm-loader-context/jsonModuleA.json @@ -0,0 +1 @@ +{} diff --git a/tests/esm-loader-context/jsonModuleB.json b/tests/esm-loader-context/jsonModuleB.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/esm-loader-context/jsonModuleB.json @@ -0,0 +1 @@ +{} diff --git a/tests/esm-loader-context/loader.mjs b/tests/esm-loader-context/loader.mjs new file mode 100644 index 00000000..d500b5a7 --- /dev/null +++ b/tests/esm-loader-context/loader.mjs @@ -0,0 +1,8 @@ +export function resolve(specifier, context, defaultResolve) { + console.log(JSON.stringify({ resolveContextKeys: Object.keys(context) })); + return defaultResolve(specifier, context); +} +export function load(url, context, defaultLoad) { + console.log(JSON.stringify({ loadContextKeys: Object.keys(context) })); + return defaultLoad(url, context); +} diff --git a/tests/esm-loader-context/moduleA.mjs b/tests/esm-loader-context/moduleA.mjs new file mode 100644 index 00000000..e69de29b diff --git a/tests/esm-loader-context/moduleB.mjs b/tests/esm-loader-context/moduleB.mjs new file mode 100644 index 00000000..e69de29b diff --git a/tests/esm-loader-context/moduleC.mjs b/tests/esm-loader-context/moduleC.mjs new file mode 100644 index 00000000..e69de29b diff --git a/tests/esm-loader-context/moduleD.mjs b/tests/esm-loader-context/moduleD.mjs new file mode 100644 index 00000000..e69de29b diff --git a/tests/spy-swc-transpiler.js b/tests/spy-swc-transpiler.js new file mode 100644 index 00000000..99e77acc --- /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 00000000..909fde69 --- /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 00000000..9dba8805 --- /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 b7dfa09c..909fde69 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 5097d63a..863b6c4f 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 b7dfa09c..909fde69 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 00000000..7cf79b13 --- /dev/null +++ b/transpilers/swc.js @@ -0,0 +1 @@ +module.exports = require('../dist/transpilers/swc') diff --git a/tsconfig.json b/tsconfig.json index bf59b8f8..158695d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,9 @@ { "$schema": "./tsconfig.schemastore-schema.json", "compilerOptions": { - "target": "es2015", - "lib": ["es2015", "dom"], + // `target` and `lib` match @tsconfig/bases for node12, since that's the oldest node LTS, so it's the oldest node we support + "target": "es2019", + "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string", "dom"], "rootDir": "src", "outDir": "dist", "module": "commonjs", diff --git a/website/docs/configuration.md b/website/docs/configuration.md index 5a1d43be..130a69eb 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -19,9 +19,9 @@ Hello, Ronald! 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`. +Use `--skipProject` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json`. -When searching, it is resolved using [the same search behavior as `tsc`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). By default, this search is performed relative to the entrypoint script. In `--cwd-mode` or if no entrypoint is specified -- for example when using the REPL -- the search is performed relative to `--cwd` / `process.cwd()`. +When searching, it is resolved using [the same search behavior as `tsc`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). By default, this search is performed relative to the entrypoint script. In `--cwdMode` or if no entrypoint is specified -- for example when using the REPL -- the search is performed relative to `--cwd` / `process.cwd()`. You can use this sample configuration as a starting point: @@ -74,7 +74,7 @@ With the latest `node` and `typescript`, this is [`@tsconfig/node16`](https://gi Older versions of `typescript` are incompatible with `@tsconfig/node16`. In those cases we will use an older default configuration. -When in doubt, `ts-node --show-config` will log the configuration being used, and `ts-node -vv` will log `node` and `typescript` versions. +When in doubt, `ts-node --showConfig` will log the configuration being used, and `ts-node -vv` will log `node` and `typescript` versions. ## `node` flags diff --git a/website/docs/how-it-works.md b/website/docs/how-it-works.md index 923c0592..f75f32c3 100644 --- a/website/docs/how-it-works.md +++ b/website/docs/how-it-works.md @@ -16,9 +16,16 @@ Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook ## Skipping `node_modules` -By default, **TypeScript Node** avoids compiling files in `/node_modules/` for three reasons: +By default, ts-node avoids compiling files in `/node_modules/` for three reasons: 1. Modules should always be published in a format node.js can consume 2. Transpiling the entire dependency tree will make your project slower 3. Differing behaviours between TypeScript and node.js (e.g. ES2015 modules) can result in a project that works until you decide to support a feature natively from node.js +If you need to import uncompiled TypeScript in `node_modules`, use [`--skipIgnore`](./options#transpilation) or [`TS_NODE_SKIP_IGNORE`](./options#transpilation) to bypass this restriction. + +## Skipping pre-compiled TypeScript + +If a compiled JavaScript file with the same name as a TypeScript file already exists, the TypeScript file will be ignored. ts-node will import the pre-compiled JavaScript. + +To force ts-node to import the TypeScript source, not the precompiled JavaScript, use [`--preferTsExts`](./options#transpilation). diff --git a/website/docs/options.md b/website/docs/options.md index 9139ea85..aadf36da 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -4,6 +4,8 @@ title: Options `ts-node` supports `--print` (`-p`), `--eval` (`-e`), `--require` (`-r`) and `--interactive` (`-i`) similar to the [node.js CLI options](https://nodejs.org/api/cli.html). +All command-line flags support both `--camelCase` and `--hyphen-case`. + _Environment variables, where available, are in `ALL_CAPS`_ ## Shell @@ -17,30 +19,31 @@ _Environment variables, where available, are in `ALL_CAPS`_ ## TSConfig - `-P, --project [path]` Path to TypeScript JSON project file
*Environment:* `TS_NODE_PROJECT` -- `--skip-project` Skip project config resolution and loading
*Default:* `false`
*Environment:* `TS_NODE_SKIP_PROJECT` -- `-c, --cwd-mode` Resolve config relative to the current directory instead of the directory of the entrypoint script -- `-O, --compiler-options [opts]` JSON object to merge with compiler options
*Environment:* `TS_NODE_COMPILER_OPTIONS` -- `--show-config` Print resolved `tsconfig.json`, including `ts-node` options, and exit +- `--skipProject` Skip project config resolution and loading
*Default:* `false`
*Environment:* `TS_NODE_SKIP_PROJECT` +- `-c, --cwdMode` Resolve config relative to the current directory instead of the directory of the entrypoint script +- `-O, --compilerOptions [opts]` JSON object to merge with compiler options
*Environment:* `TS_NODE_COMPILER_OPTIONS` +- `--showConfig` Print resolved `tsconfig.json`, including `ts-node` options, and exit ## Typechecking -- `-T, --transpile-only` Use TypeScript's faster `transpileModule`
*Default:* `false`
*Environment:* `TS_NODE_TRANSPILE_ONLY` -- `--type-check` Opposite of `--transpile-only`
*Default:* `true`
*Environment:* `TS_NODE_TYPE_CHECK` -- `-H, --compiler-host` Use TypeScript's compiler host API
*Default:* `false`
*Environment:* `TS_NODE_COMPILER_HOST` +- `-T, --transpileOnly` Use TypeScript's faster `transpileModule`
*Default:* `false`
*Environment:* `TS_NODE_TRANSPILE_ONLY` +- `--typeCheck` Opposite of `--transpileOnly`
*Default:* `true`
*Environment:* `TS_NODE_TYPE_CHECK` +- `-H, --compilerHost` Use TypeScript's compiler host API
*Default:* `false`
*Environment:* `TS_NODE_COMPILER_HOST` - `--files` Load `files`, `include` and `exclude` from `tsconfig.json` on startup
*Default:* `false`
*Environment:* `TS_NODE_FILES` -- `-D, --ignore-diagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` +- `-D, --ignoreDiagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` ## Transpilation - `-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` +- `--skipIgnore` 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 `--transpileOnly`
*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` +- `--preferTsExts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` ## Diagnostics -- `--log-error` Logs TypeScript errors to stderr instead of throwing exceptions
*Default:* `false`
*Environment:* `TS_NODE_LOG_ERROR` +- `--logError` Logs TypeScript errors to stderr instead of throwing exceptions
*Default:* `false`
*Environment:* `TS_NODE_LOG_ERROR` - `--pretty` Use pretty diagnostic formatter
*Default:* `false`
*Environment:* `TS_NODE_PRETTY` - `TS_NODE_DEBUG` Enable debug logging
@@ -51,9 +54,10 @@ _Environment variables, where available, are in `ALL_CAPS`_ - `--emit` Emit output files into `.ts-node` directory
*Default:* `false`
*Environment:* `TS_NODE_EMIT` - `--scope` Scope compiler to files within `scopeDir`. Anything outside this directory is ignored.
*Default: `false`
*Environment:* `TS_NODE_SCOPE` - `--scopeDir` Directory within which compiler is limited when `scope` is enabled.
*Default:* First of: `tsconfig.json` "rootDir" if specified, directory containing `tsconfig.json`, or cwd if no `tsconfig.json` is loaded.
*Environment:* `TS_NODE_SCOPE_DIR` -- `moduleType` Override the module type of certain files, ignoring the `package.json` `"type"` field. See [Module type overrides](./module-type-overrides.md) for details.
*Default:* obeys `package.json` `"type"` and `tsconfig.json` `"module"`
*Can only be specified via `tsconfig.json` or API.* +- `moduleTypes` Override the module type of certain files, ignoring the `package.json` `"type"` field. See [Module type overrides](./module-type-overrides.md) for details.
*Default:* obeys `package.json` `"type"` and `tsconfig.json` `"module"`
*Can only be specified via `tsconfig.json` or API.* - `TS_NODE_HISTORY` Path to history file for REPL
*Default:* `~/.ts_node_repl_history`
-- `--no-experimental-repl-await` Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl-await`](https://nodejs.org/api/cli.html#cli_no_experimental_repl_await)
*Default:* Enabled if TypeScript version is 3.8 or higher and target is ES2018 or higher.
*Environment:* `TS_NODE_EXPERIMENTAL_REPL_AWAIT` set `false` to disable +- `--noExperimentalReplAwait` Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl-await`](https://nodejs.org/api/cli.html#cli_no_experimental_repl_await)
*Default:* Enabled if TypeScript version is 3.8 or higher and target is ES2018 or higher.
*Environment:* `TS_NODE_EXPERIMENTAL_REPL_AWAIT` set `false` to disable +- `experimentalResolverFeatures` Enable experimental features that re-map imports and require calls to support: `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, `outDir` to `rootDir` mappings for composite projects and monorepos. For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514)
*Default:* `false`
*Can only be specified via `tsconfig.json` or API.* ## API diff --git a/website/docs/performance.md b/website/docs/performance.md index a875bd2e..69b2c6cb 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -9,7 +9,7 @@ These tricks will make ts-node faster. 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/transpilers.md b/website/docs/transpilers.md index eb80fd11..6d6f3709 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -1,5 +1,5 @@ --- -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 @@ -13,12 +13,11 @@ 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`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. @@ -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 2ee75225..2e2125c1 100644 --- a/website/docs/troubleshooting.md +++ b/website/docs/troubleshooting.md @@ -5,7 +5,7 @@ title: Troubleshooting ## Understanding configuration 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 +have one. If you are unsure which configuration is used, you can log it with `ts-node --showConfig`. 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 @@ -17,7 +17,7 @@ ts-node v10.0.0 node v16.1.0 compiler v4.2.2 -$ ts-node --show-config +$ ts-node --showConfig { "compilerOptions": { "target": "es6", diff --git a/website/docs/usage.md b/website/docs/usage.md index ecd6721b..d988e606 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -20,10 +20,10 @@ ts-node -p -e '"Hello, world!"' # Pipe scripts to execute with TypeScript. echo 'console.log("Hello, world!")' | ts-node -# Equivalent to ts-node --transpile-only +# Equivalent to ts-node --transpileOnly ts-node-transpile-only script.ts -# Equivalent to ts-node --cwd-mode +# Equivalent to ts-node --cwdMode ts-node-cwd script.ts ``` @@ -35,14 +35,27 @@ ts-node-cwd script.ts console.log("Hello, world!") ``` -Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, the following will fail on Linux: +Passing options via shebang requires the [`env -S` flag](https://manpages.debian.org/bullseye/coreutils/env.1.en.html#S), which is available on recent versions of `env`. ([compatibility](https://github.com/TypeStrong/ts-node/pull/1448#issuecomment-913895766)) +```typescript +#!/usr/bin/env -S ts-node --files +// This shebang works on Mac and Linux with newer versions of env +// Technically, Mac allows omitting `-S`, but Linux requires it ``` -#!/usr/bin/env ts-node --files -// This shebang is not portable. It only works on Mac + +To write scripts with maximum portability, [specify all options in your `tsconfig.json`](./configuration#via-tsconfigjson-recommended) and omit them from the shebang. + +```typescript +#!/usr/bin/env ts-node +// This shebang works everywhere ``` -Instead, specify all ts-node options in your `tsconfig.json`. +To test your version of `env` for compatibility: + +```shell +# Note that these unusual quotes are necessary +/usr/bin/env --debug '-S echo foo bar' +``` ## Programmatic diff --git a/website/readme-sources/prefix.md b/website/readme-sources/prefix.md index a24738ff..f797087f 100644 --- a/website/readme-sources/prefix.md +++ b/website/readme-sources/prefix.md @@ -14,7 +14,7 @@ You can build the readme with this command: cd website && yarn build-readme --> -# ![TypeScript Node](logo.svg?sanitize=true) +# [![TypeScript Node](logo.svg?sanitize=true)](https://typestrong.org/ts-node) [![NPM version](https://img.shields.io/npm/v/ts-node.svg?style=flat)](https://npmjs.org/package/ts-node) [![NPM downloads](https://img.shields.io/npm/dm/ts-node.svg?style=flat)](https://npmjs.org/package/ts-node)