diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index b6406486..93aa18fe 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -49,5 +49,9 @@ https://github.com/TypeStrong/ts-node-repros ``` {} ``` +* package.json: +``` +{} +``` * Operating system and version: * If Windows, are you using WSL or WSL2?: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d5649bd6..8aa6147c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,13 +6,13 @@ contact_links: url: "https://github.com/TypeStrong/ts-node/discussions" - name: Chat - about: "Alternatively, you can use the TypeScript Community Discord." - url: "https://discord.gg/typescript" + about: "Alternatively, ask on the TypeScript Community Discord." + url: "https://discord.gg/3rBctmf3dP" - name: "Help! My Types Are Missing!" about: "This is likely a configuration problem. Check our README" - url: "https://typestrong.org/ts-node/docs/types" + url: "https://typestrong.org/ts-node/docs/troubleshooting#missing-types" - name: "TSError or SyntaxError" - about: "These errors come from TypeScript and node, respectively. Use StackOverflow or Discord for usage and configuration help." + about: "These errors come from TypeScript and node, respectively. Use Discussions or Discord for usage and configuration help." url: "https://typestrong.org/ts-node/docs/troubleshooting" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index fba007e2..4fd76c31 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,10 +51,9 @@ 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, 13] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] 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 - flavor: 1 node: 12.15 nodeFlag: 12_15 @@ -96,41 +95,64 @@ jobs: nodeFlag: 14 typescript: next typescriptFlag: next + - flavor: 8 + node: 14 + nodeFlag: 14 + typescript: rc + typescriptFlag: rc # Node 16 # Node 16.11.1 # Earliest version that supports old ESM Loader Hooks API: https://github.com/TypeStrong/ts-node/pull/1522 - - flavor: 8 + - flavor: 9 node: 16.11.1 nodeFlag: 16_11_1 typescript: latest typescriptFlag: latest - - flavor: 9 + - flavor: 10 node: 16 nodeFlag: 16 typescript: latest typescriptFlag: latest downgradeNpm: true - - flavor: 10 + - flavor: 11 node: 16 nodeFlag: 16 typescript: 2.7 typescriptFlag: 2_7 downgradeNpm: true - - flavor: 11 + - flavor: 12 node: 16 nodeFlag: 16 typescript: next typescriptFlag: next downgradeNpm: true - # Node 17 - - flavor: 12 - node: 17 - nodeFlag: 17 + - flavor: 13 + node: 16 + nodeFlag: 16 + typescript: rc + typescriptFlag: rc + downgradeNpm: true + # Node 18 + - flavor: 14 + node: 18 + nodeFlag: 18 typescript: latest typescriptFlag: latest downgradeNpm: true + - flavor: 15 + node: 18 + nodeFlag: 18 + typescript: next + typescriptFlag: next + downgradeNpm: true + - flavor: 16 + node: 18 + nodeFlag: 18 + typescript: rc + typescriptFlag: rc + downgradeNpm: true # Node nightly - - flavor: 13 + - flavor: 17 node: nightly nodeFlag: nightly typescript: latest @@ -173,6 +195,7 @@ jobs: - run: | npm config set cache "$( node -p "process.cwd()" )/temp/npm-cache" - name: Cache dependencies + if: ${{ matrix.os != 'windows' }} uses: actions/cache@v2 with: path: temp/npm-cache diff --git a/.gitignore b/.gitignore index f8e97ed5..388cf246 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ /node_modules/ /tests/node_modules/ -.nyc_output/ -coverage/ +/.nyc_output/ +/coverage/ .DS_Store npm-debug.log -dist/ -tsconfig.schema.json -tsconfig.schemastore-schema.json -.idea/ -/.vscode/ +/dist/ +/tsconfig.schema.json +/tsconfig.schemastore-schema.json +/.idea/ +/.vscode/* +!/.vscode/launch.json /website/static/api /tsconfig.tsbuildinfo /temp diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index b830eb82..00000000 --- a/.prettierignore +++ /dev/null @@ -1,19 +0,0 @@ -/* -!/*.js -!/*.mjs -!/esm -!/register -!/scripts -!/src -!/tests -!/website -/website/.docusaurus -/website/docs -/website/readme-sources -/website/static -tests/main-realpath/symlink/tsconfig.json -tests/throw error.ts -tests/throw error react tsx.tsx -tests/esm/throw error.ts -tests/legacy-source-map-support-interop/index.ts -tests/main-realpath/symlink/symlink.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 8b1ffa3f..f597178f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,33 @@ "outputCapture": "std", "skipFiles": [ "/**/*.js" + ] + }, + { + "name": "Debug resolver test", + "type": "pwa-node", + "request": "launch", + "cwd": "${workspaceFolder}/tests/tmp/resolver-0029-preferSrc-typeModule-allowJs-skipIgnore-experimentalSpecifierResolutionNode", + "runtimeArgs": [ + "--loader", + "../../../esm.mjs" + ], + "program": "./src/entrypoint-0000-src-to-src.cjs" + }, + { + "name": "Debug Example: running a test fixture against local ts-node/esm loader", + "type": "pwa-node", + "request": "launch", + "cwd": "${workspaceFolder}/tests/esm", + "runtimeArgs": [ + "--loader", + "../../ts-node/esm" ], + "program": "throw error.ts", + "outputCapture": "std", + "skipFiles": [ + "/**/*.js" + ] } - ], -} + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2131146c..41f79726 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,15 @@ compiled code in `dist`. Test cases are declared in `src/test/*.spec.ts`, and test fixtures live in `./tests`. They can be run with `npm run test-local`. +To run a subset of tests: + +``` +# Use ava's --match flag to match the name of a test or suite +# https://github.com/avajs/ava/blob/main/docs/05-command-line.md +# Don't forget the * wildcards +npm run test-local -- --match '*esm loader*' +``` + Tests are run with AVA, but using a custom wrapper API to enable some TS-friendly features and grouped test suites. The tests `npm pack` ts-node into a tarball and `npm install` it into `./tests/node_modules`. This makes `./tests` a better testing environment @@ -45,6 +54,30 @@ We have a debug configuration for VSCode. Note that some tests might misbehave in the debugger. REPL tests in particular. I'm not sure why, but I think it is related to how `console` does not write to stdout when in a debug session. +### Test Context + +Ava has the concept of test "context", an object which can store reusable fields common across many tests. + +By convention, any functions that setup reusable context start with `ctx`, making them easier to tab-complete and auto-import while writing tests. + +See `ctxTsNode` for an example. + +Context setup functions are re-executed for each test case. If you don't want this, wrap the context function in lodash `once()`. Each test will still get a unique context object, but the values placed onto each context will be identical. + +Every `ctx*` function has a namespace with `Ctx` and `T` types. These make it easier/cleaner to write tests. + +### Test Macros + +Ava has the concept of test "macros", reusable functions which can declare a type of test many times with different inputs. + +Macro functions are created with `test.macro()`. + +By convention, if a macro function is meant to be imported and reused in multiple files, its name should start with `macro`. + +Macros can also be declared to require a certain "context," thanks to the namespace types described in "Test Context" above. + +See examples in `helpers.ts`. + ## Documentation Documentation is written in markdown in `website/docs` and rendered into a website by Docusaurus. The README is also generated from these markdown files. diff --git a/README.md b/README.md index 5eaea70a..56296e0e 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ The latest documentation can also be found on our website: *Environment:* `TS_NODE_PROJECT` -* `--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 +### eval + +```shell +ts-node -e +# Example +ts-node -e 'console.log("Hello world!")' +``` + +Evaluate code + +### print + +```shell +ts-node -p -e +# Example +ts-node -p -e '"Hello world!"' +``` + +Print result of `--eval` + +### interactive + +```shell +ts-node -i +``` + +Opens the REPL even if stdin does not appear to be a terminal + +### esm + +```shell +ts-node --esm +ts-node-esm +``` + +Bootstrap with the ESM loader, enabling full ESM support + +## TSConfig Options + +### project + +```shell +ts-node -P +ts-node --project +``` + +Path to tsconfig file. + +*Note the uppercase `-P`. This is different from `tsc`'s `-p/--project` option.* + +*Environment:* `TS_NODE_PROJECT` + +### skipProject + +```shell +ts-node --skipProject +``` + +Skip project config resolution and loading + +*Default:* `false`
+*Environment:* `TS_NODE_SKIP_PROJECT` + +### cwdMode + +```shell +ts-node -c +ts-node --cwdMode +ts-node-cwd +``` + +Resolve config relative to the current directory instead of the directory of the entrypoint script + +### compilerOptions + +```shell +ts-node -O +ts-node --compilerOptions +``` + +JSON object to merge with compiler options + +*Environment:* `TS_NODE_COMPILER_OPTIONS` + +### showConfig + +```shell +ts-node --showConfig +``` + +Print resolved `tsconfig.json`, including `ts-node` options, and exit ## Typechecking -* `-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, --ignoreDiagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` +### transpileOnly -## Transpilation +```shell +ts-node -T +ts-node --transpileOnly +``` -* `-I, --ignore [pattern]` Override the path patterns to skip compilation
*Default:* `/node_modules/`
*Environment:* `TS_NODE_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 -* `--preferTsExts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` +Use TypeScript's faster `transpileModule` -## Diagnostics +*Default:* `false`
+*Environment:* `TS_NODE_TRANSPILE_ONLY` -* `--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
+### typeCheck -## Advanced +```shell +ts-node --typeCheck +``` -* `-r, --require [path]` Require a node module before execution -* `--cwd` Behave as if invoked in this working directory
*Default:* `process.cwd()`
*Environment:* `TS_NODE_CWD` -* `--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` -* `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`
-* `--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.* +Opposite of `--transpileOnly` -## API +*Default:* `true`
+*Environment:* `TS_NODE_TYPE_CHECK` + +### compilerHost + +```shell +ts-node -H +ts-node --compilerHost +``` + +Use TypeScript's compiler host API + +*Default:* `false`
+*Environment:* `TS_NODE_COMPILER_HOST` + +### files + +```shell +ts-node --files +``` + +Load `files`, `include` and `exclude` from `tsconfig.json` on startup. This may +avoid certain typechecking failures. See [Missing types](#missing-types) for details. + +*Default:* `false`
+*Environment:* `TS_NODE_FILES` + +### ignoreDiagnostics + +```shell +ts-node -D +ts-node --ignoreDiagnostics +``` + +Ignore TypeScript warnings by diagnostic code + +*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` + +## Transpilation Options + +### ignore + +```shell +ts-node -I +ts-node --ignore +``` + +Override the path patterns to skip compilation + +*Default:* `/node_modules/`
+*Environment:* `TS_NODE_IGNORE` + +### skipIgnore + +```shell +ts-node --skipIgnore +``` + +Skip ignore checks + +*Default:* `false`
+*Environment:* `TS_NODE_SKIP_IGNORE` + +### compiler + +```shell +ts-node -C +ts-node --compiler +``` + +Specify a custom TypeScript compiler + +*Default:* `typescript`
+*Environment:* `TS_NODE_COMPILER` + +### swc + +```shell +ts-node --swc +``` + +Transpile with [swc](#swc). Implies `--transpileOnly` + +*Default:* `false` + +### transpiler + +```shell +ts-node --transpiler +# Example +ts-node --transpiler ts-node/transpilers/swc +``` + +Use a third-party, non-typechecking transpiler + +### preferTsExts + +```shell +ts-node --preferTsExts +``` + +Re-order file extensions so that TypeScript imports are preferred + +*Default:* `false`
+*Environment:* `TS_NODE_PREFER_TS_EXTS` + +## Diagnostic Options + +### logError + +```shell +ts-node --logError +``` + +Logs TypeScript errors to stderr instead of throwing exceptions + +*Default:* `false`
+*Environment:* `TS_NODE_LOG_ERROR` + +### pretty + +```shell +ts-node --pretty +``` + +Use pretty diagnostic formatter + +*Default:* `false`
+*Environment:* `TS_NODE_PRETTY` + +### TS_NODE_DEBUG + +```shell +TS_NODE_DEBUG=true ts-node +``` + +Enable debug logging + +## Advanced Options + +### require + +```shell +ts-node -r +ts-node --require +``` + +Require a node module before execution + +### cwd + +```shell +ts-node --cwd +``` + +Behave as if invoked in this working directory + +*Default:* `process.cwd()`
+*Environment:* `TS_NODE_CWD` + +### emit + +```shell +ts-node --emit +``` + +Emit output files into `.ts-node` directory. Requires `--compilerHost` + +*Default:* `false`
+*Environment:* `TS_NODE_EMIT` + +### scope + +```shell +ts-node --scope +``` + +Scope compiler to files within `scopeDir`. Anything outside this directory is ignored. + +\*Default: `false`
+*Environment:* `TS_NODE_SCOPE` + +### scopeDir + +```shell +ts-node --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` + +### 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 + +```shell +TS_NODE_HISTORY= ts-node +``` + +Path to history file for REPL + +*Default:* `~/.ts_node_repl_history` + +### noExperimentalReplAwait + +```shell +ts-node --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 + +### experimentalResolver + +Enable experimental hooks that re-map imports and require calls to support: + +* resolves `.js` to `.ts`, so that `import "./foo.js"` will execute `foo.ts` +* resolves `.cjs` to `.cts` +* resolves `.mjs` to `.mts` +* allows including file extensions in CommonJS, for consistency with ESM where this is often mandatory + +In the future, this hook will also support: + +* `baseUrl`, `paths` +* `rootDirs` +* `outDir` to `rootDir` mappings for composite projects and monorepos + +For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514). + +*Default:* `false`, but will likely be enabled by default in a future version
+*Can only be specified via `tsconfig.json` or API.* + +### experimentalSpecifierResolution + +```shell +ts-node --experimentalSpecifierResolution node +``` + +Like node's [`--experimental-specifier-resolution`](https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm), but can also be set in your `tsconfig.json` for convenience. +Requires `esm` to be enabled. + +*Default:* `explicit`
+ +## API Options The API includes [additional options](https://typestrong.org/ts-node/api/interfaces/RegisterOptions.html) not shown here. +# SWC + +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 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 `async`/`await` or generator functions, also install `regenerator-runtime`. + +```shell +npm i -D @swc/core @swc/helpers regenerator-runtime +``` + +Then add the following to your `tsconfig.json`. + +```jsonc title="tsconfig.json" +{ + "ts-node": { + "swc": true + } +} +``` + +> SWC uses `@swc/helpers` instead of `tslib`. If you have enabled `importHelpers`, you must also install `@swc/helpers`. + # CommonJS vs native ECMAScript modules TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. @@ -451,7 +873,7 @@ NODE_OPTIONS="--loader ts-node/esm" node ./index.ts # Troubleshooting -## Understanding configuration +## 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 --showConfig`. This is similar to @@ -501,7 +923,7 @@ $ ts-node --showConfig } ``` -## Understanding Errors +## Common errors It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. @@ -532,17 +954,112 @@ const a = foo?.bar; When you try to run this code, node 12 will throw a `SyntaxError`. To fix this, you must switch to `"target": "es2019"` or lower so TypeScript transforms `?.` into something `node` can understand. -# Make it fast +### `ERR_REQUIRE_ESM` + +This error is thrown by node when a module is `require()`d, but node believes it should execute as native ESM. This can happen for a few reasons: + +* You have installed an ESM dependency but your own code compiles to CommonJS. + * Solution: configure your project to compile and execute as native ESM. [Docs](#native-ecmascript-modules) + * Solution: downgrade the dependency to an older, CommonJS version. +* You have moved your project to ESM but still have a config file, such as `webpack.config.ts`, which must be executed as CommonJS + * Solution: if supported by the relevant tool, rename your config file to `.cts` + * Solution: Configure a module type override. [Docs](#module-type-overrides) +* You have a mix of CommonJS and native ESM in your project + * Solution: double-check all package.json "type" and tsconfig.json "module" configuration [Docs](#commonjs-vs-native-ecmascript-modules) + * Solution: consider simplifying by making your project entirely CommonJS or entirely native ESM + +### `ERR_UNKNOWN_FILE_EXTENSION` + +This error is thrown by node when a module has an unrecognized file extension, or no extension at all, and is being executed as native ESM. This can happen for a few reasons: + +* You are using a tool which has an extensionless binary, such as `mocha`. + * CommonJS supports extensionless files but native ESM does not. + * Solution: upgrade to ts-node >=[v10.6.0](https://github.com/TypeStrong/ts-node/releases/tag/v10.6.0), which implements a workaround. +* Our ESM loader is not installed. + * Solution: Use `ts-node-esm`, `ts-node --esm`, or add `"ts-node": {"esm": true}` to your tsconfig.json. [Docs](#native-ecmascript-modules) +* You have moved your project to ESM but still have a config file, such as `webpack.config.ts`, which must be executed as CommonJS + * Solution: if supported by the relevant tool, rename your config file to `.cts` + * Solution: Configure a module type override. [Docs](#module-type-overrides) + +## Missing Types + +ts-node does *not* eagerly load `files`, `include` or `exclude` by default. This is because a large majority of projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. + +Occasionally, this optimization leads to missing types. Fortunately, there are other ways to include them in typechecking. + +For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). + +Example `tsconfig.json`: + +```jsonc +{ + "compilerOptions": { + "typeRoots" : ["./node_modules/@types", "./typings"] + } +} +``` + +Example project structure: + +```text +/ +-- tsconfig.json +-- typings/ + -- / + -- index.d.ts +``` + +Example module declaration file: + +```typescript twoslash +declare module '' { + // module definitions go here +} +``` + +For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): + +```jsonc title="tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "custom-module-type": ["types/custom-module-type"] + } + } +} +``` + +Another option is [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your `compilerOptions` or structure your type definitions for `typeRoots`. Below is an example of a triple-slash directive as a relative path within your project: + +```typescript twoslash +/// +import {Greeter} from "lib_greeter" +const g = new Greeter(); +g.sayHello(); +``` + +If none of the above work, and you *must* use `files`, `include`, or `exclude`, enable our [`files`](#files) option. + +## npx, yarn dlx, and node_modules + +When executing TypeScript with `npx` or `yarn dlx`, the code resides within a temporary `node_modules` directory. + +The contents of `node_modules` are ignored by default. If execution fails, enable [`skipIgnore`](#skipignore). + + + +# Performance These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck as part of your tests or linting. In these cases, ts-node can skip typechecking. -* Enable [`transpileOnly`](#options) to skip typechecking -* Use our [`swc` integration](#swc) +* Enable [swc](#swc) * This is by far the fastest option +* Enable [`transpileOnly`](#options) to skip typechecking without swc ## With typechecking @@ -554,19 +1071,34 @@ It is often better to use `tsc --noEmit` to typecheck once before your tests run # Advanced -## How It Works +## How it works ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. +We also register a few other hooks to apply sourcemaps to stack traces and remap from `.js` imports to `.ts`. + +## Ignored files + +ts-node transforms certain files and ignores others. We refer to this mechanism as "scoping." There are various +options to configure scoping, so that ts-node transforms only the files in your project. + +> **Warning:** +> +> An ignored file can still be executed by node.js. Ignoring a file means we do not transform it from TypeScript into JavaScript, but it does not prevent execution. +> +> If a file requires transformation but is ignored, node may either fail to resolve it or attempt to execute it as vanilla JavaScript. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. + +### File extensions + `.js` and `.jsx` are only transformed when [`allowJs`](https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) is enabled. `.tsx` and `.jsx` are only transformed when [`jsx`](https://www.typescriptlang.org/docs/handbook/jsx.html) is enabled. -> **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. - -> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. +> **Warning:** +> +> When ts-node is used with `allowJs`, *all* non-ignored JavaScript files are transformed by ts-node. ### Skipping `node_modules` @@ -576,13 +1108,22 @@ By default, ts-node avoids compiling files in `/node_modules/` for three reasons 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. +If you need to import uncompiled TypeScript in `node_modules`, use [`--skipIgnore`](#skipignore) or [`TS_NODE_SKIP_IGNORE`](#skipignore) 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). +To force ts-node to import the TypeScript source, not the precompiled JavaScript, use [`--preferTsExts`](#prefertsexts). + +### Scope by directory + +Our [`scope`](#scope) and [`scopeDir`](#scopedir) options will limit transformation to files +within a directory. + +### Ignore by regexp + +Our [`ignore`](#ignore) option will ignore files matching one or more regular expressions. ## paths and baseUrl @@ -608,64 +1149,6 @@ The official TypeScript Handbook explains the intended purpose for `"paths"` in This means `"paths"` are intended to describe mappings that the build tool or runtime *already* performs, not to tell the build tool or runtime how to resolve modules. In other words, they intend us to write our imports in a way `node` already understands. For this reason, ts-node does not modify `node`'s module resolution behavior to implement `"paths"` mappings. -## Help! My Types Are Missing! - -ts-node does *not* use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. - -For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). - -Example `tsconfig.json`: - -```jsonc -{ - "compilerOptions": { - "typeRoots" : ["./node_modules/@types", "./typings"] - } -} -``` - -Example project structure: - -```text -/ --- tsconfig.json --- typings/ - -- / - -- index.d.ts -``` - -Example module declaration file: - -```typescript twoslash -declare module '' { - // module definitions go here -} -``` - -For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): - -```jsonc title="tsconfig.json" -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "custom-module-type": ["types/custom-module-type"] - } - } -} -``` - -An alternative approach for definitions of third-party libraries are [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your TypeScript `compilerOptions` or structure your custom type definitions when using `typeRoots`. Below is an example of the triple-slash directive as a relative path within your project: - -```typescript twoslash -/// -import {Greeter} from "untyped_js_lib" -const g = new Greeter(); -g.sayHello(); -``` - -**Tip:** If you *must* use `files`, `include`, or `exclude`, enable `--files` flags or set `TS_NODE_FILES=true`. - ## Third-party compilers Some projects require a patched typescript compiler which adds additional features. For example, [`ttypescript`](https://github.com/cevek/ttypescript/tree/master/packages/ttypescript) and [`ts-patch`](https://github.com/nonara/ts-patch#readme) @@ -691,45 +1174,20 @@ For example, to use `ttypescript` and `ts-transformer-keys`, add this to your `t ## Transpilers -In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a -third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations -can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project -boilerplate. +ts-node supports third-party transpilers as plugins. Transpilers such as swc can transform TypeScript into JavaScript +much faster than the TypeScript compiler. You will still benefit from ts-node's automatic `tsconfig.json` discovery, +sourcemap support, and global ts-node CLI. Plugins automatically derive an appropriate configuration from your existing +`tsconfig.json` which simplifies project boilerplate. > **What is the difference between a compiler and a transpiler?** > > For our purposes, a compiler implements TypeScript's API and can perform typechecking. > A third-party transpiler does not. Both transform TypeScript into JavaScript. -### swc - -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 vanilla `transpileOnly`. +### Third-party plugins -To use it, first install `@swc/core` or `@swc/wasm`. If using `importHelpers`, also install `@swc/helpers`. If `target` is less than "es2015" and using either `async`/`await` or generator functions, also install `regenerator-runtime`. - -```shell -npm i -D @swc/core @swc/helpers regenerator-runtime -``` - -Then add the following to your `tsconfig.json`. - -```jsonc title="tsconfig.json" -{ - "ts-node": { - "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`. +The `transpiler` option allows using third-party transpiler plugins with ts-node. `transpiler` must be given the +name of a module which can be `require()`d. The built-in `swc` plugin 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` @@ -744,28 +1202,36 @@ Then add the following to your tsconfig: } ``` -### Writing your own integration +### Write your own plugin + +To write your own transpiler plugin, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). -To write your own transpiler integration, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). +Plugins are `require()`d by ts-node, so they can be a local script or a node module published to npm. 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 one or more transpiler instances. The instances are used to transform +TypeScript into JavaScript. -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. +For a working example, check out out our bundled swc plugin: https://github.com/TypeStrong/ts-node/blob/main/src/transpilers/swc.ts ## Module type overrides -When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` -behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed -according to node's rules for the `package.json` `"type"` field. +> Wherever possible, it is recommended to use TypeScript's [`NodeNext` or `Node16` mode](https://www.typescriptlang.org/docs/handbook/esm-node.html) instead of the options described +> in this section. Setting `"module": "NodeNext"` and using the `.cts` file extension should work well for most projects. + +When deciding how a file should be compiled and executed -- as either CommonJS or native ECMAScript module -- ts-node matches +`node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` +option and executed according to node's rules for the `package.json` `"type"` field. Set `"module": "NodeNext"` and everything should work. -In some projects you may need to override this behavior for some files. For example, in a webpack -project, you may have `package.json` configured with `"type": "module"` and `tsconfig.json` with -`"module": "esnext"`. However, webpack uses our CommonJS hook to execute your `webpack.config.ts`, -so you need to force your webpack config and any supporting scripts to execute as CommonJS. +In rare cases, you may need to override this behavior for some files. For example, some tools read a `name-of-tool.config.ts` +and require that file to execute as CommonJS. If you have `package.json` configured with `"type": "module"` and `tsconfig.json` with +`"module": "esnext"`, the config is native ECMAScript by default and will raise an error. You will need to force the config and +any supporting scripts to execute as CommonJS. -In these situations, our `moduleTypes` option lets you override certain files, forcing execution as -CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. -`moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. +In these situations, our `moduleTypes` option can override certain files to be +CommonJS or ESM. Similar overriding is possible by using `.mts`, `.cts`, `.cjs` and `.mjs` file extensions. +`moduleTypes` achieves the same effect for `.ts` and `.js` files, and *also* overrides your `tsconfig.json` `"module"` +config appropriately. The following example tells ts-node to execute a webpack config as CommonJS: @@ -799,13 +1265,28 @@ Files with an overridden module type are transformed with the same limitations a This feature is meant to facilitate scenarios where normal `compilerOptions` and `package.json` configuration is not possible. For example, a `webpack.config.ts` cannot be given its own `package.json` to override `"type"`. Wherever possible you should favor using traditional `package.json` and `tsconfig.json` configurations. +## API + +ts-node's complete API is documented here: [API Docs](https://typestrong.org/ts-node/api/) + +Here are a few highlights of what you can accomplish: + +* [`create()`](https://typestrong.org/ts-node/api/index.html#create) creates ts-node's compiler service without + registering any hooks. +* [`createRepl()`](https://typestrong.org/ts-node/api/index.html#createRepl) creates an instance of our REPL service, so + you can create your own TypeScript-powered REPLs. +* [`createEsmHooks()`](https://typestrong.org/ts-node/api/index.html#createEsmHooks) creates our ESM loader hooks, + suitable for composing with other loaders or augmenting with additional features. + # Recipes -## Watching and Restarting +## Watching and restarting + +ts-node focuses on adding first-class TypeScript support to node. Watching files and code reloads are out of scope for the project. -**TypeScript Node** compiles source code via `require()`, watching files and code reloads are out of scope for the project. If you want to restart the `ts-node` process on file change, existing node.js tools such as [nodemon](https://github.com/remy/nodemon), [onchange](https://github.com/Qard/onchange) and [node-dev](https://github.com/fgnass/node-dev) work. +If you want to restart the `ts-node` process on file change, existing node.js tools such as [nodemon](https://github.com/remy/nodemon), [onchange](https://github.com/Qard/onchange) and [node-dev](https://github.com/fgnass/node-dev) work. -There's also [`ts-node-dev`](https://github.com/whitecolor/ts-node-dev), a modified version of [`node-dev`](https://github.com/fgnass/node-dev) using `ts-node` for compilation that will restart the process on file change. +There's also [`ts-node-dev`](https://github.com/whitecolor/ts-node-dev), a modified version of [`node-dev`](https://github.com/fgnass/node-dev) using `ts-node` for compilation that will restart the process on file change. Note that `ts-node-dev` is incompatible with our native ESM loader. ## AVA diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index acde0be5..b5b8af2f 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -28,6 +28,7 @@ export interface CreateOptions { emit?: boolean; esm?: boolean; experimentalReplAwait?: boolean; + experimentalSpecifierResolution?: 'node' | 'explicit'; // (undocumented) fileExists?: (path: string) => boolean; files?: boolean; @@ -59,7 +60,7 @@ export interface CreateOptions { // @public export function createRepl(options?: CreateReplOptions): ReplService; -// @public (undocumented) +// @public export interface CreateReplOptions { // (undocumented) service?: Service; @@ -75,7 +76,7 @@ export interface CreateReplOptions { stdout?: NodeJS.WritableStream; } -// @public (undocumented) +// @public export interface CreateTranspilerOptions { // (undocumented) service: Pick>; @@ -85,7 +86,13 @@ export interface CreateTranspilerOptions { export type EvalAwarePartialHost = Pick; // @public (undocumented) -export type ModuleTypes = Record; +export type ExperimentalSpecifierResolution = 'node' | 'explicit'; + +// @public (undocumented) +export type ModuleTypeOverride = 'cjs' | 'esm' | 'package'; + +// @public (undocumented) +export type ModuleTypes = Record; // @public (undocumented) export interface NodeLoaderHooksAPI1 { @@ -131,6 +138,7 @@ export namespace NodeLoaderHooksAPI2 { }, defaultLoad: NodeLoaderHooksAPI2['load']) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined; + shortCircuit?: boolean; }>; // (undocumented) export interface NodeImportAssertions { @@ -147,12 +155,16 @@ export namespace NodeLoaderHooksAPI2 { }, defaultResolve: ResolveHook) => Promise<{ url: string; format?: NodeLoaderHooksFormat; + shortCircuit?: boolean; }>; } // @public (undocumented) export type NodeLoaderHooksFormat = 'builtin' | 'commonjs' | 'dynamic' | 'json' | 'module' | 'wasm'; +// @public +export type NodeModuleEmitKind = 'nodeesm' | 'nodecjs'; + // @public @deprecated export type Register = Service; @@ -167,7 +179,7 @@ export const REGISTER_INSTANCE: unique symbol; // @public export interface RegisterOptions extends CreateOptions { - experimentalResolverFeatures?: boolean; + experimentalResolver?: boolean; } // @public (undocumented) @@ -202,13 +214,13 @@ export interface Service { ts: TSCommon; } -// @public (undocumented) +// @public export interface TranspileOptions { // (undocumented) fileName: string; } -// @public (undocumented) +// @public export interface TranspileOutput { // (undocumented) diagnostics?: _ts.Diagnostic[]; @@ -218,7 +230,7 @@ export interface TranspileOutput { sourceMapText?: string; } -// @public (undocumented) +// @public export interface Transpiler { // (undocumented) transpile(input: string, options: TranspileOptions): TranspileOutput; @@ -270,7 +282,7 @@ export interface TSCommon { // (undocumented) JsxEmit: typeof _ts.JsxEmit; // (undocumented) - ModuleKind: typeof _ts.ModuleKind; + ModuleKind: TSCommon.ModuleKindEnum; // (undocumented) ModuleResolutionKind: typeof _ts.ModuleResolutionKind; // (undocumented) @@ -307,6 +319,12 @@ export namespace TSCommon { resolveTypeReferenceDirectives?(typeDirectiveNames: string[] | _ts.FileReference[], containingFile: string, redirectedReference: _ts.ResolvedProjectReference | undefined, options: _ts.CompilerOptions, containingFileMode?: _ts.SourceFile['impliedNodeFormat'] | undefined): (_ts.ResolvedTypeReferenceDirective | undefined)[]; } // (undocumented) + export type ModuleKindEnum = typeof _ts.ModuleKind & { + Node16: typeof _ts.ModuleKind extends { + Node16: any; + } ? typeof _ts.ModuleKind['Node16'] : 100; + }; + // (undocumented) export type ModuleResolutionHost = _ts.ModuleResolutionHost; // (undocumented) export type ParsedCommandLine = _ts.ParsedCommandLine; @@ -328,10 +346,12 @@ export interface TsConfigOptions extends Omit); // (undocumented) diagnosticCodes: number[]; // (undocumented) + diagnostics: ReadonlyArray<_ts.Diagnostic>; + // (undocumented) diagnosticText: string; // (undocumented) name: string; diff --git a/ava.config.cjs b/ava.config.cjs index aa04b33b..f6dc6951 100644 --- a/ava.config.cjs +++ b/ava.config.cjs @@ -1,4 +1,5 @@ const expect = require('expect'); +const semver = require('semver'); const { createRequire } = require('module'); module.exports = { @@ -11,8 +12,12 @@ module.exports = { // This avoids passing it to spawned processes under test, which would negatively affect // their behavior. FORCE_COLOR: '3', + NODE_PATH: '' }, require: ['./src/test/remove-env-var-force-color.js'], + nodeArguments: semver.gte(process.version, '14.0.0') + ? ['--loader', './src/test/test-loader.mjs', '--no-warnings'] + : [], timeout: '300s', concurrency: 1, }; @@ -21,10 +26,15 @@ module.exports = { /* * Tests *must* install and use our most recent ts-node tarball. * We must prevent them from accidentally require-ing a different version of - * ts-node, from either node_modules or tests/node_modules + * ts-node, from either node_modules or tests/node_modules. + * + * Another possibility of interference is NODE_PATH environment variable being set, + * and ts-node being installed in any of the paths listed on NODE_PATH, to fix this, + * the NODE_PATH variable must be removed from the environment *BEFORE* running ava. + * An error will be thrown when trying to run tests with NODE_PATH set to paths with ts-node installed. */ - const { existsSync } = require('fs'); + const { existsSync, rmSync } = require('fs'); const rimraf = require('rimraf'); const { resolve } = require('path'); @@ -32,9 +42,23 @@ module.exports = { remove(resolve(__dirname, 'tests/node_modules/ts-node')); // Prove that we did it correctly - expect(() => {createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node')}).toThrow(); + (() => { + let resolved; + try { + resolved = createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node'); + } catch(err) {return} + + // require.resolve() found ts-node; this should not happen. + let errorMessage = `require.resolve('ts-node') unexpectedly resolved to ${resolved}\n`; + // Check for NODE_PATH interference. See comments above. + if(process.env.NODE_PATH) { + errorMessage += `NODE_PATH environment variable is set. This test suite does not support running with NODE_PATH. Unset it before running the tests.`; + } + throw new Error(errorMessage); + })(); function remove(p) { - if(existsSync(p)) rimraf.sync(p, {recursive: true}) + // Avoid node deprecation warning triggered by rimraf + if(existsSync(p)) (rmSync || rimraf.sync)(p, {recursive: true}) } } diff --git a/child-loader.mjs b/child-loader.mjs index 3a96eeea..6ef592ac 100644 --- a/child-loader.mjs +++ b/child-loader.mjs @@ -2,6 +2,7 @@ import { fileURLToPath } from 'url'; import { createRequire } from 'module'; const require = createRequire(fileURLToPath(import.meta.url)); +// TODO why use require() here? I think we can just `import` /** @type {import('./dist/child-loader')} */ const childLoader = require('./dist/child/child-loader'); export const { resolve, load, getFormat, transformSource } = childLoader; diff --git a/development-docs/README.md b/development-docs/README.md new file mode 100644 index 00000000..17ab824f --- /dev/null +++ b/development-docs/README.md @@ -0,0 +1,10 @@ +This directory contains a variety of documents: + +- notes +- old to-do lists +- design ideas from when I implemented various features +- templates for drafting release notes +- etc + +It is useful to me to keep these notes. If you find their presence +confusing, you can safely ignore this directory. diff --git a/development-docs/nodenextNode16.md b/development-docs/nodenextNode16.md new file mode 100644 index 00000000..89ce02c9 --- /dev/null +++ b/development-docs/nodenextNode16.md @@ -0,0 +1,53 @@ +# Adding support for NodeNext, Node16, `.cts`, `.mts`, `.cjs`, `.mjs` + +*This feature has already been implemented. Here are my notes from when +I was doing the work* + +## TODOs + +Implement node module type classifier: +- if NodeNext or Node12: ask classifier for CJS or ESM determination +Add `ForceNodeNextCJSEmit` + +Does our code check for .d.ts extensions anywhere? +- if so, teach it about .d.cts and .d.mts + +For nodenext and node12, support supplemental "flavor" information: +- + +Think about splitting out index.ts further: +- register.ts - hooking stuff +- types.ts +- env.ts - env vars and global registration types (process.symbol) +- service.ts + +# TESTS + +Matrix: + +- package.json type absent, commonjs, and module +- import and require +- from cjs and esm +- .cts, .cjs +- .mts, .mjs +- typechecking, transpileOnly, and swc +- dynamic import +- import = require +- static import +- allowJs on and off + +Notes about specific matrix entries: +- require mjs, mts from cjs throws error + +Rethink: +`getOutput`: null in transpile-only mode. Also may return emitskipped +`getOutputTranspileOnly`: configured module option +`getOutputForceCommonJS`: `commonjs` module option +`getOutputForceNodeCommonJS`: `nodenext` cjs module option +`getOutputForceESM`: `esnext` module option + +Add second layer of classification to classifier: +if classifier returns `auto` (no `moduleType` override) +- if `getOutput` emits, done +- else call `nodeModuleTypeClassifier` + - delegate to appropriate `getOutput` based on its response diff --git a/development-docs/rootDirOutDirMapping.md b/development-docs/rootDirOutDirMapping.md new file mode 100644 index 00000000..db552772 --- /dev/null +++ b/development-docs/rootDirOutDirMapping.md @@ -0,0 +1,34 @@ +## Musings about resolving between rootDir and outDir + +When /dist and /src are understood to be overlaid because of src -> dist compiling +/dist/ +/src/ + +Loop over require.extensions +/src/foo.js +/src/foo.mjs +/src/foo.cjs +/src/foo.ts +/src/foo.mts +/src/foo.cts +/src/foo/index.js +/src/foo/index.mjs +/src/foo/index.ts +// Where do we check package.json main?? + + +/dist/foo.js +/dist/foo.ts + + +_resolveLookupPaths +_findPath +_resolveFilename + +_findPath calls resolveExports calls packageExportsResolve, which is in the ESM loader + +Is anything within packageExportsResolve hooked/modified by us? File extension swapping? + + +When resolver calls statSync('./dist/foo.js') and we intercept and discover './src/foo.ts' +How to redirect? We need to rewrite whatever local variable is storing `./dist/foo.js` diff --git a/development-docs/yarnPnpInterop.md b/development-docs/yarnPnpInterop.md new file mode 100644 index 00000000..816f921f --- /dev/null +++ b/development-docs/yarnPnpInterop.md @@ -0,0 +1,59 @@ +## Yarn PnP interop + +Asked about it here: +https://discord.com/channels/226791405589233664/654372321225605128/957301175609344070 + +PnP API checks if import specifiers are for dependencies: non-relative, non-absolute + libfoo + @scope/libfoo + +When they are, it does `resolveToUnqualified` to map to an unqualified path. +This path points to the module's location on disk (in a zip, perhaps) but does +not handle file extension resolution or stuff like that. + +To interop with PnP, we need PnP to *only* `resolveToUnqualified`. +We do everything else. + +```typescript +import { Module } from 'module'; +import fs from 'fs'; + +const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:@[^/]+\/)?[^/]+)\/*(.*|)$/; + +const originalModuleResolveFilename = Module._resolveFilename; +Module._resolveFilename = function ( + request: string, + parent: typeof Module | null | undefined, + isMain: boolean, + options?: { [key: string]: any } +) { + const dependencyNameMatch = request.match(pathRegExp); + if (dependencyNameMatch !== null) { + + const [, dependencyName, subPath] = dependencyNameMatch; + + const unqualified = pnpapi.resolveToUnqualified(....); + + // Do your modified resolution on the unqualified path here + + } else { + + // Do your modified resolution here; no need for PnP + + } + +}; +``` + +PnP can be installed at runtime. + +To conditionally check if PnP is available at the start of *every* resolution: + +```typescript +// Get the pnpapi of either the issuer or the specifier. +// The latter is required when the specifier is an absolute path to a +// zip file and the issuer doesn't belong to a pnpapi +const {findPnPApi} = Module; +const pnpapi = findPnPApi ? (findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null)) : null; +if (pnpapi) {...} +``` diff --git a/dist-raw/NODE-LICENSE.md b/dist-raw/NODE-LICENSE.md new file mode 100644 index 00000000..c1de8e9d --- /dev/null +++ b/dist-raw/NODE-LICENSE.md @@ -0,0 +1,24 @@ +This directory contains portions of Node.js source code which is licensed as follows: + +--- + +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/dist-raw/README.md b/dist-raw/README.md index b7b4c4d7..9eeaed31 100644 --- a/dist-raw/README.md +++ b/dist-raw/README.md @@ -11,3 +11,26 @@ in a factory function, we will not indent the function body, to avoid whitespace One obvious problem with this approach: the code has been pulled from one version of node, whereas users of ts-node run multiple versions of node. Users running node 12 may see that ts-node behaves like node 14, for example. + +## `raw` directory + +Within the `raw` directory, we keep unmodified copies of the node source files. This allows us to use diffing tools to +compare files in `raw` to those in `dist-raw`, which will highlight all of the changes we have made. Hopefully, these +changes are as minimal as possible. + +## Naming convention + +Not used consistently, but the idea is: + +`node-(...-)-.js` + +`node-internal-errors.js` -> `github.com/nodejs/node/blob/TAG/lib/internal/errors.js` + +So, take the path within node's `lib/` directory, and replace slashes with hyphens. + +In the `raw` directory, files are suffixed with the version number or revision from which +they were downloaded. + +If they have a `stripped` suffix, this means they have large chunks of code deleted, but no other modifications. +This is useful when diffing. Sometimes our `dist-raw` files only have a small part of a much larger node source file. +It is easier to diff `raw/*-stripped.js` against `dist-raw/*.js`. diff --git a/dist-raw/node-cjs-helpers.d.ts b/dist-raw/node-cjs-helpers.d.ts deleted file mode 100644 index a57c2f83..00000000 --- a/dist-raw/node-cjs-helpers.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function addBuiltinLibsToObject(object: any): void; diff --git a/dist-raw/node-cjs-loader-utils.js b/dist-raw/node-cjs-loader-utils.js deleted file mode 100644 index b7ec0d53..00000000 --- a/dist-raw/node-cjs-loader-utils.js +++ /dev/null @@ -1,133 +0,0 @@ -// Copied from several files in node's source code. -// https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js -// Each function and variable below must have a comment linking to the source in node's github repo. - -const path = require('path'); -const packageJsonReader = require('./node-package-json-reader'); -const {JSONParse} = require('./node-primordials'); -const {normalizeSlashes} = require('../dist/util'); - -module.exports.assertScriptCanLoadAsCJSImpl = assertScriptCanLoadAsCJSImpl; - -/** - * copied from Module._extensions['.js'] - * https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L1113-L1120 - * @param {import('../src/index').Service} service - * @param {NodeJS.Module} module - * @param {string} filename - */ -function assertScriptCanLoadAsCJSImpl(service, module, filename) { - const pkg = readPackageScope(filename); - - // ts-node modification: allow our configuration to override - const tsNodeClassification = service.moduleTypeClassifier.classifyModule(normalizeSlashes(filename)); - if(tsNodeClassification.moduleType === 'cjs') return; - - // Function require shouldn't be used in ES modules. - if (tsNodeClassification.moduleType === 'esm' || (pkg && pkg.data && pkg.data.type === 'module')) { - const parentPath = module.parent && module.parent.filename; - const packageJsonPath = pkg ? path.resolve(pkg.path, 'package.json') : null; - throw createErrRequireEsm(filename, parentPath, packageJsonPath); - } -} - -// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js#L285-L301 -function readPackageScope(checkPath) { - const rootSeparatorIndex = checkPath.indexOf(path.sep); - let separatorIndex; - while ( - (separatorIndex = checkPath.lastIndexOf(path.sep)) > rootSeparatorIndex - ) { - checkPath = checkPath.slice(0, separatorIndex); - if (checkPath.endsWith(path.sep + 'node_modules')) - return false; - const pjson = readPackage(checkPath); - if (pjson) return { - path: checkPath, - data: pjson - }; - } - return false; -} - -// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js#L249 -const packageJsonCache = new Map(); - -// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L275-L304 -function readPackage(requestPath) { - const jsonPath = path.resolve(requestPath, 'package.json'); - - const existing = packageJsonCache.get(jsonPath); - if (existing !== undefined) return existing; - - const result = packageJsonReader.read(jsonPath); - const json = result.containsKeys === false ? '{}' : result.string; - if (json === undefined) { - packageJsonCache.set(jsonPath, false); - return false; - } - - try { - const parsed = JSONParse(json); - const filtered = { - name: parsed.name, - main: parsed.main, - exports: parsed.exports, - imports: parsed.imports, - type: parsed.type - }; - packageJsonCache.set(jsonPath, filtered); - return filtered; - } catch (e) { - e.path = jsonPath; - e.message = 'Error parsing ' + jsonPath + ': ' + e.message; - throw e; - } -} - -// Native ERR_REQUIRE_ESM Error is declared here: -// https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L1294-L1313 -// Error class factory is implemented here: -// function E: https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L323-L341 -// function makeNodeErrorWithCode: https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L251-L278 -// The code below should create an error that matches the native error as closely as possible. -// Third-party libraries which attempt to catch the native ERR_REQUIRE_ESM should recognize our imitation error. -function createErrRequireEsm(filename, parentPath, packageJsonPath) { - const code = 'ERR_REQUIRE_ESM' - const err = new Error(getMessage(filename, parentPath, packageJsonPath)) - // Set `name` to be used in stack trace, generate stack trace with that name baked in, then re-declare the `name` field. - // This trick is copied from node's source. - err.name = `Error [${ code }]` - err.stack - Object.defineProperty(err, 'name', { - value: 'Error', - enumerable: false, - writable: true, - configurable: true - }) - err.code = code - return err - - // Copy-pasted from https://github.com/nodejs/node/blob/b533fb3508009e5f567cc776daba8fbf665386a6/lib/internal/errors.js#L1293-L1311 - // so that our error message is identical to the native message. - function getMessage(filename, parentPath = null, packageJsonPath = null) { - const ext = path.extname(filename) - let msg = `Must use import to load ES Module: ${filename}`; - if (parentPath && packageJsonPath) { - const path = require('path'); - const basename = path.basename(filename) === path.basename(parentPath) ? - filename : path.basename(filename); - msg += - '\nrequire() of ES modules is not supported.\nrequire() of ' + - `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + - `is an ES module file as it is a ${ext} file whose nearest parent ` + - `package.json contains "type": "module" which defines all ${ext} ` + - 'files in that package scope as ES modules.\nInstead ' + - 'change the requiring code to use ' + - 'import(), or remove "type": "module" from ' + - `${packageJsonPath}.\n`; - return msg; - } - return msg; - } -} diff --git a/dist-raw/node-errors.js b/dist-raw/node-errors.js deleted file mode 100644 index 73255a7d..00000000 --- a/dist-raw/node-errors.js +++ /dev/null @@ -1,30 +0,0 @@ -exports.codes = { - ERR_INPUT_TYPE_NOT_ALLOWED: createErrorCtor(joinArgs('ERR_INPUT_TYPE_NOT_ALLOWED')), - ERR_INVALID_ARG_VALUE: createErrorCtor(joinArgs('ERR_INVALID_ARG_VALUE')), - ERR_INVALID_MODULE_SPECIFIER: createErrorCtor(joinArgs('ERR_INVALID_MODULE_SPECIFIER')), - ERR_INVALID_PACKAGE_CONFIG: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_CONFIG')), - ERR_INVALID_PACKAGE_TARGET: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_TARGET')), - ERR_MANIFEST_DEPENDENCY_MISSING: createErrorCtor(joinArgs('ERR_MANIFEST_DEPENDENCY_MISSING')), - ERR_MODULE_NOT_FOUND: createErrorCtor((path, base, type = 'package') => { - return `Cannot find ${type} '${path}' imported from ${base}` - }), - ERR_PACKAGE_IMPORT_NOT_DEFINED: createErrorCtor(joinArgs('ERR_PACKAGE_IMPORT_NOT_DEFINED')), - 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) { - return (...args) => { - return [name, ...args].join(' ') - } -} - -function createErrorCtor(errorMessageCreator) { - return class CustomError extends Error { - constructor(...args) { - super(errorMessageCreator(...args)) - } - } -} diff --git a/dist-raw/node-internal-constants.js b/dist-raw/node-internal-constants.js new file mode 100644 index 00000000..b4aa7aad --- /dev/null +++ b/dist-raw/node-internal-constants.js @@ -0,0 +1,4 @@ +// Copied from https://github.com/nodejs/node/blob/master/lib/internal/constants.js +module.exports = { + CHAR_FORWARD_SLASH: 47, /* / */ +}; diff --git a/dist-raw/node-internal-errors.js b/dist-raw/node-internal-errors.js new file mode 100644 index 00000000..ddcd6617 --- /dev/null +++ b/dist-raw/node-internal-errors.js @@ -0,0 +1,82 @@ +'use strict'; + +const path = require('path'); + +exports.codes = { + ERR_INPUT_TYPE_NOT_ALLOWED: createErrorCtor(joinArgs('ERR_INPUT_TYPE_NOT_ALLOWED')), + ERR_INVALID_ARG_VALUE: createErrorCtor(joinArgs('ERR_INVALID_ARG_VALUE')), + ERR_INVALID_MODULE_SPECIFIER: createErrorCtor(joinArgs('ERR_INVALID_MODULE_SPECIFIER')), + ERR_INVALID_PACKAGE_CONFIG: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_CONFIG')), + ERR_INVALID_PACKAGE_TARGET: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_TARGET')), + ERR_MANIFEST_DEPENDENCY_MISSING: createErrorCtor(joinArgs('ERR_MANIFEST_DEPENDENCY_MISSING')), + ERR_MODULE_NOT_FOUND: createErrorCtor((path, base, type = 'package') => { + return `Cannot find ${type} '${path}' imported from ${base}` + }), + ERR_PACKAGE_IMPORT_NOT_DEFINED: createErrorCtor(joinArgs('ERR_PACKAGE_IMPORT_NOT_DEFINED')), + 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) { + return (...args) => { + return [name, ...args].join(' ') + } +} + +function createErrorCtor(errorMessageCreator) { + return class CustomError extends Error { + constructor(...args) { + super(errorMessageCreator(...args)) + } + } +} +exports.createErrRequireEsm = createErrRequireEsm; + +// Native ERR_REQUIRE_ESM Error is declared here: +// https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L1294-L1313 +// Error class factory is implemented here: +// function E: https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L323-L341 +// function makeNodeErrorWithCode: https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L251-L278 +// The code below should create an error that matches the native error as closely as possible. +// Third-party libraries which attempt to catch the native ERR_REQUIRE_ESM should recognize our imitation error. +function createErrRequireEsm(filename, parentPath, packageJsonPath) { + const code = 'ERR_REQUIRE_ESM' + const err = new Error(getErrRequireEsmMessage(filename, parentPath, packageJsonPath)) + // Set `name` to be used in stack trace, generate stack trace with that name baked in, then re-declare the `name` field. + // This trick is copied from node's source. + err.name = `Error [${ code }]` + err.stack + Object.defineProperty(err, 'name', { + value: 'Error', + enumerable: false, + writable: true, + configurable: true + }) + err.code = code + return err +} + +// Copy-pasted from https://github.com/nodejs/node/blob/b533fb3508009e5f567cc776daba8fbf665386a6/lib/internal/errors.js#L1293-L1311 +// so that our error message is identical to the native message. +function getErrRequireEsmMessage(filename, parentPath = null, packageJsonPath = null) { + const ext = path.extname(filename) + let msg = `Must use import to load ES Module: ${filename}`; + if (parentPath && packageJsonPath) { + const path = require('path'); + const basename = path.basename(filename) === path.basename(parentPath) ? + filename : path.basename(filename); + msg += + '\nrequire() of ES modules is not supported.\nrequire() of ' + + `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + + `is an ES module file as it is a ${ext} file whose nearest parent ` + + `package.json contains "type": "module" which defines all ${ext} ` + + 'files in that package scope as ES modules.\nInstead ' + + 'change the requiring code to use ' + + 'import(), or remove "type": "module" from ' + + `${packageJsonPath}.\n`; + return msg; + } + return msg; +} diff --git a/dist-raw/node-internal-fs.js b/dist-raw/node-internal-fs.js deleted file mode 100644 index d9a2528d..00000000 --- a/dist-raw/node-internal-fs.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('fs'); - -// In node's core, this is implemented in C -// https://github.com/nodejs/node/blob/v15.3.0/src/node_file.cc#L891-L985 -function internalModuleReadJSON(path) { - let string - try { - string = fs.readFileSync(path, 'utf8') - } catch (e) { - if (e.code === 'ENOENT') return [] - throw e - } - // Node's implementation checks for the presence of relevant keys: main, name, type, exports, imports - // Node does this for performance to skip unnecessary parsing. - // This would slow us down and, based on our usage, we can skip it. - const containsKeys = true - return [string, containsKeys] -} - -module.exports = { - internalModuleReadJSON -}; diff --git a/dist-raw/node-internal-modules-cjs-helpers.js b/dist-raw/node-internal-modules-cjs-helpers.js new file mode 100644 index 00000000..bd4f7020 --- /dev/null +++ b/dist-raw/node-internal-modules-cjs-helpers.js @@ -0,0 +1,89 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js + +'use strict'; + +const { + ArrayPrototypeForEach, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeSet, + StringPrototypeIncludes, + StringPrototypeStartsWith, +} = require('./node-primordials'); + +const { getOptionValue } = require('./node-options'); +const userConditions = getOptionValue('--conditions'); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); + +/** + * @param {any} object + * @param {string} [dummyModuleName] + * @return {void} + */ +function addBuiltinLibsToObject(object, dummyModuleName) { + // Make built-in modules available directly (loaded lazily). + const Module = require('module').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + + ArrayPrototypeForEach(builtinModules, (name) => { + // Neither add underscored modules, nor ones that contain slashes (e.g., + // 'fs/promises') or ones that are already defined. + if (StringPrototypeStartsWith(name, '_') || + StringPrototypeIncludes(name, '/') || + ObjectPrototypeHasOwnProperty(object, name)) { + return; + } + // Goals of this mechanism are: + // - Lazy loading of built-in modules + // - Having all built-in modules available as non-enumerable properties + // - Allowing the user to re-assign these variables as if there were no + // pre-existing globals with the same name. + + const setReal = (val) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + ObjectDefineProperty(object, name, { + get: () => { + // Node 12 hack; remove when we drop node12 support + const lib = (dummyModule.require || require)(name); + + // Disable the current getter/setter and set up a new + // non-enumerable property. + delete object[name]; + ObjectDefineProperty(object, name, { + get: () => lib, + set: setReal, + configurable: true, + enumerable: false + }); + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + }); +} + +exports.addBuiltinLibsToObject = addBuiltinLibsToObject; +exports.cjsConditions = cjsConditions; diff --git a/dist-raw/node-internal-modules-cjs-loader.js b/dist-raw/node-internal-modules-cjs-loader.js new file mode 100644 index 00000000..cb83c353 --- /dev/null +++ b/dist-raw/node-internal-modules-cjs-loader.js @@ -0,0 +1,593 @@ +// Copied from several files in node's source code. +// https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js +// Each function and variable below must have a comment linking to the source in node's github repo. + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypePush, + JSONParse, + ObjectKeys, + RegExpPrototypeTest, + SafeMap, + SafeWeakMap, + StringPrototypeCharCodeAt, + StringPrototypeEndsWith, + StringPrototypeLastIndexOf, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = require('./node-primordials'); +const { NativeModule } = require('./node-nativemodule'); +const { pathToFileURL, fileURLToPath } = require('url'); +const fs = require('fs'); +const path = require('path'); +const { sep } = path; +const { internalModuleStat } = require('./node-internalBinding-fs'); +const packageJsonReader = require('./node-internal-modules-package_json_reader'); +const { + cjsConditions, +} = require('./node-internal-modules-cjs-helpers'); +const { getOptionValue } = require('./node-options'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +const {normalizeSlashes} = require('../dist/util'); +const {createErrRequireEsm} = require('./node-internal-errors'); +const { + codes: { + ERR_INVALID_MODULE_SPECIFIER, + }, +} = require('./node-internal-errors'); + +const { + CHAR_FORWARD_SLASH, +} = require('./node-internal-constants'); + +const Module = require('module'); + +const isWindows = process.platform === 'win32'; + +let statCache = null; + +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null && result >= 0) { + // Only set cache when `internalModuleStat(filename)` succeeds. + statCache.set(filename, result); + } + return result; +} + +// Note: +// we cannot get access to node's internal cache, which is populated from +// within node's Module constructor. So the cache here will always be empty. +// It's possible we could approximate our own cache by building it up with +// hacky workarounds, but it's not worth the complexity and flakiness. +const moduleParentCache = new SafeWeakMap(); + +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const result = packageJsonReader.read(jsonPath); + const json = result.containsKeys === false ? '{}' : result.string; + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + imports: parsed.imports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); + let separatorIndex; + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath + sep); + if (pjson) return { + data: pjson, + path: checkPath, + }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +/** + * @param {{ + * nodeEsmResolver: ReturnType, + * extensions: import('../src/file-extensions').Extensions, + * preferTsExts + * }} opts + */ +function createCjsLoader(opts) { +const {nodeEsmResolver, preferTsExts} = opts; +const {replacementsForCjs, replacementsForJs, replacementsForMjs, replacementsForJsx} = opts.extensions; +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = nodeEsmResolver; + +function tryPackage(requestPath, exts, isMain, originalPath) { + // const pkg = readPackage(requestPath)?.main; + const tmp = readPackage(requestPath) + const pkg = tmp != null ? tmp.main : undefined; + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryReplacementExtensions(filename, isMain) || + tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new SafeMap(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + // [internalFS.realpathCacheKey]: realpathCache + }); +} + +function statReplacementExtensions(p) { + const lastDotIndex = p.lastIndexOf('.'); + if(lastDotIndex >= 0) { + const ext = p.slice(lastDotIndex); + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { + const pathnameWithoutExtension = p.slice(0, lastDotIndex); + const replacementExts = + ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx + : ext === '.mjs' ? replacementsForMjs + : replacementsForCjs; + for (let i = 0; i < replacementExts.length; i++) { + const filename = pathnameWithoutExtension + replacementExts[i]; + const rc = stat(filename); + if (rc === 0) { + return [rc, filename]; + } + } + } + } + return [stat(p), p]; +} +function tryReplacementExtensions(p, isMain) { + const lastDotIndex = p.lastIndexOf('.'); + if(lastDotIndex >= 0) { + const ext = p.slice(lastDotIndex); + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { + const pathnameWithoutExtension = p.slice(0, lastDotIndex); + const replacementExts = + ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx + : ext === '.mjs' ? replacementsForMjs + : replacementsForCjs; + for (let i = 0; i < replacementExts.length; i++) { + const filename = tryFile(pathnameWithoutExtension + replacementExts[i], isMain); + if (filename) { + return filename; + } + } + } + } + return false; +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + +function trySelf(parentPath, request) { + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = '.'; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions).resolved, parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const { 1: name, 2: expansion = '' } = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + // if (pkg?.exports != null) { + if (pkg != null && pkg.exports != null) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions).resolved, null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } + } +} + +// Backwards compat for old node versions +const hasModulePathCache = !!require('module')._pathCache; +const Module_pathCache = Object.create(null); +const Module_pathCache_get = hasModulePathCache ? (cacheKey) => Module._pathCache[cacheKey] : (cacheKey) => Module_pathCache[cacheKey]; +const Module_pathCache_set = hasModulePathCache ? (cacheKey, value) => (Module._pathCache[cacheKey] = value) : (cacheKey) => (Module_pathCache[cacheKey] = value); + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +const Module_findPath = function _findPath(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00'); + const entry = Module_pathCache_get(cacheKey); + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) + return exportsResolved; + } + + const _basePath = path.resolve(curPath, request); + let filename; + + const [rc, basePath] = statReplacementExtensions(_basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module_pathCache_set(cacheKey, filename); + return filename; + } + } + + return false; +}; + +const Module_resolveFilename = function _resolveFilename(request, parent, isMain, options) { + if (StringPrototypeStartsWith(request, 'node:') || + NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = StringPrototypeStartsWith(request, './') || + StringPrototypeStartsWith(request, '../') || + ((isWindows && StringPrototypeStartsWith(request, '.\\')) || + StringPrototypeStartsWith(request, '..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) + ArrayPrototypePush(paths, lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + // if (parent?.filename) { + // node 12 hack + if (parent != null && parent.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + + // if (pkg.data?.imports != null) { + // node 12 hack + if (pkg.data != null && pkg.data.imports != null) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } + } + } + + // Try module self resolution first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = moduleParentCache.get(cursor)) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + + ArrayPrototypeJoin(requireStack, '\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + +function finalizeEsmResolution(resolved, parentPath, pkgPath) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + const actual = tryReplacementExtensions(filename) || tryFile(filename); + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} + + +return { + Module_findPath, + Module_resolveFilename +} + +} + +/** + * copied from Module._extensions['.js'] + * https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L1113-L1120 + * @param {import('../src/index').Service} service + * @param {NodeJS.Module} module + * @param {string} filename + */ +function assertScriptCanLoadAsCJSImpl(service, module, filename) { + const pkg = readPackageScope(filename); + + // ts-node modification: allow our configuration to override + const tsNodeClassification = service.moduleTypeClassifier.classifyModuleByModuleTypeOverrides(normalizeSlashes(filename)); + if(tsNodeClassification.moduleType === 'cjs') return; + + // ignore package.json when file extension is ESM-only or CJS-only + // [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] + const lastDotIndex = filename.lastIndexOf('.'); + const ext = lastDotIndex >= 0 ? filename.slice(lastDotIndex) : ''; + + if((ext === '.cts' || ext === '.cjs') && tsNodeClassification.moduleType === 'auto') return; + + // Function require shouldn't be used in ES modules. + if (ext === '.mts' || ext === '.mjs' || tsNodeClassification.moduleType === 'esm' || (pkg && pkg.data && pkg.data.type === 'module')) { + const parentPath = module.parent && module.parent.filename; + const packageJsonPath = pkg ? path.resolve(pkg.path, 'package.json') : null; + throw createErrRequireEsm(filename, parentPath, packageJsonPath); + } +} + + +module.exports = { + createCjsLoader, + assertScriptCanLoadAsCJSImpl, + readPackageScope +}; diff --git a/dist-raw/node-esm-default-get-format.js b/dist-raw/node-internal-modules-esm-get_format.js similarity index 71% rename from dist-raw/node-esm-default-get-format.js rename to dist-raw/node-internal-modules-esm-get_format.js index 65b0bf6d..1f3586ad 100644 --- a/dist-raw/node-esm-default-get-format.js +++ b/dist-raw/node-internal-modules-esm-get_format.js @@ -1,7 +1,4 @@ // Copied from https://raw.githubusercontent.com/nodejs/node/v15.3.0/lib/internal/modules/esm/get_format.js -// Then modified to suite our needs. -// Formatting is intentionally bad to keep the diff as small as possible, to make it easier to merge -// upstream changes and understand our modifications. 'use strict'; const { @@ -16,12 +13,9 @@ const experimentalJsonModules = nodeMajor > 17 || (nodeMajor === 17 && nodeMinor >= 5) || getOptionValue('--experimental-json-modules'); -const experimentalSpeciferResolution = - getOptionValue('--experimental-specifier-resolution'); const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); -const { getPackageType } = require('./node-esm-resolve-implementation.js').createResolve({tsExtensions: [], jsExtensions: []}); const { URL, fileURLToPath } = require('url'); -const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-errors').codes; +const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-internal-errors').codes; const extensionFormatMap = { '__proto__': null, @@ -45,6 +39,24 @@ if (experimentalWasmModules) if (experimentalJsonModules) extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; +/** + * + * @param {'node' | 'explicit'} [tsNodeExperimentalSpecifierResolution] + * @param {ReturnType< + * typeof import('../dist-raw/node-internal-modules-esm-resolve').createResolve + * >} nodeEsmResolver + */ +function createGetFormat(tsNodeExperimentalSpecifierResolution, nodeEsmResolver) { +// const experimentalSpeciferResolution = tsNodeExperimentalSpecifierResolution ?? getOptionValue('--experimental-specifier-resolution'); +let experimentalSpeciferResolution = tsNodeExperimentalSpecifierResolution != null ? tsNodeExperimentalSpecifierResolution : getOptionValue('--experimental-specifier-resolution'); +const { getPackageType } = nodeEsmResolver; + +/** + * @param {string} url + * @param {{}} context + * @param {any} defaultGetFormatUnused + * @returns {ReturnType} + */ function defaultGetFormat(url, context, defaultGetFormatUnused) { if (StringPrototypeStartsWith(url, 'node:')) { return { format: 'builtin' }; @@ -84,4 +96,10 @@ function defaultGetFormat(url, context, defaultGetFormatUnused) { } return { format: null }; } -exports.defaultGetFormat = defaultGetFormat; + +return {defaultGetFormat}; +} + +module.exports = { + createGetFormat +}; diff --git a/dist-raw/node-esm-resolve-implementation.js b/dist-raw/node-internal-modules-esm-resolve.js similarity index 89% rename from dist-raw/node-esm-resolve-implementation.js rename to dist-raw/node-internal-modules-esm-resolve.js index 04ea8466..2fbd832e 100644 --- a/dist-raw/node-esm-resolve-implementation.js +++ b/dist-raw/node-internal-modules-esm-resolve.js @@ -1,27 +1,13 @@ // Copied from https://raw.githubusercontent.com/nodejs/node/v15.3.0/lib/internal/modules/esm/resolve.js -// Then modified to suite our needs. -// Formatting is intentionally bad to keep the diff as small as possible, to make it easier to merge -// upstream changes and understand our modifications. -// -// Github diff to easily view the changes: -// https://github.com/TypeStrong/ts-node/compare/esm-resolver-diff..main + 'use strict'; -const [nodeMajor, nodeMinor, nodePatch] = process.versions.node.split('.').map(s => parseInt(s, 10)) +const {versionGteLt} = require('../dist/util'); + // Test for node >14.13.1 || (>=12.20.0 && <13) -const builtinModuleProtocol = nodeMajor > 14 || ( - nodeMajor === 14 && ( - nodeMinor > 13 || ( - nodeMinor === 13 && nodePatch > 0 - ) - ) - ) || ( - nodeMajor === 12 && ( - nodeMinor > 20 || ( - nodeMinor === 20 - ) - ) - ) +const builtinModuleProtocol = + versionGteLt(process.versions.node, '14.13.1') || + versionGteLt(process.versions.node, '12.20.0', '13.0.0') ? 'node:' : 'nodejs:'; @@ -34,11 +20,9 @@ const { ObjectFreeze, ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, - // RegExp, RegExpPrototypeTest, SafeMap, SafeSet, - // String, StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeLastIndexOf, @@ -50,13 +34,8 @@ const { } = require('./node-primordials'); // const internalFS = require('internal/fs/utils'); -// const { NativeModule } = require('internal/bootstrap/loaders'); -const Module = require('module') -const NativeModule = { - canBeRequiredByUsers(specifier) { - return Module.builtinModules.includes(specifier) - } -} +const Module = require('module'); +const { NativeModule } = require('./node-nativemodule'); const { realpathSync, statSync, @@ -89,22 +68,33 @@ const { ERR_UNSUPPORTED_DIR_IMPORT, ERR_UNSUPPORTED_ESM_URL_SCHEME, // } = require('internal/errors').codes; -} = require('./node-errors').codes; +} = require('./node-internal-errors').codes; // const { Module: CJSModule } = require('internal/modules/cjs/loader'); const CJSModule = Module; // const packageJsonReader = require('internal/modules/package_json_reader'); -const packageJsonReader = require('./node-package-json-reader'); +const packageJsonReader = require('./node-internal-modules-package_json_reader'); const userConditions = getOptionValue('--conditions'); const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); const pendingDeprecation = getOptionValue('--pending-deprecation'); +/** + * @param {{ + * extensions: import('../src/file-extensions').Extensions, + * preferTsExts: boolean | undefined; + * tsNodeExperimentalSpecifierResolution: import('../src/index').ExperimentalSpecifierResolution | undefined; + * }} opts + */ function createResolve(opts) { // TODO receive cached fs implementations here -const {tsExtensions, jsExtensions, preferTsExts} = opts; +const {preferTsExts, tsNodeExperimentalSpecifierResolution, extensions} = opts; +const esrnExtensions = extensions.experimentalSpecifierResolutionAddsIfOmitted; +const {legacyMainResolveAddsIfOmitted, replacementsForCjs, replacementsForJs, replacementsForMjs, replacementsForJsx} = extensions; +// const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution ?? getOptionValue('--experimental-specifier-resolution'); +const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution != null ? tsNodeExperimentalSpecifierResolution : getOptionValue('--experimental-specifier-resolution'); const emittedPackageWarnings = new SafeSet(); function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) { @@ -150,11 +140,20 @@ function getConditionsSet(conditions) { const realpathCache = new SafeMap(); const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ -function tryStatSync(path) { +const statSupportsThrowIfNoEntry = versionGteLt(process.versions.node, '15.3.0') || + versionGteLt(process.versions.node, '14.17.0', '15.0.0'); +const tryStatSync = statSupportsThrowIfNoEntry ? tryStatSyncWithoutErrors : tryStatSyncWithErrors; +const statsIfNotFound = new Stats(); +function tryStatSyncWithoutErrors(path) { + const stats = statSync(path, { throwIfNoEntry: false }); + if(stats != null) return stats; + return statsIfNotFound; +} +function tryStatSyncWithErrors(path) { try { return statSync(path); } catch { - return new Stats(); + return statsIfNotFound; } } @@ -257,89 +256,73 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) { let guess; if (packageConfig.main !== undefined) { // Note: fs check redundances will be handled by Descriptor cache here. - if (fileExists(guess = new URL(`./${packageConfig.main}`, - packageJSONUrl))) { + if(guess = resolveReplacementExtensions(new URL(`./${packageConfig.main}`, packageJSONUrl))) { return guess; } - if (fileExists(guess = new URL(`./${packageConfig.main}.js`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.json`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.node`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + if (fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl))) { return guess; } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, - packageJSONUrl))) { - return guess; + for(const extension of legacyMainResolveAddsIfOmitted) { + if (fileExists(guess = new URL(`./${packageConfig.main}${extension}`, + packageJSONUrl))) { + return guess; + } } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, - packageJSONUrl))) { - return guess; + for(const extension of legacyMainResolveAddsIfOmitted) { + if (fileExists(guess = new URL(`./${packageConfig.main}/index${extension}`, + packageJSONUrl))) { + return guess; + } } // Fallthrough. } - if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { - return guess; - } - // So fs. - if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { - return guess; + for(const extension of legacyMainResolveAddsIfOmitted) { + if (fileExists(guess = new URL(`./index${extension}`, packageJSONUrl))) { + return guess; + } } // Not found. throw new ERR_MODULE_NOT_FOUND( fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); } +/** attempts replacement extensions, then tries exact name, then attempts appending extensions */ function resolveExtensionsWithTryExactName(search) { - if (fileExists(search)) return search; const resolvedReplacementExtension = resolveReplacementExtensions(search); if(resolvedReplacementExtension) return resolvedReplacementExtension; + if (fileExists(search)) return search; return resolveExtensions(search); } -const extensions = Array.from(new Set([ - ...(preferTsExts ? tsExtensions : []), - '.js', - ...jsExtensions, - '.json', '.node', '.mjs', - ...tsExtensions -])); - +// This appends missing extensions function resolveExtensions(search) { - for (let i = 0; i < extensions.length; i++) { - const extension = extensions[i]; + for (let i = 0; i < esrnExtensions.length; i++) { + const extension = esrnExtensions[i]; const guess = new URL(`${search.pathname}${extension}`, search); if (fileExists(guess)) return guess; } return undefined; } -/** - * TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions. - * IMPORTANT: preserve ordering according to preferTsExts; this affects resolution behavior! - */ -const replacementExtensions = extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)); - +/** This replaces JS with TS extensions */ function resolveReplacementExtensions(search) { - if (search.pathname.match(/\.js$/)) { - const pathnameWithoutExtension = search.pathname.slice(0, search.pathname.length - 3); - for (let i = 0; i < replacementExtensions.length; i++) { - const extension = replacementExtensions[i]; + const lastDotIndex = search.pathname.lastIndexOf('.'); + if(lastDotIndex >= 0) { + const ext = search.pathname.slice(lastDotIndex); + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { + const pathnameWithoutExtension = search.pathname.slice(0, lastDotIndex); + const replacementExts = + ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx + : ext === '.mjs' ? replacementsForMjs + : replacementsForCjs; const guess = new URL(search.toString()); - guess.pathname = `${pathnameWithoutExtension}${extension}`; - if (fileExists(guess)) return guess; + for (let i = 0; i < replacementExts.length; i++) { + const extension = replacementExts[i]; + guess.pathname = `${pathnameWithoutExtension}${extension}`; + if (fileExists(guess)) return guess; + } } } return undefined; @@ -356,7 +339,7 @@ function finalizeResolution(resolved, base) { resolved.pathname, 'must not include encoded "/" or "\\" characters', fileURLToPath(base)); - if (getOptionValue('--experimental-specifier-resolution') === 'node') { + if (experimentalSpecifierResolution === 'node') { const path = fileURLToPath(resolved); let file = resolveExtensionsWithTryExactName(resolved); if (file !== undefined) return file; @@ -569,7 +552,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { * @param {object} packageConfig * @param {string} base * @param {Set} conditions - * @returns {URL} + * @returns {{resolved: URL, exact: boolean}} */ function packageExportsResolve( packageJSONUrl, packageSubpath, packageConfig, base, conditions) { diff --git a/dist-raw/node-package-json-reader.js b/dist-raw/node-internal-modules-package_json_reader.js similarity index 90% rename from dist-raw/node-package-json-reader.js rename to dist-raw/node-internal-modules-package_json_reader.js index e9f82c6f..9266bc1c 100644 --- a/dist-raw/node-package-json-reader.js +++ b/dist-raw/node-internal-modules-package_json_reader.js @@ -2,7 +2,7 @@ 'use strict'; const { SafeMap } = require('./node-primordials'); -const { internalModuleReadJSON } = require('./node-internal-fs'); +const { internalModuleReadJSON } = require('./node-internalBinding-fs'); const { pathToFileURL } = require('url'); const { toNamespacedPath } = require('path'); // const { getOptionValue } = require('./node-options'); @@ -13,7 +13,7 @@ let manifest; /** * @param {string} jsonPath - * @return {[string, boolean]} + * @return {{string: string, containsKeys: boolean}} */ function read(jsonPath) { if (cache.has(jsonPath)) { diff --git a/dist-raw/node-repl-await.js b/dist-raw/node-internal-repl-await.js similarity index 100% rename from dist-raw/node-repl-await.js rename to dist-raw/node-internal-repl-await.js diff --git a/dist-raw/node-internalBinding-fs.js b/dist-raw/node-internalBinding-fs.js new file mode 100644 index 00000000..e85bc825 --- /dev/null +++ b/dist-raw/node-internalBinding-fs.js @@ -0,0 +1,58 @@ +const fs = require('fs'); +const {versionGteLt} = require('../dist/util'); + +// In node's core, this is implemented in C +// https://github.com/nodejs/node/blob/v15.3.0/src/node_file.cc#L891-L985 +/** + * @param {string} path + * @returns {[] | [string, boolean]} + */ +function internalModuleReadJSON(path) { + let string + try { + string = fs.readFileSync(path, 'utf8') + } catch (e) { + if (e.code === 'ENOENT') return [] + throw e + } + // Node's implementation checks for the presence of relevant keys: main, name, type, exports, imports + // Node does this for performance to skip unnecessary parsing. + // This would slow us down and, based on our usage, we can skip it. + const containsKeys = true + return [string, containsKeys] +} + +// In node's core, this is implemented in C +// https://github.com/nodejs/node/blob/63e7dc1e5c71b70c80ed9eda230991edb00811e2/src/node_file.cc#L987-L1005 +/** + * @param {string} path + * @returns {number} 0 = file, 1 = dir, negative = error + */ +function internalModuleStat(path) { + const stat = fs.statSync(path, { throwIfNoEntry: false }); + if(!stat) return -1; + if(stat.isFile()) return 0; + if(stat.isDirectory()) return 1; +} + +/** + * @param {string} path + * @returns {number} 0 = file, 1 = dir, negative = error + */ +function internalModuleStatInefficient(path) { + try { + const stat = fs.statSync(path); + if(stat.isFile()) return 0; + if(stat.isDirectory()) return 1; + } catch(e) { + return -e.errno || -1; + } +} + +const statSupportsThrowIfNoEntry = versionGteLt(process.versions.node, '15.3.0') || + versionGteLt(process.versions.node, '14.17.0', '15.0.0'); + +module.exports = { + internalModuleReadJSON, + internalModuleStat: statSupportsThrowIfNoEntry ? internalModuleStat : internalModuleStatInefficient +}; diff --git a/dist-raw/node-nativemodule.js b/dist-raw/node-nativemodule.js new file mode 100644 index 00000000..ea54fcd4 --- /dev/null +++ b/dist-raw/node-nativemodule.js @@ -0,0 +1,9 @@ + +// Node imports this from 'internal/bootstrap/loaders' +const Module = require('module'); +const NativeModule = { + canBeRequiredByUsers(specifier) { + return Module.builtinModules.includes(specifier) + } +}; +exports.NativeModule = NativeModule; diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index 21d8cfd1..a7c1574a 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -20,6 +20,7 @@ module.exports = { RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest), SafeMap: Map, SafeSet: Set, + SafeWeakMap: WeakMap, StringPrototypeEndsWith: (str, ...rest) => String.prototype.endsWith.apply(str, rest), StringPrototypeIncludes: (str, ...rest) => String.prototype.includes.apply(str, rest), StringPrototypeLastIndexOf: (str, ...rest) => String.prototype.lastIndexOf.apply(str, rest), @@ -30,5 +31,7 @@ module.exports = { StringPrototypeSplit: (str, ...rest) => String.prototype.split.apply(str, rest), StringPrototypeStartsWith: (str, ...rest) => String.prototype.startsWith.apply(str, rest), StringPrototypeSubstr: (str, ...rest) => String.prototype.substr.apply(str, rest), + StringPrototypeCharCodeAt: (str, ...rest) => String.prototype.charCodeAt.apply(str, rest), + StringPrototypeMatch: (str, ...rest) => String.prototype.match.apply(str, rest), SyntaxError: SyntaxError }; diff --git a/dprint.json b/dprint.json new file mode 100644 index 00000000..3fcf217d --- /dev/null +++ b/dprint.json @@ -0,0 +1,37 @@ +{ + "incremental": true, + "lineWidth": 80, + "prettier": { + "associations": [ + "**/*.{ts,tsx,js,mjs,cjs,jsx,json,md}" + ], + "singleQuote": true + }, + // Run `dprint output-file-paths` to see which files are being matched + "includes": [ + "/*.{js,mjs}", + "/esm/**", + "/register/**", + "/scripts/**", + "/src/**", + "/tests/**", + "/website/**" + ], + "excludes": [ + "/website/.docusaurus", + "/website/build", + "/website/docs", + "/website/readme-sources", + "/website/static", + "tests/main-realpath/symlink/tsconfig.json", + "tests/throw error.ts", + "tests/throw error react tsx.tsx", + "tests/esm/throw error.ts", + "tests/legacy-source-map-support-interop/index.ts", + "tests/main-realpath/symlink/symlink.tsx", + "tests/tmp" + ], + "plugins": [ + "https://plugins.dprint.dev/prettier-0.6.2.exe-plugin@36dd4f8b9710ab323c471017ecd00a20cf1dca2728c12242c7dabb8dfacad0e2" + ] +} diff --git a/justfile b/justfile new file mode 100644 index 00000000..e6b8b1a9 --- /dev/null +++ b/justfile @@ -0,0 +1,66 @@ +# I like using `just` instead of `npm run` or `yarn`. +# If you're like me, this justfile is for you. +# If you are not, you can safely ignore this file. + +set positional-arguments +export PATH := justfile_directory() + "/node_modules/.bin:" + env_var('PATH') + +default: test-local + +regenerate: + #!/usr/bin/env sh + node -e ' + const fs = require("fs"); + let acc = fs.readFileSync("justfile", "utf8").replace(/(# CUT\n)[\s\S]+/, "$1\n"); + for(const [key, value] of Object.entries(require("./package.json").scripts)) { + acc += `${key} *ARGS:\n ${value.replace(/npm run /g, "just ").replace(/ --$/, "")} "$@"\n`; + } + fs.writeFileSync("justfile", acc); + ' + +install: + npm install + +# EVERYTHING BELOW THIS LINE IS AUTO-GENERATED FROM PACKAGE.JSON +# DO NOT MODIFY BY HAND + +# CUT + +lint *ARGS: + dprint check "$@" +lint-fix *ARGS: + dprint fmt "$@" +clean *ARGS: + rimraf temp dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz tests/tmp "$@" +rebuild *ARGS: + just clean && just build "$@" +build *ARGS: + just build-nopack && just build-pack "$@" +build-nopack *ARGS: + just build-tsc && just build-configSchema "$@" +build-tsc *ARGS: + tsc -b ./tsconfig.build-dist.json "$@" +build-configSchema *ARGS: + typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.build-schema.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema "$@" +build-pack *ARGS: + node ./scripts/build-pack.js "$@" +test-spec *ARGS: + ava "$@" +test-cov *ARGS: + nyc ava "$@" +test *ARGS: + just build && just lint && just test-cov "$@" +test-local *ARGS: + just lint-fix && just build-tsc && just build-pack && just test-spec "$@" +pre-debug *ARGS: + just build-tsc && just build-pack "$@" +coverage-report *ARGS: + nyc report --reporter=lcov "$@" +prepare *ARGS: + just clean && just build-nopack "$@" +api-extractor *ARGS: + api-extractor run --local --verbose "$@" +esm-usage-example *ARGS: + just build-tsc && cd esm-usage-example && node --experimental-specifier-resolution node --loader ../esm.mjs ./index "$@" +esm-usage-example2 *ARGS: + just build-tsc && cd tests && TS_NODE_PROJECT=./module-types/override-to-cjs/tsconfig.json node --loader ../esm.mjs ./module-types/override-to-cjs/test.cjs "$@" diff --git a/nyc.config.js b/nyc.config.js index 3f15fe94..5b03f070 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -2,6 +2,9 @@ module.exports = { all: true, include: ['tests/node_modules/ts-node/**'], exclude: ['**/*.d.ts', 'tests/node_modules/ts-node/node_modules/**'], + // Very important that nyc does not add additional `require.extensions` hooks. + // It affects module resolution behavior under test + extension: ['.js'], excludeNodeModules: false, excludeAfterRemap: false, }; diff --git a/package-lock.json b/package-lock.json index 19319b31..825e70c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.7.0", + "version": "10.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -314,14 +314,15 @@ "@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true }, "@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.0.tgz", + "integrity": "sha512-pOQRG+w/T1KogjiuO4uqqa+dw/IIb8kDY0ctYfiJstWv7TOTmtuAkx8ZB4YgauDNn2huHR33oruOgi45VcatOg==", "requires": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/trace-mapping": "0.3.9" } }, "@istanbuljs/load-nyc-config": { @@ -401,6 +402,25 @@ "chalk": "^4.0.0" } }, + "@jridgewell/resolve-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz", + "integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@microsoft/api-extractor": { "version": "7.19.4", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.4.tgz", @@ -438,6 +458,12 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true } } }, @@ -763,12 +789,6 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, - "@types/chai": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.10.tgz", - "integrity": "sha512-Ejh1AXTY8lm+x91X/yar3G2z4x9RyKwdTVdyyu7Xj3dNB35fMNCnEWqTO9FgS3zjzlRNqk1MruYhgb8yhRN9rA==", - "dev": true - }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -858,6 +878,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, "@types/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -874,10 +900,15 @@ "dev": true }, "@types/react": { - "version": "16.0.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.31.tgz", - "integrity": "sha512-ft7OuDGUo39e+9LGwUewf2RyEaNBOjWbHUmD5bzjNuSuDabccE/1IuO7iR0dkzLjVUKxTMq69E+FmKfbgBcfbQ==", - "dev": true + "version": "16.14.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.26.tgz", + "integrity": "sha512-c/5CYyciOO4XdFcNhZW1O2woVx86k4T+DO2RorHZL7EhitkNQgSD/SgpdZJAUJa/qjVgOmTM44gHkAdZSXeQuQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } }, "@types/retry": { "version": "0.12.1", @@ -895,6 +926,12 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@types/semver": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.1.0.tgz", @@ -1092,12 +1129,6 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, - "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "dev": true - }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1561,20 +1592,6 @@ "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=", "dev": true }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", - "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" - } - }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -1611,12 +1628,6 @@ } } }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "chokidar": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", @@ -1913,6 +1924,12 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "dev": true + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1946,15 +1963,6 @@ "mimic-response": "^1.0.0" } }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2059,6 +2067,12 @@ "is-obj": "^2.0.0" } }, + "dprint": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/dprint/-/dprint-0.25.0.tgz", + "integrity": "sha512-ISJu3EOYz9W24jMkBTotxdsSg4bvYhndaF+fvI9FzXjoT24WARCchcWf95IwCHMcHZVDd6gNoPfgGqzCpuT8Zg==", + "dev": true + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -2342,12 +2356,6 @@ "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "get-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", @@ -3722,12 +3730,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -3810,12 +3812,6 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, - "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "dev": true - }, "pretty-format": { "version": "27.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.2.tgz", @@ -4610,6 +4606,17 @@ "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.0", "yn": "3.1.1" + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + } } }, "tslib": { @@ -4618,12 +4625,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "type-detect": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", - "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", - "dev": true - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -4669,9 +4670,9 @@ } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true }, "typescript-json-schema": { @@ -4708,6 +4709,12 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true } } }, @@ -4817,9 +4824,9 @@ "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==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, "validate-npm-package-license": { "version": "3.0.4", diff --git a/package.json b/package.json index 1f158f1f..2eeb8f6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.7.0", + "version": "10.8.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { @@ -46,6 +46,7 @@ "/transpilers/", "/dist/", "!/dist/test", + "/dist-raw/NODE-LICENSE.md", "/dist-raw/**.js", "/register/", "/esm/", @@ -60,13 +61,13 @@ "/node16/" ], "scripts": { - "lint": "prettier --check .", - "lint-fix": "prettier --write .", - "clean": "rimraf dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz", + "lint": "dprint check", + "lint-fix": "dprint fmt", + "clean": "rimraf temp dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz tests/node_modules tests/tmp", "rebuild": "npm run clean && npm run build", "build": "npm run build-nopack && npm run build-pack", "build-nopack": "npm run build-tsc && npm run build-configSchema", - "build-tsc": "tsc", + "build-tsc": "tsc -b ./tsconfig.build-dist.json", "build-configSchema": "typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.build-schema.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema", "build-pack": "node ./scripts/build-pack.js", "test-spec": "ava", @@ -113,25 +114,23 @@ "@microsoft/api-extractor": "^7.19.4", "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", - "@types/chai": "^4.0.4", "@types/diff": "^4.0.2", "@types/lodash": "^4.14.151", "@types/node": "13.13.5", "@types/proper-lockfile": "^4.1.2", "@types/proxyquire": "^1.3.28", - "@types/react": "^16.0.2", + "@types/react": "^16.14.19", "@types/rimraf": "^3.0.0", "@types/semver": "^7.1.0", "@yarnpkg/fslib": "^2.4.0", "ava": "^3.15.0", "axios": "^0.21.1", - "chai": "^4.0.1", + "dprint": "^0.25.0", "expect": "^27.0.2", "get-stream": "^6.0.0", "lodash": "^4.17.15", "ntypescript": "^1.201507091536.1", "nyc": "^15.0.1", - "prettier": "^2.5.1", "proper-lockfile": "^4.1.2", "proxyquire": "^2.0.0", "react": "^16.14.0", @@ -139,7 +138,7 @@ "semver": "^7.1.3", "throat": "^6.0.1", "typedoc": "^0.22.10", - "typescript": "4.5.5", + "typescript": "4.6.4", "typescript-json-schema": "^0.53.0", "util.promisify": "^1.0.1" }, @@ -158,7 +157,7 @@ } }, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -169,14 +168,14 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "prettier": { "singleQuote": true }, "volta": { - "node": "17.5.0", + "node": "18.1.0", "npm": "6.14.15" } } diff --git a/raw/README.md b/raw/README.md new file mode 100644 index 00000000..154e9bf5 --- /dev/null +++ b/raw/README.md @@ -0,0 +1 @@ +See the README.md in `dist-raw` diff --git a/raw/download-and-compare.sh b/raw/download-and-compare.sh new file mode 100755 index 00000000..fdfc9c28 --- /dev/null +++ b/raw/download-and-compare.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s inherit_errexit +__dirname="$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$__dirname" + +# This script serves as helpful documentation for where these files came from. + +# TODO augment this script to update esm-resolver-diff branch +# https://github.com/TypeStrong/ts-node/compare/esm-resolver-diff..main + +function download() { + echo "// Copied from https://github.com/nodejs/node/blob/$version/$path$ext"$'\n' > "$local-$version$ext" + curl "https://raw.githubusercontent.com/nodejs/node/$version/$path$ext" >> "$local-$version$ext" +} +compare() { + diff "$local-$version$ext" "../dist-raw/$local$ext" || true +} +assertStrippedIsOnlyDeletions() { + [ "$( diff "$1.js" "$1-stripped.js" | grep -E '^>' )" = '' ] +} + +ext=.js + +#### + +path=lib/internal/modules/cjs/loader +local=node-internal-modules-cjs-loader +version=v17.0.1 +download +# compare + +version=v15.3.0 +download +# compare + +version=2d5d77306f6dff9110c1f77fefab25f973415770 +download +# compare + +#### + +path=lib/internal/modules/cjs/helpers +local=node-internal-modules-cjs-helpers +version=v17.0.1 +download +# compare + +#### + +path=lib/internal/modules/esm/resolve +local=node-internal-modules-esm-resolve +version=v13.12.0 +download +# compare + +version=v15.3.0 +download +# compare + +#### + +path=lib/internal/modules/esm/get_format +local=node-internal-modules-esm-get_format +version=v15.3.0 +download +# compare + +#### + +path=lib/internal/repl/await +local=node-internal-repl-await +version=v17.0.0 +download +# compare + +version=88799930794045795e8abac874730f9eba7e2300 +download +# compare + +#### + +path=lib/internal/modules/package_json_reader +local=node-internal-modules-package_json_reader +version=v15.3.0 +download +# compare + +#### + +path=lib/internal/errors +local=node-internal-errors +version=2d5d77306f6dff9110c1f77fefab25f973415770 +download +# compare + +version=b533fb3508009e5f567cc776daba8fbf665386a6 +download +# compare + +#### + +# Verify that -stripped.js files have only deletions, no other changes +set -x + +assertStrippedIsOnlyDeletions node-internal-modules-cjs-loader-v15.3.0 +assertStrippedIsOnlyDeletions node-internal-modules-cjs-loader-v17.0.1 +assertStrippedIsOnlyDeletions node-internal-modules-cjs-helpers-v17.0.1 +assertStrippedIsOnlyDeletions node-internal-modules-esm-resolve-v15.3.0 +assertStrippedIsOnlyDeletions node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770 +assertStrippedIsOnlyDeletions node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6 diff --git a/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770-stripped.js b/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770-stripped.js new file mode 100644 index 00000000..d15b8483 --- /dev/null +++ b/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770-stripped.js @@ -0,0 +1,99 @@ +// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js + +'use strict'; + +const codes = {}; + +function makeNodeErrorWithCode(Base, key) { + return class NodeError extends Base { + constructor(...args) { + if (excludedStackFn === undefined) { + super(); + } else { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(); + // Reset the limit and setting the name property. + Error.stackTraceLimit = limit; + } + const message = getMessage(key, args, this); + ObjectDefineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, super.name, key); + this.code = key; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + }; +} + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + if (def === SystemError) { + def = makeSystemErrorWithCode(sym); + } else { + def = makeNodeErrorWithCode(def, sym); + } + + if (otherClasses.length !== 0) { + otherClasses.forEach((clazz) => { + def[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + codes[sym] = def; +} + +module.exports = { + addCodeToName, // Exported for NghttpError + codes, + dnsException, + errnoException, + exceptionWithHostPort, + getMessage, + hideStackFrames, + isStackOverflowError, + connResetException, + uvErrmapGet, + uvException, + uvExceptionWithHostPort, + SystemError, + // This is exported only to facilitate testing. + E, + kNoOverride, + prepareStackTrace, + maybeOverridePrepareStackTrace, + overrideStackTrace, + kEnhanceStackBeforeInspector, + fatalExceptionStackEnhancers +}; + +E('ERR_REQUIRE_ESM', + (filename, parentPath = null, packageJsonPath = null) => { + let msg = `Must use import to load ES Module: ${filename}`; + if (parentPath && packageJsonPath) { + const path = require('path'); + const basename = path.basename(filename) === path.basename(parentPath) ? + filename : path.basename(filename); + msg += + '\nrequire() of ES modules is not supported.\nrequire() of ' + + `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + + 'is an ES module file as it is a .js file whose nearest parent ' + + 'package.json contains "type": "module" which defines all .js ' + + 'files in that package scope as ES modules.\nInstead rename ' + + `${basename} to end in .cjs, change the requiring code to use ` + + 'import(), or remove "type": "module" from ' + + `${packageJsonPath}.\n`; + return msg; + } + return msg; + }, Error); diff --git a/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770.js b/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770.js new file mode 100644 index 00000000..15eca36b --- /dev/null +++ b/raw/node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770.js @@ -0,0 +1,1454 @@ +// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js + +/* eslint node-core/documented-errors: "error" */ +/* eslint node-core/alphabetize-errors: "error" */ +/* eslint node-core/prefer-util-format-errors: "error" */ + +'use strict'; + +// The whole point behind this internal module is to allow Node.js to no +// longer be forced to treat every error message change as a semver-major +// change. The NodeError classes here all expose a `code` property whose +// value statically and permanently identifies the error. While the error +// message may change, the code should not. + +const { + ArrayIsArray, + Error, + JSONStringify, + Map, + MathAbs, + NumberIsInteger, + ObjectDefineProperty, + ObjectKeys, + StringPrototypeSlice, + StringPrototypeStartsWith, + Symbol, + SymbolFor, + WeakMap, +} = primordials; + +const sep = process.platform === 'win32' ? '\\' : '/'; + +const messages = new Map(); +const codes = {}; + +const classRegExp = /^([A-Z][a-z0-9]*)+$/; +// Sorted by a rough estimate on most frequently used entries. +const kTypes = [ + 'string', + 'function', + 'number', + 'object', + // Accept 'Function' and 'Object' as alternative to the lower cased version. + 'Function', + 'Object', + 'boolean', + 'bigint', + 'symbol' +]; + +const { kMaxLength } = internalBinding('buffer'); + +const MainContextError = Error; +const ErrorToString = Error.prototype.toString; +const overrideStackTrace = new WeakMap(); +const kNoOverride = Symbol('kNoOverride'); +const prepareStackTrace = (globalThis, error, trace) => { + // API for node internals to override error stack formatting + // without interfering with userland code. + if (overrideStackTrace.has(error)) { + const f = overrideStackTrace.get(error); + overrideStackTrace.delete(error); + return f(error, trace); + } + + const globalOverride = + maybeOverridePrepareStackTrace(globalThis, error, trace); + if (globalOverride !== kNoOverride) return globalOverride; + + // Normal error formatting: + // + // Error: Message + // at function (file) + // at file + const errorString = ErrorToString.call(error); + if (trace.length === 0) { + return errorString; + } + return `${errorString}\n at ${trace.join('\n at ')}`; +}; + +const maybeOverridePrepareStackTrace = (globalThis, error, trace) => { + // Polyfill of V8's Error.prepareStackTrace API. + // https://crbug.com/v8/7848 + // `globalThis` is the global that contains the constructor which + // created `error`. + if (typeof globalThis.Error.prepareStackTrace === 'function') { + return globalThis.Error.prepareStackTrace(error, trace); + } + // We still have legacy usage that depends on the main context's `Error` + // being used, even when the error is from a different context. + // TODO(devsnek): evaluate if this can be eventually deprecated/removed. + if (typeof MainContextError.prepareStackTrace === 'function') { + return MainContextError.prepareStackTrace(error, trace); + } + + return kNoOverride; +}; + +let excludedStackFn; + +// Lazily loaded +let util; +let assert; + +let internalUtil = null; +function lazyInternalUtil() { + if (!internalUtil) { + internalUtil = require('internal/util'); + } + return internalUtil; +} + +let internalUtilInspect = null; +function lazyInternalUtilInspect() { + if (!internalUtilInspect) { + internalUtilInspect = require('internal/util/inspect'); + } + return internalUtilInspect; +} + +let buffer; +function lazyBuffer() { + if (buffer === undefined) + buffer = require('buffer').Buffer; + return buffer; +} + +// A specialized Error that includes an additional info property with +// additional information about the error condition. +// It has the properties present in a UVException but with a custom error +// message followed by the uv error code and uv error message. +// It also has its own error code with the original uv error context put into +// `err.info`. +// The context passed into this error must have .code, .syscall and .message, +// and may have .path and .dest. +class SystemError extends Error { + constructor(key, context) { + if (excludedStackFn === undefined) { + super(); + } else { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(); + // Reset the limit and setting the name property. + Error.stackTraceLimit = limit; + } + const prefix = getMessage(key, [], this); + let message = `${prefix}: ${context.syscall} returned ` + + `${context.code} (${context.message})`; + + if (context.path !== undefined) + message += ` ${context.path}`; + if (context.dest !== undefined) + message += ` => ${context.dest}`; + + ObjectDefineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, 'SystemError', key); + + this.code = key; + + ObjectDefineProperty(this, 'info', { + value: context, + enumerable: true, + configurable: true, + writable: false + }); + + ObjectDefineProperty(this, 'errno', { + get() { + return context.errno; + }, + set: (value) => { + context.errno = value; + }, + enumerable: true, + configurable: true + }); + + ObjectDefineProperty(this, 'syscall', { + get() { + return context.syscall; + }, + set: (value) => { + context.syscall = value; + }, + enumerable: true, + configurable: true + }); + + if (context.path !== undefined) { + // TODO(BridgeAR): Investigate why and when the `.toString()` was + // introduced. The `path` and `dest` properties in the context seem to + // always be of type string. We should probably just remove the + // `.toString()` and `Buffer.from()` operations and set the value on the + // context as the user did. + ObjectDefineProperty(this, 'path', { + get() { + return context.path != null ? + context.path.toString() : context.path; + }, + set: (value) => { + context.path = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true + }); + } + + if (context.dest !== undefined) { + ObjectDefineProperty(this, 'dest', { + get() { + return context.dest != null ? + context.dest.toString() : context.dest; + }, + set: (value) => { + context.dest = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true + }); + } + } + + toString() { + return `${this.name} [${this.code}]: ${this.message}`; + } + + [SymbolFor('nodejs.util.inspect.custom')](recurseTimes, ctx) { + return lazyInternalUtilInspect().inspect(this, { + ...ctx, + getters: true, + customInspect: false + }); + } +} + +function makeSystemErrorWithCode(key) { + return class NodeError extends SystemError { + constructor(ctx) { + super(key, ctx); + } + }; +} + +function makeNodeErrorWithCode(Base, key) { + return class NodeError extends Base { + constructor(...args) { + if (excludedStackFn === undefined) { + super(); + } else { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(); + // Reset the limit and setting the name property. + Error.stackTraceLimit = limit; + } + const message = getMessage(key, args, this); + ObjectDefineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, super.name, key); + this.code = key; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + }; +} + +// This function removes unnecessary frames from Node.js core errors. +function hideStackFrames(fn) { + return function hidden(...args) { + // Make sure the most outer `hideStackFrames()` function is used. + let setStackFn = false; + if (excludedStackFn === undefined) { + excludedStackFn = hidden; + setStackFn = true; + } + try { + return fn(...args); + } finally { + if (setStackFn === true) { + excludedStackFn = undefined; + } + } + }; +} + +function addCodeToName(err, name, code) { + // Set the stack + if (excludedStackFn !== undefined) { + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(err, excludedStackFn); + } + // Add the error code to the name to include it in the stack trace. + err.name = `${name} [${code}]`; + // Access the stack to generate the error message including the error code + // from the name. + err.stack; + // Reset the name to the actual name. + if (name === 'SystemError') { + ObjectDefineProperty(err, 'name', { + value: name, + enumerable: false, + writable: true, + configurable: true + }); + } else { + delete err.name; + } +} + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + if (def === SystemError) { + def = makeSystemErrorWithCode(sym); + } else { + def = makeNodeErrorWithCode(def, sym); + } + + if (otherClasses.length !== 0) { + otherClasses.forEach((clazz) => { + def[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + codes[sym] = def; +} + +function getMessage(key, args, self) { + const msg = messages.get(key); + + if (assert === undefined) assert = require('internal/assert'); + + if (typeof msg === 'function') { + assert( + msg.length <= args.length, // Default options do not count. + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${msg.length}).` + ); + return msg.apply(self, args); + } + + const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length; + assert( + expectedLength === args.length, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).` + ); + if (args.length === 0) + return msg; + + args.unshift(msg); + return lazyInternalUtilInspect().format.apply(null, args); +} + +let uvBinding; + +function lazyUv() { + if (!uvBinding) { + uvBinding = internalBinding('uv'); + } + return uvBinding; +} + +const uvUnmappedError = ['UNKNOWN', 'unknown error']; + +function uvErrmapGet(name) { + uvBinding = lazyUv(); + if (!uvBinding.errmap) { + uvBinding.errmap = uvBinding.getErrorMap(); + } + return uvBinding.errmap.get(name); +} + + +/** + * This creates an error compatible with errors produced in the C++ + * function UVException using a context object with data assembled in C++. + * The goal is to migrate them to ERR_* errors later when compatibility is + * not a concern. + * + * @param {Object} ctx + * @returns {Error} + */ +function uvException(ctx) { + const [ code, uvmsg ] = uvErrmapGet(ctx.errno) || uvUnmappedError; + let message = `${code}: ${ctx.message || uvmsg}, ${ctx.syscall}`; + + let path; + let dest; + if (ctx.path) { + path = ctx.path.toString(); + message += ` '${path}'`; + } + if (ctx.dest) { + dest = ctx.dest.toString(); + message += ` -> '${dest}'`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // Pass the message to the constructor instead of setting it on the object + // to make sure it is the same as the one created in C++ + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + Error.stackTraceLimit = tmpLimit; + + for (const prop of ObjectKeys(ctx)) { + if (prop === 'message' || prop === 'path' || prop === 'dest') { + continue; + } + err[prop] = ctx[prop]; + } + + err.code = code; + if (path) { + err.path = path; + } + if (dest) { + err.dest = dest; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(err, excludedStackFn || uvException); + return err; +} + +/** + * This creates an error compatible with errors produced in the C++ + * This function should replace the deprecated + * `exceptionWithHostPort()` function. + * + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + * @returns {Error} + */ +function uvExceptionWithHostPort(err, syscall, address, port) { + const [ code, uvmsg ] = uvErrmapGet(err) || uvUnmappedError; + const message = `${syscall} ${code}: ${uvmsg}`; + let details = ''; + + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(`${message}${details}`); + Error.stackTraceLimit = tmpLimit; + ex.code = code; + ex.errno = err; + ex.syscall = syscall; + ex.address = address; + if (port) { + ex.port = port; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || uvExceptionWithHostPort); + return ex; +} + +/** + * This used to be util._errnoException(). + * + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} [original] + * @returns {Error} + */ +function errnoException(err, syscall, original) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + if (util === undefined) util = require('util'); + const code = util.getSystemErrorName(err); + const message = original ? + `${syscall} ${code} ${original}` : `${syscall} ${code}`; + + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(message); + ex.errno = err; + ex.code = code; + ex.syscall = syscall; + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || errnoException); + return ex; +} + +/** + * Deprecated, new function is `uvExceptionWithHostPort()` + * New function added the error description directly + * from C++. this method for backwards compatibility + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + * @param {string} [additional] + * @returns {Error} + */ +function exceptionWithHostPort(err, syscall, address, port, additional) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + if (util === undefined) util = require('util'); + const code = util.getSystemErrorName(err); + let details = ''; + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + if (additional) { + details += ` - Local (${additional})`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(`${syscall} ${code}${details}`); + Error.stackTraceLimit = tmpLimit; + ex.errno = err; + ex.code = code; + ex.syscall = syscall; + ex.address = address; + if (port) { + ex.port = port; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || exceptionWithHostPort); + return ex; +} + +/** + * @param {number|string} code - A libuv error number or a c-ares error code + * @param {string} syscall + * @param {string} [hostname] + * @returns {Error} + */ +function dnsException(code, syscall, hostname) { + let errno; + // If `code` is of type number, it is a libuv error number, else it is a + // c-ares error code. + // TODO(joyeecheung): translate c-ares error codes into numeric ones and + // make them available in a property that's not error.errno (since they + // can be in conflict with libuv error codes). Also make sure + // util.getSystemErrorName() can understand them when an being informed that + // the number is a c-ares error code. + if (typeof code === 'number') { + errno = code; + // ENOTFOUND is not a proper POSIX error, but this error has been in place + // long enough that it's not practical to remove it. + if (code === lazyUv().UV_EAI_NODATA || code === lazyUv().UV_EAI_NONAME) { + code = 'ENOTFOUND'; // Fabricated error name. + } else { + code = lazyInternalUtil().getSystemErrorName(code); + } + } + const message = `${syscall} ${code}${hostname ? ` ${hostname}` : ''}`; + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(message); + Error.stackTraceLimit = tmpLimit; + ex.errno = errno; + ex.code = code; + ex.syscall = syscall; + if (hostname) { + ex.hostname = hostname; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || dnsException); + return ex; +} + +function connResetException(msg) { + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(msg); + ex.code = 'ECONNRESET'; + return ex; +} + +let maxStack_ErrorName; +let maxStack_ErrorMessage; +/** + * Returns true if `err.name` and `err.message` are equal to engine-specific + * values indicating max call stack size has been exceeded. + * "Maximum call stack size exceeded" in V8. + * + * @param {Error} err + * @returns {boolean} + */ +function isStackOverflowError(err) { + if (maxStack_ErrorMessage === undefined) { + try { + function overflowStack() { overflowStack(); } + overflowStack(); + } catch (err) { + maxStack_ErrorMessage = err.message; + maxStack_ErrorName = err.name; + } + } + + return err && err.name === maxStack_ErrorName && + err.message === maxStack_ErrorMessage; +} + +// Only use this for integers! Decimal numbers do not work with this function. +function addNumericalSeparator(val) { + let res = ''; + let i = val.length; + const start = val[0] === '-' ? 1 : 0; + for (; i >= start + 4; i -= 3) { + res = `_${val.slice(i - 3, i)}${res}`; + } + return `${val.slice(0, i)}${res}`; +} + +// Used to enhance the stack that will be picked up by the inspector +const kEnhanceStackBeforeInspector = Symbol('kEnhanceStackBeforeInspector'); + +// These are supposed to be called only on fatal exceptions before +// the process exits. +const fatalExceptionStackEnhancers = { + beforeInspector(error) { + if (typeof error[kEnhanceStackBeforeInspector] !== 'function') { + return error.stack; + } + + try { + // Set the error.stack here so it gets picked up by the + // inspector. + error.stack = error[kEnhanceStackBeforeInspector](); + } catch { + // We are just enhancing the error. If it fails, ignore it. + } + return error.stack; + }, + afterInspector(error) { + const originalStack = error.stack; + const { + inspect, + inspectDefaultOptions: { + colors: defaultColors + } + } = lazyInternalUtilInspect(); + const colors = (internalBinding('util').guessHandleType(2) === 'TTY' && + require('internal/tty').hasColors()) || + defaultColors; + try { + return inspect(error, { colors }); + } catch { + return originalStack; + } + } +}; + +module.exports = { + addCodeToName, // Exported for NghttpError + codes, + dnsException, + errnoException, + exceptionWithHostPort, + getMessage, + hideStackFrames, + isStackOverflowError, + connResetException, + uvErrmapGet, + uvException, + uvExceptionWithHostPort, + SystemError, + // This is exported only to facilitate testing. + E, + kNoOverride, + prepareStackTrace, + maybeOverridePrepareStackTrace, + overrideStackTrace, + kEnhanceStackBeforeInspector, + fatalExceptionStackEnhancers +}; + +// To declare an error message, use the E(sym, val, def) function above. The sym +// must be an upper case string. The val can be either a function or a string. +// The def must be an error class. +// The return value of the function must be a string. +// Examples: +// E('EXAMPLE_KEY1', 'This is the error value', Error); +// E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`, RangeError); +// +// Once an error code has been assigned, the code itself MUST NOT change and +// any given error code must never be reused to identify a different error. +// +// Any error code added here should also be added to the documentation +// +// Note: Please try to keep these in alphabetical order +// +// Note: Node.js specific errors must begin with the prefix ERR_ +E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError); +E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError); +E('ERR_ASSERTION', '%s', Error); +E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError); +E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError); +E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError); +E('ERR_BUFFER_OUT_OF_BOUNDS', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name = undefined) => { + if (name) { + return `"${name}" is outside of buffer bounds`; + } + return 'Attempt to access memory outside buffer bounds'; + }, RangeError); +E('ERR_BUFFER_TOO_LARGE', + `Cannot create a Buffer larger than 0x${kMaxLength.toString(16)} bytes`, + RangeError); +E('ERR_CANNOT_WATCH_SIGINT', 'Cannot watch for SIGINT signals', Error); +E('ERR_CHILD_CLOSED_BEFORE_REPLY', + 'Child closed before reply received', Error); +E('ERR_CHILD_PROCESS_IPC_REQUIRED', + "Forked processes must have an IPC channel, missing value 'ipc' in %s", + Error); +E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', + RangeError); +E('ERR_CONSOLE_WRITABLE_STREAM', + 'Console expects a writable stream instance for %s', TypeError); +E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error); +E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s', Error); +E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', + 'Custom engines not supported by this OpenSSL', Error); +E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s', TypeError); +E('ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + 'Public key is not valid for specified curve', Error); +E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found', Error); +E('ERR_CRYPTO_FIPS_FORCED', + 'Cannot set FIPS mode, it was forced with --force-fips at startup.', Error); +E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.', + Error); +E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error); +E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', + Error); +E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); +E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + 'Invalid key object type %s, expected %s.', TypeError); +E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); +E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); +E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error); +E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); +// Switch to TypeError. The current implementation does not seem right. +E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); +E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', + 'Input buffers must have the same byte length', RangeError); +E('ERR_DIR_CLOSED', 'Directory handle was closed', Error); +E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]', + Error); +E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE', + 'A callback was registered through ' + + 'process.setUncaughtExceptionCaptureCallback(), which is mutually ' + + 'exclusive with using the `domain` module', + Error); +E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE', + 'The `domain` module is in use, which is mutually exclusive with calling ' + + 'process.setUncaughtExceptionCaptureCallback()', + Error); +E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { + this.errno = ret; + return `The encoded data was not valid for encoding ${encoding}`; +}, TypeError); +E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', + RangeError); +E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); +E('ERR_FALSY_VALUE_REJECTION', function(reason) { + this.reason = reason; + return 'Promise was rejected with falsy value'; +}, Error); +E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM', + 'The feature %s is unavailable on the current platform' + + ', which is being used to run Node.js', + TypeError); +E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GB', RangeError); +E('ERR_FS_INVALID_SYMLINK_TYPE', + 'Symlink type must be one of "dir", "file", or "junction". Received "%s"', + Error); // Switch to TypeError. The current implementation does not seem right +E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN', + 'HTTP/2 ALTSVC frames require a valid origin', TypeError); +E('ERR_HTTP2_ALTSVC_LENGTH', + 'HTTP/2 ALTSVC frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_CONNECT_AUTHORITY', + ':authority header is required for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_PATH', + 'The :path header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_SCHEME', + 'The :scheme header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_GOAWAY_SESSION', + 'New streams cannot be created after receiving a GOAWAY', Error); +E('ERR_HTTP2_HEADERS_AFTER_RESPOND', + 'Cannot specify additional headers after response initiated', Error); +E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.', Error); +E('ERR_HTTP2_HEADER_SINGLE_VALUE', + 'Header field "%s" must only have a single value', TypeError); +E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + 'Informational status codes cannot be used', RangeError); +E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', + 'HTTP/1 Connection specific headers are forbidden: "%s"', TypeError); +E('ERR_HTTP2_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError); +E('ERR_HTTP2_INVALID_INFO_STATUS', + 'Invalid informational status code: %s', RangeError); +E('ERR_HTTP2_INVALID_ORIGIN', + 'HTTP/2 ORIGIN frames require a valid origin', TypeError); +E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + 'Packed settings length must be a multiple of six', RangeError); +E('ERR_HTTP2_INVALID_PSEUDOHEADER', + '"%s" is an invalid pseudoheader or is used incorrectly', TypeError); +E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed', Error); +E('ERR_HTTP2_INVALID_SETTING_VALUE', + // Using default arguments here is important so the arguments are not counted + // towards `Function#length`. + function(name, actual, min = undefined, max = undefined) { + this.actual = actual; + if (min !== undefined) { + this.min = min; + this.max = max; + } + return `Invalid value for setting "${name}": ${actual}`; + }, TypeError, RangeError); +E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed', Error); +E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + 'Maximum number of pending settings acknowledgements', Error); +E('ERR_HTTP2_NESTED_PUSH', + 'A push stream cannot initiate another push stream.', Error); +E('ERR_HTTP2_NO_SOCKET_MANIPULATION', + 'HTTP/2 sockets should not be directly manipulated (e.g. read and written)', + Error); +E('ERR_HTTP2_ORIGIN_LENGTH', + 'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_OUT_OF_STREAMS', + 'No stream ID is available because maximum stream ID has been reached', + Error); +E('ERR_HTTP2_PAYLOAD_FORBIDDEN', + 'Responses with %s status must not have a payload', Error); +E('ERR_HTTP2_PING_CANCEL', 'HTTP2 ping cancelled', Error); +E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes', RangeError); +E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', + 'Cannot set HTTP/2 pseudo-headers', TypeError); +E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams', Error); +E('ERR_HTTP2_SEND_FILE', 'Directories cannot be sent', Error); +E('ERR_HTTP2_SEND_FILE_NOSEEK', + 'Offset or length can only be specified for regular files', Error); +E('ERR_HTTP2_SESSION_ERROR', 'Session closed with error code %s', Error); +E('ERR_HTTP2_SETTINGS_CANCEL', 'HTTP2 session settings canceled', Error); +E('ERR_HTTP2_SOCKET_BOUND', + 'The socket is already bound to an Http2Session', Error); +E('ERR_HTTP2_SOCKET_UNBOUND', + 'The socket has been disconnected from the Http2Session', Error); +E('ERR_HTTP2_STATUS_101', + 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2', Error); +E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s', RangeError); +E('ERR_HTTP2_STREAM_CANCEL', function(error) { + let msg = 'The pending stream has been canceled'; + if (error) { + this.cause = error; + if (typeof error.message === 'string') + msg += ` (caused by: ${error.message})`; + } + return msg; +}, Error); +E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error); +E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', + 'A stream cannot depend on itself', Error); +E('ERR_HTTP2_TRAILERS_ALREADY_SENT', + 'Trailing headers have already been sent', Error); +E('ERR_HTTP2_TRAILERS_NOT_READY', + 'Trailing headers cannot be sent until after the wantTrailers event is ' + + 'emitted', Error); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error); +E('ERR_HTTP_HEADERS_SENT', + 'Cannot %s headers after they are sent to the client', Error); +E('ERR_HTTP_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError); +E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError); +E('ERR_HTTP_TRAILER_INVALID', + 'Trailers are invalid with this transfer encoding', Error); +E('ERR_INCOMPATIBLE_OPTION_PAIR', + 'Option "%s" cannot be used in combination with option "%s"', TypeError); +E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' + + 'input via --eval, --print, or STDIN', Error); +E('ERR_INSPECTOR_ALREADY_CONNECTED', '%s is already connected', Error); +E('ERR_INSPECTOR_CLOSED', 'Session was closed', Error); +E('ERR_INSPECTOR_COMMAND', 'Inspector error %d: %s', Error); +E('ERR_INSPECTOR_NOT_ACTIVE', 'Inspector is not active', Error); +E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available', Error); +E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected', Error); +E('ERR_INSPECTOR_NOT_WORKER', 'Current thread is not a worker', Error); +E('ERR_INTERNAL_ASSERTION', (message) => { + const suffix = 'This is caused by either a bug in Node.js ' + + 'or incorrect usage of Node.js internals.\n' + + 'Please open an issue with this stack trace at ' + + 'https://github.com/nodejs/node/issues\n'; + return message === undefined ? suffix : `${message}\n${suffix}`; +}, Error); +E('ERR_INVALID_ADDRESS_FAMILY', function(addressType, host, port) { + this.host = host; + this.port = port; + return `Invalid address family: ${addressType} ${host}:${port}`; +}, RangeError); +E('ERR_INVALID_ARG_TYPE', + (name, expected, actual) => { + assert(typeof name === 'string', "'name' must be a string"); + if (!ArrayIsArray(expected)) { + expected = [expected]; + } + + let msg = 'The '; + if (name.endsWith(' argument')) { + // For cases like 'first argument' + msg += `${name} `; + } else { + const type = name.includes('.') ? 'property' : 'argument'; + msg += `"${name}" ${type} `; + } + msg += 'must be '; + + const types = []; + const instances = []; + const other = []; + + for (const value of expected) { + assert(typeof value === 'string', + 'All expected entries have to be of type string'); + if (kTypes.includes(value)) { + types.push(value.toLowerCase()); + } else if (classRegExp.test(value)) { + instances.push(value); + } else { + assert(value !== 'object', + 'The value "object" should be written as "Object"'); + other.push(value); + } + } + + // Special handle `object` in case other instances are allowed to outline + // the differences between each other. + if (instances.length > 0) { + const pos = types.indexOf('object'); + if (pos !== -1) { + types.splice(pos, 1); + instances.push('Object'); + } + } + + if (types.length > 0) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(', ')}, or ${last}`; + } else if (types.length === 2) { + msg += `one of type ${types[0]} or ${types[1]}`; + } else { + msg += `of type ${types[0]}`; + } + if (instances.length > 0 || other.length > 0) + msg += ' or '; + } + + if (instances.length > 0) { + if (instances.length > 2) { + const last = instances.pop(); + msg += `an instance of ${instances.join(', ')}, or ${last}`; + } else { + msg += `an instance of ${instances[0]}`; + if (instances.length === 2) { + msg += ` or ${instances[1]}`; + } + } + if (other.length > 0) + msg += ' or '; + } + + if (other.length > 0) { + if (other.length > 2) { + const last = other.pop(); + msg += `one of ${other.join(', ')}, or ${last}`; + } else if (other.length === 2) { + msg += `one of ${other[0]} or ${other[1]}`; + } else { + if (other[0].toLowerCase() !== other[0]) + msg += 'an '; + msg += `${other[0]}`; + } + } + + if (actual == null) { + msg += `. Received ${actual}`; + } else if (typeof actual === 'function' && actual.name) { + msg += `. Received function ${actual.name}`; + } else if (typeof actual === 'object') { + if (actual.constructor && actual.constructor.name) { + msg += `. Received an instance of ${actual.constructor.name}`; + } else { + const inspected = lazyInternalUtilInspect() + .inspect(actual, { depth: -1 }); + msg += `. Received ${inspected}`; + } + } else { + let inspected = lazyInternalUtilInspect() + .inspect(actual, { colors: false }); + if (inspected.length > 25) + inspected = `${inspected.slice(0, 25)}...`; + msg += `. Received type ${typeof actual} (${inspected})`; + } + return msg; + }, TypeError); +E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => { + let inspected = lazyInternalUtilInspect().inspect(value); + if (inspected.length > 128) { + inspected = `${inspected.slice(0, 128)}...`; + } + return `The argument '${name}' ${reason}. Received ${inspected}`; +}, TypeError, RangeError); +E('ERR_INVALID_ASYNC_ID', 'Invalid %s value: %s', RangeError); +E('ERR_INVALID_BUFFER_SIZE', + 'Buffer size must be a multiple of %s', RangeError); +E('ERR_INVALID_CALLBACK', + 'Callback must be a function. Received %O', TypeError); +E('ERR_INVALID_CHAR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name, field = undefined) => { + let msg = `Invalid character in ${name}`; + if (field !== undefined) { + msg += ` ["${field}"]`; + } + return msg; + }, TypeError); +E('ERR_INVALID_CURSOR_POS', + 'Cannot set cursor row without setting its column', TypeError); +E('ERR_INVALID_FD', + '"fd" must be a positive integer: %s', RangeError); +E('ERR_INVALID_FD_TYPE', 'Unsupported fd type: %s', TypeError); +E('ERR_INVALID_FILE_URL_HOST', + 'File URL host must be "localhost" or empty on %s', TypeError); +E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError); +E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); +E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError); +E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); +E('ERR_INVALID_MODULE_SPECIFIER', (pkgPath, subpath, base = undefined) => { + if (subpath === undefined) { + return `Invalid package name '${pkgPath}' imported from ${base}`; + } else if (base === undefined) { + assert(subpath !== '.'); + return `Package subpath '${subpath}' is not a valid module request for ` + + `the "exports" resolution of ${pkgPath}${sep}package.json`; + } else { + return `Package subpath '${subpath}' is not a valid module request for ` + + `the "exports" resolution of ${pkgPath} imported from ${base}`; + } +}, TypeError); +E('ERR_INVALID_OPT_VALUE', (name, value) => + `The value "${String(value)}" is invalid for option "${name}"`, + TypeError, + RangeError); +E('ERR_INVALID_OPT_VALUE_ENCODING', + 'The value "%s" is invalid for option "encoding"', TypeError); +E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => { + if (hasMessage) + return `Invalid package config ${path}${sep}package.json, ${message}`; + else + return `Invalid JSON in ${path} imported from ${message}`; +}, Error); +E('ERR_INVALID_PACKAGE_TARGET', + (pkgPath, key, subpath, target, base = undefined) => { + const relError = typeof target === 'string' && + target.length && !StringPrototypeStartsWith(target, './'); + if (key === null) { + if (subpath !== '') { + return `Invalid "exports" target ${JSONStringify(target)} defined ` + + `for '${subpath}' in the package config ${pkgPath} imported from ` + + `${base}.${relError ? '; targets must start with "./"' : ''}`; + } else { + return `Invalid "exports" main target ${target} defined in the ` + + `package config ${pkgPath} imported from ${base}${relError ? + '; targets must start with "./"' : ''}`; + } + } else if (key === '.') { + return `Invalid "exports" main target ${JSONStringify(target)} defined ` + + `in the package config ${pkgPath}${sep}package.json${relError ? + '; targets must start with "./"' : ''}`; + } else if (relError) { + return `Invalid "exports" target ${JSONStringify(target)} defined for '${ + StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + + `package config ${pkgPath}${sep}package.json; ` + + 'targets must start with "./"'; + } else { + return `Invalid "exports" target ${JSONStringify(target)} defined for '${ + StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + + `package config ${pkgPath}${sep}package.json`; + } + }, Error); +E('ERR_INVALID_PERFORMANCE_MARK', + 'The "%s" performance mark has not been set', Error); +E('ERR_INVALID_PROTOCOL', + 'Protocol "%s" not supported. Expected "%s"', + TypeError); +E('ERR_INVALID_REPL_EVAL_CONFIG', + 'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError); +E('ERR_INVALID_REPL_INPUT', '%s', TypeError); +E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => { + return `Expected a valid ${input} to be returned for the "${prop}" from the` + + ` "${name}" function but got ${value}.`; +}, TypeError); +E('ERR_INVALID_RETURN_PROPERTY_VALUE', (input, name, prop, value) => { + let type; + if (value && value.constructor && value.constructor.name) { + type = `instance of ${value.constructor.name}`; + } else { + type = `type ${typeof value}`; + } + return `Expected ${input} to be returned for the "${prop}" from the` + + ` "${name}" function but got ${type}.`; +}, TypeError); +E('ERR_INVALID_RETURN_VALUE', (input, name, value) => { + let type; + if (value && value.constructor && value.constructor.name) { + type = `instance of ${value.constructor.name}`; + } else { + type = `type ${typeof value}`; + } + return `Expected ${input} to be returned from the "${name}"` + + ` function but got ${type}.`; +}, TypeError); +E('ERR_INVALID_SYNC_FORK_INPUT', + 'Asynchronous forks do not support ' + + 'Buffer, TypedArray, DataView or string input: %s', + TypeError); +E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError); +E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError); +E('ERR_INVALID_URI', 'URI malformed', URIError); +E('ERR_INVALID_URL', function(input) { + this.input = input; + return `Invalid URL: ${input}`; +}, TypeError); +E('ERR_INVALID_URL_SCHEME', + (expected) => { + if (typeof expected === 'string') + expected = [expected]; + assert(expected.length <= 2); + const res = expected.length === 2 ? + `one of scheme ${expected[0]} or ${expected[1]}` : + `of scheme ${expected[0]}`; + return `The URL must be ${res}`; + }, TypeError); +E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); +E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); +E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); +E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error); +E('ERR_MANIFEST_ASSERT_INTEGRITY', + (moduleURL, realIntegrities) => { + let msg = `The content of "${ + moduleURL + }" does not match the expected integrity.`; + if (realIntegrities.size) { + const sri = [...realIntegrities.entries()].map(([alg, dgs]) => { + return `${alg}-${dgs}`; + }).join(' '); + msg += ` Integrities found are: ${sri}`; + } else { + msg += ' The resource was not found in the policy.'; + } + return msg; + }, Error); +E('ERR_MANIFEST_DEPENDENCY_MISSING', + 'Manifest resource %s does not list %s as a dependency specifier', + Error); +E('ERR_MANIFEST_INTEGRITY_MISMATCH', + 'Manifest resource %s has multiple entries but integrity lists do not match', + SyntaxError); +E('ERR_MANIFEST_INVALID_RESOURCE_FIELD', + 'Manifest resource %s has invalid property value for %s', + TypeError); +E('ERR_MANIFEST_TDZ', 'Manifest initialization has not yet run', Error); +E('ERR_MANIFEST_UNKNOWN_ONERROR', + 'Manifest specified unknown error behavior "%s".', + SyntaxError); +E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error); +E('ERR_MISSING_ARGS', + (...args) => { + assert(args.length > 0, 'At least one arg needs to be specified'); + let msg = 'The '; + const len = args.length; + args = args.map((a) => `"${a}"`); + switch (len) { + case 1: + msg += `${args[0]} argument`; + break; + case 2: + msg += `${args[0]} and ${args[1]} arguments`; + break; + default: + msg += args.slice(0, len - 1).join(', '); + msg += `, and ${args[len - 1]} arguments`; + break; + } + return `${msg} must be specified`; + }, TypeError); +E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', + 'The ES Module loader may not return a format of \'dynamic\' when no ' + + 'dynamicInstantiate function was provided', Error); +E('ERR_MISSING_OPTION', '%s is required', TypeError); +E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => { + return `Cannot find ${type} '${path}' imported from ${base}`; +}, Error); +E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); +E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); +E('ERR_NAPI_INVALID_DATAVIEW_ARGS', + 'byte_offset + byte_length should be less than or equal to the size in ' + + 'bytes of the array passed in', + RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT', + 'start offset of %s should be a multiple of %s', RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_LENGTH', + 'Invalid typed array length', RangeError); +E('ERR_NO_CRYPTO', + 'Node.js is not compiled with OpenSSL crypto support', Error); +E('ERR_NO_ICU', + '%s is not supported on Node.js compiled without ICU', TypeError); +E('ERR_OUT_OF_RANGE', + (str, range, input, replaceDefaultBoolean = false) => { + assert(range, 'Missing "range" argument'); + let msg = replaceDefaultBoolean ? str : + `The value of "${str}" is out of range.`; + let received; + if (NumberIsInteger(input) && MathAbs(input) > 2 ** 32) { + received = addNumericalSeparator(String(input)); + } else if (typeof input === 'bigint') { + received = String(input); + if (input > 2n ** 32n || input < -(2n ** 32n)) { + received = addNumericalSeparator(received); + } + received += 'n'; + } else { + received = lazyInternalUtilInspect().inspect(input); + } + msg += ` It must be ${range}. Received ${received}`; + return msg; + }, RangeError); +E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => { + if (subpath === '.') { + return `No "exports" main resolved in ${pkgPath}${sep}package.json`; + } else if (base === undefined) { + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath}${sep}package.json`; + } else { + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath} imported from ${base}`; + } +}, Error); +E('ERR_REQUIRE_ESM', + (filename, parentPath = null, packageJsonPath = null) => { + let msg = `Must use import to load ES Module: ${filename}`; + if (parentPath && packageJsonPath) { + const path = require('path'); + const basename = path.basename(filename) === path.basename(parentPath) ? + filename : path.basename(filename); + msg += + '\nrequire() of ES modules is not supported.\nrequire() of ' + + `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + + 'is an ES module file as it is a .js file whose nearest parent ' + + 'package.json contains "type": "module" which defines all .js ' + + 'files in that package scope as ES modules.\nInstead rename ' + + `${basename} to end in .cjs, change the requiring code to use ` + + 'import(), or remove "type": "module" from ' + + `${packageJsonPath}.\n`; + return msg; + } + return msg; + }, Error); +E('ERR_SCRIPT_EXECUTION_INTERRUPTED', + 'Script execution was interrupted by `SIGINT`', Error); +E('ERR_SERVER_ALREADY_LISTEN', + 'Listen method has been called more than once without closing.', Error); +E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error); +E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error); +E('ERR_SOCKET_BAD_BUFFER_SIZE', + 'Buffer size must be a positive integer', TypeError); +E('ERR_SOCKET_BAD_PORT', (name, port, allowZero = true) => { + assert(typeof allowZero === 'boolean', + "The 'allowZero' argument must be of type boolean."); + const operator = allowZero ? '>=' : '>'; + return `${name} should be ${operator} 0 and < 65536. Received ${port}.`; +}, RangeError); +E('ERR_SOCKET_BAD_TYPE', + 'Bad socket type specified. Valid types are: udp4, udp6', TypeError); +E('ERR_SOCKET_BUFFER_SIZE', + 'Could not get or set buffer size', + SystemError); +E('ERR_SOCKET_CLOSED', 'Socket is closed', Error); +E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error); +E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error); +E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error); +E('ERR_SRI_PARSE', + 'Subresource Integrity string %j had an unexpected %j at position %d', + SyntaxError); +E('ERR_STREAM_ALREADY_FINISHED', + 'Cannot call %s after a stream was finished', + Error); +E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error); +E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error); +E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); +E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error); +E('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF', Error); +E('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', + 'stream.unshift() after end event', Error); +E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error); +E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error); +E('ERR_SYNTHETIC', 'JavaScript Callstack', Error); +E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError); +E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) { + this.reason = reason; + this.host = host; + this.cert = cert; + return `Hostname/IP does not match certificate's altnames: ${reason}`; +}, Error); +E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error); +E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error); +E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError), +E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established', + Error), +E('ERR_TLS_INVALID_PROTOCOL_VERSION', + '%j is not a valid %s TLS protocol version', TypeError); +E('ERR_TLS_PROTOCOL_VERSION_CONFLICT', + 'TLS protocol version %j conflicts with secureProtocol %j', TypeError); +E('ERR_TLS_RENEGOTIATION_DISABLED', + 'TLS session renegotiation disabled for this socket', Error); + +// This should probably be a `TypeError`. +E('ERR_TLS_REQUIRED_SERVER_NAME', + '"servername" is required parameter for Server.addContext', Error); +E('ERR_TLS_SESSION_ATTACK', 'TLS session renegotiation attack detected', Error); +E('ERR_TLS_SNI_FROM_SERVER', + 'Cannot issue SNI from a TLS server-side socket', Error); +E('ERR_TRACE_EVENTS_CATEGORY_REQUIRED', + 'At least one category is required', TypeError); +E('ERR_TRACE_EVENTS_UNAVAILABLE', 'Trace events are unavailable', Error); + +// This should probably be a `RangeError`. +E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError); +E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' + + 'handler', Error); +E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + '`process.setupUncaughtExceptionCapture()` was called while a capture ' + + 'callback was already active', + Error); +E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters', TypeError); +E('ERR_UNHANDLED_ERROR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (err = undefined) => { + const msg = 'Unhandled error.'; + if (err === undefined) return msg; + return `${msg} (${err})`; + }, Error); +E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error); +E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); +E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); +E('ERR_UNKNOWN_FILE_EXTENSION', + 'Unknown file extension "%s" for %s', + TypeError); +E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); +E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); +E('ERR_UNSUPPORTED_ESM_URL_SCHEME', 'Only file and data URLs are supported ' + + 'by the default ESM loader', Error); + +E('ERR_V8BREAKITERATOR', + 'Full ICU data not installed. See https://github.com/nodejs/node/wiki/Intl', + Error); + +// This should probably be a `TypeError`. +E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', + 'At least one valid performance entry type is required', Error); +E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', + 'A dynamic import callback was not specified.', TypeError); +E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error); +E('ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA', + 'Cached data cannot be created for a module which has been evaluated', Error); +E('ERR_VM_MODULE_DIFFERENT_CONTEXT', + 'Linked modules must use the same context', Error); +E('ERR_VM_MODULE_LINKING_ERRORED', + 'Linking has already failed for the provided module', Error); +E('ERR_VM_MODULE_NOT_MODULE', + 'Provided module is not an instance of Module', Error); +E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); +E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error); +E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); +E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') => + `Initiated Worker with ${msg}: ${errors.join(', ')}`, + Error); +E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error); +E('ERR_WORKER_OUT_OF_MEMORY', + 'Worker terminated due to reaching memory limit: %s', Error); +E('ERR_WORKER_PATH', (filename) => + 'The worker script or module filename must be an absolute path or a ' + + 'relative path starting with \'./\' or \'../\'.' + + (filename.startsWith('file://') ? + ' Wrap file:// URLs with `new URL`.' : '' + ) + + ` Received "${filename}"`, + TypeError); +E('ERR_WORKER_UNSERIALIZABLE_ERROR', + 'Serializing an uncaught exception failed', Error); +E('ERR_WORKER_UNSUPPORTED_EXTENSION', + 'The worker script extension must be ".js", ".mjs", or ".cjs". Received "%s"', + TypeError); +E('ERR_WORKER_UNSUPPORTED_OPERATION', + '%s is not supported in workers', TypeError); +E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); diff --git a/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6-stripped.js b/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6-stripped.js new file mode 100644 index 00000000..101c0b51 --- /dev/null +++ b/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6-stripped.js @@ -0,0 +1,44 @@ +// Copied from https://github.com/nodejs/node/blob/b533fb3508009e5f567cc776daba8fbf665386a6/lib/internal/errors.js + +'use strict'; + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + if (def === SystemError) { + def = makeSystemErrorWithCode(sym); + } else { + def = makeNodeErrorWithCode(def, sym); + } + + if (otherClasses.length !== 0) { + otherClasses.forEach((clazz) => { + def[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + codes[sym] = def; +} + +E('ERR_REQUIRE_ESM', + (filename, parentPath = null, packageJsonPath = null) => { + let msg = `Must use import to load ES Module: ${filename}`; + if (parentPath && packageJsonPath) { + const path = require('path'); + const basename = path.basename(filename) === path.basename(parentPath) ? + filename : path.basename(filename); + msg += + '\nrequire() of ES modules is not supported.\nrequire() of ' + + `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + + 'is an ES module file as it is a .js file whose nearest parent ' + + 'package.json contains "type": "module" which defines all .js ' + + 'files in that package scope as ES modules.\nInstead rename ' + + `${basename} to end in .cjs, change the requiring code to use ` + + 'import(), or remove "type": "module" from ' + + `${packageJsonPath}.\n`; + return msg; + } + return msg; + }, Error); diff --git a/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6.js b/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6.js new file mode 100644 index 00000000..c6b0e382 --- /dev/null +++ b/raw/node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6.js @@ -0,0 +1,1454 @@ +// Copied from https://github.com/nodejs/node/blob/b533fb3508009e5f567cc776daba8fbf665386a6/lib/internal/errors.js + +/* eslint node-core/documented-errors: "error" */ +/* eslint node-core/alphabetize-errors: "error" */ +/* eslint node-core/prefer-util-format-errors: "error" */ + +'use strict'; + +// The whole point behind this internal module is to allow Node.js to no +// longer be forced to treat every error message change as a semver-major +// change. The NodeError classes here all expose a `code` property whose +// value statically and permanently identifies the error. While the error +// message may change, the code should not. + +const { + ArrayIsArray, + Error, + JSONStringify, + Map, + MathAbs, + NumberIsInteger, + ObjectDefineProperty, + ObjectKeys, + StringPrototypeSlice, + StringPrototypeStartsWith, + Symbol, + SymbolFor, + WeakMap, +} = primordials; + +const sep = process.platform === 'win32' ? '\\' : '/'; + +const messages = new Map(); +const codes = {}; + +const classRegExp = /^([A-Z][a-z0-9]*)+$/; +// Sorted by a rough estimate on most frequently used entries. +const kTypes = [ + 'string', + 'function', + 'number', + 'object', + // Accept 'Function' and 'Object' as alternative to the lower cased version. + 'Function', + 'Object', + 'boolean', + 'bigint', + 'symbol' +]; + +const { kMaxLength } = internalBinding('buffer'); + +const MainContextError = Error; +const ErrorToString = Error.prototype.toString; +const overrideStackTrace = new WeakMap(); +const kNoOverride = Symbol('kNoOverride'); +const prepareStackTrace = (globalThis, error, trace) => { + // API for node internals to override error stack formatting + // without interfering with userland code. + if (overrideStackTrace.has(error)) { + const f = overrideStackTrace.get(error); + overrideStackTrace.delete(error); + return f(error, trace); + } + + const globalOverride = + maybeOverridePrepareStackTrace(globalThis, error, trace); + if (globalOverride !== kNoOverride) return globalOverride; + + // Normal error formatting: + // + // Error: Message + // at function (file) + // at file + const errorString = ErrorToString.call(error); + if (trace.length === 0) { + return errorString; + } + return `${errorString}\n at ${trace.join('\n at ')}`; +}; + +const maybeOverridePrepareStackTrace = (globalThis, error, trace) => { + // Polyfill of V8's Error.prepareStackTrace API. + // https://crbug.com/v8/7848 + // `globalThis` is the global that contains the constructor which + // created `error`. + if (typeof globalThis.Error.prepareStackTrace === 'function') { + return globalThis.Error.prepareStackTrace(error, trace); + } + // We still have legacy usage that depends on the main context's `Error` + // being used, even when the error is from a different context. + // TODO(devsnek): evaluate if this can be eventually deprecated/removed. + if (typeof MainContextError.prepareStackTrace === 'function') { + return MainContextError.prepareStackTrace(error, trace); + } + + return kNoOverride; +}; + +let excludedStackFn; + +// Lazily loaded +let util; +let assert; + +let internalUtil = null; +function lazyInternalUtil() { + if (!internalUtil) { + internalUtil = require('internal/util'); + } + return internalUtil; +} + +let internalUtilInspect = null; +function lazyInternalUtilInspect() { + if (!internalUtilInspect) { + internalUtilInspect = require('internal/util/inspect'); + } + return internalUtilInspect; +} + +let buffer; +function lazyBuffer() { + if (buffer === undefined) + buffer = require('buffer').Buffer; + return buffer; +} + +// A specialized Error that includes an additional info property with +// additional information about the error condition. +// It has the properties present in a UVException but with a custom error +// message followed by the uv error code and uv error message. +// It also has its own error code with the original uv error context put into +// `err.info`. +// The context passed into this error must have .code, .syscall and .message, +// and may have .path and .dest. +class SystemError extends Error { + constructor(key, context) { + if (excludedStackFn === undefined) { + super(); + } else { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(); + // Reset the limit and setting the name property. + Error.stackTraceLimit = limit; + } + const prefix = getMessage(key, [], this); + let message = `${prefix}: ${context.syscall} returned ` + + `${context.code} (${context.message})`; + + if (context.path !== undefined) + message += ` ${context.path}`; + if (context.dest !== undefined) + message += ` => ${context.dest}`; + + ObjectDefineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, 'SystemError', key); + + this.code = key; + + ObjectDefineProperty(this, 'info', { + value: context, + enumerable: true, + configurable: true, + writable: false + }); + + ObjectDefineProperty(this, 'errno', { + get() { + return context.errno; + }, + set: (value) => { + context.errno = value; + }, + enumerable: true, + configurable: true + }); + + ObjectDefineProperty(this, 'syscall', { + get() { + return context.syscall; + }, + set: (value) => { + context.syscall = value; + }, + enumerable: true, + configurable: true + }); + + if (context.path !== undefined) { + // TODO(BridgeAR): Investigate why and when the `.toString()` was + // introduced. The `path` and `dest` properties in the context seem to + // always be of type string. We should probably just remove the + // `.toString()` and `Buffer.from()` operations and set the value on the + // context as the user did. + ObjectDefineProperty(this, 'path', { + get() { + return context.path != null ? + context.path.toString() : context.path; + }, + set: (value) => { + context.path = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true + }); + } + + if (context.dest !== undefined) { + ObjectDefineProperty(this, 'dest', { + get() { + return context.dest != null ? + context.dest.toString() : context.dest; + }, + set: (value) => { + context.dest = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true + }); + } + } + + toString() { + return `${this.name} [${this.code}]: ${this.message}`; + } + + [SymbolFor('nodejs.util.inspect.custom')](recurseTimes, ctx) { + return lazyInternalUtilInspect().inspect(this, { + ...ctx, + getters: true, + customInspect: false + }); + } +} + +function makeSystemErrorWithCode(key) { + return class NodeError extends SystemError { + constructor(ctx) { + super(key, ctx); + } + }; +} + +function makeNodeErrorWithCode(Base, key) { + return class NodeError extends Base { + constructor(...args) { + if (excludedStackFn === undefined) { + super(); + } else { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(); + // Reset the limit and setting the name property. + Error.stackTraceLimit = limit; + } + const message = getMessage(key, args, this); + ObjectDefineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, super.name, key); + this.code = key; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + }; +} + +// This function removes unnecessary frames from Node.js core errors. +function hideStackFrames(fn) { + return function hidden(...args) { + // Make sure the most outer `hideStackFrames()` function is used. + let setStackFn = false; + if (excludedStackFn === undefined) { + excludedStackFn = hidden; + setStackFn = true; + } + try { + return fn(...args); + } finally { + if (setStackFn === true) { + excludedStackFn = undefined; + } + } + }; +} + +function addCodeToName(err, name, code) { + // Set the stack + if (excludedStackFn !== undefined) { + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(err, excludedStackFn); + } + // Add the error code to the name to include it in the stack trace. + err.name = `${name} [${code}]`; + // Access the stack to generate the error message including the error code + // from the name. + err.stack; + // Reset the name to the actual name. + if (name === 'SystemError') { + ObjectDefineProperty(err, 'name', { + value: name, + enumerable: false, + writable: true, + configurable: true + }); + } else { + delete err.name; + } +} + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + if (def === SystemError) { + def = makeSystemErrorWithCode(sym); + } else { + def = makeNodeErrorWithCode(def, sym); + } + + if (otherClasses.length !== 0) { + otherClasses.forEach((clazz) => { + def[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + codes[sym] = def; +} + +function getMessage(key, args, self) { + const msg = messages.get(key); + + if (assert === undefined) assert = require('internal/assert'); + + if (typeof msg === 'function') { + assert( + msg.length <= args.length, // Default options do not count. + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${msg.length}).` + ); + return msg.apply(self, args); + } + + const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length; + assert( + expectedLength === args.length, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).` + ); + if (args.length === 0) + return msg; + + args.unshift(msg); + return lazyInternalUtilInspect().format.apply(null, args); +} + +let uvBinding; + +function lazyUv() { + if (!uvBinding) { + uvBinding = internalBinding('uv'); + } + return uvBinding; +} + +const uvUnmappedError = ['UNKNOWN', 'unknown error']; + +function uvErrmapGet(name) { + uvBinding = lazyUv(); + if (!uvBinding.errmap) { + uvBinding.errmap = uvBinding.getErrorMap(); + } + return uvBinding.errmap.get(name); +} + + +/** + * This creates an error compatible with errors produced in the C++ + * function UVException using a context object with data assembled in C++. + * The goal is to migrate them to ERR_* errors later when compatibility is + * not a concern. + * + * @param {Object} ctx + * @returns {Error} + */ +function uvException(ctx) { + const [ code, uvmsg ] = uvErrmapGet(ctx.errno) || uvUnmappedError; + let message = `${code}: ${ctx.message || uvmsg}, ${ctx.syscall}`; + + let path; + let dest; + if (ctx.path) { + path = ctx.path.toString(); + message += ` '${path}'`; + } + if (ctx.dest) { + dest = ctx.dest.toString(); + message += ` -> '${dest}'`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // Pass the message to the constructor instead of setting it on the object + // to make sure it is the same as the one created in C++ + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + Error.stackTraceLimit = tmpLimit; + + for (const prop of ObjectKeys(ctx)) { + if (prop === 'message' || prop === 'path' || prop === 'dest') { + continue; + } + err[prop] = ctx[prop]; + } + + err.code = code; + if (path) { + err.path = path; + } + if (dest) { + err.dest = dest; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(err, excludedStackFn || uvException); + return err; +} + +/** + * This creates an error compatible with errors produced in the C++ + * This function should replace the deprecated + * `exceptionWithHostPort()` function. + * + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + * @returns {Error} + */ +function uvExceptionWithHostPort(err, syscall, address, port) { + const [ code, uvmsg ] = uvErrmapGet(err) || uvUnmappedError; + const message = `${syscall} ${code}: ${uvmsg}`; + let details = ''; + + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(`${message}${details}`); + Error.stackTraceLimit = tmpLimit; + ex.code = code; + ex.errno = err; + ex.syscall = syscall; + ex.address = address; + if (port) { + ex.port = port; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || uvExceptionWithHostPort); + return ex; +} + +/** + * This used to be util._errnoException(). + * + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} [original] + * @returns {Error} + */ +function errnoException(err, syscall, original) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + if (util === undefined) util = require('util'); + const code = util.getSystemErrorName(err); + const message = original ? + `${syscall} ${code} ${original}` : `${syscall} ${code}`; + + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(message); + ex.errno = err; + ex.code = code; + ex.syscall = syscall; + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || errnoException); + return ex; +} + +/** + * Deprecated, new function is `uvExceptionWithHostPort()` + * New function added the error description directly + * from C++. this method for backwards compatibility + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + * @param {string} [additional] + * @returns {Error} + */ +function exceptionWithHostPort(err, syscall, address, port, additional) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + if (util === undefined) util = require('util'); + const code = util.getSystemErrorName(err); + let details = ''; + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + if (additional) { + details += ` - Local (${additional})`; + } + + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(`${syscall} ${code}${details}`); + Error.stackTraceLimit = tmpLimit; + ex.errno = err; + ex.code = code; + ex.syscall = syscall; + ex.address = address; + if (port) { + ex.port = port; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || exceptionWithHostPort); + return ex; +} + +/** + * @param {number|string} code - A libuv error number or a c-ares error code + * @param {string} syscall + * @param {string} [hostname] + * @returns {Error} + */ +function dnsException(code, syscall, hostname) { + let errno; + // If `code` is of type number, it is a libuv error number, else it is a + // c-ares error code. + // TODO(joyeecheung): translate c-ares error codes into numeric ones and + // make them available in a property that's not error.errno (since they + // can be in conflict with libuv error codes). Also make sure + // util.getSystemErrorName() can understand them when an being informed that + // the number is a c-ares error code. + if (typeof code === 'number') { + errno = code; + // ENOTFOUND is not a proper POSIX error, but this error has been in place + // long enough that it's not practical to remove it. + if (code === lazyUv().UV_EAI_NODATA || code === lazyUv().UV_EAI_NONAME) { + code = 'ENOTFOUND'; // Fabricated error name. + } else { + code = lazyInternalUtil().getSystemErrorName(code); + } + } + const message = `${syscall} ${code}${hostname ? ` ${hostname}` : ''}`; + // Reducing the limit improves the performance significantly. We do not loose + // the stack frames due to the `captureStackTrace()` function that is called + // later. + const tmpLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(message); + Error.stackTraceLimit = tmpLimit; + ex.errno = errno; + ex.code = code; + ex.syscall = syscall; + if (hostname) { + ex.hostname = hostname; + } + + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ex, excludedStackFn || dnsException); + return ex; +} + +function connResetException(msg) { + // eslint-disable-next-line no-restricted-syntax + const ex = new Error(msg); + ex.code = 'ECONNRESET'; + return ex; +} + +let maxStack_ErrorName; +let maxStack_ErrorMessage; +/** + * Returns true if `err.name` and `err.message` are equal to engine-specific + * values indicating max call stack size has been exceeded. + * "Maximum call stack size exceeded" in V8. + * + * @param {Error} err + * @returns {boolean} + */ +function isStackOverflowError(err) { + if (maxStack_ErrorMessage === undefined) { + try { + function overflowStack() { overflowStack(); } + overflowStack(); + } catch (err) { + maxStack_ErrorMessage = err.message; + maxStack_ErrorName = err.name; + } + } + + return err && err.name === maxStack_ErrorName && + err.message === maxStack_ErrorMessage; +} + +// Only use this for integers! Decimal numbers do not work with this function. +function addNumericalSeparator(val) { + let res = ''; + let i = val.length; + const start = val[0] === '-' ? 1 : 0; + for (; i >= start + 4; i -= 3) { + res = `_${val.slice(i - 3, i)}${res}`; + } + return `${val.slice(0, i)}${res}`; +} + +// Used to enhance the stack that will be picked up by the inspector +const kEnhanceStackBeforeInspector = Symbol('kEnhanceStackBeforeInspector'); + +// These are supposed to be called only on fatal exceptions before +// the process exits. +const fatalExceptionStackEnhancers = { + beforeInspector(error) { + if (typeof error[kEnhanceStackBeforeInspector] !== 'function') { + return error.stack; + } + + try { + // Set the error.stack here so it gets picked up by the + // inspector. + error.stack = error[kEnhanceStackBeforeInspector](); + } catch { + // We are just enhancing the error. If it fails, ignore it. + } + return error.stack; + }, + afterInspector(error) { + const originalStack = error.stack; + const { + inspect, + inspectDefaultOptions: { + colors: defaultColors + } + } = lazyInternalUtilInspect(); + const colors = (internalBinding('util').guessHandleType(2) === 'TTY' && + require('internal/tty').hasColors()) || + defaultColors; + try { + return inspect(error, { colors }); + } catch { + return originalStack; + } + } +}; + +module.exports = { + addCodeToName, // Exported for NghttpError + codes, + dnsException, + errnoException, + exceptionWithHostPort, + getMessage, + hideStackFrames, + isStackOverflowError, + connResetException, + uvErrmapGet, + uvException, + uvExceptionWithHostPort, + SystemError, + // This is exported only to facilitate testing. + E, + kNoOverride, + prepareStackTrace, + maybeOverridePrepareStackTrace, + overrideStackTrace, + kEnhanceStackBeforeInspector, + fatalExceptionStackEnhancers +}; + +// To declare an error message, use the E(sym, val, def) function above. The sym +// must be an upper case string. The val can be either a function or a string. +// The def must be an error class. +// The return value of the function must be a string. +// Examples: +// E('EXAMPLE_KEY1', 'This is the error value', Error); +// E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`, RangeError); +// +// Once an error code has been assigned, the code itself MUST NOT change and +// any given error code must never be reused to identify a different error. +// +// Any error code added here should also be added to the documentation +// +// Note: Please try to keep these in alphabetical order +// +// Note: Node.js specific errors must begin with the prefix ERR_ +E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError); +E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError); +E('ERR_ASSERTION', '%s', Error); +E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError); +E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError); +E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError); +E('ERR_BUFFER_OUT_OF_BOUNDS', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name = undefined) => { + if (name) { + return `"${name}" is outside of buffer bounds`; + } + return 'Attempt to access memory outside buffer bounds'; + }, RangeError); +E('ERR_BUFFER_TOO_LARGE', + `Cannot create a Buffer larger than 0x${kMaxLength.toString(16)} bytes`, + RangeError); +E('ERR_CANNOT_WATCH_SIGINT', 'Cannot watch for SIGINT signals', Error); +E('ERR_CHILD_CLOSED_BEFORE_REPLY', + 'Child closed before reply received', Error); +E('ERR_CHILD_PROCESS_IPC_REQUIRED', + "Forked processes must have an IPC channel, missing value 'ipc' in %s", + Error); +E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', + RangeError); +E('ERR_CONSOLE_WRITABLE_STREAM', + 'Console expects a writable stream instance for %s', TypeError); +E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error); +E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s', Error); +E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', + 'Custom engines not supported by this OpenSSL', Error); +E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s', TypeError); +E('ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + 'Public key is not valid for specified curve', Error); +E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found', Error); +E('ERR_CRYPTO_FIPS_FORCED', + 'Cannot set FIPS mode, it was forced with --force-fips at startup.', Error); +E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.', + Error); +E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error); +E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', + Error); +E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); +E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + 'Invalid key object type %s, expected %s.', TypeError); +E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); +E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); +E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error); +E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); +// Switch to TypeError. The current implementation does not seem right. +E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); +E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', + 'Input buffers must have the same byte length', RangeError); +E('ERR_DIR_CLOSED', 'Directory handle was closed', Error); +E('ERR_DIR_CONCURRENT_OPERATION', + 'Cannot do synchronous work on directory handle with concurrent ' + + 'asynchronous operations', Error); +E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]', + Error); +E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE', + 'A callback was registered through ' + + 'process.setUncaughtExceptionCaptureCallback(), which is mutually ' + + 'exclusive with using the `domain` module', + Error); +E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE', + 'The `domain` module is in use, which is mutually exclusive with calling ' + + 'process.setUncaughtExceptionCaptureCallback()', + Error); +E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { + this.errno = ret; + return `The encoded data was not valid for encoding ${encoding}`; +}, TypeError); +E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', + RangeError); +E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); +E('ERR_FALSY_VALUE_REJECTION', function(reason) { + this.reason = reason; + return 'Promise was rejected with falsy value'; +}, Error); +E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM', + 'The feature %s is unavailable on the current platform' + + ', which is being used to run Node.js', + TypeError); +E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GB', RangeError); +E('ERR_FS_INVALID_SYMLINK_TYPE', + 'Symlink type must be one of "dir", "file", or "junction". Received "%s"', + Error); // Switch to TypeError. The current implementation does not seem right +E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN', + 'HTTP/2 ALTSVC frames require a valid origin', TypeError); +E('ERR_HTTP2_ALTSVC_LENGTH', + 'HTTP/2 ALTSVC frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_CONNECT_AUTHORITY', + ':authority header is required for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_PATH', + 'The :path header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_SCHEME', + 'The :scheme header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_GOAWAY_SESSION', + 'New streams cannot be created after receiving a GOAWAY', Error); +E('ERR_HTTP2_HEADERS_AFTER_RESPOND', + 'Cannot specify additional headers after response initiated', Error); +E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.', Error); +E('ERR_HTTP2_HEADER_SINGLE_VALUE', + 'Header field "%s" must only have a single value', TypeError); +E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + 'Informational status codes cannot be used', RangeError); +E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', + 'HTTP/1 Connection specific headers are forbidden: "%s"', TypeError); +E('ERR_HTTP2_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError); +E('ERR_HTTP2_INVALID_INFO_STATUS', + 'Invalid informational status code: %s', RangeError); +E('ERR_HTTP2_INVALID_ORIGIN', + 'HTTP/2 ORIGIN frames require a valid origin', TypeError); +E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + 'Packed settings length must be a multiple of six', RangeError); +E('ERR_HTTP2_INVALID_PSEUDOHEADER', + '"%s" is an invalid pseudoheader or is used incorrectly', TypeError); +E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed', Error); +E('ERR_HTTP2_INVALID_SETTING_VALUE', + // Using default arguments here is important so the arguments are not counted + // towards `Function#length`. + function(name, actual, min = undefined, max = undefined) { + this.actual = actual; + if (min !== undefined) { + this.min = min; + this.max = max; + } + return `Invalid value for setting "${name}": ${actual}`; + }, TypeError, RangeError); +E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed', Error); +E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + 'Maximum number of pending settings acknowledgements', Error); +E('ERR_HTTP2_NESTED_PUSH', + 'A push stream cannot initiate another push stream.', Error); +E('ERR_HTTP2_NO_SOCKET_MANIPULATION', + 'HTTP/2 sockets should not be directly manipulated (e.g. read and written)', + Error); +E('ERR_HTTP2_ORIGIN_LENGTH', + 'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_OUT_OF_STREAMS', + 'No stream ID is available because maximum stream ID has been reached', + Error); +E('ERR_HTTP2_PAYLOAD_FORBIDDEN', + 'Responses with %s status must not have a payload', Error); +E('ERR_HTTP2_PING_CANCEL', 'HTTP2 ping cancelled', Error); +E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes', RangeError); +E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', + 'Cannot set HTTP/2 pseudo-headers', TypeError); +E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams', Error); +E('ERR_HTTP2_SEND_FILE', 'Directories cannot be sent', Error); +E('ERR_HTTP2_SEND_FILE_NOSEEK', + 'Offset or length can only be specified for regular files', Error); +E('ERR_HTTP2_SESSION_ERROR', 'Session closed with error code %s', Error); +E('ERR_HTTP2_SETTINGS_CANCEL', 'HTTP2 session settings canceled', Error); +E('ERR_HTTP2_SOCKET_BOUND', + 'The socket is already bound to an Http2Session', Error); +E('ERR_HTTP2_SOCKET_UNBOUND', + 'The socket has been disconnected from the Http2Session', Error); +E('ERR_HTTP2_STATUS_101', + 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2', Error); +E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s', RangeError); +E('ERR_HTTP2_STREAM_CANCEL', function(error) { + let msg = 'The pending stream has been canceled'; + if (error) { + this.cause = error; + if (typeof error.message === 'string') + msg += ` (caused by: ${error.message})`; + } + return msg; +}, Error); +E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error); +E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', + 'A stream cannot depend on itself', Error); +E('ERR_HTTP2_TRAILERS_ALREADY_SENT', + 'Trailing headers have already been sent', Error); +E('ERR_HTTP2_TRAILERS_NOT_READY', + 'Trailing headers cannot be sent until after the wantTrailers event is ' + + 'emitted', Error); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error); +E('ERR_HTTP_HEADERS_SENT', + 'Cannot %s headers after they are sent to the client', Error); +E('ERR_HTTP_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError); +E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError); +E('ERR_HTTP_TRAILER_INVALID', + 'Trailers are invalid with this transfer encoding', Error); +E('ERR_INCOMPATIBLE_OPTION_PAIR', + 'Option "%s" cannot be used in combination with option "%s"', TypeError); +E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' + + 'input via --eval, --print, or STDIN', Error); +E('ERR_INSPECTOR_ALREADY_CONNECTED', '%s is already connected', Error); +E('ERR_INSPECTOR_CLOSED', 'Session was closed', Error); +E('ERR_INSPECTOR_COMMAND', 'Inspector error %d: %s', Error); +E('ERR_INSPECTOR_NOT_ACTIVE', 'Inspector is not active', Error); +E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available', Error); +E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected', Error); +E('ERR_INSPECTOR_NOT_WORKER', 'Current thread is not a worker', Error); +E('ERR_INTERNAL_ASSERTION', (message) => { + const suffix = 'This is caused by either a bug in Node.js ' + + 'or incorrect usage of Node.js internals.\n' + + 'Please open an issue with this stack trace at ' + + 'https://github.com/nodejs/node/issues\n'; + return message === undefined ? suffix : `${message}\n${suffix}`; +}, Error); +E('ERR_INVALID_ADDRESS_FAMILY', function(addressType, host, port) { + this.host = host; + this.port = port; + return `Invalid address family: ${addressType} ${host}:${port}`; +}, RangeError); +E('ERR_INVALID_ARG_TYPE', + (name, expected, actual) => { + assert(typeof name === 'string', "'name' must be a string"); + if (!ArrayIsArray(expected)) { + expected = [expected]; + } + + let msg = 'The '; + if (name.endsWith(' argument')) { + // For cases like 'first argument' + msg += `${name} `; + } else { + const type = name.includes('.') ? 'property' : 'argument'; + msg += `"${name}" ${type} `; + } + msg += 'must be '; + + const types = []; + const instances = []; + const other = []; + + for (const value of expected) { + assert(typeof value === 'string', + 'All expected entries have to be of type string'); + if (kTypes.includes(value)) { + types.push(value.toLowerCase()); + } else if (classRegExp.test(value)) { + instances.push(value); + } else { + assert(value !== 'object', + 'The value "object" should be written as "Object"'); + other.push(value); + } + } + + // Special handle `object` in case other instances are allowed to outline + // the differences between each other. + if (instances.length > 0) { + const pos = types.indexOf('object'); + if (pos !== -1) { + types.splice(pos, 1); + instances.push('Object'); + } + } + + if (types.length > 0) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(', ')}, or ${last}`; + } else if (types.length === 2) { + msg += `one of type ${types[0]} or ${types[1]}`; + } else { + msg += `of type ${types[0]}`; + } + if (instances.length > 0 || other.length > 0) + msg += ' or '; + } + + if (instances.length > 0) { + if (instances.length > 2) { + const last = instances.pop(); + msg += `an instance of ${instances.join(', ')}, or ${last}`; + } else { + msg += `an instance of ${instances[0]}`; + if (instances.length === 2) { + msg += ` or ${instances[1]}`; + } + } + if (other.length > 0) + msg += ' or '; + } + + if (other.length > 0) { + if (other.length > 2) { + const last = other.pop(); + msg += `one of ${other.join(', ')}, or ${last}`; + } else if (other.length === 2) { + msg += `one of ${other[0]} or ${other[1]}`; + } else { + if (other[0].toLowerCase() !== other[0]) + msg += 'an '; + msg += `${other[0]}`; + } + } + + if (actual == null) { + msg += `. Received ${actual}`; + } else if (typeof actual === 'function' && actual.name) { + msg += `. Received function ${actual.name}`; + } else if (typeof actual === 'object') { + if (actual.constructor && actual.constructor.name) { + msg += `. Received an instance of ${actual.constructor.name}`; + } else { + const inspected = lazyInternalUtilInspect() + .inspect(actual, { depth: -1 }); + msg += `. Received ${inspected}`; + } + } else { + let inspected = lazyInternalUtilInspect() + .inspect(actual, { colors: false }); + if (inspected.length > 25) + inspected = `${inspected.slice(0, 25)}...`; + msg += `. Received type ${typeof actual} (${inspected})`; + } + return msg; + }, TypeError); +E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => { + let inspected = lazyInternalUtilInspect().inspect(value); + if (inspected.length > 128) { + inspected = `${inspected.slice(0, 128)}...`; + } + return `The argument '${name}' ${reason}. Received ${inspected}`; +}, TypeError, RangeError); +E('ERR_INVALID_ASYNC_ID', 'Invalid %s value: %s', RangeError); +E('ERR_INVALID_BUFFER_SIZE', + 'Buffer size must be a multiple of %s', RangeError); +E('ERR_INVALID_CALLBACK', + 'Callback must be a function. Received %O', TypeError); +E('ERR_INVALID_CHAR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name, field = undefined) => { + let msg = `Invalid character in ${name}`; + if (field !== undefined) { + msg += ` ["${field}"]`; + } + return msg; + }, TypeError); +E('ERR_INVALID_CURSOR_POS', + 'Cannot set cursor row without setting its column', TypeError); +E('ERR_INVALID_FD', + '"fd" must be a positive integer: %s', RangeError); +E('ERR_INVALID_FD_TYPE', 'Unsupported fd type: %s', TypeError); +E('ERR_INVALID_FILE_URL_HOST', + 'File URL host must be "localhost" or empty on %s', TypeError); +E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError); +E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); +E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError); +E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); +E('ERR_INVALID_MODULE_SPECIFIER', (pkgPath, subpath, base = undefined) => { + if (subpath === undefined) { + return `Invalid package name '${pkgPath}' imported from ${base}`; + } else if (base === undefined) { + assert(subpath !== '.'); + return `Package subpath '${subpath}' is not a valid module request for ` + + `the "exports" resolution of ${pkgPath}${sep}package.json`; + } + return `Package subpath '${subpath}' is not a valid module request for ` + + `the "exports" resolution of ${pkgPath} imported from ${base}`; +}, TypeError); +E('ERR_INVALID_OPT_VALUE', (name, value) => + `The value "${String(value)}" is invalid for option "${name}"`, + TypeError, + RangeError); +E('ERR_INVALID_OPT_VALUE_ENCODING', + 'The value "%s" is invalid for option "encoding"', TypeError); +E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => { + if (hasMessage) + return `Invalid package config ${path}${sep}package.json, ${message}`; + return `Invalid JSON in ${path} imported from ${message}`; +}, Error); +E('ERR_INVALID_PACKAGE_TARGET', + (pkgPath, key, subpath, target, base = undefined) => { + const relError = typeof target === 'string' && + target.length && !StringPrototypeStartsWith(target, './'); + if (key === null) { + if (subpath !== '') { + return `Invalid "exports" target ${JSONStringify(target)} defined ` + + `for '${subpath}' in the package config ${pkgPath} imported from ` + + `${base}.${relError ? '; targets must start with "./"' : ''}`; + } + return `Invalid "exports" main target ${target} defined in the ` + + `package config ${pkgPath} imported from ${base}${relError ? + '; targets must start with "./"' : ''}`; + } else if (key === '.') { + return `Invalid "exports" main target ${JSONStringify(target)} defined ` + + `in the package config ${pkgPath}${sep}package.json${relError ? + '; targets must start with "./"' : ''}`; + } else if (relError) { + return `Invalid "exports" target ${JSONStringify(target)} defined for '${ + StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + + `package config ${pkgPath}${sep}package.json; ` + + 'targets must start with "./"'; + } + return `Invalid "exports" target ${JSONStringify(target)} defined for '${ + StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + + `package config ${pkgPath}${sep}package.json`; + }, Error); +E('ERR_INVALID_PERFORMANCE_MARK', + 'The "%s" performance mark has not been set', Error); +E('ERR_INVALID_PROTOCOL', + 'Protocol "%s" not supported. Expected "%s"', + TypeError); +E('ERR_INVALID_REPL_EVAL_CONFIG', + 'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError); +E('ERR_INVALID_REPL_INPUT', '%s', TypeError); +E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => { + return `Expected a valid ${input} to be returned for the "${prop}" from the` + + ` "${name}" function but got ${value}.`; +}, TypeError); +E('ERR_INVALID_RETURN_PROPERTY_VALUE', (input, name, prop, value) => { + let type; + if (value && value.constructor && value.constructor.name) { + type = `instance of ${value.constructor.name}`; + } else { + type = `type ${typeof value}`; + } + return `Expected ${input} to be returned for the "${prop}" from the` + + ` "${name}" function but got ${type}.`; +}, TypeError); +E('ERR_INVALID_RETURN_VALUE', (input, name, value) => { + let type; + if (value && value.constructor && value.constructor.name) { + type = `instance of ${value.constructor.name}`; + } else { + type = `type ${typeof value}`; + } + return `Expected ${input} to be returned from the "${name}"` + + ` function but got ${type}.`; +}, TypeError); +E('ERR_INVALID_SYNC_FORK_INPUT', + 'Asynchronous forks do not support ' + + 'Buffer, TypedArray, DataView or string input: %s', + TypeError); +E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError); +E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError); +E('ERR_INVALID_URI', 'URI malformed', URIError); +E('ERR_INVALID_URL', function(input) { + this.input = input; + return `Invalid URL: ${input}`; +}, TypeError); +E('ERR_INVALID_URL_SCHEME', + (expected) => { + if (typeof expected === 'string') + expected = [expected]; + assert(expected.length <= 2); + const res = expected.length === 2 ? + `one of scheme ${expected[0]} or ${expected[1]}` : + `of scheme ${expected[0]}`; + return `The URL must be ${res}`; + }, TypeError); +E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); +E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); +E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); +E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error); +E('ERR_MANIFEST_ASSERT_INTEGRITY', + (moduleURL, realIntegrities) => { + let msg = `The content of "${ + moduleURL + }" does not match the expected integrity.`; + if (realIntegrities.size) { + const sri = [...realIntegrities.entries()].map(([alg, dgs]) => { + return `${alg}-${dgs}`; + }).join(' '); + msg += ` Integrities found are: ${sri}`; + } else { + msg += ' The resource was not found in the policy.'; + } + return msg; + }, Error); +E('ERR_MANIFEST_DEPENDENCY_MISSING', + 'Manifest resource %s does not list %s as a dependency specifier', + Error); +E('ERR_MANIFEST_INTEGRITY_MISMATCH', + 'Manifest resource %s has multiple entries but integrity lists do not match', + SyntaxError); +E('ERR_MANIFEST_INVALID_RESOURCE_FIELD', + 'Manifest resource %s has invalid property value for %s', + TypeError); +E('ERR_MANIFEST_TDZ', 'Manifest initialization has not yet run', Error); +E('ERR_MANIFEST_UNKNOWN_ONERROR', + 'Manifest specified unknown error behavior "%s".', + SyntaxError); +E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error); +E('ERR_MISSING_ARGS', + (...args) => { + assert(args.length > 0, 'At least one arg needs to be specified'); + let msg = 'The '; + const len = args.length; + args = args.map((a) => `"${a}"`); + switch (len) { + case 1: + msg += `${args[0]} argument`; + break; + case 2: + msg += `${args[0]} and ${args[1]} arguments`; + break; + default: + msg += args.slice(0, len - 1).join(', '); + msg += `, and ${args[len - 1]} arguments`; + break; + } + return `${msg} must be specified`; + }, TypeError); +E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', + 'The ES Module loader may not return a format of \'dynamic\' when no ' + + 'dynamicInstantiate function was provided', Error); +E('ERR_MISSING_OPTION', '%s is required', TypeError); +E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => { + return `Cannot find ${type} '${path}' imported from ${base}`; +}, Error); +E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); +E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); +E('ERR_NAPI_INVALID_DATAVIEW_ARGS', + 'byte_offset + byte_length should be less than or equal to the size in ' + + 'bytes of the array passed in', + RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT', + 'start offset of %s should be a multiple of %s', RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_LENGTH', + 'Invalid typed array length', RangeError); +E('ERR_NO_CRYPTO', + 'Node.js is not compiled with OpenSSL crypto support', Error); +E('ERR_NO_ICU', + '%s is not supported on Node.js compiled without ICU', TypeError); +E('ERR_OUT_OF_RANGE', + (str, range, input, replaceDefaultBoolean = false) => { + assert(range, 'Missing "range" argument'); + let msg = replaceDefaultBoolean ? str : + `The value of "${str}" is out of range.`; + let received; + if (NumberIsInteger(input) && MathAbs(input) > 2 ** 32) { + received = addNumericalSeparator(String(input)); + } else if (typeof input === 'bigint') { + received = String(input); + if (input > 2n ** 32n || input < -(2n ** 32n)) { + received = addNumericalSeparator(received); + } + received += 'n'; + } else { + received = lazyInternalUtilInspect().inspect(input); + } + msg += ` It must be ${range}. Received ${received}`; + return msg; + }, RangeError); +E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => { + if (subpath === '.') { + return `No "exports" main resolved in ${pkgPath}${sep}package.json`; + } else if (base === undefined) { + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath}${sep}package.json`; + } + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath} imported from ${base}`; +}, Error); +E('ERR_REQUIRE_ESM', + (filename, parentPath = null, packageJsonPath = null) => { + let msg = `Must use import to load ES Module: ${filename}`; + if (parentPath && packageJsonPath) { + const path = require('path'); + const basename = path.basename(filename) === path.basename(parentPath) ? + filename : path.basename(filename); + msg += + '\nrequire() of ES modules is not supported.\nrequire() of ' + + `${filename} ${parentPath ? `from ${parentPath} ` : ''}` + + 'is an ES module file as it is a .js file whose nearest parent ' + + 'package.json contains "type": "module" which defines all .js ' + + 'files in that package scope as ES modules.\nInstead rename ' + + `${basename} to end in .cjs, change the requiring code to use ` + + 'import(), or remove "type": "module" from ' + + `${packageJsonPath}.\n`; + return msg; + } + return msg; + }, Error); +E('ERR_SCRIPT_EXECUTION_INTERRUPTED', + 'Script execution was interrupted by `SIGINT`', Error); +E('ERR_SERVER_ALREADY_LISTEN', + 'Listen method has been called more than once without closing.', Error); +E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error); +E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error); +E('ERR_SOCKET_BAD_BUFFER_SIZE', + 'Buffer size must be a positive integer', TypeError); +E('ERR_SOCKET_BAD_PORT', (name, port, allowZero = true) => { + assert(typeof allowZero === 'boolean', + "The 'allowZero' argument must be of type boolean."); + const operator = allowZero ? '>=' : '>'; + return `${name} should be ${operator} 0 and < 65536. Received ${port}.`; +}, RangeError); +E('ERR_SOCKET_BAD_TYPE', + 'Bad socket type specified. Valid types are: udp4, udp6', TypeError); +E('ERR_SOCKET_BUFFER_SIZE', + 'Could not get or set buffer size', + SystemError); +E('ERR_SOCKET_CLOSED', 'Socket is closed', Error); +E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error); +E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error); +E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error); +E('ERR_SRI_PARSE', + 'Subresource Integrity string %j had an unexpected %j at position %d', + SyntaxError); +E('ERR_STREAM_ALREADY_FINISHED', + 'Cannot call %s after a stream was finished', + Error); +E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error); +E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error); +E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); +E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error); +E('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF', Error); +E('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', + 'stream.unshift() after end event', Error); +E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error); +E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error); +E('ERR_SYNTHETIC', 'JavaScript Callstack', Error); +E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError); +E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) { + this.reason = reason; + this.host = host; + this.cert = cert; + return `Hostname/IP does not match certificate's altnames: ${reason}`; +}, Error); +E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error); +E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error); +E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError), +E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established', + Error), +E('ERR_TLS_INVALID_PROTOCOL_VERSION', + '%j is not a valid %s TLS protocol version', TypeError); +E('ERR_TLS_PROTOCOL_VERSION_CONFLICT', + 'TLS protocol version %j conflicts with secureProtocol %j', TypeError); +E('ERR_TLS_RENEGOTIATION_DISABLED', + 'TLS session renegotiation disabled for this socket', Error); + +// This should probably be a `TypeError`. +E('ERR_TLS_REQUIRED_SERVER_NAME', + '"servername" is required parameter for Server.addContext', Error); +E('ERR_TLS_SESSION_ATTACK', 'TLS session renegotiation attack detected', Error); +E('ERR_TLS_SNI_FROM_SERVER', + 'Cannot issue SNI from a TLS server-side socket', Error); +E('ERR_TRACE_EVENTS_CATEGORY_REQUIRED', + 'At least one category is required', TypeError); +E('ERR_TRACE_EVENTS_UNAVAILABLE', 'Trace events are unavailable', Error); + +// This should probably be a `RangeError`. +E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError); +E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' + + 'handler', Error); +E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + '`process.setupUncaughtExceptionCapture()` was called while a capture ' + + 'callback was already active', + Error); +E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters', TypeError); +E('ERR_UNHANDLED_ERROR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (err = undefined) => { + const msg = 'Unhandled error.'; + if (err === undefined) return msg; + return `${msg} (${err})`; + }, Error); +E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error); +E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); +E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); +E('ERR_UNKNOWN_FILE_EXTENSION', + 'Unknown file extension "%s" for %s', + TypeError); +E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); +E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); +E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " + +'resolving ES modules, imported from %s', Error); +E('ERR_UNSUPPORTED_ESM_URL_SCHEME', 'Only file and data URLs are supported ' + + 'by the default ESM loader', Error); + +E('ERR_V8BREAKITERATOR', + 'Full ICU data not installed. See https://github.com/nodejs/node/wiki/Intl', + Error); + +// This should probably be a `TypeError`. +E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', + 'At least one valid performance entry type is required', Error); +E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', + 'A dynamic import callback was not specified.', TypeError); +E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error); +E('ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA', + 'Cached data cannot be created for a module which has been evaluated', Error); +E('ERR_VM_MODULE_DIFFERENT_CONTEXT', + 'Linked modules must use the same context', Error); +E('ERR_VM_MODULE_LINKING_ERRORED', + 'Linking has already failed for the provided module', Error); +E('ERR_VM_MODULE_NOT_MODULE', + 'Provided module is not an instance of Module', Error); +E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); +E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error); +E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); +E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') => + `Initiated Worker with ${msg}: ${errors.join(', ')}`, + Error); +E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error); +E('ERR_WORKER_OUT_OF_MEMORY', + 'Worker terminated due to reaching memory limit: %s', Error); +E('ERR_WORKER_PATH', (filename) => + 'The worker script or module filename must be an absolute path or a ' + + 'relative path starting with \'./\' or \'../\'.' + + (filename.startsWith('file://') ? + ' Wrap file:// URLs with `new URL`.' : '' + ) + + ` Received "${filename}"`, + TypeError); +E('ERR_WORKER_UNSERIALIZABLE_ERROR', + 'Serializing an uncaught exception failed', Error); +E('ERR_WORKER_UNSUPPORTED_EXTENSION', + 'The worker script extension must be ".js", ".mjs", or ".cjs". Received "%s"', + TypeError); +E('ERR_WORKER_UNSUPPORTED_OPERATION', + '%s is not supported in workers', TypeError); +E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); diff --git a/dist-raw/node-cjs-helpers.js b/raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js similarity index 54% rename from dist-raw/node-cjs-helpers.js rename to raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js index 20bfb44f..746acaaf 100644 --- a/dist-raw/node-cjs-helpers.js +++ b/raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js @@ -1,11 +1,41 @@ -const {ArrayPrototypeForEach, StringPrototypeStartsWith, ObjectPrototypeHasOwnProperty, StringPrototypeIncludes, ObjectDefineProperty} = require('./node-primordials'); +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js -exports.addBuiltinLibsToObject = addBuiltinLibsToObject; +'use strict'; -// Copied from https://github.com/nodejs/node/blob/21f5a56914a3b24ad77535ef369b93c6b1c11d18/lib/internal/modules/cjs/helpers.js#L133-L178 -function addBuiltinLibsToObject(object) { +const { + ArrayPrototypeForEach, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeSet, + StringPrototypeIncludes, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = primordials; + +const { getOptionValue } = require('internal/options'); +const userConditions = getOptionValue('--conditions'); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); + +function addBuiltinLibsToObject(object, dummyModuleName) { // Make built-in modules available directly (loaded lazily). - const { builtinModules } = require('module').Module; + const Module = require('internal/modules/cjs/loader').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + ArrayPrototypeForEach(builtinModules, (name) => { // Neither add underscored modules, nor ones that contain slashes (e.g., // 'fs/promises') or ones that are already defined. @@ -29,7 +59,7 @@ function addBuiltinLibsToObject(object) { ObjectDefineProperty(object, name, { get: () => { - const lib = require(name); + const lib = dummyModule.require(name); // Disable the current getter/setter and set up a new // non-enumerable property. diff --git a/raw/node-internal-modules-cjs-helpers-v17.0.1.js b/raw/node-internal-modules-cjs-helpers-v17.0.1.js new file mode 100644 index 00000000..3dffdd34 --- /dev/null +++ b/raw/node-internal-modules-cjs-helpers-v17.0.1.js @@ -0,0 +1,231 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js + +'use strict'; + +const { + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeSome, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeMap, + SafeSet, + StringPrototypeCharCodeAt, + StringPrototypeIncludes, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = primordials; +const { + ERR_MANIFEST_DEPENDENCY_MISSING, + ERR_UNKNOWN_BUILTIN_MODULE +} = require('internal/errors').codes; +const { NativeModule } = require('internal/bootstrap/loaders'); + +const { validateString } = require('internal/validators'); +const path = require('path'); +const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); + +const { getOptionValue } = require('internal/options'); +const userConditions = getOptionValue('--conditions'); + +let debug = require('internal/util/debuglog').debuglog('module', (fn) => { + debug = fn; +}); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); + +function loadNativeModule(filename, request) { + const mod = NativeModule.map.get(filename); + if (mod?.canBeRequiredByUsers) { + debug('load native module %s', request); + // compileForPublicLoader() throws if mod.canBeRequiredByUsers is false: + mod.compileForPublicLoader(); + return mod; + } +} + +// Invoke with makeRequireFunction(module) where |module| is the Module object +// to use as the context for the require() function. +// Use redirects to set up a mapping from a policy and restrict dependencies +const urlToFileCache = new SafeMap(); +function makeRequireFunction(mod, redirects) { + const Module = mod.constructor; + + let require; + if (redirects) { + const id = mod.filename || mod.id; + const conditions = cjsConditions; + const { resolve, reaction } = redirects; + require = function require(specifier) { + let missing = true; + const destination = resolve(specifier, conditions); + if (destination === true) { + missing = false; + } else if (destination) { + const href = destination.href; + if (destination.protocol === 'node:') { + const specifier = destination.pathname; + const mod = loadNativeModule(specifier, href); + if (mod && mod.canBeRequiredByUsers) { + return mod.exports; + } + throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier); + } else if (destination.protocol === 'file:') { + let filepath; + if (urlToFileCache.has(href)) { + filepath = urlToFileCache.get(href); + } else { + filepath = fileURLToPath(destination); + urlToFileCache.set(href, filepath); + } + return mod.require(filepath); + } + } + if (missing) { + reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( + id, + specifier, + ArrayPrototypeJoin([...conditions], ', ') + )); + } + return mod.require(specifier); + }; + } else { + require = function require(path) { + return mod.require(path); + }; + } + + function resolve(request, options) { + validateString(request, 'request'); + return Module._resolveFilename(request, mod, false, options); + } + + require.resolve = resolve; + + function paths(request) { + validateString(request, 'request'); + return Module._resolveLookupPaths(request, mod); + } + + resolve.paths = paths; + + require.main = process.mainModule; + + // Enable support to add extra extension types. + require.extensions = Module._extensions; + + require.cache = Module._cache; + + return require; +} + +/** + * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) + * because the buffer-to-string conversion in `fs.readFileSync()` + * translates it to FEFF, the UTF-16 BOM. + */ +function stripBOM(content) { + if (StringPrototypeCharCodeAt(content) === 0xFEFF) { + content = StringPrototypeSlice(content, 1); + } + return content; +} + +function addBuiltinLibsToObject(object, dummyModuleName) { + // Make built-in modules available directly (loaded lazily). + const Module = require('internal/modules/cjs/loader').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + + ArrayPrototypeForEach(builtinModules, (name) => { + // Neither add underscored modules, nor ones that contain slashes (e.g., + // 'fs/promises') or ones that are already defined. + if (StringPrototypeStartsWith(name, '_') || + StringPrototypeIncludes(name, '/') || + ObjectPrototypeHasOwnProperty(object, name)) { + return; + } + // Goals of this mechanism are: + // - Lazy loading of built-in modules + // - Having all built-in modules available as non-enumerable properties + // - Allowing the user to re-assign these variables as if there were no + // pre-existing globals with the same name. + + const setReal = (val) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + ObjectDefineProperty(object, name, { + get: () => { + const lib = dummyModule.require(name); + + // Disable the current getter/setter and set up a new + // non-enumerable property. + delete object[name]; + ObjectDefineProperty(object, name, { + get: () => lib, + set: setReal, + configurable: true, + enumerable: false + }); + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + }); +} + +function normalizeReferrerURL(referrer) { + if (typeof referrer === 'string' && path.isAbsolute(referrer)) { + return pathToFileURL(referrer).href; + } + return new URL(referrer).href; +} + +// For error messages only - used to check if ESM syntax is in use. +function hasEsmSyntax(code) { + debug('Checking for ESM syntax'); + const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + let root; + try { + root = parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }); + } catch { + return false; + } + + return ArrayPrototypeSome(root.body, (stmt) => + stmt.type === 'ExportDefaultDeclaration' || + stmt.type === 'ExportNamedDeclaration' || + stmt.type === 'ImportDeclaration' || + stmt.type === 'ExportAllDeclaration'); +} + +module.exports = { + addBuiltinLibsToObject, + cjsConditions, + hasEsmSyntax, + loadNativeModule, + makeRequireFunction, + normalizeReferrerURL, + stripBOM, +}; diff --git a/raw/node-internal-modules-cjs-loader-2d5d77306f6dff9110c1f77fefab25f973415770.js b/raw/node-internal-modules-cjs-loader-2d5d77306f6dff9110c1f77fefab25f973415770.js new file mode 100644 index 00000000..ffba3418 --- /dev/null +++ b/raw/node-internal-modules-cjs-loader-2d5d77306f6dff9110c1f77fefab25f973415770.js @@ -0,0 +1,1364 @@ +// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ArrayIsArray, + Error, + JSONParse, + Map, + Number, + ObjectCreate, + ObjectDefineProperty, + ObjectFreeze, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectIs, + ObjectKeys, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + ReflectSet, + RegExpPrototypeTest, + SafeMap, + String, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = primordials; + +const { NativeModule } = require('internal/bootstrap/loaders'); +const { + maybeCacheSourceMap, + rekeySourceMap +} = require('internal/source_map/source_map_cache'); +const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); +const { deprecate } = require('internal/util'); +const vm = require('vm'); +const assert = require('internal/assert'); +const fs = require('fs'); +const internalFS = require('internal/fs/utils'); +const path = require('path'); +const { emitWarningSync } = require('internal/process/warning'); +const { + internalModuleReadJSON, + internalModuleStat +} = internalBinding('fs'); +const { safeGetenv } = internalBinding('credentials'); +const { + makeRequireFunction, + normalizeReferrerURL, + stripBOM, + loadNativeModule +} = require('internal/modules/cjs/helpers'); +const { getOptionValue } = require('internal/options'); +const enableSourceMaps = getOptionValue('--enable-source-maps'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +const manifest = getOptionValue('--experimental-policy') ? + require('internal/process/policy').manifest : + null; +const { compileFunction } = internalBinding('contextify'); + +// Whether any user-provided CJS modules had been loaded (executed). +// Used for internal assertions. +let hasLoadedAnyUserCJSModule = false; + +const { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_OPT_VALUE, + ERR_INVALID_PACKAGE_CONFIG, + ERR_INVALID_PACKAGE_TARGET, + ERR_INVALID_MODULE_SPECIFIER, + ERR_PACKAGE_PATH_NOT_EXPORTED, + ERR_REQUIRE_ESM +} = require('internal/errors').codes; +const { validateString } = require('internal/validators'); +const pendingDeprecation = getOptionValue('--pending-deprecation'); + +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + +let asyncESM, ModuleJob, ModuleWrap, kInstantiated; + +const { + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH, + CHAR_COLON +} = require('internal/constants'); + + +const { + isProxy +} = require('internal/util/types'); + +const isWindows = process.platform === 'win32'; + +const relativeResolveCache = ObjectCreate(null); + +let requireDepth = 0; +let statCache = null; + +function enrichCJSError(err) { + const stack = err.stack.split('\n'); + + const lineWithErr = stack[1]; + + /* + The regular expression below targets the most common import statement + usage. However, some cases are not matching, cases like import statement + after a comment block and/or after a variable definition. + */ + if (err.message.startsWith('Unexpected token \'export\'') || + (RegExpPrototypeTest(/^\s*import(?=[ {'"*])\s*(?![ (])/, lineWithErr))) { + // Emit the warning synchronously because we are in the middle of handling + // a SyntaxError that will throw and likely terminate the process before an + // asynchronous warning would be emitted. + emitWarningSync( + 'To load an ES module, set "type": "module" in the package.json or use ' + + 'the .mjs extension.' + ); + } +} + +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null) statCache.set(filename, result); + return result; +} + +function updateChildren(parent, child, scan) { + const children = parent && parent.children; + if (children && !(scan && children.includes(child))) + children.push(child); +} + +function Module(id = '', parent) { + this.id = id; + this.path = path.dirname(id); + this.exports = {}; + this.parent = parent; + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; +} + +const builtinModules = []; +for (const [id, mod] of NativeModule.map) { + if (mod.canBeRequiredByUsers) { + builtinModules.push(id); + } +} + +ObjectFreeze(builtinModules); +Module.builtinModules = builtinModules; + +Module._cache = ObjectCreate(null); +Module._pathCache = ObjectCreate(null); +Module._extensions = ObjectCreate(null); +let modulePaths = []; +Module.globalPaths = []; + +let patched = false; + +// eslint-disable-next-line func-style +let wrap = function(script) { + return Module.wrapper[0] + script + Module.wrapper[1]; +}; + +const wrapper = [ + '(function (exports, require, module, __filename, __dirname) { ', + '\n});' +]; + +let wrapperProxy = new Proxy(wrapper, { + set(target, property, value, receiver) { + patched = true; + return ReflectSet(target, property, value, receiver); + }, + + defineProperty(target, property, descriptor) { + patched = true; + return ObjectDefineProperty(target, property, descriptor); + } +}); + +ObjectDefineProperty(Module, 'wrap', { + get() { + return wrap; + }, + + set(value) { + patched = true; + wrap = value; + } +}); + +ObjectDefineProperty(Module, 'wrapper', { + get() { + return wrapperProxy; + }, + + set(value) { + patched = true; + wrapperProxy = value; + } +}); + +const debug = require('internal/util/debuglog').debuglog('module'); +Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); + +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath)); + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + if (manifest) { + const jsonURL = pathToFileURL(jsonPath); + manifest.assertIntegrity(jsonURL, json); + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = checkPath.indexOf(path.sep); + let separatorIndex; + while ( + (separatorIndex = checkPath.lastIndexOf(path.sep)) > rootSeparatorIndex + ) { + checkPath = checkPath.slice(0, separatorIndex); + if (checkPath.endsWith(path.sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath); + if (pjson) return { + path: checkPath, + data: pjson + }; + } + return false; +} + +function readPackageMain(requestPath) { + const pkg = readPackage(requestPath); + return pkg ? pkg.main : undefined; +} + +function readPackageExports(requestPath) { + const pkg = readPackage(requestPath); + return pkg ? pkg.exports : undefined; +} + +function tryPackage(requestPath, exts, isMain, originalPath) { + const pkg = readPackageMain(requestPath); + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else if (pendingDeprecation) { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new Map(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + [internalFS.realpathCacheKey]: realpathCache + }); +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +// Find the longest (possibly multi-dot) extension registered in +// Module._extensions +function findLongestRegisteredExtension(filename) { + const name = path.basename(filename); + let currentExtension; + let index; + let startIndex = 0; + while ((index = name.indexOf('.', startIndex)) !== -1) { + startIndex = index + 1; + if (index === 0) continue; // Skip dotfiles like .gitignore + currentExtension = name.slice(index); + if (Module._extensions[currentExtension]) return currentExtension; + } + return '.js'; +} + +function trySelf(parentPath, request) { + const { data: pkg, path: basePath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = ''; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + const fromExports = applyExports(basePath, expansion); + if (fromExports) { + return tryFile(fromExports, false); + } + assert(fromExports !== false); +} + +function isConditionalDotExportSugar(exports, basePath) { + if (typeof exports === 'string') + return true; + if (ArrayIsArray(exports)) + return true; + if (typeof exports !== 'object') + return false; + let isConditional = false; + let firstCheck = true; + for (const key of ObjectKeys(exports)) { + const curIsConditional = key[0] !== '.'; + if (firstCheck) { + firstCheck = false; + isConditional = curIsConditional; + } else if (isConditional !== curIsConditional) { + throw new ERR_INVALID_PACKAGE_CONFIG(basePath, '"exports" cannot ' + + 'contain some keys starting with \'.\' and some not. The exports ' + + 'object must either be an object of package subpath keys or an ' + + 'object of main entry condition name keys only.'); + } + } + return isConditional; +} + +function applyExports(basePath, expansion) { + const mappingKey = `.${expansion}`; + + let pkgExports = readPackageExports(basePath); + if (pkgExports === undefined || pkgExports === null) + return false; + + if (isConditionalDotExportSugar(pkgExports, basePath)) + pkgExports = { '.': pkgExports }; + + if (typeof pkgExports === 'object') { + if (ObjectPrototypeHasOwnProperty(pkgExports, mappingKey)) { + const mapping = pkgExports[mappingKey]; + return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', + mappingKey); + } + + let dirMatch = ''; + for (const candidateKey of ObjectKeys(pkgExports)) { + if (candidateKey[candidateKey.length - 1] !== '/') continue; + if (candidateKey.length > dirMatch.length && + StringPrototypeStartsWith(mappingKey, candidateKey)) { + dirMatch = candidateKey; + } + } + + if (dirMatch !== '') { + const mapping = pkgExports[dirMatch]; + const subpath = StringPrototypeSlice(mappingKey, dirMatch.length); + const resolved = resolveExportsTarget(pathToFileURL(basePath + '/'), + mapping, subpath, mappingKey); + // Extension searching for folder exports only + const rc = stat(resolved); + if (rc === 0) return resolved; + if (!(RegExpPrototypeTest(trailingSlashRegex, resolved))) { + const exts = ObjectKeys(Module._extensions); + const filename = tryExtensions(resolved, exts, false); + if (filename) return filename; + } + if (rc === 1) { + const exts = ObjectKeys(Module._extensions); + const filename = tryPackage(resolved, exts, false, + basePath + expansion); + if (filename) return filename; + } + // Undefined means not found + return; + } + } + + throw new ERR_PACKAGE_PATH_NOT_EXPORTED(basePath, mappingKey); +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const [, name, expansion = ''] = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) { + return false; + } + + const basePath = path.resolve(nmPath, name); + const fromExports = applyExports(basePath, expansion); + if (fromExports) { + return tryFile(fromExports, false); + } + return fromExports; +} + +function isArrayIndex(p) { + assert(typeof p === 'string'); + const n = Number(p); + if (String(n) !== p) + return false; + if (ObjectIs(n, +0)) + return true; + if (!Number.isInteger(n)) + return false; + return n >= 0 && n < (2 ** 32) - 1; +} + +function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { + if (typeof target === 'string') { + let resolvedTarget, resolvedTargetPath; + const pkgPathPath = baseUrl.pathname; + if (StringPrototypeStartsWith(target, './')) { + resolvedTarget = new URL(target, baseUrl); + resolvedTargetPath = resolvedTarget.pathname; + if (!StringPrototypeStartsWith(resolvedTargetPath, pkgPathPath) || + StringPrototypeIndexOf(resolvedTargetPath, '/node_modules/', + pkgPathPath.length - 1) !== -1) + resolvedTarget = undefined; + } + if (subpath.length > 0 && target[target.length - 1] !== '/') + resolvedTarget = undefined; + if (resolvedTarget === undefined) + throw new ERR_INVALID_PACKAGE_TARGET(StringPrototypeSlice(baseUrl.pathname + , 0, -1), mappingKey, subpath, target); + const resolved = new URL(subpath, resolvedTarget); + const resolvedPath = resolved.pathname; + if (StringPrototypeStartsWith(resolvedPath, resolvedTargetPath) && + StringPrototypeIndexOf(resolvedPath, '/node_modules/', + pkgPathPath.length - 1) === -1) { + return fileURLToPath(resolved); + } + throw new ERR_INVALID_MODULE_SPECIFIER(StringPrototypeSlice(baseUrl.pathname + , 0, -1), mappingKey); + } else if (ArrayIsArray(target)) { + if (target.length === 0) + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + let lastException; + for (const targetValue of target) { + try { + return resolveExportsTarget(baseUrl, targetValue, subpath, mappingKey); + } catch (e) { + lastException = e; + if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && + e.code !== 'ERR_INVALID_PACKAGE_TARGET') + throw e; + } + } + // Throw last fallback error + assert(lastException !== undefined); + throw lastException; + } else if (typeof target === 'object' && target !== null) { + const keys = ObjectKeys(target); + if (keys.some(isArrayIndex)) { + throw new ERR_INVALID_PACKAGE_CONFIG(baseUrl, '"exports" cannot ' + + 'contain numeric property keys.'); + } + for (const p of keys) { + switch (p) { + case 'node': + case 'require': + try { + return resolveExportsTarget(baseUrl, target[p], subpath, + mappingKey); + } catch (e) { + if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; + } + break; + case 'default': + try { + return resolveExportsTarget(baseUrl, target.default, subpath, + mappingKey); + } catch (e) { + if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; + } + } + } + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + } else if (target === null) { + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + } + throw new ERR_INVALID_PACKAGE_TARGET( + StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target); +} + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +Module._findPath = function(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : paths.join('\x00')); + const entry = Module._pathCache[cacheKey]; + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + // Undefined means not found, false means no exports + if (exportsResolved === undefined) + break; + if (exportsResolved) { + return exportsResolved; + } + } + + const basePath = path.resolve(curPath, request); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module._pathCache[cacheKey] = filename; + return filename; + } + } + + return false; +}; + +// 'node_modules' character codes reversed +const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; +const nmLen = nmChars.length; +if (isWindows) { + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + + // return root node_modules when path is 'D:\\'. + // path.resolve will make sure from.length >=3 in Windows. + if (from.charCodeAt(from.length - 1) === CHAR_BACKWARD_SLASH && + from.charCodeAt(from.length - 2) === CHAR_COLON) + return [from + 'node_modules']; + + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = from.charCodeAt(i); + // The path segment separator check ('\' and '/') was used to get + // node_modules path for every path segment. + // Use colon as an extra condition since we can get node_modules + // path for drive root like 'C:\node_modules' and don't need to + // parse drive name. + if (code === CHAR_BACKWARD_SLASH || + code === CHAR_FORWARD_SLASH || + code === CHAR_COLON) { + if (p !== nmLen) + paths.push(from.slice(0, last) + '\\node_modules'); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + return paths; + }; +} else { // posix + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + // Return early not only to avoid unnecessary work, but to *avoid* returning + // an array of two items for a root: [ '//node_modules', '/node_modules' ] + if (from === '/') + return ['/node_modules']; + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = from.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + if (p !== nmLen) + paths.push(from.slice(0, last) + '/node_modules'); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + // Append /node_modules to handle root paths. + paths.push('/node_modules'); + + return paths; + }; +} + +Module._resolveLookupPaths = function(request, parent) { + if (NativeModule.canBeRequiredByUsers(request)) { + debug('looking for %j in []', request); + return null; + } + + // Check for node modules paths. + if (request.charAt(0) !== '.' || + (request.length > 1 && + request.charAt(1) !== '.' && + request.charAt(1) !== '/' && + (!isWindows || request.charAt(1) !== '\\'))) { + + let paths = modulePaths; + if (parent != null && parent.paths && parent.paths.length) { + paths = parent.paths.concat(paths); + } + + debug('looking for %j in %j', request, paths); + return paths.length > 0 ? paths : null; + } + + // In REPL, parent.filename is null. + if (!parent || !parent.id || !parent.filename) { + // Make require('./path/to/foo') work - normally the path is taken + // from realpath(__filename) but in REPL there is no filename + const mainPaths = ['.']; + + debug('looking for %j in %j', request, mainPaths); + return mainPaths; + } + + debug('RELATIVE: requested: %s from parent.id %s', request, parent.id); + + const parentDir = [path.dirname(parent.filename)]; + debug('looking for %j', parentDir); + return parentDir; +}; + +function emitCircularRequireWarning(prop) { + process.emitWarning( + `Accessing non-existent property '${String(prop)}' of module exports ` + + 'inside circular dependency' + ); +} + +// A Proxy that can be used as the prototype of a module.exports object and +// warns when non-existent properties are accessed. +const CircularRequirePrototypeWarningProxy = new Proxy({}, { + get(target, prop) { + // Allow __esModule access in any case because it is used in the output + // of transpiled code to determine whether something comes from an + // ES module, and is not used as a regular key of `module.exports`. + if (prop in target || prop === '__esModule') return target[prop]; + emitCircularRequireWarning(prop); + return undefined; + }, + + getOwnPropertyDescriptor(target, prop) { + if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule') + return ObjectGetOwnPropertyDescriptor(target, prop); + emitCircularRequireWarning(prop); + return undefined; + } +}); + +// Object.prototype and ObjectPrototype refer to our 'primordials' versions +// and are not identical to the versions on the global object. +const PublicObjectPrototype = global.Object.prototype; + +function getExportsForCircularRequire(module) { + if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === PublicObjectPrototype && + // Exclude transpiled ES6 modules / TypeScript code because those may + // employ unusual patterns for accessing 'module.exports'. That should + // be okay because ES6 modules have a different approach to circular + // dependencies anyway. + !module.exports.__esModule) { + // This is later unset once the module is done loading. + ObjectSetPrototypeOf( + module.exports, CircularRequirePrototypeWarningProxy); + } + + return module.exports; +} + +// Check the cache for the requested file. +// 1. If a module already exists in the cache: return its exports object. +// 2. If the module is native: call +// `NativeModule.prototype.compileForPublicLoader()` and return the exports. +// 3. Otherwise, create a new module for the file and save it to the cache. +// Then have it load the file contents before returning its exports +// object. +Module._load = function(request, parent, isMain) { + let relResolveCacheIdentifier; + if (parent) { + debug('Module._load REQUEST %s parent: %s', request, parent.id); + // Fast path for (lazy loaded) modules in the same directory. The indirect + // caching is required to allow cache invalidation without changing the old + // cache key names. + relResolveCacheIdentifier = `${parent.path}\x00${request}`; + const filename = relativeResolveCache[relResolveCacheIdentifier]; + if (filename !== undefined) { + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + return cachedModule.exports; + } + delete relativeResolveCache[relResolveCacheIdentifier]; + } + } + + const filename = Module._resolveFilename(request, parent, isMain); + + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + return cachedModule.exports; + } + + const mod = loadNativeModule(filename, request); + if (mod && mod.canBeRequiredByUsers) return mod.exports; + + // Don't call updateChildren(), Module constructor already does. + const module = new Module(filename, parent); + + if (isMain) { + process.mainModule = module; + module.id = '.'; + } + + Module._cache[filename] = module; + if (parent !== undefined) { + relativeResolveCache[relResolveCacheIdentifier] = filename; + } + + let threw = true; + try { + // Intercept exceptions that occur during the first tick and rekey them + // on error instance rather than module instance (which will immediately be + // garbage collected). + if (enableSourceMaps) { + try { + module.load(filename); + } catch (err) { + rekeySourceMap(Module._cache[filename], err); + throw err; /* node-do-not-add-exception-line */ + } + } else { + module.load(filename); + } + threw = false; + } finally { + if (threw) { + delete Module._cache[filename]; + if (parent !== undefined) { + delete relativeResolveCache[relResolveCacheIdentifier]; + const children = parent && parent.children; + if (ArrayIsArray(children)) { + const index = children.indexOf(module); + if (index !== -1) { + children.splice(index, 1); + } + } + } + } else if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === + CircularRequirePrototypeWarningProxy) { + ObjectSetPrototypeOf(module.exports, PublicObjectPrototype); + } + } + + return module.exports; +}; + +Module._resolveFilename = function(request, parent, isMain, options) { + if (NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = request.startsWith('./') || + request.startsWith('../') || + ((isWindows && request.startsWith('.\\')) || + request.startsWith('..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!paths.includes(lookupPaths[j])) + paths.push(lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_OPT_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + if (parent && parent.filename) { + const filename = trySelf(parent.filename, request); + if (filename) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : paths.join('\x00')); + Module._pathCache[cacheKey] = filename; + return filename; + } + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = cursor.parent) { + requireStack.push(cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + requireStack.join('\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + + +// Given a file name, pass it to the proper extension handler. +Module.prototype.load = function(filename) { + debug('load %j for module %j', filename, this.id); + + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); + + const extension = findLongestRegisteredExtension(filename); + // allow .mjs to be overridden + if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) { + throw new ERR_REQUIRE_ESM(filename); + } + Module._extensions[extension](this, filename); + this.loaded = true; + + const ESMLoader = asyncESM.ESMLoader; + const url = `${pathToFileURL(filename)}`; + const module = ESMLoader.moduleMap.get(url); + // Create module entry at load time to snapshot exports correctly + const exports = this.exports; + // Called from cjs translator + if (module !== undefined && module.module !== undefined) { + if (module.module.getStatus() >= kInstantiated) + module.module.setExport('default', exports); + } else { + // Preemptively cache + // We use a function to defer promise creation for async hooks. + ESMLoader.moduleMap.set( + url, + // Module job creation will start promises. + // We make it a function to lazily trigger those promises + // for async hooks compatibility. + () => new ModuleJob(ESMLoader, url, () => + new ModuleWrap(url, undefined, ['default'], function() { + this.setExport('default', exports); + }) + , false /* isMain */, false /* inspectBrk */) + ); + } +}; + + +// Loads a module at the given file path. Returns that module's +// `exports` property. +Module.prototype.require = function(id) { + validateString(id, 'id'); + if (id === '') { + throw new ERR_INVALID_ARG_VALUE('id', id, + 'must be a non-empty string'); + } + requireDepth++; + try { + return Module._load(id, this, /* isMain */ false); + } finally { + requireDepth--; + } +}; + + +// Resolved path to process.argv[1] will be lazily placed here +// (needed for setting breakpoint when called with --inspect-brk) +let resolvedArgv; +let hasPausedEntry = false; + +function wrapSafe(filename, content, cjsModuleInstance) { + if (patched) { + const wrapper = Module.wrap(content); + return vm.runInThisContext(wrapper, { + filename, + lineOffset: 0, + displayErrors: true, + importModuleDynamically: async (specifier) => { + const loader = asyncESM.ESMLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + }, + }); + } + let compiled; + try { + compiled = compileFunction( + content, + filename, + 0, + 0, + undefined, + false, + undefined, + [], + [ + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + ] + ); + } catch (err) { + if (process.mainModule === cjsModuleInstance) + enrichCJSError(err); + throw err; + } + + const { callbackMap } = internalBinding('module_wrap'); + callbackMap.set(compiled.cacheKey, { + importModuleDynamically: async (specifier) => { + const loader = asyncESM.ESMLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + } + }); + + return compiled.function; +} + +// Run the file contents in the correct scope or sandbox. Expose +// the correct helper variables (require, module, exports) to +// the file. +// Returns exception, if any. +Module.prototype._compile = function(content, filename) { + let moduleURL; + let redirects; + if (manifest) { + moduleURL = pathToFileURL(filename); + redirects = manifest.getRedirector(moduleURL); + manifest.assertIntegrity(moduleURL, content); + } + + maybeCacheSourceMap(filename, content, this); + const compiledWrapper = wrapSafe(filename, content, this); + + let inspectorWrapper = null; + if (getOptionValue('--inspect-brk') && process._eval == null) { + if (!resolvedArgv) { + // We enter the repl if we're not given a filename argument. + if (process.argv[1]) { + try { + resolvedArgv = Module._resolveFilename(process.argv[1], null, false); + } catch { + // We only expect this codepath to be reached in the case of a + // preloaded module (it will fail earlier with the main entry) + assert(ArrayIsArray(getOptionValue('--require'))); + } + } else { + resolvedArgv = 'repl'; + } + } + + // Set breakpoint on module start + if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) { + hasPausedEntry = true; + inspectorWrapper = internalBinding('inspector').callAndPauseOnStart; + } + } + const dirname = path.dirname(filename); + const require = makeRequireFunction(this, redirects); + let result; + const exports = this.exports; + const thisValue = exports; + const module = this; + if (requireDepth === 0) statCache = new Map(); + if (inspectorWrapper) { + result = inspectorWrapper(compiledWrapper, thisValue, exports, + require, module, filename, dirname); + } else { + result = compiledWrapper.call(thisValue, exports, require, module, + filename, dirname); + } + hasLoadedAnyUserCJSModule = true; + if (requireDepth === 0) statCache = null; + return result; +}; + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + if (filename.endsWith('.js')) { + const pkg = readPackageScope(filename); + // Function require shouldn't be used in ES modules. + if (pkg && pkg.data && pkg.data.type === 'module') { + const parentPath = module.parent && module.parent.filename; + const packageJsonPath = path.resolve(pkg.path, 'package.json'); + throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); + } + } + const content = fs.readFileSync(filename, 'utf8'); + module._compile(content, filename); +}; + + +// Native extension for .json +Module._extensions['.json'] = function(module, filename) { + const content = fs.readFileSync(filename, 'utf8'); + + if (manifest) { + const moduleURL = pathToFileURL(filename); + manifest.assertIntegrity(moduleURL, content); + } + + try { + module.exports = JSONParse(stripBOM(content)); + } catch (err) { + err.message = filename + ': ' + err.message; + throw err; + } +}; + + +// Native extension for .node +Module._extensions['.node'] = function(module, filename) { + if (manifest) { + const content = fs.readFileSync(filename); + const moduleURL = pathToFileURL(filename); + manifest.assertIntegrity(moduleURL, content); + } + // Be aware this doesn't use `content` + return process.dlopen(module, path.toNamespacedPath(filename)); +}; + +function createRequireFromPath(filename) { + // Allow a directory to be passed as the filename + const trailingSlash = + filename.endsWith('/') || (isWindows && filename.endsWith('\\')); + + const proxyPath = trailingSlash ? + path.join(filename, 'noop.js') : + filename; + + const m = new Module(proxyPath); + m.filename = proxyPath; + + m.paths = Module._nodeModulePaths(m.path); + return makeRequireFunction(m, null); +} + +Module.createRequireFromPath = deprecate( + createRequireFromPath, + 'Module.createRequireFromPath() is deprecated. ' + + 'Use Module.createRequire() instead.', + 'DEP0130' +); + +const createRequireError = 'must be a file URL object, file URL string, or ' + + 'absolute path string'; + +function createRequire(filename) { + let filepath; + + if (filename instanceof URL || + (typeof filename === 'string' && !path.isAbsolute(filename))) { + try { + filepath = fileURLToPath(filename); + } catch { + throw new ERR_INVALID_ARG_VALUE('filename', filename, + createRequireError); + } + } else if (typeof filename !== 'string') { + throw new ERR_INVALID_ARG_VALUE('filename', filename, createRequireError); + } else { + filepath = filename; + } + return createRequireFromPath(filepath); +} + +Module.createRequire = createRequire; + +Module._initPaths = function() { + const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME'); + const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH'); + + // process.execPath is $PREFIX/bin/node except on Windows where it is + // $PREFIX\node.exe where $PREFIX is the root of the Node.js installation. + const prefixDir = isWindows ? + path.resolve(process.execPath, '..') : + path.resolve(process.execPath, '..', '..'); + + let paths = [path.resolve(prefixDir, 'lib', 'node')]; + + if (homeDir) { + paths.unshift(path.resolve(homeDir, '.node_libraries')); + paths.unshift(path.resolve(homeDir, '.node_modules')); + } + + if (nodePath) { + paths = nodePath.split(path.delimiter).filter(function pathsFilterCB(path) { + return !!path; + }).concat(paths); + } + + modulePaths = paths; + + // Clone as a shallow copy, for introspection. + Module.globalPaths = modulePaths.slice(0); +}; + +Module._preloadModules = function(requests) { + if (!ArrayIsArray(requests)) + return; + + // Preloaded modules have a dummy parent module which is deemed to exist + // in the current working directory. This seeds the search path for + // preloaded modules. + const parent = new Module('internal/preload', null); + try { + parent.paths = Module._nodeModulePaths(process.cwd()); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + } + for (let n = 0; n < requests.length; n++) + parent.require(requests[n]); +}; + +Module.syncBuiltinESMExports = function syncBuiltinESMExports() { + for (const mod of NativeModule.map.values()) { + if (mod.canBeRequiredByUsers) { + mod.syncExports(); + } + } +}; + +// Backwards compatibility +Module.Module = Module; + +// We have to load the esm things after module.exports! +asyncESM = require('internal/process/esm_loader'); +ModuleJob = require('internal/modules/esm/module_job'); +({ ModuleWrap, kInstantiated } = internalBinding('module_wrap')); diff --git a/raw/node-internal-modules-cjs-loader-v15.3.0-stripped.js b/raw/node-internal-modules-cjs-loader-v15.3.0-stripped.js new file mode 100644 index 00000000..c3380752 --- /dev/null +++ b/raw/node-internal-modules-cjs-loader-v15.3.0-stripped.js @@ -0,0 +1,92 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js + +'use strict'; + +const { + JSONParse, + SafeMap, + StringPrototypeEndsWith, + StringPrototypeLastIndexOf, + StringPrototypeIndexOf, + StringPrototypeSlice, +} = primordials; +const path = require('path'); +const packageJsonReader = require('internal/modules/package_json_reader'); + +const { + ERR_REQUIRE_ESM +} = require('internal/errors').codes; + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const result = packageJsonReader.read(jsonPath); + const json = result.containsKeys === false ? '{}' : result.string; + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + imports: parsed.imports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); + let separatorIndex; + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath + sep); + if (pjson) return { + data: pjson, + path: checkPath, + }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + if (StringPrototypeEndsWith(filename, '.js')) { + const pkg = readPackageScope(filename); + // Function require shouldn't be used in ES modules. + if (pkg && pkg.data && pkg.data.type === 'module') { + const parent = moduleParentCache.get(module); + const parentPath = parent && parent.filename; + const packageJsonPath = path.resolve(pkg.path, 'package.json'); + throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); + } + } + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached && cached.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } + module._compile(content, filename); +}; diff --git a/raw/node-internal-modules-cjs-loader-v15.3.0.js b/raw/node-internal-modules-cjs-loader-v15.3.0.js new file mode 100644 index 00000000..89472479 --- /dev/null +++ b/raw/node-internal-modules-cjs-loader-v15.3.0.js @@ -0,0 +1,1272 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeConcat, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + Boolean, + Error, + JSONParse, + ObjectCreate, + ObjectDefineProperty, + ObjectFreeze, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectPrototype, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + ReflectApply, + ReflectSet, + RegExpPrototypeTest, + SafeMap, + SafeWeakMap, + String, + StringPrototypeCharAt, + StringPrototypeCharCodeAt, + StringPrototypeEndsWith, + StringPrototypeLastIndexOf, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, +} = primordials; + +// Map used to store CJS parsing data. +const cjsParseCache = new SafeWeakMap(); + +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + +const { NativeModule } = require('internal/bootstrap/loaders'); +const { + getSourceMapsEnabled, + maybeCacheSourceMap, + rekeySourceMap +} = require('internal/source_map/source_map_cache'); +const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); +const { deprecate } = require('internal/util'); +const vm = require('vm'); +const assert = require('internal/assert'); +const fs = require('fs'); +const internalFS = require('internal/fs/utils'); +const path = require('path'); +const { sep } = path; +const { internalModuleStat } = internalBinding('fs'); +const packageJsonReader = require('internal/modules/package_json_reader'); +const { safeGetenv } = internalBinding('credentials'); +const { + makeRequireFunction, + normalizeReferrerURL, + stripBOM, + cjsConditions, + loadNativeModule +} = require('internal/modules/cjs/helpers'); +const { getOptionValue } = require('internal/options'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : + null; +const { compileFunction } = internalBinding('contextify'); + +// Whether any user-provided CJS modules had been loaded (executed). +// Used for internal assertions. +let hasLoadedAnyUserCJSModule = false; + +const { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_MODULE_SPECIFIER, + ERR_REQUIRE_ESM +} = require('internal/errors').codes; +const { validateString } = require('internal/validators'); +const pendingDeprecation = getOptionValue('--pending-deprecation'); + +const { + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH, + CHAR_COLON +} = require('internal/constants'); + +const { + isProxy +} = require('internal/util/types'); + +const asyncESM = require('internal/process/esm_loader'); +const { enrichCJSError } = require('internal/modules/esm/translators'); +const { kEvaluated } = internalBinding('module_wrap'); +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = require('internal/modules/esm/resolve'); + +const isWindows = process.platform === 'win32'; + +const relativeResolveCache = ObjectCreate(null); + +let requireDepth = 0; +let statCache = null; + +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null) statCache.set(filename, result); + return result; +} + +function updateChildren(parent, child, scan) { + const children = parent && parent.children; + if (children && !(scan && ArrayPrototypeIncludes(children, child))) + ArrayPrototypePush(children, child); +} + +const moduleParentCache = new SafeWeakMap(); +function Module(id = '', parent) { + this.id = id; + this.path = path.dirname(id); + this.exports = {}; + moduleParentCache.set(this, parent); + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; +} + +const builtinModules = []; +for (const [id, mod] of NativeModule.map) { + if (mod.canBeRequiredByUsers) { + ArrayPrototypePush(builtinModules, id); + } +} + +ObjectFreeze(builtinModules); +Module.builtinModules = builtinModules; + +Module._cache = ObjectCreate(null); +Module._pathCache = ObjectCreate(null); +Module._extensions = ObjectCreate(null); +let modulePaths = []; +Module.globalPaths = []; + +let patched = false; + +// eslint-disable-next-line func-style +let wrap = function(script) { + return Module.wrapper[0] + script + Module.wrapper[1]; +}; + +const wrapper = [ + '(function (exports, require, module, __filename, __dirname) { ', + '\n});' +]; + +let wrapperProxy = new Proxy(wrapper, { + set(target, property, value, receiver) { + patched = true; + return ReflectSet(target, property, value, receiver); + }, + + defineProperty(target, property, descriptor) { + patched = true; + return ObjectDefineProperty(target, property, descriptor); + } +}); + +ObjectDefineProperty(Module, 'wrap', { + get() { + return wrap; + }, + + set(value) { + patched = true; + wrap = value; + } +}); + +ObjectDefineProperty(Module, 'wrapper', { + get() { + return wrapperProxy; + }, + + set(value) { + patched = true; + wrapperProxy = value; + } +}); + +function getModuleParent() { + return moduleParentCache.get(this); +} + +function setModuleParent(value) { + moduleParentCache.set(this, value); +} + +ObjectDefineProperty(Module.prototype, 'parent', { + get: pendingDeprecation ? deprecate( + getModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : getModuleParent, + set: pendingDeprecation ? deprecate( + setModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : setModuleParent, +}); + +let debug = require('internal/util/debuglog').debuglog('module', (fn) => { + debug = fn; +}); +Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); + +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const result = packageJsonReader.read(jsonPath); + const json = result.containsKeys === false ? '{}' : result.string; + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + imports: parsed.imports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); + let separatorIndex; + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath + sep); + if (pjson) return { + data: pjson, + path: checkPath, + }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +function tryPackage(requestPath, exts, isMain, originalPath) { + const pkg = readPackage(requestPath)?.main; + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else if (pendingDeprecation) { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new SafeMap(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + [internalFS.realpathCacheKey]: realpathCache + }); +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +// Find the longest (possibly multi-dot) extension registered in +// Module._extensions +function findLongestRegisteredExtension(filename) { + const name = path.basename(filename); + let currentExtension; + let index; + let startIndex = 0; + while ((index = StringPrototypeIndexOf(name, '.', startIndex)) !== -1) { + startIndex = index + 1; + if (index === 0) continue; // Skip dotfiles like .gitignore + currentExtension = StringPrototypeSlice(name, index); + if (Module._extensions[currentExtension]) return currentExtension; + } + return '.js'; +} + +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + +function trySelf(parentPath, request) { + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = '.'; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions), request, parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const [, name, expansion = ''] = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + if (pkg && pkg.exports !== null && pkg.exports !== undefined) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions), request, null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } + } +} + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +Module._findPath = function(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00'); + const entry = Module._pathCache[cacheKey]; + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) + return exportsResolved; + } + + const basePath = path.resolve(curPath, request); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module._pathCache[cacheKey] = filename; + return filename; + } + } + + return false; +}; + +// 'node_modules' character codes reversed +const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; +const nmLen = nmChars.length; +if (isWindows) { + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + + // return root node_modules when path is 'D:\\'. + // path.resolve will make sure from.length >=3 in Windows. + if (StringPrototypeCharCodeAt(from, from.length - 1) === + CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) + return [from + 'node_modules']; + + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = StringPrototypeCharCodeAt(from, i); + // The path segment separator check ('\' and '/') was used to get + // node_modules path for every path segment. + // Use colon as an extra condition since we can get node_modules + // path for drive root like 'C:\node_modules' and don't need to + // parse drive name. + if (code === CHAR_BACKWARD_SLASH || + code === CHAR_FORWARD_SLASH || + code === CHAR_COLON) { + if (p !== nmLen) + ArrayPrototypePush( + paths, + StringPrototypeSlice(from, 0, last) + '\\node_modules' + ); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + return paths; + }; +} else { // posix + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + // Return early not only to avoid unnecessary work, but to *avoid* returning + // an array of two items for a root: [ '//node_modules', '/node_modules' ] + if (from === '/') + return ['/node_modules']; + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = StringPrototypeCharCodeAt(from, i); + if (code === CHAR_FORWARD_SLASH) { + if (p !== nmLen) + ArrayPrototypePush( + paths, + StringPrototypeSlice(from, 0, last) + '/node_modules' + ); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + // Append /node_modules to handle root paths. + ArrayPrototypePush(paths, '/node_modules'); + + return paths; + }; +} + +Module._resolveLookupPaths = function(request, parent) { + if (NativeModule.canBeRequiredByUsers(request)) { + debug('looking for %j in []', request); + return null; + } + + // Check for node modules paths. + if (StringPrototypeCharAt(request, 0) !== '.' || + (request.length > 1 && + StringPrototypeCharAt(request, 1) !== '.' && + StringPrototypeCharAt(request, 1) !== '/' && + (!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) { + + let paths = modulePaths; + if (parent != null && parent.paths && parent.paths.length) { + paths = ArrayPrototypeConcat(parent.paths, paths); + } + + debug('looking for %j in %j', request, paths); + return paths.length > 0 ? paths : null; + } + + // In REPL, parent.filename is null. + if (!parent || !parent.id || !parent.filename) { + // Make require('./path/to/foo') work - normally the path is taken + // from realpath(__filename) but in REPL there is no filename + const mainPaths = ['.']; + + debug('looking for %j in %j', request, mainPaths); + return mainPaths; + } + + debug('RELATIVE: requested: %s from parent.id %s', request, parent.id); + + const parentDir = [path.dirname(parent.filename)]; + debug('looking for %j', parentDir); + return parentDir; +}; + +function emitCircularRequireWarning(prop) { + process.emitWarning( + `Accessing non-existent property '${String(prop)}' of module exports ` + + 'inside circular dependency' + ); +} + +// A Proxy that can be used as the prototype of a module.exports object and +// warns when non-existent properties are accessed. +const CircularRequirePrototypeWarningProxy = new Proxy({}, { + get(target, prop) { + // Allow __esModule access in any case because it is used in the output + // of transpiled code to determine whether something comes from an + // ES module, and is not used as a regular key of `module.exports`. + if (prop in target || prop === '__esModule') return target[prop]; + emitCircularRequireWarning(prop); + return undefined; + }, + + getOwnPropertyDescriptor(target, prop) { + if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule') + return ObjectGetOwnPropertyDescriptor(target, prop); + emitCircularRequireWarning(prop); + return undefined; + } +}); + +function getExportsForCircularRequire(module) { + if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === ObjectPrototype && + // Exclude transpiled ES6 modules / TypeScript code because those may + // employ unusual patterns for accessing 'module.exports'. That should + // be okay because ES6 modules have a different approach to circular + // dependencies anyway. + !module.exports.__esModule) { + // This is later unset once the module is done loading. + ObjectSetPrototypeOf( + module.exports, CircularRequirePrototypeWarningProxy); + } + + return module.exports; +} + +// Check the cache for the requested file. +// 1. If a module already exists in the cache: return its exports object. +// 2. If the module is native: call +// `NativeModule.prototype.compileForPublicLoader()` and return the exports. +// 3. Otherwise, create a new module for the file and save it to the cache. +// Then have it load the file contents before returning its exports +// object. +Module._load = function(request, parent, isMain) { + let relResolveCacheIdentifier; + if (parent) { + debug('Module._load REQUEST %s parent: %s', request, parent.id); + // Fast path for (lazy loaded) modules in the same directory. The indirect + // caching is required to allow cache invalidation without changing the old + // cache key names. + relResolveCacheIdentifier = `${parent.path}\x00${request}`; + const filename = relativeResolveCache[relResolveCacheIdentifier]; + if (filename !== undefined) { + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + return cachedModule.exports; + } + delete relativeResolveCache[relResolveCacheIdentifier]; + } + } + + const filename = Module._resolveFilename(request, parent, isMain); + + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) { + const parseCachedModule = cjsParseCache.get(cachedModule); + if (!parseCachedModule || parseCachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + parseCachedModule.loaded = true; + } else { + return cachedModule.exports; + } + } + + const mod = loadNativeModule(filename, request); + if (mod && mod.canBeRequiredByUsers) return mod.exports; + + // Don't call updateChildren(), Module constructor already does. + const module = cachedModule || new Module(filename, parent); + + if (isMain) { + process.mainModule = module; + module.id = '.'; + } + + Module._cache[filename] = module; + if (parent !== undefined) { + relativeResolveCache[relResolveCacheIdentifier] = filename; + } + + let threw = true; + try { + // Intercept exceptions that occur during the first tick and rekey them + // on error instance rather than module instance (which will immediately be + // garbage collected). + if (getSourceMapsEnabled()) { + try { + module.load(filename); + } catch (err) { + rekeySourceMap(Module._cache[filename], err); + throw err; /* node-do-not-add-exception-line */ + } + } else { + module.load(filename); + } + threw = false; + } finally { + if (threw) { + delete Module._cache[filename]; + if (parent !== undefined) { + delete relativeResolveCache[relResolveCacheIdentifier]; + const children = parent && parent.children; + if (ArrayIsArray(children)) { + const index = ArrayPrototypeIndexOf(children, module); + if (index !== -1) { + ArrayPrototypeSplice(children, index, 1); + } + } + } + } else if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === + CircularRequirePrototypeWarningProxy) { + ObjectSetPrototypeOf(module.exports, ObjectPrototype); + } + } + + return module.exports; +}; + +Module._resolveFilename = function(request, parent, isMain, options) { + if (NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = StringPrototypeStartsWith(request, './') || + StringPrototypeStartsWith(request, '../') || + ((isWindows && StringPrototypeStartsWith(request, '.\\')) || + StringPrototypeStartsWith(request, '..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) + ArrayPrototypePush(paths, lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + if (parent && parent.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + if (pkg.data && pkg.data.imports !== null && + pkg.data.imports !== undefined) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), request, parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } + } + } + + // Try module self resoultion first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = moduleParentCache.get(cursor)) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + + ArrayPrototypeJoin(requireStack, '\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + +function finalizeEsmResolution(match, request, parentPath, pkgPath) { + const { resolved, exact } = match; + if (RegExpPrototypeTest(encodedSepRegEx, resolved)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + let actual = tryFile(filename); + if (!exact && !actual) { + const exts = ObjectKeys(Module._extensions); + actual = tryExtensions(filename, exts, false) || + tryPackage(filename, exts, false, request); + } + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} + +// Given a file name, pass it to the proper extension handler. +Module.prototype.load = function(filename) { + debug('load %j for module %j', filename, this.id); + + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); + + const extension = findLongestRegisteredExtension(filename); + // allow .mjs to be overridden + if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) + throw new ERR_REQUIRE_ESM(filename); + + Module._extensions[extension](this, filename); + this.loaded = true; + + const ESMLoader = asyncESM.ESMLoader; + // Create module entry at load time to snapshot exports correctly + const exports = this.exports; + // Preemptively cache + if ((module?.module === undefined || + module.module.getStatus() < kEvaluated) && + !ESMLoader.cjsCache.has(this)) + ESMLoader.cjsCache.set(this, exports); +}; + + +// Loads a module at the given file path. Returns that module's +// `exports` property. +Module.prototype.require = function(id) { + validateString(id, 'id'); + if (id === '') { + throw new ERR_INVALID_ARG_VALUE('id', id, + 'must be a non-empty string'); + } + requireDepth++; + try { + return Module._load(id, this, /* isMain */ false); + } finally { + requireDepth--; + } +}; + + +// Resolved path to process.argv[1] will be lazily placed here +// (needed for setting breakpoint when called with --inspect-brk) +let resolvedArgv; +let hasPausedEntry = false; + +function wrapSafe(filename, content, cjsModuleInstance) { + if (patched) { + const wrapper = Module.wrap(content); + return vm.runInThisContext(wrapper, { + filename, + lineOffset: 0, + displayErrors: true, + importModuleDynamically: async (specifier) => { + const loader = asyncESM.ESMLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + }, + }); + } + let compiled; + try { + compiled = compileFunction( + content, + filename, + 0, + 0, + undefined, + false, + undefined, + [], + [ + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + ] + ); + } catch (err) { + if (process.mainModule === cjsModuleInstance) + enrichCJSError(err); + throw err; + } + + const { callbackMap } = internalBinding('module_wrap'); + callbackMap.set(compiled.cacheKey, { + importModuleDynamically: async (specifier) => { + const loader = asyncESM.ESMLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + } + }); + + return compiled.function; +} + +// Run the file contents in the correct scope or sandbox. Expose +// the correct helper variables (require, module, exports) to +// the file. +// Returns exception, if any. +Module.prototype._compile = function(content, filename) { + let moduleURL; + let redirects; + if (policy?.manifest) { + moduleURL = pathToFileURL(filename); + redirects = policy.manifest.getDependencyMapper(moduleURL); + policy.manifest.assertIntegrity(moduleURL, content); + } + + maybeCacheSourceMap(filename, content, this); + const compiledWrapper = wrapSafe(filename, content, this); + + let inspectorWrapper = null; + if (getOptionValue('--inspect-brk') && process._eval == null) { + if (!resolvedArgv) { + // We enter the repl if we're not given a filename argument. + if (process.argv[1]) { + try { + resolvedArgv = Module._resolveFilename(process.argv[1], null, false); + } catch { + // We only expect this codepath to be reached in the case of a + // preloaded module (it will fail earlier with the main entry) + assert(ArrayIsArray(getOptionValue('--require'))); + } + } else { + resolvedArgv = 'repl'; + } + } + + // Set breakpoint on module start + if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) { + hasPausedEntry = true; + inspectorWrapper = internalBinding('inspector').callAndPauseOnStart; + } + } + const dirname = path.dirname(filename); + const require = makeRequireFunction(this, redirects); + let result; + const exports = this.exports; + const thisValue = exports; + const module = this; + if (requireDepth === 0) statCache = new SafeMap(); + if (inspectorWrapper) { + result = inspectorWrapper(compiledWrapper, thisValue, exports, + require, module, filename, dirname); + } else { + result = ReflectApply(compiledWrapper, thisValue, + [exports, require, module, filename, dirname]); + } + hasLoadedAnyUserCJSModule = true; + if (requireDepth === 0) statCache = null; + return result; +}; + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + if (StringPrototypeEndsWith(filename, '.js')) { + const pkg = readPackageScope(filename); + // Function require shouldn't be used in ES modules. + if (pkg && pkg.data && pkg.data.type === 'module') { + const parent = moduleParentCache.get(module); + const parentPath = parent && parent.filename; + const packageJsonPath = path.resolve(pkg.path, 'package.json'); + throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); + } + } + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached && cached.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } + module._compile(content, filename); +}; + + +// Native extension for .json +Module._extensions['.json'] = function(module, filename) { + const content = fs.readFileSync(filename, 'utf8'); + + if (policy?.manifest) { + const moduleURL = pathToFileURL(filename); + policy.manifest.assertIntegrity(moduleURL, content); + } + + try { + module.exports = JSONParse(stripBOM(content)); + } catch (err) { + err.message = filename + ': ' + err.message; + throw err; + } +}; + + +// Native extension for .node +Module._extensions['.node'] = function(module, filename) { + if (policy?.manifest) { + const content = fs.readFileSync(filename); + const moduleURL = pathToFileURL(filename); + policy.manifest.assertIntegrity(moduleURL, content); + } + // Be aware this doesn't use `content` + return process.dlopen(module, path.toNamespacedPath(filename)); +}; + +function createRequireFromPath(filename) { + // Allow a directory to be passed as the filename + const trailingSlash = + StringPrototypeEndsWith(filename, '/') || + (isWindows && StringPrototypeEndsWith(filename, '\\')); + + const proxyPath = trailingSlash ? + path.join(filename, 'noop.js') : + filename; + + const m = new Module(proxyPath); + m.filename = proxyPath; + + m.paths = Module._nodeModulePaths(m.path); + return makeRequireFunction(m, null); +} + +Module.createRequireFromPath = deprecate( + createRequireFromPath, + 'Module.createRequireFromPath() is deprecated. ' + + 'Use Module.createRequire() instead.', + 'DEP0130' +); + +const createRequireError = 'must be a file URL object, file URL string, or ' + + 'absolute path string'; + +function createRequire(filename) { + let filepath; + + if (isURLInstance(filename) || + (typeof filename === 'string' && !path.isAbsolute(filename))) { + try { + filepath = fileURLToPath(filename); + } catch { + throw new ERR_INVALID_ARG_VALUE('filename', filename, + createRequireError); + } + } else if (typeof filename !== 'string') { + throw new ERR_INVALID_ARG_VALUE('filename', filename, createRequireError); + } else { + filepath = filename; + } + return createRequireFromPath(filepath); +} + +Module.createRequire = createRequire; + +Module._initPaths = function() { + const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME'); + const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH'); + + // process.execPath is $PREFIX/bin/node except on Windows where it is + // $PREFIX\node.exe where $PREFIX is the root of the Node.js installation. + const prefixDir = isWindows ? + path.resolve(process.execPath, '..') : + path.resolve(process.execPath, '..', '..'); + + let paths = [path.resolve(prefixDir, 'lib', 'node')]; + + if (homeDir) { + paths.unshift(path.resolve(homeDir, '.node_libraries')); + paths.unshift(path.resolve(homeDir, '.node_modules')); + } + + if (nodePath) { + paths = ArrayPrototypeConcat(ArrayPrototypeFilter( + StringPrototypeSplit(nodePath, path.delimiter), + Boolean + ), paths); + } + + modulePaths = paths; + + // Clone as a shallow copy, for introspection. + Module.globalPaths = ArrayPrototypeSlice(modulePaths); +}; + +Module._preloadModules = function(requests) { + if (!ArrayIsArray(requests)) + return; + + // Preloaded modules have a dummy parent module which is deemed to exist + // in the current working directory. This seeds the search path for + // preloaded modules. + const parent = new Module('internal/preload', null); + try { + parent.paths = Module._nodeModulePaths(process.cwd()); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + } + for (let n = 0; n < requests.length; n++) + parent.require(requests[n]); +}; + +Module.syncBuiltinESMExports = function syncBuiltinESMExports() { + for (const mod of NativeModule.map.values()) { + if (mod.canBeRequiredByUsers) { + mod.syncExports(); + } + } +}; + +// Backwards compatibility +Module.Module = Module; diff --git a/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js b/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js new file mode 100644 index 00000000..5788979d --- /dev/null +++ b/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js @@ -0,0 +1,554 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/loader.js + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeConcat, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + ArrayPrototypeUnshift, + ArrayPrototypeUnshiftApply, + Boolean, + Error, + JSONParse, + ObjectCreate, + ObjectDefineProperty, + ObjectFreeze, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectPrototype, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + Proxy, + ReflectApply, + ReflectSet, + RegExpPrototypeExec, + RegExpPrototypeTest, + SafeMap, + SafeWeakMap, + String, + StringPrototypeCharAt, + StringPrototypeCharCodeAt, + StringPrototypeEndsWith, + StringPrototypeLastIndexOf, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, +} = primordials; + +const { NativeModule } = require('internal/bootstrap/loaders'); +const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); +const fs = require('fs'); +const internalFS = require('internal/fs/utils'); +const path = require('path'); +const { sep } = path; +const { internalModuleStat } = internalBinding('fs'); +const packageJsonReader = require('internal/modules/package_json_reader'); +const { + cjsConditions, + hasEsmSyntax, +} = require('internal/modules/cjs/helpers'); +const { getOptionValue } = require('internal/options'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : + null; + +const { + codes: { + ERR_INVALID_MODULE_SPECIFIER, + ERR_REQUIRE_ESM, + }, +} = require('internal/errors'); + +const { + CHAR_FORWARD_SLASH, +} = require('internal/constants'); + +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = require('internal/modules/esm/resolve'); + +const isWindows = process.platform === 'win32'; + +const relativeResolveCache = ObjectCreate(null); + +let requireDepth = 0; +let statCache = null; +let isPreloading = false; + +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null && result >= 0) { + // Only set cache when `internalModuleStat(filename)` succeeds. + statCache.set(filename, result); + } + return result; +} + +const moduleParentCache = new SafeWeakMap(); + +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const result = packageJsonReader.read(jsonPath); + const json = result.containsKeys === false ? '{}' : result.string; + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + imports: parsed.imports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); + let separatorIndex; + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath + sep); + if (pjson) return { + data: pjson, + path: checkPath, + }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +function tryPackage(requestPath, exts, isMain, originalPath) { + const pkg = readPackage(requestPath)?.main; + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new SafeMap(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + [internalFS.realpathCacheKey]: realpathCache + }); +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + +function trySelf(parentPath, request) { + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = '.'; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions), parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const { 1: name, 2: expansion = '' } = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + if (pkg?.exports != null) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions), null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } + } +} + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +Module._findPath = function(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00'); + const entry = Module._pathCache[cacheKey]; + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) + return exportsResolved; + } + + const basePath = path.resolve(curPath, request); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module._pathCache[cacheKey] = filename; + return filename; + } + } + + return false; +}; + +Module._resolveFilename = function(request, parent, isMain, options) { + if (StringPrototypeStartsWith(request, 'node:') || + NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = StringPrototypeStartsWith(request, './') || + StringPrototypeStartsWith(request, '../') || + ((isWindows && StringPrototypeStartsWith(request, '.\\')) || + StringPrototypeStartsWith(request, '..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) + ArrayPrototypePush(paths, lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + if (parent?.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + if (pkg.data?.imports != null) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } + } + } + + // Try module self resolution first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = moduleParentCache.get(cursor)) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + + ArrayPrototypeJoin(requireStack, '\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + +function finalizeEsmResolution(resolved, parentPath, pkgPath) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + const actual = tryFile(filename); + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached?.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } + if (StringPrototypeEndsWith(filename, '.js')) { + const pkg = readPackageScope(filename); + // Function require shouldn't be used in ES modules. + if (pkg?.data?.type === 'module') { + const parent = moduleParentCache.get(module); + const parentPath = parent?.filename; + const packageJsonPath = path.resolve(pkg.path, 'package.json'); + const usesEsm = hasEsmSyntax(content); + const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath, + packageJsonPath); + // Attempt to reconstruct the parent require frame. + if (Module._cache[parentPath]) { + let parentSource; + try { + parentSource = fs.readFileSync(parentPath, 'utf8'); + } catch {} + if (parentSource) { + const errLine = StringPrototypeSplit( + StringPrototypeSlice(err.stack, StringPrototypeIndexOf( + err.stack, ' at ')), '\n', 1)[0]; + const { 1: line, 2: col } = + RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || []; + if (line && col) { + const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1]; + const frame = `${parentPath}:${line}\n${srcLine}\n${ + StringPrototypeRepeat(' ', col - 1)}^\n`; + setArrowMessage(err, frame); + } + } + } + throw err; + } + } + module._compile(content, filename); +}; diff --git a/raw/node-internal-modules-cjs-loader-v17.0.1.js b/raw/node-internal-modules-cjs-loader-v17.0.1.js new file mode 100644 index 00000000..95f9a158 --- /dev/null +++ b/raw/node-internal-modules-cjs-loader-v17.0.1.js @@ -0,0 +1,1285 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/loader.js + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeConcat, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + ArrayPrototypeUnshift, + ArrayPrototypeUnshiftApply, + Boolean, + Error, + JSONParse, + ObjectCreate, + ObjectDefineProperty, + ObjectFreeze, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectPrototype, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + Proxy, + ReflectApply, + ReflectSet, + RegExpPrototypeExec, + RegExpPrototypeTest, + SafeMap, + SafeWeakMap, + String, + StringPrototypeCharAt, + StringPrototypeCharCodeAt, + StringPrototypeEndsWith, + StringPrototypeLastIndexOf, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, +} = primordials; + +// Map used to store CJS parsing data. +const cjsParseCache = new SafeWeakMap(); + +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + +const { NativeModule } = require('internal/bootstrap/loaders'); +const { + maybeCacheSourceMap, +} = require('internal/source_map/source_map_cache'); +const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); +const { deprecate } = require('internal/util'); +const vm = require('vm'); +const assert = require('internal/assert'); +const fs = require('fs'); +const internalFS = require('internal/fs/utils'); +const path = require('path'); +const { sep } = path; +const { internalModuleStat } = internalBinding('fs'); +const packageJsonReader = require('internal/modules/package_json_reader'); +const { safeGetenv } = internalBinding('credentials'); +const { + cjsConditions, + hasEsmSyntax, + loadNativeModule, + makeRequireFunction, + normalizeReferrerURL, + stripBOM, +} = require('internal/modules/cjs/helpers'); +const { getOptionValue } = require('internal/options'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : + null; + +// Whether any user-provided CJS modules had been loaded (executed). +// Used for internal assertions. +let hasLoadedAnyUserCJSModule = false; + +const { + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_MODULE_SPECIFIER, + ERR_REQUIRE_ESM, + ERR_UNKNOWN_BUILTIN_MODULE, + }, + setArrowMessage, +} = require('internal/errors'); +const { validateString } = require('internal/validators'); +const pendingDeprecation = getOptionValue('--pending-deprecation'); + +const { + CHAR_FORWARD_SLASH, + CHAR_BACKWARD_SLASH, + CHAR_COLON +} = require('internal/constants'); + +const { + isProxy +} = require('internal/util/types'); + +const asyncESM = require('internal/process/esm_loader'); +const { enrichCJSError } = require('internal/modules/esm/translators'); +const { kEvaluated } = internalBinding('module_wrap'); +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = require('internal/modules/esm/resolve'); + +const isWindows = process.platform === 'win32'; + +const relativeResolveCache = ObjectCreate(null); + +let requireDepth = 0; +let statCache = null; +let isPreloading = false; + +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null && result >= 0) { + // Only set cache when `internalModuleStat(filename)` succeeds. + statCache.set(filename, result); + } + return result; +} + +function updateChildren(parent, child, scan) { + const children = parent?.children; + if (children && !(scan && ArrayPrototypeIncludes(children, child))) + ArrayPrototypePush(children, child); +} + +const moduleParentCache = new SafeWeakMap(); +function Module(id = '', parent) { + this.id = id; + this.path = path.dirname(id); + this.exports = {}; + moduleParentCache.set(this, parent); + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; +} + +const builtinModules = []; +for (const { 0: id, 1: mod } of NativeModule.map) { + if (mod.canBeRequiredByUsers) { + ArrayPrototypePush(builtinModules, id); + } +} + +ObjectFreeze(builtinModules); +Module.builtinModules = builtinModules; + +Module._cache = ObjectCreate(null); +Module._pathCache = ObjectCreate(null); +Module._extensions = ObjectCreate(null); +let modulePaths = []; +Module.globalPaths = []; + +let patched = false; + +// eslint-disable-next-line func-style +let wrap = function(script) { + return Module.wrapper[0] + script + Module.wrapper[1]; +}; + +const wrapper = [ + '(function (exports, require, module, __filename, __dirname) { ', + '\n});', +]; + +let wrapperProxy = new Proxy(wrapper, { + set(target, property, value, receiver) { + patched = true; + return ReflectSet(target, property, value, receiver); + }, + + defineProperty(target, property, descriptor) { + patched = true; + return ObjectDefineProperty(target, property, descriptor); + } +}); + +ObjectDefineProperty(Module, 'wrap', { + get() { + return wrap; + }, + + set(value) { + patched = true; + wrap = value; + } +}); + +ObjectDefineProperty(Module, 'wrapper', { + get() { + return wrapperProxy; + }, + + set(value) { + patched = true; + wrapperProxy = value; + } +}); + +const isPreloadingDesc = { get() { return isPreloading; } }; +ObjectDefineProperty(Module.prototype, 'isPreloading', isPreloadingDesc); +ObjectDefineProperty(NativeModule.prototype, 'isPreloading', isPreloadingDesc); + +function getModuleParent() { + return moduleParentCache.get(this); +} + +function setModuleParent(value) { + moduleParentCache.set(this, value); +} + +ObjectDefineProperty(Module.prototype, 'parent', { + get: pendingDeprecation ? deprecate( + getModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : getModuleParent, + set: pendingDeprecation ? deprecate( + setModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : setModuleParent, +}); + +let debug = require('internal/util/debuglog').debuglog('module', (fn) => { + debug = fn; +}); +Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); + +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. + +const packageJsonCache = new SafeMap(); + +function readPackage(requestPath) { + const jsonPath = path.resolve(requestPath, 'package.json'); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) return existing; + + const result = packageJsonReader.read(jsonPath); + const json = result.containsKeys === false ? '{}' : result.string; + if (json === undefined) { + packageJsonCache.set(jsonPath, false); + return false; + } + + try { + const parsed = JSONParse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + imports: parsed.imports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = 'Error parsing ' + jsonPath + ': ' + e.message; + throw e; + } +} + +function readPackageScope(checkPath) { + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); + let separatorIndex; + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) + return false; + const pjson = readPackage(checkPath + sep); + if (pjson) return { + data: pjson, + path: checkPath, + }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +function tryPackage(requestPath, exts, isMain, originalPath) { + const pkg = readPackage(requestPath)?.main; + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new SafeMap(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + [internalFS.realpathCacheKey]: realpathCache + }); +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +// Find the longest (possibly multi-dot) extension registered in +// Module._extensions +function findLongestRegisteredExtension(filename) { + const name = path.basename(filename); + let currentExtension; + let index; + let startIndex = 0; + while ((index = StringPrototypeIndexOf(name, '.', startIndex)) !== -1) { + startIndex = index + 1; + if (index === 0) continue; // Skip dotfiles like .gitignore + currentExtension = StringPrototypeSlice(name, index); + if (Module._extensions[currentExtension]) return currentExtension; + } + return '.js'; +} + +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + +function trySelf(parentPath, request) { + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = '.'; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions), parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const { 1: name, 2: expansion = '' } = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + if (pkg?.exports != null) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions), null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } + } +} + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +Module._findPath = function(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00'); + const entry = Module._pathCache[cacheKey]; + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) + return exportsResolved; + } + + const basePath = path.resolve(curPath, request); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module._pathCache[cacheKey] = filename; + return filename; + } + } + + return false; +}; + +// 'node_modules' character codes reversed +const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; +const nmLen = nmChars.length; +if (isWindows) { + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + + // return root node_modules when path is 'D:\\'. + // path.resolve will make sure from.length >=3 in Windows. + if (StringPrototypeCharCodeAt(from, from.length - 1) === + CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) + return [from + 'node_modules']; + + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = StringPrototypeCharCodeAt(from, i); + // The path segment separator check ('\' and '/') was used to get + // node_modules path for every path segment. + // Use colon as an extra condition since we can get node_modules + // path for drive root like 'C:\node_modules' and don't need to + // parse drive name. + if (code === CHAR_BACKWARD_SLASH || + code === CHAR_FORWARD_SLASH || + code === CHAR_COLON) { + if (p !== nmLen) + ArrayPrototypePush( + paths, + StringPrototypeSlice(from, 0, last) + '\\node_modules' + ); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + return paths; + }; +} else { // posix + // 'from' is the __dirname of the module. + Module._nodeModulePaths = function(from) { + // Guarantee that 'from' is absolute. + from = path.resolve(from); + // Return early not only to avoid unnecessary work, but to *avoid* returning + // an array of two items for a root: [ '//node_modules', '/node_modules' ] + if (from === '/') + return ['/node_modules']; + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + const paths = []; + for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { + const code = StringPrototypeCharCodeAt(from, i); + if (code === CHAR_FORWARD_SLASH) { + if (p !== nmLen) + ArrayPrototypePush( + paths, + StringPrototypeSlice(from, 0, last) + '/node_modules' + ); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + // Append /node_modules to handle root paths. + ArrayPrototypePush(paths, '/node_modules'); + + return paths; + }; +} + +Module._resolveLookupPaths = function(request, parent) { + if (NativeModule.canBeRequiredByUsers(request)) { + debug('looking for %j in []', request); + return null; + } + + // Check for node modules paths. + if (StringPrototypeCharAt(request, 0) !== '.' || + (request.length > 1 && + StringPrototypeCharAt(request, 1) !== '.' && + StringPrototypeCharAt(request, 1) !== '/' && + (!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) { + + let paths = modulePaths; + if (parent?.paths?.length) { + paths = ArrayPrototypeConcat(parent.paths, paths); + } + + debug('looking for %j in %j', request, paths); + return paths.length > 0 ? paths : null; + } + + // In REPL, parent.filename is null. + if (!parent || !parent.id || !parent.filename) { + // Make require('./path/to/foo') work - normally the path is taken + // from realpath(__filename) but in REPL there is no filename + const mainPaths = ['.']; + + debug('looking for %j in %j', request, mainPaths); + return mainPaths; + } + + debug('RELATIVE: requested: %s from parent.id %s', request, parent.id); + + const parentDir = [path.dirname(parent.filename)]; + debug('looking for %j', parentDir); + return parentDir; +}; + +function emitCircularRequireWarning(prop) { + process.emitWarning( + `Accessing non-existent property '${String(prop)}' of module exports ` + + 'inside circular dependency' + ); +} + +// A Proxy that can be used as the prototype of a module.exports object and +// warns when non-existent properties are accessed. +const CircularRequirePrototypeWarningProxy = new Proxy({}, { + get(target, prop) { + // Allow __esModule access in any case because it is used in the output + // of transpiled code to determine whether something comes from an + // ES module, and is not used as a regular key of `module.exports`. + if (prop in target || prop === '__esModule') return target[prop]; + emitCircularRequireWarning(prop); + return undefined; + }, + + getOwnPropertyDescriptor(target, prop) { + if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule') + return ObjectGetOwnPropertyDescriptor(target, prop); + emitCircularRequireWarning(prop); + return undefined; + } +}); + +function getExportsForCircularRequire(module) { + if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === ObjectPrototype && + // Exclude transpiled ES6 modules / TypeScript code because those may + // employ unusual patterns for accessing 'module.exports'. That should + // be okay because ES6 modules have a different approach to circular + // dependencies anyway. + !module.exports.__esModule) { + // This is later unset once the module is done loading. + ObjectSetPrototypeOf( + module.exports, CircularRequirePrototypeWarningProxy); + } + + return module.exports; +} + +// Check the cache for the requested file. +// 1. If a module already exists in the cache: return its exports object. +// 2. If the module is native: call +// `NativeModule.prototype.compileForPublicLoader()` and return the exports. +// 3. Otherwise, create a new module for the file and save it to the cache. +// Then have it load the file contents before returning its exports +// object. +Module._load = function(request, parent, isMain) { + let relResolveCacheIdentifier; + if (parent) { + debug('Module._load REQUEST %s parent: %s', request, parent.id); + // Fast path for (lazy loaded) modules in the same directory. The indirect + // caching is required to allow cache invalidation without changing the old + // cache key names. + relResolveCacheIdentifier = `${parent.path}\x00${request}`; + const filename = relativeResolveCache[relResolveCacheIdentifier]; + if (filename !== undefined) { + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + return cachedModule.exports; + } + delete relativeResolveCache[relResolveCacheIdentifier]; + } + } + + const filename = Module._resolveFilename(request, parent, isMain); + if (StringPrototypeStartsWith(filename, 'node:')) { + // Slice 'node:' prefix + const id = StringPrototypeSlice(filename, 5); + + const module = loadNativeModule(id, request); + if (!module?.canBeRequiredByUsers) { + throw new ERR_UNKNOWN_BUILTIN_MODULE(filename); + } + + return module.exports; + } + + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) { + const parseCachedModule = cjsParseCache.get(cachedModule); + if (!parseCachedModule || parseCachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + parseCachedModule.loaded = true; + } else { + return cachedModule.exports; + } + } + + const mod = loadNativeModule(filename, request); + if (mod?.canBeRequiredByUsers) return mod.exports; + + // Don't call updateChildren(), Module constructor already does. + const module = cachedModule || new Module(filename, parent); + + if (isMain) { + process.mainModule = module; + module.id = '.'; + } + + Module._cache[filename] = module; + if (parent !== undefined) { + relativeResolveCache[relResolveCacheIdentifier] = filename; + } + + let threw = true; + try { + module.load(filename); + threw = false; + } finally { + if (threw) { + delete Module._cache[filename]; + if (parent !== undefined) { + delete relativeResolveCache[relResolveCacheIdentifier]; + const children = parent?.children; + if (ArrayIsArray(children)) { + const index = ArrayPrototypeIndexOf(children, module); + if (index !== -1) { + ArrayPrototypeSplice(children, index, 1); + } + } + } + } else if (module.exports && + !isProxy(module.exports) && + ObjectGetPrototypeOf(module.exports) === + CircularRequirePrototypeWarningProxy) { + ObjectSetPrototypeOf(module.exports, ObjectPrototype); + } + } + + return module.exports; +}; + +Module._resolveFilename = function(request, parent, isMain, options) { + if (StringPrototypeStartsWith(request, 'node:') || + NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = StringPrototypeStartsWith(request, './') || + StringPrototypeStartsWith(request, '../') || + ((isWindows && StringPrototypeStartsWith(request, '.\\')) || + StringPrototypeStartsWith(request, '..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) + ArrayPrototypePush(paths, lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + if (parent?.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + if (pkg.data?.imports != null) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } + } + } + + // Try module self resolution first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = moduleParentCache.get(cursor)) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + + ArrayPrototypeJoin(requireStack, '\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + +function finalizeEsmResolution(resolved, parentPath, pkgPath) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + const actual = tryFile(filename); + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} + +// Given a file name, pass it to the proper extension handler. +Module.prototype.load = function(filename) { + debug('load %j for module %j', filename, this.id); + + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); + + const extension = findLongestRegisteredExtension(filename); + // allow .mjs to be overridden + if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) + throw new ERR_REQUIRE_ESM(filename, true); + + Module._extensions[extension](this, filename); + this.loaded = true; + + const esmLoader = asyncESM.esmLoader; + // Create module entry at load time to snapshot exports correctly + const exports = this.exports; + // Preemptively cache + if ((module?.module === undefined || + module.module.getStatus() < kEvaluated) && + !esmLoader.cjsCache.has(this)) + esmLoader.cjsCache.set(this, exports); +}; + + +// Loads a module at the given file path. Returns that module's +// `exports` property. +Module.prototype.require = function(id) { + validateString(id, 'id'); + if (id === '') { + throw new ERR_INVALID_ARG_VALUE('id', id, + 'must be a non-empty string'); + } + requireDepth++; + try { + return Module._load(id, this, /* isMain */ false); + } finally { + requireDepth--; + } +}; + + +// Resolved path to process.argv[1] will be lazily placed here +// (needed for setting breakpoint when called with --inspect-brk) +let resolvedArgv; +let hasPausedEntry = false; + +function wrapSafe(filename, content, cjsModuleInstance) { + if (patched) { + const wrapper = Module.wrap(content); + return vm.runInThisContext(wrapper, { + filename, + lineOffset: 0, + displayErrors: true, + importModuleDynamically: async (specifier) => { + const loader = asyncESM.esmLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + }, + }); + } + try { + return vm.compileFunction(content, [ + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + ], { + filename, + importModuleDynamically(specifier) { + const loader = asyncESM.esmLoader; + return loader.import(specifier, normalizeReferrerURL(filename)); + }, + }); + } catch (err) { + if (process.mainModule === cjsModuleInstance) + enrichCJSError(err, content); + throw err; + } +} + +// Run the file contents in the correct scope or sandbox. Expose +// the correct helper variables (require, module, exports) to +// the file. +// Returns exception, if any. +Module.prototype._compile = function(content, filename) { + let moduleURL; + let redirects; + if (policy?.manifest) { + moduleURL = pathToFileURL(filename); + redirects = policy.manifest.getDependencyMapper(moduleURL); + policy.manifest.assertIntegrity(moduleURL, content); + } + + maybeCacheSourceMap(filename, content, this); + const compiledWrapper = wrapSafe(filename, content, this); + + let inspectorWrapper = null; + if (getOptionValue('--inspect-brk') && process._eval == null) { + if (!resolvedArgv) { + // We enter the repl if we're not given a filename argument. + if (process.argv[1]) { + try { + resolvedArgv = Module._resolveFilename(process.argv[1], null, false); + } catch { + // We only expect this codepath to be reached in the case of a + // preloaded module (it will fail earlier with the main entry) + assert(ArrayIsArray(getOptionValue('--require'))); + } + } else { + resolvedArgv = 'repl'; + } + } + + // Set breakpoint on module start + if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) { + hasPausedEntry = true; + inspectorWrapper = internalBinding('inspector').callAndPauseOnStart; + } + } + const dirname = path.dirname(filename); + const require = makeRequireFunction(this, redirects); + let result; + const exports = this.exports; + const thisValue = exports; + const module = this; + if (requireDepth === 0) statCache = new SafeMap(); + if (inspectorWrapper) { + result = inspectorWrapper(compiledWrapper, thisValue, exports, + require, module, filename, dirname); + } else { + result = ReflectApply(compiledWrapper, thisValue, + [exports, require, module, filename, dirname]); + } + hasLoadedAnyUserCJSModule = true; + if (requireDepth === 0) statCache = null; + return result; +}; + +// Native extension for .js +Module._extensions['.js'] = function(module, filename) { + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached?.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } + if (StringPrototypeEndsWith(filename, '.js')) { + const pkg = readPackageScope(filename); + // Function require shouldn't be used in ES modules. + if (pkg?.data?.type === 'module') { + const parent = moduleParentCache.get(module); + const parentPath = parent?.filename; + const packageJsonPath = path.resolve(pkg.path, 'package.json'); + const usesEsm = hasEsmSyntax(content); + const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath, + packageJsonPath); + // Attempt to reconstruct the parent require frame. + if (Module._cache[parentPath]) { + let parentSource; + try { + parentSource = fs.readFileSync(parentPath, 'utf8'); + } catch {} + if (parentSource) { + const errLine = StringPrototypeSplit( + StringPrototypeSlice(err.stack, StringPrototypeIndexOf( + err.stack, ' at ')), '\n', 1)[0]; + const { 1: line, 2: col } = + RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || []; + if (line && col) { + const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1]; + const frame = `${parentPath}:${line}\n${srcLine}\n${ + StringPrototypeRepeat(' ', col - 1)}^\n`; + setArrowMessage(err, frame); + } + } + } + throw err; + } + } + module._compile(content, filename); +}; + + +// Native extension for .json +Module._extensions['.json'] = function(module, filename) { + const content = fs.readFileSync(filename, 'utf8'); + + if (policy?.manifest) { + const moduleURL = pathToFileURL(filename); + policy.manifest.assertIntegrity(moduleURL, content); + } + + try { + module.exports = JSONParse(stripBOM(content)); + } catch (err) { + err.message = filename + ': ' + err.message; + throw err; + } +}; + + +// Native extension for .node +Module._extensions['.node'] = function(module, filename) { + if (policy?.manifest) { + const content = fs.readFileSync(filename); + const moduleURL = pathToFileURL(filename); + policy.manifest.assertIntegrity(moduleURL, content); + } + // Be aware this doesn't use `content` + return process.dlopen(module, path.toNamespacedPath(filename)); +}; + +function createRequireFromPath(filename) { + // Allow a directory to be passed as the filename + const trailingSlash = + StringPrototypeEndsWith(filename, '/') || + (isWindows && StringPrototypeEndsWith(filename, '\\')); + + const proxyPath = trailingSlash ? + path.join(filename, 'noop.js') : + filename; + + const m = new Module(proxyPath); + m.filename = proxyPath; + + m.paths = Module._nodeModulePaths(m.path); + return makeRequireFunction(m, null); +} + +const createRequireError = 'must be a file URL object, file URL string, or ' + + 'absolute path string'; + +function createRequire(filename) { + let filepath; + + if (isURLInstance(filename) || + (typeof filename === 'string' && !path.isAbsolute(filename))) { + try { + filepath = fileURLToPath(filename); + } catch { + throw new ERR_INVALID_ARG_VALUE('filename', filename, + createRequireError); + } + } else if (typeof filename !== 'string') { + throw new ERR_INVALID_ARG_VALUE('filename', filename, createRequireError); + } else { + filepath = filename; + } + return createRequireFromPath(filepath); +} + +Module.createRequire = createRequire; + +Module._initPaths = function() { + const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME'); + const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH'); + + // process.execPath is $PREFIX/bin/node except on Windows where it is + // $PREFIX\node.exe where $PREFIX is the root of the Node.js installation. + const prefixDir = isWindows ? + path.resolve(process.execPath, '..') : + path.resolve(process.execPath, '..', '..'); + + const paths = [path.resolve(prefixDir, 'lib', 'node')]; + + if (homeDir) { + ArrayPrototypeUnshift(paths, path.resolve(homeDir, '.node_libraries')); + ArrayPrototypeUnshift(paths, path.resolve(homeDir, '.node_modules')); + } + + if (nodePath) { + ArrayPrototypeUnshiftApply(paths, ArrayPrototypeFilter( + StringPrototypeSplit(nodePath, path.delimiter), + Boolean + )); + } + + modulePaths = paths; + + // Clone as a shallow copy, for introspection. + Module.globalPaths = ArrayPrototypeSlice(modulePaths); +}; + +Module._preloadModules = function(requests) { + if (!ArrayIsArray(requests)) + return; + + isPreloading = true; + + // Preloaded modules have a dummy parent module which is deemed to exist + // in the current working directory. This seeds the search path for + // preloaded modules. + const parent = new Module('internal/preload', null); + try { + parent.paths = Module._nodeModulePaths(process.cwd()); + } catch (e) { + if (e.code !== 'ENOENT') { + isPreloading = false; + throw e; + } + } + for (let n = 0; n < requests.length; n++) + parent.require(requests[n]); + isPreloading = false; +}; + +Module.syncBuiltinESMExports = function syncBuiltinESMExports() { + for (const mod of NativeModule.map.values()) { + if (mod.canBeRequiredByUsers) { + mod.syncExports(); + } + } +}; + +// Backwards compatibility +Module.Module = Module; diff --git a/raw/node-internal-modules-esm-get_format-v15.3.0.js b/raw/node-internal-modules-esm-get_format-v15.3.0.js new file mode 100644 index 00000000..71b749ed --- /dev/null +++ b/raw/node-internal-modules-esm-get_format-v15.3.0.js @@ -0,0 +1,80 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/esm/get_format.js + +'use strict'; +const { + RegExpPrototypeExec, + StringPrototypeStartsWith, +} = primordials; +const { extname } = require('path'); +const { getOptionValue } = require('internal/options'); + +const experimentalJsonModules = getOptionValue('--experimental-json-modules'); +const experimentalSpeciferResolution = + getOptionValue('--experimental-specifier-resolution'); +const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); +const { getPackageType } = require('internal/modules/esm/resolve'); +const { URL, fileURLToPath } = require('internal/url'); +const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; + +const extensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'module', + '.mjs': 'module' +}; + +const legacyExtensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'commonjs', + '.json': 'commonjs', + '.mjs': 'module', + '.node': 'commonjs' +}; + +if (experimentalWasmModules) + extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm'; + +if (experimentalJsonModules) + extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; + +function defaultGetFormat(url, context, defaultGetFormatUnused) { + if (StringPrototypeStartsWith(url, 'node:')) { + return { format: 'builtin' }; + } + const parsed = new URL(url); + if (parsed.protocol === 'data:') { + const [ , mime ] = RegExpPrototypeExec( + /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + parsed.pathname, + ) || [ null, null, null ]; + const format = ({ + '__proto__': null, + 'text/javascript': 'module', + 'application/json': experimentalJsonModules ? 'json' : null, + 'application/wasm': experimentalWasmModules ? 'wasm' : null + })[mime] || null; + return { format }; + } else if (parsed.protocol === 'file:') { + const ext = extname(parsed.pathname); + let format; + if (ext === '.js') { + format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs'; + } else { + format = extensionFormatMap[ext]; + } + if (!format) { + if (experimentalSpeciferResolution === 'node') { + process.emitWarning( + 'The Node.js specifier resolution in ESM is experimental.', + 'ExperimentalWarning'); + format = legacyExtensionFormatMap[ext]; + } else { + throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url)); + } + } + return { format: format || null }; + } + return { format: null }; +} +exports.defaultGetFormat = defaultGetFormat; diff --git a/raw/node-esm-resolve-implementation-v13.12.0.js b/raw/node-internal-modules-esm-resolve-v13.12.0.js similarity index 99% rename from raw/node-esm-resolve-implementation-v13.12.0.js rename to raw/node-internal-modules-esm-resolve-v13.12.0.js index 1c2b8e67..0313bb4d 100644 --- a/raw/node-esm-resolve-implementation-v13.12.0.js +++ b/raw/node-internal-modules-esm-resolve-v13.12.0.js @@ -1,3 +1,5 @@ +// Copied from https://github.com/nodejs/node/blob/v13.12.0/lib/internal/modules/esm/resolve.js + 'use strict'; const { @@ -661,4 +663,3 @@ module.exports = { defaultResolve, getPackageType }; - diff --git a/raw/node-esm-resolve-implementation-v15.3.0.js b/raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js similarity index 99% rename from raw/node-esm-resolve-implementation-v15.3.0.js rename to raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js index 782b892a..53599c9e 100644 --- a/raw/node-esm-resolve-implementation-v15.3.0.js +++ b/raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js @@ -1,3 +1,5 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/esm/resolve.js + 'use strict'; const { @@ -9,11 +11,9 @@ const { ObjectFreeze, ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, - RegExp, RegExpPrototypeTest, SafeMap, SafeSet, - String, StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeLastIndexOf, @@ -897,4 +897,3 @@ module.exports = { packageExportsResolve, packageImportsResolve }; - diff --git a/raw/node-internal-modules-esm-resolve-v15.3.0.js b/raw/node-internal-modules-esm-resolve-v15.3.0.js new file mode 100644 index 00000000..19a7e216 --- /dev/null +++ b/raw/node-internal-modules-esm-resolve-v15.3.0.js @@ -0,0 +1,901 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/esm/resolve.js + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeJoin, + ArrayPrototypeShift, + JSONParse, + JSONStringify, + ObjectFreeze, + ObjectGetOwnPropertyNames, + ObjectPrototypeHasOwnProperty, + RegExp, + RegExpPrototypeTest, + SafeMap, + SafeSet, + String, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeReplace, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeSubstr, +} = primordials; +const internalFS = require('internal/fs/utils'); +const { NativeModule } = require('internal/bootstrap/loaders'); +const { + realpathSync, + statSync, + Stats, +} = require('fs'); +const { getOptionValue } = require('internal/options'); +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : + null; +const { sep, relative } = require('path'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +const typeFlag = getOptionValue('--input-type'); +const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); +const { + ERR_INPUT_TYPE_NOT_ALLOWED, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_MODULE_SPECIFIER, + ERR_INVALID_PACKAGE_CONFIG, + ERR_INVALID_PACKAGE_TARGET, + ERR_MANIFEST_DEPENDENCY_MISSING, + ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_IMPORT_NOT_DEFINED, + ERR_PACKAGE_PATH_NOT_EXPORTED, + ERR_UNSUPPORTED_DIR_IMPORT, + ERR_UNSUPPORTED_ESM_URL_SCHEME, +} = require('internal/errors').codes; +const { Module: CJSModule } = require('internal/modules/cjs/loader'); + +const packageJsonReader = require('internal/modules/package_json_reader'); +const userConditions = getOptionValue('--conditions'); +const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); +const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); + +const pendingDeprecation = getOptionValue('--pending-deprecation'); +const emittedPackageWarnings = new SafeSet(); +function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) { + const pjsonPath = fileURLToPath(pjsonUrl); + if (!pendingDeprecation) { + const nodeModulesIndex = StringPrototypeLastIndexOf(pjsonPath, + '/node_modules/'); + if (nodeModulesIndex !== -1) { + const afterNodeModulesPath = StringPrototypeSlice(pjsonPath, + nodeModulesIndex + 14, + -13); + try { + const { packageSubpath } = parsePackageName(afterNodeModulesPath); + if (packageSubpath === '.') + return; + } catch {} + } + } + if (emittedPackageWarnings.has(pjsonPath + '|' + match)) + return; + emittedPackageWarnings.add(pjsonPath + '|' + match); + process.emitWarning( + `Use of deprecated folder mapping "${match}" in the ${isExports ? + '"exports"' : '"imports"'} field module resolution of the package at ${ + pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.\n` + + `Update this package.json to use a subpath pattern like "${match}*".`, + 'DeprecationWarning', + 'DEP0148' + ); +} + +function getConditionsSet(conditions) { + if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { + if (!ArrayIsArray(conditions)) { + throw new ERR_INVALID_ARG_VALUE('conditions', conditions, + 'expected an array'); + } + return new SafeSet(conditions); + } + return DEFAULT_CONDITIONS_SET; +} + +const realpathCache = new SafeMap(); +const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ + +function tryStatSync(path) { + try { + return statSync(path); + } catch { + return new Stats(); + } +} + +function getPackageConfig(path, specifier, base) { + const existing = packageJSONCache.get(path); + if (existing !== undefined) { + return existing; + } + const source = packageJsonReader.read(path).string; + if (source === undefined) { + const packageConfig = { + pjsonPath: path, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let packageJSON; + try { + packageJSON = JSONParse(source); + } catch (error) { + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); + } + + let { imports, main, name, type } = packageJSON; + const { exports } = packageJSON; + if (typeof imports !== 'object' || imports === null) imports = undefined; + if (typeof main !== 'string') main = undefined; + if (typeof name !== 'string') name = undefined; + // Ignore unknown types for forwards compatibility + if (type !== 'module' && type !== 'commonjs') type = 'none'; + + const packageConfig = { + pjsonPath: path, + exists: true, + main, + name, + type, + exports, + imports, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; +} + +function getPackageScopeConfig(resolved) { + let packageJSONUrl = new URL('./package.json', resolved); + while (true) { + const packageJSONPath = packageJSONUrl.pathname; + if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) + break; + const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), + resolved); + if (packageConfig.exists) return packageConfig; + + const lastPackageJSONUrl = packageJSONUrl; + packageJSONUrl = new URL('../package.json', packageJSONUrl); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; + } + const packageJSONPath = fileURLToPath(packageJSONUrl); + const packageConfig = { + pjsonPath: packageJSONPath, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(packageJSONPath, packageConfig); + return packageConfig; +} + +/* + * Legacy CommonJS main resolution: + * 1. let M = pkg_url + (json main field) + * 2. TRY(M, M.js, M.json, M.node) + * 3. TRY(M/index.js, M/index.json, M/index.node) + * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) + * 5. NOT_FOUND + */ +function fileExists(url) { + return tryStatSync(fileURLToPath(url)).isFile(); +} + +function legacyMainResolve(packageJSONUrl, packageConfig, base) { + let guess; + if (packageConfig.main !== undefined) { + // Note: fs check redundances will be handled by Descriptor cache here. + if (fileExists(guess = new URL(`./${packageConfig.main}`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.node`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, + packageJSONUrl))) { + return guess; + } + // Fallthrough. + } + if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { + return guess; + } + // So fs. + if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { + return guess; + } + // Not found. + throw new ERR_MODULE_NOT_FOUND( + fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); +} + +function resolveExtensionsWithTryExactName(search) { + if (fileExists(search)) return search; + return resolveExtensions(search); +} + +const extensions = ['.js', '.json', '.node', '.mjs']; +function resolveExtensions(search) { + for (let i = 0; i < extensions.length; i++) { + const extension = extensions[i]; + const guess = new URL(`${search.pathname}${extension}`, search); + if (fileExists(guess)) return guess; + } + return undefined; +} + +function resolveIndex(search) { + return resolveExtensions(new URL('index', search)); +} + +const encodedSepRegEx = /%2F|%2C/i; +function finalizeResolution(resolved, base) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved.pathname, 'must not include encoded "/" or "\\" characters', + fileURLToPath(base)); + + const path = fileURLToPath(resolved); + if (getOptionValue('--experimental-specifier-resolution') === 'node') { + let file = resolveExtensionsWithTryExactName(resolved); + if (file !== undefined) return file; + if (!StringPrototypeEndsWith(path, '/')) { + file = resolveIndex(new URL(`${resolved}/`)); + if (file !== undefined) return file; + } else { + return resolveIndex(resolved) || resolved; + } + throw new ERR_MODULE_NOT_FOUND( + resolved.pathname, fileURLToPath(base), 'module'); + } + + const stats = tryStatSync(StringPrototypeEndsWith(path, '/') ? + StringPrototypeSlice(path, -1) : path); + if (stats.isDirectory()) { + const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base)); + err.url = String(resolved); + throw err; + } else if (!stats.isFile()) { + throw new ERR_MODULE_NOT_FOUND( + path || resolved.pathname, base && fileURLToPath(base), 'module'); + } + + return resolved; +} + +function throwImportNotDefined(specifier, packageJSONUrl, base) { + throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( + specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), + fileURLToPath(base)); +} + +function throwExportsNotFound(subpath, packageJSONUrl, base) { + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, + base && fileURLToPath(base)); +} + +function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) { + const reason = `request is not a valid subpath for the "${internal ? + 'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, + base && fileURLToPath(base)); +} + +function throwInvalidPackageTarget( + subpath, target, packageJSONUrl, internal, base) { + if (typeof target === 'object' && target !== null) { + target = JSONStringify(target, null, ''); + } else { + target = `${target}`; + } + throw new ERR_INVALID_PACKAGE_TARGET( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, + internal, base && fileURLToPath(base)); +} + +const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; +const patternRegEx = /\*/g; + +function resolvePackageTargetString( + target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (!StringPrototypeStartsWith(target, './')) { + if (internal && !StringPrototypeStartsWith(target, '../') && + !StringPrototypeStartsWith(target, '/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch {} + if (!isURL) { + const exportTarget = pattern ? + StringPrototypeReplace(target, patternRegEx, subpath) : + target + subpath; + return packageResolve(exportTarget, packageJSONUrl, conditions); + } + } + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + } + + if (RegExpPrototypeTest(invalidSegmentRegEx, StringPrototypeSlice(target, 2))) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + const resolved = new URL(target, packageJSONUrl); + const resolvedPath = resolved.pathname; + const packagePath = new URL('.', packageJSONUrl).pathname; + + if (!StringPrototypeStartsWith(resolvedPath, packagePath)) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (subpath === '') return resolved; + + if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) + throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); + + if (pattern) + return new URL(StringPrototypeReplace(resolved.href, patternRegEx, + subpath)); + return new URL(subpath, resolved); +} + +/** + * @param {string} key + * @returns {boolean} + */ +function isArrayIndex(key) { + const keyNum = +key; + if (`${keyNum}` !== key) return false; + return keyNum >= 0 && keyNum < 0xFFFF_FFFF; +} + +function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, + base, pattern, internal, conditions) { + if (typeof target === 'string') { + return resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, + conditions); + } else if (ArrayIsArray(target)) { + if (target.length === 0) + return null; + + let lastException; + for (let i = 0; i < target.length; i++) { + const targetItem = target[i]; + let resolved; + try { + resolved = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, conditions); + } catch (e) { + lastException = e; + if (e.code === 'ERR_INVALID_PACKAGE_TARGET') + continue; + throw e; + } + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } + return resolved; + } + if (lastException === undefined || lastException === null) + return lastException; + throw lastException; + } else if (typeof target === 'object' && target !== null) { + const keys = ObjectGetOwnPropertyNames(target); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (isArrayIndex(key)) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain numeric property keys.'); + } + } + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key === 'default' || conditions.has(key)) { + const conditionalTarget = target[key]; + const resolved = resolvePackageTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, + pattern, internal, conditions); + if (resolved === undefined) + continue; + return resolved; + } + } + return undefined; + } else if (target === null) { + return null; + } + throwInvalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, + base); +} + +function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { + if (typeof exports === 'string' || ArrayIsArray(exports)) return true; + if (typeof exports !== 'object' || exports === null) return false; + + const keys = ObjectGetOwnPropertyNames(exports); + let isConditionalSugar = false; + let i = 0; + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const curIsConditionalSugar = key === '' || key[0] !== '.'; + if (i++ === 0) { + isConditionalSugar = curIsConditionalSugar; + } else if (isConditionalSugar !== curIsConditionalSugar) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain some keys starting with \'.\' and some not.' + + ' The exports object must either be an object of package subpath keys' + + ' or an object of main entry condition name keys only.'); + } + } + return isConditionalSugar; +} + +/** + * @param {URL} packageJSONUrl + * @param {string} packageSubpath + * @param {object} packageConfig + * @param {string} base + * @param {Set} conditions + * @returns {URL} + */ +function packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions) { + let exports = packageConfig.exports; + if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) + exports = { '.': exports }; + + if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { + const target = exports[packageSubpath]; + const resolved = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, false, conditions + ); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return { resolved, exact: true }; + } + + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(packageSubpath, + StringPrototypeSlice(key, 0, -1)) && + packageSubpath.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(packageSubpath, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = exports[bestMatch]; + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, + bestMatch, base, pattern, false, + conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + if (!pattern) + emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base); + return { resolved, exact: pattern }; + } + + throwExportsNotFound(packageSubpath, packageJSONUrl, base); +} + +function packageImportsResolve(name, base, conditions) { + if (name === '#' || StringPrototypeStartsWith(name, '#/')) { + const reason = 'is not a valid internal imports specifier name'; + throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); + } + let packageJSONUrl; + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + const imports = packageConfig.imports; + if (imports) { + if (ObjectPrototypeHasOwnProperty(imports, name)) { + const resolved = resolvePackageTarget( + packageJSONUrl, imports[name], '', name, base, false, true, conditions + ); + if (resolved !== null) + return { resolved, exact: true }; + } else { + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(imports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(name, + StringPrototypeSlice(key, 0, -1)) && + name.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(name, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(name, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, pattern, true, + conditions); + if (resolved !== null) { + if (!pattern) + emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base); + return { resolved, exact: pattern }; + } + } + } + } + } + throwImportNotDefined(name, packageJSONUrl, base); +} + +function getPackageType(url) { + const packageConfig = getPackageScopeConfig(url); + return packageConfig.type; +} + +function parsePackageName(specifier, base) { + let separatorIndex = StringPrototypeIndexOf(specifier, '/'); + let validPackageName = true; + let isScoped = false; + if (specifier[0] === '@') { + isScoped = true; + if (separatorIndex === -1 || specifier.length === 0) { + validPackageName = false; + } else { + separatorIndex = StringPrototypeIndexOf( + specifier, '/', separatorIndex + 1); + } + } + + const packageName = separatorIndex === -1 ? + specifier : StringPrototypeSlice(specifier, 0, separatorIndex); + + // Package name cannot have leading . and cannot have percent-encoding or + // separators. + for (let i = 0; i < packageName.length; i++) { + if (packageName[i] === '%' || packageName[i] === '\\') { + validPackageName = false; + break; + } + } + + if (!validPackageName) { + throw new ERR_INVALID_MODULE_SPECIFIER( + specifier, 'is not a valid package name', fileURLToPath(base)); + } + + const packageSubpath = '.' + (separatorIndex === -1 ? '' : + StringPrototypeSlice(specifier, separatorIndex)); + + return { packageName, packageSubpath, isScoped }; +} + +/** + * @param {string} specifier + * @param {URL} base + * @param {Set} conditions + * @returns {URL} + */ +function packageResolve(specifier, base, conditions) { + const { packageName, packageSubpath, isScoped } = + parsePackageName(specifier, base); + + // ResolveSelf + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && + packageConfig.exports !== undefined && packageConfig.exports !== null) { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; + } + } + + let packageJSONUrl = + new URL('./node_modules/' + packageName + '/package.json', base); + let packageJSONPath = fileURLToPath(packageJSONUrl); + let lastPath; + do { + const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0, + packageJSONPath.length - 13)); + if (!stat.isDirectory()) { + lastPath = packageJSONPath; + packageJSONUrl = new URL((isScoped ? + '../../../../node_modules/' : '../../../node_modules/') + + packageName + '/package.json', packageJSONUrl); + packageJSONPath = fileURLToPath(packageJSONUrl); + continue; + } + + // Package match. + const packageConfig = getPackageConfig(packageJSONPath, specifier, base); + if (packageConfig.exports !== undefined && packageConfig.exports !== null) + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; + if (packageSubpath === '.') + return legacyMainResolve(packageJSONUrl, packageConfig, base); + return new URL(packageSubpath, packageJSONUrl); + // Cross-platform root check. + } while (packageJSONPath.length !== lastPath.length); + + // eslint can't handle the above code. + // eslint-disable-next-line no-unreachable + throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); +} + +function isBareSpecifier(specifier) { + return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.'; +} + +function isRelativeSpecifier(specifier) { + if (specifier[0] === '.') { + if (specifier.length === 1 || specifier[1] === '/') return true; + if (specifier[1] === '.') { + if (specifier.length === 2 || specifier[2] === '/') return true; + } + } + return false; +} + +function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { + if (specifier === '') return false; + if (specifier[0] === '/') return true; + return isRelativeSpecifier(specifier); +} + +/** + * @param {string} specifier + * @param {URL} base + * @param {Set} conditions + * @returns {URL} + */ +function moduleResolve(specifier, base, conditions) { + // Order swapped from spec for minor perf gain. + // Ok since relative URLs cannot parse as URLs. + let resolved; + if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { + resolved = new URL(specifier, base); + } else if (specifier[0] === '#') { + ({ resolved } = packageImportsResolve(specifier, base, conditions)); + } else { + try { + resolved = new URL(specifier); + } catch { + resolved = packageResolve(specifier, base, conditions); + } + } + return finalizeResolution(resolved, base); +} + +/** + * Try to resolve an import as a CommonJS module + * @param {string} specifier + * @param {string} parentURL + * @returns {boolean|string} + */ +function resolveAsCommonJS(specifier, parentURL) { + try { + const parent = fileURLToPath(parentURL); + const tmpModule = new CJSModule(parent, null); + tmpModule.paths = CJSModule._nodeModulePaths(parent); + + let found = CJSModule._resolveFilename(specifier, tmpModule, false); + + // If it is a relative specifier return the relative path + // to the parent + if (isRelativeSpecifier(specifier)) { + found = relative(parent, found); + // Add '.separator if the path does not start with '..separator' + // This should be a safe assumption because when loading + // esm modules there should be always a file specified so + // there should not be a specifier like '..' or '.' + if (!StringPrototypeStartsWith(found, `..${sep}`)) { + found = `.${sep}${found}`; + } + } else if (isBareSpecifier(specifier)) { + // If it is a bare specifier return the relative path within the + // module + const pkg = StringPrototypeSplit(specifier, '/')[0]; + const index = StringPrototypeIndexOf(found, pkg); + if (index !== -1) { + found = StringPrototypeSlice(found, index); + } + } + // Normalize the path separator to give a valid suggestion + // on Windows + if (process.platform === 'win32') { + found = StringPrototypeReplace(found, new RegExp(`\\${sep}`, 'g'), '/'); + } + return found; + } catch { + return false; + } +} + +function defaultResolve(specifier, context = {}, defaultResolveUnused) { + let { parentURL, conditions } = context; + if (parentURL && policy?.manifest) { + const redirects = policy.manifest.getDependencyMapper(parentURL); + if (redirects) { + const { resolve, reaction } = redirects; + const destination = resolve(specifier, new SafeSet(conditions)); + let missing = true; + if (destination === true) { + missing = false; + } else if (destination) { + const href = destination.href; + return { url: href }; + } + if (missing) { + reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( + parentURL, + specifier, + ArrayPrototypeJoin([...conditions], ', ')) + ); + } + } + } + let parsed; + try { + parsed = new URL(specifier); + if (parsed.protocol === 'data:') { + return { + url: specifier + }; + } + } catch {} + if (parsed && parsed.protocol === 'node:') + return { url: specifier }; + if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:') + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed); + if (NativeModule.canBeRequiredByUsers(specifier)) { + return { + url: 'node:' + specifier + }; + } + if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) { + // This is gonna blow up, we want the error + new URL(specifier, parentURL); + } + + const isMain = parentURL === undefined; + if (isMain) { + parentURL = pathToFileURL(`${process.cwd()}/`).href; + + // This is the initial entry point to the program, and --input-type has + // been passed as an option; but --input-type can only be used with + // --eval, --print or STDIN string input. It is not allowed with file + // input, to avoid user confusion over how expansive the effect of the + // flag should be (i.e. entry point only, package scope surrounding the + // entry point, etc.). + if (typeFlag) + throw new ERR_INPUT_TYPE_NOT_ALLOWED(); + } + + conditions = getConditionsSet(conditions); + let url; + try { + url = moduleResolve(specifier, parentURL, conditions); + } catch (error) { + // Try to give the user a hint of what would have been the + // resolved CommonJS module + if (error.code === 'ERR_MODULE_NOT_FOUND' || + error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { + if (StringPrototypeStartsWith(specifier, 'file://')) { + specifier = fileURLToPath(specifier); + } + const found = resolveAsCommonJS(specifier, parentURL); + if (found) { + // Modify the stack and message string to include the hint + const lines = StringPrototypeSplit(error.stack, '\n'); + const hint = `Did you mean to import ${found}?`; + error.stack = + ArrayPrototypeShift(lines) + '\n' + + hint + '\n' + + ArrayPrototypeJoin(lines, '\n'); + error.message += `\n${hint}`; + } + } + throw error; + } + + if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { + const urlPath = fileURLToPath(url); + const real = realpathSync(urlPath, { + [internalFS.realpathCacheKey]: realpathCache + }); + const old = url; + url = pathToFileURL( + real + (StringPrototypeEndsWith(urlPath, sep) ? '/' : '')); + url.search = old.search; + url.hash = old.hash; + } + + return { url: `${url}` }; +} + +module.exports = { + DEFAULT_CONDITIONS, + defaultResolve, + encodedSepRegEx, + getPackageType, + packageExportsResolve, + packageImportsResolve +}; diff --git a/raw/node-internal-modules-package_json_reader-v15.3.0.js b/raw/node-internal-modules-package_json_reader-v15.3.0.js new file mode 100644 index 00000000..51bd1a03 --- /dev/null +++ b/raw/node-internal-modules-package_json_reader-v15.3.0.js @@ -0,0 +1,43 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/package_json_reader.js + +'use strict'; + +const { SafeMap } = primordials; +const { internalModuleReadJSON } = internalBinding('fs'); +const { pathToFileURL } = require('url'); +const { toNamespacedPath } = require('path'); + +const cache = new SafeMap(); + +let manifest; + +/** + * + * @param {string} jsonPath + */ +function read(jsonPath) { + if (cache.has(jsonPath)) { + return cache.get(jsonPath); + } + + const [string, containsKeys] = internalModuleReadJSON( + toNamespacedPath(jsonPath) + ); + const result = { string, containsKeys }; + const { getOptionValue } = require('internal/options'); + if (string !== undefined) { + if (manifest === undefined) { + manifest = getOptionValue('--experimental-policy') ? + require('internal/process/policy').manifest : + null; + } + if (manifest !== null) { + const jsonURL = pathToFileURL(jsonPath); + manifest.assertIntegrity(jsonURL, string); + } + } + cache.set(jsonPath, result); + return result; +} + +module.exports = { read }; diff --git a/raw/node-repl-await.js b/raw/node-internal-repl-await-88799930794045795e8abac874730f9eba7e2300.js similarity index 98% rename from raw/node-repl-await.js rename to raw/node-internal-repl-await-88799930794045795e8abac874730f9eba7e2300.js index b8b9153f..0f95d97e 100644 --- a/raw/node-repl-await.js +++ b/raw/node-internal-repl-await-88799930794045795e8abac874730f9eba7e2300.js @@ -1,4 +1,5 @@ -// downloaded from https://github.com/nodejs/node/blob/88799930794045795e8abac874730f9eba7e2300/lib/internal/repl/await.js +// Copied from https://github.com/nodejs/node/blob/88799930794045795e8abac874730f9eba7e2300/lib/internal/repl/await.js + 'use strict'; const { diff --git a/raw/node-internal-repl-await-v17.0.0.js b/raw/node-internal-repl-await-v17.0.0.js new file mode 100644 index 00000000..db79785a --- /dev/null +++ b/raw/node-internal-repl-await-v17.0.0.js @@ -0,0 +1,256 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.0/lib/internal/repl/await.js + +'use strict'; + +const { + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypePop, + ArrayPrototypePush, + FunctionPrototype, + ObjectKeys, + RegExpPrototypeSymbolReplace, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeIndexOf, + StringPrototypeRepeat, + StringPrototypeSplit, + StringPrototypeStartsWith, + SyntaxError, +} = primordials; + +const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; +const walk = require('internal/deps/acorn/acorn-walk/dist/walk'); +const { Recoverable } = require('internal/repl'); + +function isTopLevelDeclaration(state) { + return state.ancestors[state.ancestors.length - 2] === state.body; +} + +const noop = FunctionPrototype; +const visitorsWithoutAncestors = { + ClassDeclaration(node, state, c) { + if (isTopLevelDeclaration(state)) { + state.prepend(node, `${node.id.name}=`); + ArrayPrototypePush( + state.hoistedDeclarationStatements, + `let ${node.id.name}; ` + ); + } + + walk.base.ClassDeclaration(node, state, c); + }, + ForOfStatement(node, state, c) { + if (node.await === true) { + state.containsAwait = true; + } + walk.base.ForOfStatement(node, state, c); + }, + FunctionDeclaration(node, state, c) { + state.prepend(node, `this.${node.id.name} = ${node.id.name}; `); + ArrayPrototypePush( + state.hoistedDeclarationStatements, + `var ${node.id.name}; ` + ); + }, + FunctionExpression: noop, + ArrowFunctionExpression: noop, + MethodDefinition: noop, + AwaitExpression(node, state, c) { + state.containsAwait = true; + walk.base.AwaitExpression(node, state, c); + }, + ReturnStatement(node, state, c) { + state.containsReturn = true; + walk.base.ReturnStatement(node, state, c); + }, + VariableDeclaration(node, state, c) { + const variableKind = node.kind; + const isIterableForDeclaration = ArrayPrototypeIncludes( + ['ForOfStatement', 'ForInStatement'], + state.ancestors[state.ancestors.length - 2].type + ); + + if (variableKind === 'var' || isTopLevelDeclaration(state)) { + state.replace( + node.start, + node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0), + variableKind === 'var' && isIterableForDeclaration ? + '' : + 'void' + (node.declarations.length === 1 ? '' : ' (') + ); + + if (!isIterableForDeclaration) { + ArrayPrototypeForEach(node.declarations, (decl) => { + state.prepend(decl, '('); + state.append(decl, decl.init ? ')' : '=undefined)'); + }); + + if (node.declarations.length !== 1) { + state.append(node.declarations[node.declarations.length - 1], ')'); + } + } + + const variableIdentifiersToHoist = [ + ['var', []], + ['let', []], + ]; + function registerVariableDeclarationIdentifiers(node) { + switch (node.type) { + case 'Identifier': + ArrayPrototypePush( + variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1], + node.name + ); + break; + case 'ObjectPattern': + ArrayPrototypeForEach(node.properties, (property) => { + registerVariableDeclarationIdentifiers(property.value); + }); + break; + case 'ArrayPattern': + ArrayPrototypeForEach(node.elements, (element) => { + registerVariableDeclarationIdentifiers(element); + }); + break; + } + } + + ArrayPrototypeForEach(node.declarations, (decl) => { + registerVariableDeclarationIdentifiers(decl.id); + }); + + ArrayPrototypeForEach( + variableIdentifiersToHoist, + ({ 0: kind, 1: identifiers }) => { + if (identifiers.length > 0) { + ArrayPrototypePush( + state.hoistedDeclarationStatements, + `${kind} ${ArrayPrototypeJoin(identifiers, ', ')}; ` + ); + } + } + ); + } + + walk.base.VariableDeclaration(node, state, c); + } +}; + +const visitors = {}; +for (const nodeType of ObjectKeys(walk.base)) { + const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType]; + visitors[nodeType] = (node, state, c) => { + const isNew = node !== state.ancestors[state.ancestors.length - 1]; + if (isNew) { + ArrayPrototypePush(state.ancestors, node); + } + callback(node, state, c); + if (isNew) { + ArrayPrototypePop(state.ancestors); + } + }; +} + +function processTopLevelAwait(src) { + const wrapPrefix = '(async () => { '; + const wrapped = `${wrapPrefix}${src} })()`; + const wrappedArray = StringPrototypeSplit(wrapped, ''); + let root; + try { + root = parser.parse(wrapped, { ecmaVersion: 'latest' }); + } catch (e) { + if (StringPrototypeStartsWith(e.message, 'Unterminated ')) + throw new Recoverable(e); + // If the parse error is before the first "await", then use the execution + // error. Otherwise we must emit this parse error, making it look like a + // proper syntax error. + const awaitPos = StringPrototypeIndexOf(src, 'await'); + const errPos = e.pos - wrapPrefix.length; + if (awaitPos > errPos) + return null; + // Convert keyword parse errors on await into their original errors when + // possible. + if (errPos === awaitPos + 6 && + StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence')) + return null; + if (errPos === awaitPos + 7 && + StringPrototypeIncludes(e.message, 'Unexpected token')) + return null; + const line = e.loc.line; + const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column; + let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' + + StringPrototypeRepeat(' ', column) + + '^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, ''); + // V8 unexpected token errors include the token string. + if (StringPrototypeEndsWith(message, 'Unexpected token')) + message += " '" + + // Wrapper end may cause acorn to report error position after the source + (src[e.pos - wrapPrefix.length] ?? src[src.length - 1]) + + "'"; + // eslint-disable-next-line no-restricted-syntax + throw new SyntaxError(message); + } + const body = root.body[0].expression.callee.body; + const state = { + body, + ancestors: [], + hoistedDeclarationStatements: [], + replace(from, to, str) { + for (let i = from; i < to; i++) { + wrappedArray[i] = ''; + } + if (from === to) str += wrappedArray[from]; + wrappedArray[from] = str; + }, + prepend(node, str) { + wrappedArray[node.start] = str + wrappedArray[node.start]; + }, + append(node, str) { + wrappedArray[node.end - 1] += str; + }, + containsAwait: false, + containsReturn: false + }; + + walk.recursive(body, state, visitors); + + // Do not transform if + // 1. False alarm: there isn't actually an await expression. + // 2. There is a top-level return, which is not allowed. + if (!state.containsAwait || state.containsReturn) { + return null; + } + + for (let i = body.body.length - 1; i >= 0; i--) { + const node = body.body[i]; + if (node.type === 'EmptyStatement') continue; + if (node.type === 'ExpressionStatement') { + // For an expression statement of the form + // ( expr ) ; + // ^^^^^^^^^^ // node + // ^^^^ // node.expression + // + // We do not want the left parenthesis before the `return` keyword; + // therefore we prepend the `return (` to `node`. + // + // On the other hand, we do not want the right parenthesis after the + // semicolon. Since there can only be more right parentheses between + // node.expression.end and the semicolon, appending one more to + // node.expression should be fine. + state.prepend(node, 'return ('); + state.append(node.expression, ')'); + } + break; + } + + return ( + ArrayPrototypeJoin(state.hoistedDeclarationStatements, '') + + ArrayPrototypeJoin(wrappedArray, '') + ); +} + +module.exports = { + processTopLevelAwait +}; diff --git a/scripts/build-pack.js b/scripts/build-pack.js index ac5e1976..5ee399b5 100644 --- a/scripts/build-pack.js +++ b/scripts/build-pack.js @@ -23,7 +23,10 @@ exec( console.error(err); process.exit(1); } - const tempTarballPath = join(tempDir, readdirSync(tempDir)[0]); + const tempTarballPath = join( + tempDir, + readdirSync(tempDir).find((name) => name.endsWith('.tgz')) + ); writeFileSync(tarballPath, readFileSync(tempTarballPath)); unlinkSync(tempTarballPath); rmdirSync(tempDir); diff --git a/src/bin.ts b/src/bin.ts index 3e972dd9..8b5f9176 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -4,7 +4,7 @@ import { join, resolve, dirname, parse as parsePath, relative } from 'path'; import { inspect } from 'util'; import Module = require('module'); let arg: typeof import('arg'); -import { parse, createRequire, hasOwnProperty } from './util'; +import { parse, createRequire, hasOwnProperty, versionGteLt } from './util'; import { EVAL_FILENAME, EvalState, @@ -21,13 +21,13 @@ import { VERSION, TSError, register, - versionGteLt, createEsmHooks, createFromPreloadedConfig, DEFAULTS, + ExperimentalSpecifierResolution, } from './index'; import type { TSInternal } from './ts-compiler-types'; -import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; +import { addBuiltinLibsToObject } from '../dist-raw/node-internal-modules-cjs-helpers'; import { callInChild } from './child/spawn-child'; import { findAndReadConfig } from './configuration'; @@ -141,6 +141,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': Boolean, '--scopeDir': String, '--noExperimentalReplAwait': Boolean, + '--experimentalSpecifierResolution': String, // Aliases. '-e': '--eval', @@ -174,6 +175,8 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--log-error': '--logError', '--scope-dir': '--scopeDir', '--no-experimental-repl-await': '--noExperimentalReplAwait', + '--experimental-specifier-resolution': + '--experimentalSpecifierResolution', }, { argv, @@ -216,6 +219,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, + '--experimentalSpecifierResolution': experimentalSpecifierResolution, '--esm': esm, _: restArgs, } = args; @@ -254,6 +258,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { scope, scopeDir, noExperimentalReplAwait, + experimentalSpecifierResolution, esm, }; } @@ -301,6 +306,8 @@ Options: --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 + --experimentalSpecifierResolution [node|explicit] + Equivalent to node's --experimental-specifier-resolution `); process.exit(0); @@ -362,6 +369,8 @@ function phase3(payload: BootstrapState) { argsRequire, scope, scopeDir, + esm, + experimentalSpecifierResolution, } = payload.parseArgvResult; const { cwd, scriptPath } = payload.phase2Result!; @@ -389,6 +398,9 @@ function phase3(payload: BootstrapState) { scope, scopeDir, preferTsExts, + esm, + experimentalSpecifierResolution: + experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts index 0ac01813..e65ffe2f 100644 --- a/src/child/child-loader.ts +++ b/src/child/child-loader.ts @@ -1,7 +1,3 @@ -// TODO same version check as ESM loader, but export stubs -// Also export a binder function that allows re-binding where the stubs -// delegate. - import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from '..'; import { filterHooksByAPIVersion } from '../esm'; diff --git a/src/child/child-require.ts b/src/child/child-require.ts index 2ee15522..cac676e2 100644 --- a/src/child/child-require.ts +++ b/src/child/child-require.ts @@ -14,7 +14,7 @@ if (Array.isArray(_process._events.warning)) { _process._events.warning = onWarning; } -const messageMatch = /--(?:experimental-)?loader\b/; +const messageMatch = /(?:--(?:experimental-)?loader\b|\bCustom ESM Loaders\b)/; function onWarning(this: any, warning: Error, ...rest: any[]) { // Suppress warning about how `--loader` is experimental if ( diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 74bf4017..12368fce 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -2,7 +2,7 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; import { brotliCompressSync } from 'zlib'; import { pathToFileURL } from 'url'; -import { versionGteLt } from '..'; +import { versionGteLt } from '../util'; const argPrefix = '--brotli-base64-config='; diff --git a/src/cjs-resolve-filename-hook.ts b/src/cjs-resolve-filename-hook.ts deleted file mode 100644 index 9c6f66b0..00000000 --- a/src/cjs-resolve-filename-hook.ts +++ /dev/null @@ -1,59 +0,0 @@ -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/cjs-resolve-hooks.ts b/src/cjs-resolve-hooks.ts new file mode 100644 index 00000000..9300d8f7 --- /dev/null +++ b/src/cjs-resolve-hooks.ts @@ -0,0 +1,67 @@ +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; + _findPath(request: string, paths: string[], isMain: boolean): string; +}; + +interface ModuleResolveFilenameOptions { + paths?: Array; +} + +/** + * @internal + */ +export function installCommonjsResolveHooksIfNecessary(tsNodeService: Service) { + const Module = require('module') as ModuleConstructorWithInternals; + const originalResolveFilename = Module._resolveFilename; + const originalFindPath = Module._findPath; + const shouldInstallHook = tsNodeService.options.experimentalResolver; + if (shouldInstallHook) { + const { Module_findPath, Module_resolveFilename } = + tsNodeService.getNodeCjsLoader(); + Module._resolveFilename = _resolveFilename; + Module._findPath = _findPath; + function _resolveFilename( + this: any, + request: string, + parent?: Module, + isMain?: boolean, + options?: ModuleResolveFilenameOptions, + ...rest: [] + ): string { + if (!tsNodeService.enabled()) + return originalResolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + + return Module_resolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + } + function _findPath(this: any): string { + if (!tsNodeService.enabled()) + return originalFindPath.apply(this, arguments as any); + return Module_findPath.apply(this, arguments as any); + } + } +} diff --git a/src/configuration.ts b/src/configuration.ts index 13b7ad28..5142a358 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -271,10 +271,6 @@ export function readConfig( // Remove resolution of "files". const files = rawApiOptions.files ?? tsNodeOptionsFromTsconfig.files ?? DEFAULTS.files; - if (!files) { - config.files = []; - config.include = []; - } // Only if a config file is *not* loaded, load an implicit configuration from @tsconfig/bases const skipDefaultCompilerOptions = configFilePath != null; @@ -309,7 +305,9 @@ export function readConfig( { fileExists, readFile, - readDirectory: ts.sys.readDirectory, + // Only used for globbing "files", "include", "exclude" + // When `files` option disabled, we want to avoid the fs calls + readDirectory: files ? ts.sys.readDirectory : () => [], useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, }, basePath, @@ -382,8 +380,9 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { moduleTypes, experimentalReplAwait, swc, - experimentalResolverFeatures, + experimentalResolver, esm, + experimentalSpecifierResolution, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -407,8 +406,9 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { scopeDir, moduleTypes, swc, - experimentalResolverFeatures, + experimentalResolver, esm, + experimentalSpecifierResolution, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = @@ -417,3 +417,40 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { null as any as keyof TsConfigOptions; return { recognized: filteredTsConfigOptions, unrecognized }; } + +/** @internal */ +export const ComputeAsCommonRootOfFiles = Symbol(); + +/** + * Some TS compiler options have defaults which are not provided by TS's config parsing functions. + * This function centralizes the logic for computing those defaults. + * @internal + */ +export function getTsConfigDefaults( + config: _ts.ParsedCommandLine, + basePath: string, + _files: string[] | undefined, + _include: string[] | undefined, + _exclude: string[] | undefined +) { + const { composite = false } = config.options; + let rootDir: string | typeof ComputeAsCommonRootOfFiles = + config.options.rootDir!; + if (rootDir == null) { + if (composite) rootDir = basePath; + // Return this symbol to avoid computing from `files`, which would require fs calls + else rootDir = ComputeAsCommonRootOfFiles; + } + const { outDir = rootDir } = config.options; + // Docs are wrong: https://www.typescriptlang.org/tsconfig#include + // Docs say **, but it's actually **/*; compiler throws error for ** + const include = _files ? [] : ['**/*']; + const files = _files ?? []; + // Docs are misleading: https://www.typescriptlang.org/tsconfig#exclude + // Docs say it excludes node_modules, bower_components, jspm_packages, but actually those are excluded via behavior of "include" + const exclude = _exclude ?? [outDir]; // TODO technically, outDir is absolute path, but exclude should be relative glob pattern? + + // TODO compute baseUrl + + return { rootDir, outDir, include, files, exclude, composite }; +} diff --git a/src/esm.ts b/src/esm.ts index 38f4f0d5..cb128045 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -1,10 +1,4 @@ -import { - register, - getExtensions, - RegisterOptions, - Service, - versionGteLt, -} from './index'; +import { register, RegisterOptions, Service } from './index'; import { parse as parseUrl, format as formatUrl, @@ -14,12 +8,8 @@ import { } from 'url'; import { extname } from 'path'; import * as assert from 'assert'; -import { normalizeSlashes } from './util'; +import { normalizeSlashes, versionGteLt } from './util'; import { createRequire } from 'module'; -const { - createResolve, -} = require('../dist-raw/node-esm-resolve-implementation'); -const { defaultGetFormat } = require('../dist-raw/node-esm-default-get-format'); // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts @@ -69,7 +59,11 @@ export namespace NodeLoaderHooksAPI2 { parentURL: string; }, defaultResolve: ResolveHook - ) => Promise<{ url: string; format?: NodeLoaderHooksFormat }>; + ) => Promise<{ + url: string; + format?: NodeLoaderHooksFormat; + shortCircuit?: boolean; + }>; export type LoadHook = ( url: string, context: { @@ -80,6 +74,7 @@ export namespace NodeLoaderHooksAPI2 { ) => Promise<{ format: NodeLoaderHooksFormat; source: string | Buffer | undefined; + shortCircuit?: boolean; }>; export type NodeImportConditions = unknown; export interface NodeImportAssertions { @@ -101,12 +96,7 @@ export interface NodeImportAssertions { } // The hooks API changed in node version X so we need to check for backwards compatibility. -// TODO: When the new API is backported to v12, v14, update these version checks accordingly. -const newHooksAPI = - versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.12.0', '17.0.0') || - versionGteLt(process.versions.node, '14.999.999', '15.0.0') || - versionGteLt(process.versions.node, '12.999.999', '13.0.0'); +const newHooksAPI = versionGteLt(process.versions.node, '16.12.0'); /** @internal */ export function filterHooksByAPIVersion( @@ -132,10 +122,9 @@ export function createEsmHooks(tsNodeService: Service) { tsNodeService.enableExperimentalEsmLoaderInterop(); // Custom implementation that considers additional file extensions and automatically adds file extensions - const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeService.config), - preferTsExts: tsNodeService.options.preferTsExts, - }); + const nodeResolveImplementation = tsNodeService.getNodeEsmResolver(); + const nodeGetFormatImplementation = tsNodeService.getNodeEsmGetFormat(); + const extensions = tsNodeService.extensions; const hooksAPI = filterHooksByAPIVersion({ resolve, @@ -173,7 +162,7 @@ export function createEsmHooks(tsNodeService: Service) { // See: https://github.com/nodejs/node/discussions/41711 // nodejs will likely implement a similar fallback. Till then, we can do our users a favor and fallback today. async function entrypointFallback( - cb: () => ReturnType + cb: () => ReturnType | Awaited> ): ReturnType { try { const resolution = await cb(); @@ -205,32 +194,34 @@ export function createEsmHooks(tsNodeService: Service) { } } - const parsed = parseUrl(specifier); - const { pathname, protocol, hostname } = parsed; + return addShortCircuitFlag(async () => { + const parsed = parseUrl(specifier); + const { pathname, protocol, hostname } = parsed; - if (!isFileUrlOrNodeStyleSpecifier(parsed)) { - return entrypointFallback(defer); - } + if (!isFileUrlOrNodeStyleSpecifier(parsed)) { + return entrypointFallback(defer); + } - if (protocol !== null && protocol !== 'file:') { - return entrypointFallback(defer); - } + if (protocol !== null && protocol !== 'file:') { + return entrypointFallback(defer); + } - // Malformed file:// URL? We should always see `null` or `''` - if (hostname) { - // TODO file://./foo sets `hostname` to `'.'`. Perhaps we should special-case this. - return entrypointFallback(defer); - } + // Malformed file:// URL? We should always see `null` or `''` + if (hostname) { + // TODO file://./foo sets `hostname` to `'.'`. Perhaps we should special-case this. + return entrypointFallback(defer); + } - // pathname is the path to be resolved + // pathname is the path to be resolved - return entrypointFallback(() => - nodeResolveImplementation.defaultResolve( - specifier, - context, - defaultResolve - ) - ); + return entrypointFallback(() => + nodeResolveImplementation.defaultResolve( + specifier, + context, + defaultResolve + ) + ); + }); } // `load` from new loader hook API (See description at the top of this file) @@ -245,47 +236,55 @@ export function createEsmHooks(tsNodeService: Service) { format: NodeLoaderHooksFormat; source: string | Buffer | undefined; }> { - // If we get a format hint from resolve() on the context then use it - // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node - const format = - context.format ?? - (await getFormat(url, context, defaultGetFormat)).format; - - let source = undefined; - if (format !== 'builtin' && format !== 'commonjs') { - // Call the new defaultLoad() to get the source - const { source: rawSource } = await defaultLoad( - url, - { - ...context, - format, - }, - defaultLoad - ); + return addShortCircuitFlag(async () => { + // If we get a format hint from resolve() on the context then use it + // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node + const format = + context.format ?? + ( + await getFormat( + url, + context, + nodeGetFormatImplementation.defaultGetFormat + ) + ).format; + + let source = undefined; + if (format !== 'builtin' && format !== 'commonjs') { + // Call the new defaultLoad() to get the source + const { source: rawSource } = await defaultLoad( + url, + { + ...context, + format, + }, + defaultLoad + ); + + if (rawSource === undefined || rawSource === null) { + throw new Error( + `Failed to load raw source: Format was '${format}' and url was '${url}''.` + ); + } - if (rawSource === undefined || rawSource === null) { - throw new Error( - `Failed to load raw source: Format was '${format}' and url was '${url}''.` + // Emulate node's built-in old defaultTransformSource() so we can re-use the old transformSource() hook + const defaultTransformSource: typeof transformSource = async ( + source, + _context, + _defaultTransformSource + ) => ({ source }); + + // Call the old hook + const { source: transformedSource } = await transformSource( + rawSource, + { url, format }, + defaultTransformSource ); + source = transformedSource; } - // Emulate node's built-in old defaultTransformSource() so we can re-use the old transformSource() hook - const defaultTransformSource: typeof transformSource = async ( - source, - _context, - _defaultTransformSource - ) => ({ source }); - - // Call the old hook - const { source: transformedSource } = await transformSource( - rawSource, - { url, format }, - defaultTransformSource - ); - source = transformedSource; - } - - return { format, source }; + return { format, source }; + }); } async function getFormat( @@ -323,24 +322,45 @@ export function createEsmHooks(tsNodeService: Service) { const nativePath = fileURLToPath(url); - // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js - const ext = extname(nativePath); let nodeSays: { format: NodeLoaderHooksFormat }; - if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { + + // If file has extension not understood by node, then ask node how it would treat the emitted extension. + // E.g. .mts compiles to .mjs, so ask node how to classify an .mjs file. + const ext = extname(nativePath); + const tsNodeIgnored = tsNodeService.ignored(nativePath); + const nodeEquivalentExt = extensions.nodeEquivalents.get(ext); + if (nodeEquivalentExt && !tsNodeIgnored) { nodeSays = await entrypointFallback(() => - defer(formatUrl(pathToFileURL(nativePath + '.js'))) + defer(formatUrl(pathToFileURL(nativePath + nodeEquivalentExt))) ); } else { - nodeSays = await entrypointFallback(defer); + try { + nodeSays = await entrypointFallback(defer); + } catch (e) { + if ( + e instanceof Error && + tsNodeIgnored && + extensions.nodeDoesNotUnderstand.includes(ext) + ) { + e.message += + `\n\n` + + `Hint:\n` + + `ts-node is configured to ignore this file.\n` + + `If you want ts-node to handle this file, consider enabling the "skipIgnore" option or adjusting your "ignore" patterns.\n` + + `https://typestrong.org/ts-node/docs/scope\n`; + } + throw e; + } } // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification if ( !tsNodeService.ignored(nativePath) && (nodeSays.format === 'commonjs' || nodeSays.format === 'module') ) { - const { moduleType } = tsNodeService.moduleTypeClassifier.classifyModule( - normalizeSlashes(nativePath) - ); + const { moduleType } = + tsNodeService.moduleTypeClassifier.classifyModuleByModuleTypeOverrides( + normalizeSlashes(nativePath) + ); if (moduleType === 'cjs') { return { format: 'commonjs' }; } else if (moduleType === 'esm') { @@ -384,3 +404,13 @@ export function createEsmHooks(tsNodeService: Service) { return hooksAPI; } + +async function addShortCircuitFlag(fn: () => Promise) { + const ret = await fn(); + // Not sure if this is necessary; being lazy. Can revisit in the future. + if (ret == null) return ret; + return { + ...ret, + shortCircuit: true, + }; +} diff --git a/src/file-extensions.ts b/src/file-extensions.ts new file mode 100644 index 00000000..87e8be1c --- /dev/null +++ b/src/file-extensions.ts @@ -0,0 +1,150 @@ +import type * as _ts from 'typescript'; +import type { RegisterOptions } from '.'; +import { versionGteLt } from './util'; + +/** + * Centralized specification of how we deal with file extensions based on + * project options: + * which ones we do/don't support, in what situations, etc. These rules drive + * logic elsewhere. + * @internal + * */ +export type Extensions = ReturnType; + +const nodeEquivalents = new Map([ + ['.ts', '.js'], + ['.tsx', '.js'], + ['.jsx', '.js'], + ['.mts', '.mjs'], + ['.cts', '.cjs'], +]); + +// All extensions understood by vanilla node +const vanillaNodeExtensions: readonly string[] = [ + '.js', + '.json', + '.node', + '.mjs', + '.cjs', +]; + +// Extensions added by vanilla node's require() if you omit them: +// js, json, node +// Extensions added by vanilla node if you omit them with --experimental-specifier-resolution=node +// js, json, node, mjs +// Extensions added by ESM codepath's legacy package.json "main" resolver +// js, json, node (not mjs!) + +const nodeDoesNotUnderstand: readonly string[] = [ + '.ts', + '.tsx', + '.jsx', + '.cts', + '.mts', +]; + +/** + * [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] + * @internal + */ +export function getExtensions( + config: _ts.ParsedCommandLine, + options: RegisterOptions, + tsVersion: string +) { + // TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions + const tsSupportsMtsCtsExts = versionGteLt(tsVersion, '4.5.0'); + + const requiresHigherTypescriptVersion: string[] = []; + if (!tsSupportsMtsCtsExts) + requiresHigherTypescriptVersion.push('.cts', '.cjs', '.mts', '.mjs'); + + const allPossibleExtensionsSortedByPreference = Array.from( + new Set([ + ...(options.preferTsExts ? nodeDoesNotUnderstand : []), + ...vanillaNodeExtensions, + ...nodeDoesNotUnderstand, + ]) + ); + + const compiledJsUnsorted: string[] = ['.ts']; + const compiledJsxUnsorted: string[] = []; + + if (config.options.jsx) compiledJsxUnsorted.push('.tsx'); + if (tsSupportsMtsCtsExts) compiledJsUnsorted.push('.mts', '.cts'); + if (config.options.allowJs) { + compiledJsUnsorted.push('.js'); + if (config.options.jsx) compiledJsxUnsorted.push('.jsx'); + if (tsSupportsMtsCtsExts) compiledJsUnsorted.push('.mjs', '.cjs'); + } + + const compiledUnsorted = [...compiledJsUnsorted, ...compiledJsxUnsorted]; + const compiled = allPossibleExtensionsSortedByPreference.filter((ext) => + compiledUnsorted.includes(ext) + ); + + const compiledNodeDoesNotUnderstand = nodeDoesNotUnderstand.filter((ext) => + compiled.includes(ext) + ); + + /** + * TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions. + * IMPORTANT: Must preserve ordering according to preferTsExts! + * Must include the .js/.mjs/.cjs extension in the array! + * This affects resolution behavior! + * [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] + */ + const r = allPossibleExtensionsSortedByPreference.filter((ext) => + [...compiledUnsorted, '.js', '.mjs', '.cjs', '.mts', '.cts'].includes(ext) + ); + const replacementsForJs = r.filter((ext) => + ['.js', '.jsx', '.ts', '.tsx'].includes(ext) + ); + const replacementsForJsx = r.filter((ext) => ['.jsx', '.tsx'].includes(ext)); + const replacementsForMjs = r.filter((ext) => ['.mjs', '.mts'].includes(ext)); + const replacementsForCjs = r.filter((ext) => ['.cjs', '.cts'].includes(ext)); + const replacementsForJsOrMjs = r.filter((ext) => + ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts'].includes(ext) + ); + + // Node allows omitting .js or .mjs extension in certain situations (CJS, ESM w/experimental flag) + // So anything that compiles to .js or .mjs can also be omitted. + const experimentalSpecifierResolutionAddsIfOmitted = Array.from( + new Set([...replacementsForJsOrMjs, '.json', '.node']) + ); + // Same as above, except node curiuosly doesn't do .mjs here + const legacyMainResolveAddsIfOmitted = Array.from( + new Set([...replacementsForJs, '.json', '.node']) + ); + + return { + /** All file extensions we transform, ordered by resolution preference according to preferTsExts */ + compiled, + /** Resolved extensions that vanilla node will not understand; we should handle them */ + nodeDoesNotUnderstand, + /** Like the above, but only the ones we're compiling */ + compiledNodeDoesNotUnderstand, + /** + * Mapping from extensions understood by tsc to the equivalent for node, + * as far as getFormat is concerned. + */ + nodeEquivalents, + /** + * Extensions that we can support if the user upgrades their typescript version. + * Used when raising hints. + */ + requiresHigherTypescriptVersion, + /** + * --experimental-specifier-resolution=node will add these extensions. + */ + experimentalSpecifierResolutionAddsIfOmitted, + /** + * ESM loader will add these extensions to package.json "main" field + */ + legacyMainResolveAddsIfOmitted, + replacementsForMjs, + replacementsForCjs, + replacementsForJsx, + replacementsForJs, + }; +} diff --git a/src/index.ts b/src/index.ts index 996556a7..607d5976 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,10 +11,13 @@ import type { Transpiler, TranspilerFactory } from './transpilers/types'; import { cachedLookup, createProjectLocalResolveHelper, + hasOwnProperty, normalizeSlashes, + once, parse, ProjectLocalResolveHelper, split, + versionGteLt, yn, } from './util'; import { findAndReadConfig, loadCompiler } from './configuration'; @@ -26,9 +29,15 @@ import { import { createResolverFunctions } from './resolver-functions'; import type { createEsmHooks as createEsmHooksFn } from './esm'; import { - installCommonjsResolveHookIfNecessary, + installCommonjsResolveHooksIfNecessary, ModuleConstructorWithInternals, -} from './cjs-resolve-filename-hook'; +} from './cjs-resolve-hooks'; +import { classifyModule } from './node-module-type-classifier'; +import type * as _nodeInternalModulesEsmResolve from '../dist-raw/node-internal-modules-esm-resolve'; +import type * as _nodeInternalModulesEsmGetFormat from '../dist-raw/node-internal-modules-esm-get_format'; +import type * as _nodeInternalModulesCjsLoader from '../dist-raw/node-internal-modules-cjs-loader'; +import { Extensions, getExtensions } from './file-extensions'; +import { createTsTranspileModule } from './ts-transpile-module'; export { TSCommon }; export { @@ -58,33 +67,6 @@ export type { const engineSupportsPackageTypeField = parseInt(process.versions.node.split('.')[0], 10) >= 12; -/** @internal */ -export function versionGteLt( - version: string, - gteRequirement: string, - ltRequirement?: string -) { - const [major, minor, patch, extra] = parse(version); - const [gteMajor, gteMinor, gtePatch] = parse(gteRequirement); - const isGte = - major > gteMajor || - (major === gteMajor && - (minor > gteMinor || (minor === gteMinor && patch >= gtePatch))); - let isLt = true; - if (ltRequirement) { - const [ltMajor, ltMinor, ltPatch] = parse(ltRequirement); - isLt = - major < ltMajor || - (major === ltMajor && - (minor < ltMinor || (minor === ltMinor && patch < ltPatch))); - } - return isGte && isLt; - - function parse(requirement: string) { - return requirement.split(/[\.-]/).map((s) => parseInt(s, 10)); - } -} - /** * Assert that script can be loaded as CommonJS when we attempt to require it. * If it should be loaded as ESM, throw ERR_REQUIRE_ESM like node does. @@ -96,7 +78,9 @@ let assertScriptCanLoadAsCJS: ( module: NodeJS.Module, filename: string ) => void = engineSupportsPackageTypeField - ? require('../dist-raw/node-cjs-loader-utils').assertScriptCanLoadAsCJSImpl + ? ( + require('../dist-raw/node-internal-modules-cjs-loader') as typeof _nodeInternalModulesCjsLoader + ).assertScriptCanLoadAsCJSImpl : () => { /* noop */ }; @@ -183,6 +167,8 @@ export const VERSION = require('../package.json').version; /** * Options for creating a new TypeScript compiler instance. + + * @category Basic */ export interface CreateOptions { /** @@ -339,8 +325,9 @@ export interface CreateOptions { experimentalReplAwait?: boolean; /** * Override certain paths to be compiled and executed as CommonJS or ECMAScript modules. - * When overridden, the tsconfig "module" and package.json "type" fields are overridden. - * This is useful because TypeScript files cannot use the .cjs nor .mjs file extensions; + * When overridden, the tsconfig "module" and package.json "type" fields are overridden, and + * the file extension is ignored. + * This is useful if you cannot use .mts, .cts, .mjs, or .cjs file extensions; * it achieves the same effect. * * Each key is a glob pattern following the same rules as tsconfig's "include" array. @@ -367,7 +354,9 @@ export interface CreateOptions { */ tsTrace?: (str: string) => void; /** - * TODO DOCS YAY + * Enable native ESM support. + * + * For details, see https://typestrong.org/ts-node/docs/imports#native-ecmascript-modules */ esm?: boolean; /** @@ -378,9 +367,16 @@ export interface CreateOptions { * @default false */ preferTsExts?: boolean; + /** + * Like node's `--experimental-specifier-resolution`, , but can also be set in your `tsconfig.json` for convenience. + * + * For details, see https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm + */ + experimentalSpecifierResolution?: 'node' | 'explicit'; } -export type ModuleTypes = Record; +export type ModuleTypes = Record; +export type ModuleTypeOverride = 'cjs' | 'esm' | 'package'; /** @internal */ export interface OptionBasePaths { @@ -392,6 +388,8 @@ export interface OptionBasePaths { /** * Options for registering a TypeScript compiler instance globally. + + * @category Basic */ export interface RegisterOptions extends CreateOptions { /** @@ -401,9 +399,11 @@ export interface RegisterOptions extends CreateOptions { * * For details, see https://github.com/TypeStrong/ts-node/issues/1514 */ - experimentalResolverFeatures?: boolean; + experimentalResolver?: boolean; } +export type ExperimentalSpecifierResolution = 'node' | 'explicit'; + /** * Must be an interface to support `typescript-json-schema`. */ @@ -464,14 +464,24 @@ export const DEFAULTS: RegisterOptions = { export class TSError extends BaseError { name = 'TSError'; diagnosticText!: string; + diagnostics!: ReadonlyArray<_ts.Diagnostic>; - constructor(diagnosticText: string, public diagnosticCodes: number[]) { + constructor( + diagnosticText: string, + public diagnosticCodes: number[], + diagnostics: ReadonlyArray<_ts.Diagnostic> = [] + ) { super(`⨯ Unable to compile TypeScript:\n${diagnosticText}`); Object.defineProperty(this, 'diagnosticText', { configurable: true, writable: true, value: diagnosticText, }); + Object.defineProperty(this, 'diagnostics', { + configurable: true, + writable: true, + value: diagnostics, + }); } /** @@ -515,6 +525,20 @@ export interface Service { transpileOnly: boolean; /** @internal */ projectLocalResolveHelper: ProjectLocalResolveHelper; + /** @internal */ + getNodeEsmResolver: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-esm-resolve').createResolve + >; + /** @internal */ + getNodeEsmGetFormat: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-esm-get_format').createGetFormat + >; + /** @internal */ + getNodeCjsLoader: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-cjs-loader').createCjsLoader + >; + /** @internal */ + extensions: Extensions; } /** @@ -534,24 +558,16 @@ export interface DiagnosticFilter { diagnosticsIgnored: number[]; } -/** @internal */ -export function getExtensions(config: _ts.ParsedCommandLine) { - const tsExtensions = ['.ts']; - const jsExtensions = []; - - // Enable additional extensions when JSX or `allowJs` is enabled. - if (config.options.jsx) tsExtensions.push('.tsx'); - if (config.options.allowJs) jsExtensions.push('.js'); - if (config.options.jsx && config.options.allowJs) jsExtensions.push('.jsx'); - return { tsExtensions, jsExtensions }; -} - /** * Create a new TypeScript compiler instance and register it onto node.js + * + * @category Basic */ export function register(opts?: RegisterOptions): Service; /** * Register TypeScript compiler instance onto node.js + + * @category Basic */ export function register(service: Service): Service; export function register( @@ -565,8 +581,6 @@ export function register( } const originalJsHandler = require.extensions['.js']; - const { tsExtensions, jsExtensions } = getExtensions(service.config); - const extensions = [...tsExtensions, ...jsExtensions]; // Expose registered instance globally. process[REGISTER_INSTANCE] = service; @@ -574,12 +588,12 @@ export function register( // Register the extensions. registerExtensions( service.options.preferTsExts, - extensions, + service.extensions.compiled, service, originalJsHandler ); - installCommonjsResolveHookIfNecessary(service); + installCommonjsResolveHooksIfNecessary(service); // Require specified modules before start-up. (Module as ModuleConstructorWithInternals)._preloadModules( @@ -591,6 +605,8 @@ export function register( /** * Create TypeScript compiler instance. + * + * @category Basic */ export function create(rawOptions: CreateOptions = {}): Service { const foundConfigResult = findAndReadConfig(rawOptions); @@ -712,6 +728,7 @@ export function createFromPreloadedConfig( const diagnosticHost: _ts.FormatDiagnosticsHost = { getNewLine: () => ts.sys.newLine, getCurrentDirectory: () => cwd, + // TODO switch to getCanonicalFileName we already create later in scope getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? (x) => x : (x) => x.toLowerCase(), @@ -722,41 +739,47 @@ export function createFromPreloadedConfig( 'Transformers function is unavailable in "--transpile-only"' ); } - let createTranspiler: - | ((compilerOptions: TSCommon.CompilerOptions) => Transpiler) - | undefined; - if (transpiler) { - if (!transpileOnly) - throw new Error( - 'Custom transpiler can only be used when transpileOnly is enabled.' + let createTranspiler = initializeTranspilerFactory(); + function initializeTranspilerFactory() { + if (transpiler) { + if (!transpileOnly) + throw new Error( + 'Custom transpiler can only be used when transpileOnly is enabled.' + ); + const transpilerName = + typeof transpiler === 'string' ? transpiler : transpiler[0]; + const transpilerOptions = + typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; + const transpilerConfigLocalResolveHelper = transpilerBasePath + ? createProjectLocalResolveHelper(transpilerBasePath) + : projectLocalResolveHelper; + const transpilerPath = transpilerConfigLocalResolveHelper( + transpilerName, + true ); - const transpilerName = - typeof transpiler === 'string' ? transpiler : transpiler[0]; - const transpilerOptions = - typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - const transpilerConfigLocalResolveHelper = transpilerBasePath - ? createProjectLocalResolveHelper(transpilerBasePath) - : projectLocalResolveHelper; - const transpilerPath = transpilerConfigLocalResolveHelper( - transpilerName, - true - ); - const transpilerFactory = require(transpilerPath) - .create as TranspilerFactory; - createTranspiler = function (compilerOptions) { - return transpilerFactory({ - service: { - options, - config: { - ...config, - options: compilerOptions, + const transpilerFactory = require(transpilerPath) + .create as TranspilerFactory; + return createTranspiler; + + function createTranspiler( + compilerOptions: TSCommon.CompilerOptions, + nodeModuleEmitKind?: NodeModuleEmitKind + ) { + return transpilerFactory?.({ + service: { + options, + config: { + ...config, + options: compilerOptions, + }, + projectLocalResolveHelper, }, - projectLocalResolveHelper, - }, - transpilerConfigLocalResolveHelper, - ...transpilerOptions, - }); - }; + transpilerConfigLocalResolveHelper, + nodeModuleEmitKind, + ...transpilerOptions, + }); + } + } } /** @@ -817,7 +840,7 @@ export function createFromPreloadedConfig( function createTSError(diagnostics: ReadonlyArray<_ts.Diagnostic>) { const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost); const diagnosticCodes = diagnostics.map((x) => x.code); - return new TSError(diagnosticText, diagnosticCodes); + return new TSError(diagnosticText, diagnosticCodes, diagnostics); } function reportTSError(configDiagnosticList: _ts.Diagnostic[]) { @@ -834,19 +857,39 @@ export function createFromPreloadedConfig( // Render the configuration errors. if (configDiagnosticList.length) reportTSError(configDiagnosticList); + const jsxEmitPreserve = config.options.jsx === ts.JsxEmit.Preserve; /** * Get the extension for a transpiled file. + * [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] */ - const getExtension = - config.options.jsx === ts.JsxEmit.Preserve - ? (path: string) => (/\.[tj]sx$/.test(path) ? '.jsx' : '.js') - : (_: string) => '.js'; + function getEmitExtension(path: string) { + const lastDotIndex = path.lastIndexOf('.'); + if (lastDotIndex >= 0) { + const ext = path.slice(lastDotIndex); + switch (ext) { + case '.js': + case '.ts': + return '.js'; + case '.jsx': + case '.tsx': + return jsxEmitPreserve ? '.jsx' : '.js'; + case '.mjs': + case '.mts': + return '.mjs'; + case '.cjs': + case '.cts': + return '.cjs'; + } + } + return '.js'; + } type GetOutputFunction = (code: string, fileName: string) => SourceOutput; /** - * Create the basic required function using transpile mode. + * Get output from TS compiler w/typechecking. `undefined` in `transpileOnly` + * mode. */ - let getOutput: GetOutputFunction; + let getOutput: GetOutputFunction | undefined; let getTypeInfo: ( _code: string, _fileName: string, @@ -868,7 +911,7 @@ export function createFromPreloadedConfig( const rootFileNames = new Set(config.fileNames); const cachedReadFile = cachedLookup(debugFn('readFile', readFile)); - // Use language services by default (TODO: invert next major version). + // Use language services by default if (!options.compilerHost) { let projectVersion = 1; const fileVersions = new Map( @@ -1031,9 +1074,13 @@ export function createFromPreloadedConfig( }; getTypeInfo = (code: string, fileName: string, position: number) => { - updateMemoryCache(code, fileName); + const normalizedFileName = normalizeSlashes(fileName); + updateMemoryCache(code, normalizedFileName); - const info = service.getQuickInfoAtPosition(fileName, position); + const info = service.getQuickInfoAtPosition( + normalizedFileName, + position + ); const name = ts.displayPartsToString(info ? info.displayParts : []); const comment = ts.displayPartsToString(info ? info.documentation : []); @@ -1217,9 +1264,10 @@ export function createFromPreloadedConfig( }; getTypeInfo = (code: string, fileName: string, position: number) => { - updateMemoryCache(code, fileName); + const normalizedFileName = normalizeSlashes(fileName); + updateMemoryCache(code, normalizedFileName); - const sourceFile = builderProgram.getSourceFile(fileName); + const sourceFile = builderProgram.getSourceFile(normalizedFileName); if (!sourceFile) throw new TypeError(`Unable to read file: ${fileName}`); @@ -1254,8 +1302,6 @@ export function createFromPreloadedConfig( } } } else { - getOutput = createTranspileOnlyGetOutputFunction(); - getTypeInfo = () => { throw new TypeError( 'Type information is unavailable in "--transpile-only"' @@ -1264,18 +1310,37 @@ export function createFromPreloadedConfig( } function createTranspileOnlyGetOutputFunction( - overrideModuleType?: _ts.ModuleKind + overrideModuleType?: _ts.ModuleKind, + nodeModuleEmitKind?: NodeModuleEmitKind ): GetOutputFunction { const compilerOptions = { ...config.options }; if (overrideModuleType !== undefined) compilerOptions.module = overrideModuleType; - let customTranspiler = createTranspiler?.(compilerOptions); + let customTranspiler = createTranspiler?.( + compilerOptions, + nodeModuleEmitKind + ); + let tsTranspileModule = versionGteLt(ts.version, '4.7.0') + ? createTsTranspileModule(ts, { + compilerOptions, + reportDiagnostics: true, + transformers: transformers as _ts.CustomTransformers | undefined, + }) + : undefined; return (code: string, fileName: string): SourceOutput => { let result: _ts.TranspileOutput; if (customTranspiler) { result = customTranspiler.transpile(code, { fileName, }); + } else if (tsTranspileModule) { + result = tsTranspileModule( + code, + { + fileName, + }, + nodeModuleEmitKind === 'nodeesm' ? 'module' : 'commonjs' + ); } else { result = ts.transpileModule(code, { fileName, @@ -1295,44 +1360,86 @@ export function createFromPreloadedConfig( }; } - // When either is undefined, it means normal `getOutput` should be used - const getOutputForceCommonJS = - config.options.module === ts.ModuleKind.CommonJS - ? undefined - : createTranspileOnlyGetOutputFunction(ts.ModuleKind.CommonJS); + // When true, these mean that a `moduleType` override will cause a different emit + // than the TypeScript compiler, so we *must* overwrite the emit. + const shouldOverwriteEmitWhenForcingCommonJS = + config.options.module !== ts.ModuleKind.CommonJS; // [MUST_UPDATE_FOR_NEW_MODULEKIND] - const getOutputForceESM = + const shouldOverwriteEmitWhenForcingEsm = !( config.options.module === ts.ModuleKind.ES2015 || (ts.ModuleKind.ES2020 && config.options.module === ts.ModuleKind.ES2020) || (ts.ModuleKind.ES2022 && config.options.module === ts.ModuleKind.ES2022) || config.options.module === ts.ModuleKind.ESNext - ? undefined - : // [MUST_UPDATE_FOR_NEW_MODULEKIND] - createTranspileOnlyGetOutputFunction( - ts.ModuleKind.ES2022 || ts.ModuleKind.ES2020 || ts.ModuleKind.ES2015 - ); + ); + /** + * node16 or nodenext + * [MUST_UPDATE_FOR_NEW_MODULEKIND] + */ + const isNodeModuleType = + (ts.ModuleKind.Node16 && config.options.module === ts.ModuleKind.Node16) || + (ts.ModuleKind.NodeNext && + config.options.module === ts.ModuleKind.NodeNext); + const getOutputForceCommonJS = createTranspileOnlyGetOutputFunction( + ts.ModuleKind.CommonJS + ); + const getOutputForceNodeCommonJS = createTranspileOnlyGetOutputFunction( + ts.ModuleKind.NodeNext, + 'nodecjs' + ); + const getOutputForceNodeESM = createTranspileOnlyGetOutputFunction( + ts.ModuleKind.NodeNext, + 'nodeesm' + ); + // [MUST_UPDATE_FOR_NEW_MODULEKIND] + const getOutputForceESM = createTranspileOnlyGetOutputFunction( + ts.ModuleKind.ES2022 || 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); - // Must always call normal getOutput to throw typechecking errors - let [value, sourceMap, emitSkipped] = getOutput(code, normalizedFileName); + moduleTypeClassifier.classifyModuleByModuleTypeOverrides( + normalizedFileName + ); + let value: string | undefined = ''; + let sourceMap: string | undefined = ''; + let emitSkipped = true; + if (getOutput) { + // Must always call normal getOutput to throw typechecking errors + [value, sourceMap, emitSkipped] = getOutput(code, normalizedFileName); + } // If module classification contradicts the above, call the relevant transpiler - if (classification.moduleType === 'cjs' && getOutputForceCommonJS) { + if ( + classification.moduleType === 'cjs' && + (shouldOverwriteEmitWhenForcingCommonJS || emitSkipped) + ) { [value, sourceMap] = getOutputForceCommonJS(code, normalizedFileName); - } else if (classification.moduleType === 'esm' && getOutputForceESM) { + } else if ( + classification.moduleType === 'esm' && + (shouldOverwriteEmitWhenForcingEsm || emitSkipped) + ) { [value, sourceMap] = getOutputForceESM(code, normalizedFileName); } else if (emitSkipped) { - [value, sourceMap] = getOutputTranspileOnly(code, normalizedFileName); + // Happens when ts compiler skips emit or in transpileOnly mode + const classification = classifyModule(fileName, isNodeModuleType); + [value, sourceMap] = + classification === 'nodecjs' + ? getOutputForceNodeCommonJS(code, normalizedFileName) + : classification === 'nodeesm' + ? getOutputForceNodeESM(code, normalizedFileName) + : classification === 'cjs' + ? getOutputForceCommonJS(code, normalizedFileName) + : classification === 'esm' + ? getOutputForceESM(code, normalizedFileName) + : getOutputTranspileOnly(code, normalizedFileName); } const output = updateOutput( value!, normalizedFileName, sourceMap!, - getExtension + getEmitExtension ); outputCache.set(normalizedFileName, { content: output }); return output; @@ -1341,14 +1448,11 @@ export function createFromPreloadedConfig( let active = true; const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled); - const extensions = getExtensions(config); + const extensions = getExtensions(config, options, ts.version); const ignored = (fileName: string) => { if (!active) return true; const ext = extname(fileName); - if ( - extensions.tsExtensions.includes(ext) || - extensions.jsExtensions.includes(ext) - ) { + if (extensions.compiled.includes(ext)) { return !isScoped(fileName) || shouldIgnore(fileName); } return true; @@ -1363,6 +1467,34 @@ export function createFromPreloadedConfig( }); } + const getNodeEsmResolver = once(() => + ( + require('../dist-raw/node-internal-modules-esm-resolve') as typeof _nodeInternalModulesEsmResolve + ).createResolve({ + extensions, + preferTsExts: options.preferTsExts, + tsNodeExperimentalSpecifierResolution: + options.experimentalSpecifierResolution, + }) + ); + const getNodeEsmGetFormat = once(() => + ( + require('../dist-raw/node-internal-modules-esm-get_format') as typeof _nodeInternalModulesEsmGetFormat + ).createGetFormat( + options.experimentalSpecifierResolution, + getNodeEsmResolver() + ) + ); + const getNodeCjsLoader = once(() => + ( + require('../dist-raw/node-internal-modules-cjs-loader') as typeof _nodeInternalModulesCjsLoader + ).createCjsLoader({ + extensions, + preferTsExts: options.preferTsExts, + nodeEsmResolver: getNodeEsmResolver(), + }) + ); + return { [TS_NODE_SERVICE_BRAND]: true, ts, @@ -1381,6 +1513,10 @@ export function createFromPreloadedConfig( enableExperimentalEsmLoaderInterop, transpileOnly, projectLocalResolveHelper, + getNodeEsmResolver, + getNodeEsmGetFormat, + getNodeCjsLoader, + extensions, }; } @@ -1396,17 +1532,6 @@ function createIgnore(ignoreBaseDir: string, ignore: RegExp[]) { }; } -/** - * "Refreshes" an extension on `require.extensions`. - * - * @param {string} ext - */ -function reorderRequireExtension(ext: string) { - const old = require.extensions[ext]; - delete require.extensions[ext]; - require.extensions[ext] = old; -} - /** * Register the extensions to support when importing files. */ @@ -1416,18 +1541,35 @@ function registerExtensions( service: Service, originalJsHandler: (m: NodeModule, filename: string) => any ) { + const exts = new Set(extensions); + // Can't add these extensions cuz would allow omitting file extension; node requires ext for .cjs and .mjs + // Unless they're already registered by something else (nyc does this): + // then we *must* hook them or else our transformer will not be called. + for (const cannotAdd of ['.mts', '.cts', '.mjs', '.cjs']) { + if (exts.has(cannotAdd) && !hasOwnProperty(require.extensions, cannotAdd)) { + // Unrecognized file exts can be transformed via the `.js` handler. + exts.add('.js'); + exts.delete(cannotAdd); + } + } + // Register new extensions. - for (const ext of extensions) { + for (const ext of exts) { registerExtension(ext, service, originalJsHandler); } if (preferTsExts) { const preferredExtensions = new Set([ - ...extensions, + ...exts, ...Object.keys(require.extensions), ]); - for (const ext of preferredExtensions) reorderRequireExtension(ext); + // Re-sort iteration order of Object.keys() + for (const ext of preferredExtensions) { + const old = Object.getOwnPropertyDescriptor(require.extensions, ext); + delete require.extensions[ext]; + Object.defineProperty(require.extensions, ext, old!); + } } } @@ -1471,7 +1613,7 @@ function updateOutput( outputText: string, fileName: string, sourceMap: string, - getExtension: (fileName: string) => string + getEmitExtension: (fileName: string) => string ) { const base64Map = Buffer.from( updateSourceMap(sourceMap, fileName), @@ -1484,7 +1626,7 @@ function updateOutput( const prefixLength = prefix.length; const baseName = /*foo.tsx*/ basename(fileName); const extName = /*.tsx*/ extname(fileName); - const extension = /*.js*/ getExtension(fileName); + const extension = /*.js*/ getEmitExtension(fileName); const sourcemapFilename = baseName.slice(0, -extName.length) + extension + '.map'; const sourceMapLengthWithoutPercentEncoding = @@ -1585,7 +1727,17 @@ function getTokenAtPosition( * * Node changed the hooks API, so there are two possible APIs. This function * detects your node version and returns the appropriate API. + * + * @category ESM Loader */ export const createEsmHooks: typeof createEsmHooksFn = ( tsNodeService: Service ) => (require('./esm') as typeof import('./esm')).createEsmHooks(tsNodeService); + +/** + * When using `module: nodenext` or `module: node12`, there are two possible styles of emit depending in file extension or package.json "type": + * + * - CommonJS with dynamic imports preserved (not transformed into `require()` calls) + * - ECMAScript modules with `import foo = require()` transformed into `require = createRequire(); const foo = require()` + */ +export type NodeModuleEmitKind = 'nodeesm' | 'nodecjs'; diff --git a/src/module-type-classifier.ts b/src/module-type-classifier.ts index dfe15328..34a4fba5 100644 --- a/src/module-type-classifier.ts +++ b/src/module-type-classifier.ts @@ -1,20 +1,24 @@ -import { dirname } from 'path'; +import type { ModuleTypeOverride, ModuleTypes } from '.'; import { getPatternFromSpec } from './ts-internals'; import { cachedLookup, normalizeSlashes } from './util'; -// Logic to support out `moduleTypes` option, which allows overriding node's default ESM / CJS +// Logic to support our `moduleTypes` option, which allows overriding node's default ESM / CJS // classification of `.js` files based on package.json `type` field. -/** @internal */ -export type ModuleType = 'cjs' | 'esm' | 'package'; +/** + * Seperate internal type because `auto` is clearer than `package`, but changing + * the public API is a breaking change. + * @internal + */ +export type InternalModuleTypeOverride = 'cjs' | 'esm' | 'auto'; /** @internal */ export interface ModuleTypeClassification { - moduleType: ModuleType; + moduleType: InternalModuleTypeOverride; } /** @internal */ export interface ModuleTypeClassifierOptions { basePath?: string; - patterns?: Record; + patterns?: ModuleTypes; } /** @internal */ export type ModuleTypeClassifier = ReturnType< @@ -42,17 +46,18 @@ export function createModuleTypeClassifier( } ); - const classifications: Record = { - package: { - moduleType: 'package', - }, - cjs: { - moduleType: 'cjs', - }, - esm: { - moduleType: 'esm', - }, - }; + const classifications: Record = + { + package: { + moduleType: 'auto', + }, + cjs: { + moduleType: 'cjs', + }, + esm: { + moduleType: 'esm', + }, + }; const auto = classifications.package; // Passed path must be normalized! @@ -69,7 +74,7 @@ export function createModuleTypeClassifier( } return { - classifyModule: patternTypePairs.length + classifyModuleByModuleTypeOverrides: patternTypePairs.length ? classifyModule : classifyModuleAuto, }; diff --git a/src/node-module-type-classifier.ts b/src/node-module-type-classifier.ts new file mode 100644 index 00000000..19a9e836 --- /dev/null +++ b/src/node-module-type-classifier.ts @@ -0,0 +1,37 @@ +import { readPackageScope } from '../dist-raw/node-internal-modules-cjs-loader'; + +/** + * Determine how to emit a module based on tsconfig "module" and package.json "type" + * + * Supports module=nodenext/node16 with transpileOnly, where we cannot ask the + * TS typechecker to tell us if a file is CJS or ESM. + * + * Return values indicate: + * - cjs + * - esm + * - nodecjs == node-flavored cjs where dynamic imports are *not* transformed into `require()` + * - undefined == emit according to tsconfig `module` config, whatever that is + * @internal + */ +export function classifyModule( + nativeFilename: string, + isNodeModuleType: boolean +): 'nodecjs' | 'cjs' | 'esm' | 'nodeesm' | undefined { + // [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] + const lastDotIndex = nativeFilename.lastIndexOf('.'); + const ext = lastDotIndex >= 0 ? nativeFilename.slice(lastDotIndex) : ''; + switch (ext) { + case '.cjs': + case '.cts': + return isNodeModuleType ? 'nodecjs' : 'cjs'; + case '.mjs': + case '.mts': + return isNodeModuleType ? 'nodeesm' : 'esm'; + } + if (isNodeModuleType) { + const packageScope = readPackageScope(nativeFilename); + if (packageScope && packageScope.data.type === 'module') return 'nodeesm'; + return 'nodecjs'; + } + return undefined; +} diff --git a/src/node-resolver-functions.ts.disabled b/src/node-resolver-functions.ts.disabled new file mode 100644 index 00000000..3d56b028 --- /dev/null +++ b/src/node-resolver-functions.ts.disabled @@ -0,0 +1,389 @@ +import { cachedLookup, normalizeSlashes } from './util'; +import type * as _ts from 'typescript'; +import { dirname, resolve } from 'path'; +import { getPatternFromSpec } from './ts-internals'; +import type { TSInternal } from './ts-compiler-types'; +import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; +import { + getTsConfigDefaults, + ComputeAsCommonRootOfFiles, +} from './configuration'; + +interface SrcOutPair { + preferSrc: boolean; + root: string; + out: string; +} +// interface RootDirsSet { +// rootDirs: string[]; +// } + +function contains(parentDirWithTrailingSlash: string, childDir: string) { + return childDir.startsWith(parentDirWithTrailingSlash); +} + +class SolutionResolver {} + +class ProjectResolver { + files: string[]; + includeRe: RegExp; + excludeRe: Regexp; + + constuctor( + ts: TSInternal, + tsconfig: _ts.ParsedCommandLine, + configFilePathOrCwd: string, + basePath: string, + files: string[] | undefined, + include: string[] | undefined, + exclude: string[] | undefined + ) { + // const configBaseDir = normalizeSlashes(dirname(configFilePathOrCwd)); + const { + rootDir, + include: includeSpecs, + files: filesArray, + exclude: excludeSpecs, + } = getTsConfigDefaults(tsconfig, basePath, files, include, exclude); + if (rootDir === ComputeAsCommonRootOfFiles) { + throw new Error( + 'Cannot determine rootDir if composite is not set. Either enable composite or set explicit rootDir' + ); + } + + this.files = filesArray.map((f) => normalizeSlashes(resolve(basePath, f))); + const reString = ts.getRegularExpressionForWildcard( + includeSpecs, + basePath, + 'files' + ); + this.includeRe = new RegExp(reString ?? '$^'); + const reString2 = ts.getRegularExpressionForWildcard( + excludeSpecs as string[], + basePath, + 'exclude' + ); + this.excludeRe = new RegExp(reString2 ?? '$^'); + } +} + +function createNodeResolverFunctions(opts: { + allowJs: boolean; + jsx: boolean; + cjsMjs: boolean; +}) { + const { allowJs, cjsMjs, jsx } = opts; + + const rootOutPairs: SrcOutPair[] = []; + // const rootDirsSets: RootDirsSet[] = []; + + /** + * Must be passed normalized slashes! + * Assumes root and out are different! + */ + function addRootOutPair(root: string, out: string, preferSrc = true) { + root = ensureTrailingSlash(root); + out = ensureTrailingSlash(out); + rootOutPairs.push({ root, out, preferSrc }); + } + + function ensureTrailingSlash(str: string) { + if (str.includes('\\')) + throw new Error('String must have normalized slashes'); + if (!str.endsWith('/')) str += '/'; + return str; + } + + // function mapFromOutToRoot(directory: string) { + // directory = ensureTrailingSlash(directory); + // for(const {out, root} of rootOutPairs) { + // if(directory.startsWith(out)) { + // return root + directory.slice(out.length); + // } + // } + // return directory; + // } + function mapFromRootToOut(directory: string) { + directory = ensureTrailingSlash(directory); + for (const { out, root } of rootOutPairs) { + if (directory.startsWith(root)) { + // TODO how to exclude node_modules from this mapping?? + // Check regexp patterns to see if this file is included? + return out + directory.slice(root.length); + } + } + return directory; + } + + // /** Must be passed normalized slashes! */ + // function addRootDirs(rootDirs: string[]) { + // rootDirsSets.push({ + // rootDirs: rootDirs.map(rootDir => rootDir.endsWith('/') ? rootDir : rootDir + '/') + // }); + // } + + const getAlternativeDirectoriesCached = cachedLookup( + getAlternativeDirectories + ); + + /** Get array of alternative directories to check because they're overlaid over each other */ + function* getAlternativeDirectories(directory: string) { + directory = ensureTrailingSlash(directory); + for (const { out, preferSrc, root } of rootOutPairs) { + if (contains(root, directory)) { + if (preferSrc) { + yield directory; // directory is in src; preferred + yield out + directory.slice(root.length); + } else { + yield out + directory.slice(root.length); + yield directory; + } + } else if (contains(out, directory)) { + if (preferSrc) { + yield root + directory.slice(out.length); + yield directory; // directory is in out + } else { + yield directory; + yield root + directory.slice(out.length); + } + } else { + yield directory; + } + } + // for(const rootDirsSet of rootDirsSets) { + // const alternatives2: string[] = []; + // for(const alternative of alternatives!) { + // ForRootDirsInSingleSet: + // for(const rootDir of rootDirsSet.rootDirs) { + // if(contains(rootDir, alternative)) { + // // alternative matches; replace it with each rootDir in the set + // for(const rootDir2 of rootDirsSet.rootDirs) { + // alternatives2.push(rootDir2 + alternative.slice(rootDir.length)); + // } + // break ForRootDirsInSingleSet; + // } + // } + // // alternative does not match; passthrough + // alternatives2.push(alternative); + // } + // alternatives = alternatives2; + // } + } + + // If extension is omitted and we are expected to add one, try these + const extensionlessExtensions = [ + 'js', + 'cjs', + 'mjs', + jsx && 'jsx', + 'ts', + 'cts', + 'mts', + jsx && 'tsx', + ]; + // If extension already specified, and is recognized, attempt these replacements + const jsExtensions = ['js', jsx && 'jsx', 'ts', jsx && 'tsx'].filter( + (v) => v + ) as string[]; + const cjsExtensions = ['cjs', 'cts']; + const mjsExtensions = ['mjs', 'mts']; + + /** + * Get alternative filenames to check because they're equivalent. + * + * Alternatives should only be attempted in: + * -- rootDir, if was able to map root<==>out + * -- otherwise attempt in dir, whatever it is. + */ + function* getAlternativeFilenames( + filename: string, + allowOmitFileExtension: boolean + ) { + // TODO be sure to avoid .d.ts, .d.mts, and .d.cts + const lastDotIndex = filename.lastIndexOf('.'); + let emittedReplacements = false; + if (lastDotIndex > 0) { + const endsInDts = + filename.endsWith('.d.ts') || + filename.endsWith('.d.cts') || + filename.endsWith('.d.mts'); + if (!endsInDts) { + const name = filename.slice(0, lastDotIndex); + const extension = filename.slice(lastDotIndex + 1); + const replacements = + extension === 'js' + ? jsExtensions + : extension === 'cjs' + ? cjsExtensions + : extension === 'mjs' + ? mjsExtensions + : undefined; + if (replacements) { + emittedReplacements = true; + for (const replacement of replacements) { + yield name + '.' + replacement; + } + } + } + } + if (!emittedReplacements) yield filename; + if (allowOmitFileExtension) { + for (const replacement of extensionlessExtensions) { + yield filename + '.' + replacement; + } + } + } + + return { + addRootOutPair, + getAlternativeDirectories, + getAlternativeDirectoriesCached, + getAlternativeFilenames, + }; +} + +/* +. +dist + +If rootDir matches any rootDirs entry: +- generate list of alternative rootDir +- map each to outDir + +foo +bar +baz + +For path foo/hello: + +foo/hello +bar/hello +baz/hello +dist/foo/hello +dist/bar/hello +dist/baz/hello + +For path node_modules/lodash +node_modules/lodash +dist/node_modules/lodash + +If directory is outside of common root of all mappings, skip + +If parent mappings were computed, how can they be augmented? +For each directory, a given mapping is either APPLIED, IRRELEVANT, or NOTYET +- src <-> out +- if any rootDirs are child of src or dist +*/ + +/* + +src/foo +dist/foo + +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz + +foo/src/lib +dist + +foo/src/lib +bar/src/lib +baz/src/lib +dist/lib + +outDir mapping: src/foo->dist +rootDirs mappings: ./foo, ./bar, ./baz +src/foo +src/bar +src/baz +dist + +outDir mapping: src->dist +rootDirs mappings: ./foo, ./bar, ./baz +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz + +expand src by rootDirs +then expand each by root->out mappings +*/ + +/* +For now, think about *only* rootDir<->outDir mappings +Sort all rootDir by length descending +Sort all outDir by length descending +Attempt mapping for each. +As soon as mapping succeeds for *any single entry*, stop attempting others. +*/ + +/* +rootDirs +src/foo +src/bar +src/baz + +preprocess to include rootDir<->outDir +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz +*/ + +/* +First must map importer from src to dist if possible. + +Then attempt import relative to the dist location, +checking dist<-src whenever possible. + +*/ + +// const ts: typeof _ts; + +// ts.createLanguageServiceSourceFile +// ts.createSourceFile +// ts.createUnparsedSourceFile +// ts.create + +/* +To use experimentalResolver: +- must set explicit rootDir, imply it with composite, or imply it with references + +When resolving an import + +##Step one: convert importer path to outDir if appropriate + +If importer is within rootDir & matched by include/files/exclude, map it to outDir +- Abandon mapping if `outDir` and `rootDir` identical +- Abandon mapping if import already within outDir +- sort by rootDir specificity so that workspaces are matched prior to ancestors +- mapping logic can be cached per __dirname&tsconfig pair, but must still consider if filename matches regexps + - allows supporting multiple tsconfigs with same rootDir +- allows respecting `path` mappings when we know which tsconfig governs the importer, because we have a single tsconfig to use for mappings + +##Step two: convert (many) target paths to rootDir if appropriate + +While resolving, we check many directories. +If resolution target is within `outDir`, attempt mapping to `rootDir` + - Abandon mapping if `outDir` and `rootDir` identical + - Abandon mapping if target already within `rootDir` + - Abandon mapping if is not matched by include/files/exclude + - HOW TO CHECK THIS BEFORE WE HAVE A FILE EXTENSION? For each include/files/exclude, generate a version that ignores file extensions? Test the directory with it? + - `TsconfigFilesMatcher.directoryChildMightMatch(dirname)` + - `TsconfigFilesMatcher.directoryAncestorMightMatch(dirname)` + + - OPTIMIZATION + - If none of the include / files patterns contain `node_modules`, and if target directory after basedir contains `node_modules`, then we *know* the entire + directory tree is excluded + - This optimization should apply to almost all projects + + - OPTIMIZATION detect when all file extensions are treated identically?? +*/ diff --git a/src/repl.ts b/src/repl.ts index c6371bdb..eed95a0d 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -22,7 +22,7 @@ function getProcessTopLevelAwait() { if (_processTopLevelAwait === undefined) { ({ processTopLevelAwait: _processTopLevelAwait, - } = require('../dist-raw/node-repl-await')); + } = require('../dist-raw/node-internal-repl-await')); } return _processTopLevelAwait; } @@ -115,6 +115,7 @@ export interface ReplService { readonly console: Console; } +/** @category REPL */ export interface CreateReplOptions { service?: Service; state?: EvalState; @@ -123,7 +124,6 @@ export interface CreateReplOptions { stderr?: NodeJS.WritableStream; /** @internal */ composeWithEvalAwarePartialHost?: EvalAwarePartialHost; - // TODO collapse both of the following two flags into a single `isInteractive` or `isLineByLine` flag. /** * @internal * Ignore diagnostics that are annoying when interactively entering input line-by-line. @@ -144,6 +144,8 @@ export interface CreateReplOptions { * const service = tsNode.create({...repl.evalAwarePartialHost}); * repl.setService(service); * repl.start(); + * + * @category REPL */ export function createRepl(options: CreateReplOptions = {}) { const { ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl = true } = options; @@ -506,6 +508,7 @@ function appendCompileAndEvalInput(options: { service: Service; state: EvalState; input: string; + wrappedErr?: unknown; /** Enable top-level await but only if the TSNode service allows it. */ enableTopLevelAwait?: boolean; context: Context | undefined; @@ -513,10 +516,22 @@ function appendCompileAndEvalInput(options: { const { service, state, - input, + wrappedErr, enableTopLevelAwait = false, context, } = options; + let { input } = options; + + // It's confusing for `{ a: 1 }` to be interpreted as a block statement + // rather than an object literal. So, we first try to wrap it in + // parentheses, so that it will be interpreted as an expression. + // Based on https://github.com/nodejs/node/blob/c2e6822153bad023ab7ebd30a6117dcc049e475c/lib/repl.js#L413-L422 + let wrappedCmd = false; + if (!wrappedErr && /^\s*{/.test(input) && !/;\s*$/.test(input)) { + input = `(${input.trim()})\n`; + wrappedCmd = true; + } + const lines = state.lines; const isCompletion = !/\n$/.test(input); const undo = appendToEvalState(state, input); @@ -533,6 +548,20 @@ function appendCompileAndEvalInput(options: { output = service.compile(state.input, state.path, -lines); } catch (err) { undo(); + + if (wrappedCmd) { + if (err instanceof TSError && err.diagnosticCodes[0] === 2339) { + // Ensure consistent and more sane behavior between { a: 1 }['b'] and ({ a: 1 }['b']) + throw err; + } + // Unwrap and try again + return appendCompileAndEvalInput({ + ...options, + wrappedErr: err, + }); + } + + if (wrappedErr) throw wrappedErr; throw err; } @@ -676,17 +705,28 @@ function lineCount(value: string) { } /** - * TS diagnostic codes which are recoverable, meaning that the user likely entered and incomplete line of code + * TS diagnostic codes which are recoverable, meaning that the user likely entered an incomplete line of code * and should be prompted for the next. For example, starting a multi-line for() loop and not finishing it. + * null value means code is always recoverable. `Set` means code is only recoverable when occurring alongside at least one + * of the other codes. */ -const RECOVERY_CODES: Set = new Set([ - 1003, // "Identifier expected." - 1005, // "')' expected." - 1109, // "Expression expected." - 1126, // "Unexpected end of text." - 1160, // "Unterminated template literal." - 1161, // "Unterminated regular expression literal." - 2355, // "A function whose declared type is neither 'void' nor 'any' must return a value." +const RECOVERY_CODES: Map | null> = new Map([ + [1003, null], // "Identifier expected." + [1005, null], // "')' expected.", "'}' expected." + [1109, null], // "Expression expected." + [1126, null], // "Unexpected end of text." + [ + 1136, // "Property assignment expected." + new Set([1005]), // happens when typing out an object literal or block scope across multiple lines: '{ foo: 123,' + ], + [1160, null], // "Unterminated template literal." + [1161, null], // "Unterminated regular expression literal." + [2355, null], // "A function whose declared type is neither 'void' nor 'any' must return a value." + [2391, null], // "Function implementation is missing or not immediately following the declaration." + [ + 7010, // "Function, which lacks return-type annotation, implicitly has an 'any' return type." + new Set([1005]), // happens when fn signature spread across multiple lines: 'function a(\nb: any\n) {' + ], ]); /** @@ -705,7 +745,13 @@ const topLevelAwaitDiagnosticCodes = [ * Check if a function can recover gracefully. */ function isRecoverable(error: TSError) { - return error.diagnosticCodes.every((code) => RECOVERY_CODES.has(code)); + return error.diagnosticCodes.every((code) => { + const deps = RECOVERY_CODES.get(code); + return ( + deps === null || + (deps && error.diagnosticCodes.some((code) => deps.has(code))) + ); + }); } /** diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index ecb5d98e..9884c29c 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -64,12 +64,17 @@ export function createResolverFunctions(kwargs: { ) => { const { resolvedFileName } = resolvedModule; if (resolvedFileName === undefined) return; - // .ts is always switched to internal + // [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS] + // .ts,.mts,.cts is always switched to internal // .js is switched on-demand if ( resolvedModule.isExternalLibraryImport && ((resolvedFileName.endsWith('.ts') && !resolvedFileName.endsWith('.d.ts')) || + (resolvedFileName.endsWith('.cts') && + !resolvedFileName.endsWith('.d.cts')) || + (resolvedFileName.endsWith('.mts') && + !resolvedFileName.endsWith('.d.mts')) || isFileKnownToBeInternal(resolvedFileName) || isFileInInternalBucket(resolvedFileName)) ) { diff --git a/src/test/diagnostics.spec.ts b/src/test/diagnostics.spec.ts new file mode 100644 index 00000000..994f6921 --- /dev/null +++ b/src/test/diagnostics.spec.ts @@ -0,0 +1,67 @@ +import type { TSError } from '..'; +import { ctxTsNode, ts } from './helpers'; +import { context, expect } from './testlib'; +import * as semver from 'semver'; +import { once } from 'lodash'; +const test = context(ctxTsNode); + +test.suite('TSError diagnostics', ({ context }) => { + const test = context( + once(async (t) => { + const service = t.context.tsNodeUnderTest.create({ + compilerOptions: { target: 'es5' }, + skipProject: true, + }); + try { + service.compile('new Error(123)', 'test.ts'); + } catch (err) { + return { service, err }; + } + return { service, err: undefined }; + }) + ); + + const diagnosticCode = 2345; + const diagnosticMessage = semver.satisfies(ts.version, '2.7') + ? "Argument of type '123' " + + "is not assignable to parameter of type 'string | undefined'." + : "Argument of type 'number' " + + "is not assignable to parameter of type 'string'."; + const diagnosticErrorMessage = `TS${diagnosticCode}: ${diagnosticMessage}`; + + const cwdBefore = process.cwd(); + test('should throw errors', ({ log, context: { err, service } }) => { + log({ + version: ts.version, + serviceVersion: service.ts.version, + cwdBefore, + cwd: process.cwd(), + configFilePath: service.configFilePath, + config: service.config.options, + }); + expect(err).toBeDefined(); + expect((err as Error).message).toMatch(diagnosticErrorMessage); + }); + + test('should throw errors with diagnostic text', ({ context: { err } }) => { + expect((err as TSError).diagnosticText).toMatch(diagnosticErrorMessage); + }); + + test('should throw errors with diagnostic codes', ({ context: { err } }) => { + expect((err as TSError).diagnosticCodes).toEqual([2345]); + }); + + test('should throw errors with complete diagnostic information', ({ + context: { err }, + }) => { + const diagnostics = (err as TSError).diagnostics; + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toMatchObject({ + code: 2345, + start: 10, + length: 3, + messageText: expect.stringMatching(diagnosticMessage), + }); + }); +}); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index cc673e54..54242abf 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -10,7 +10,7 @@ import { BIN_PATH_JS, CMD_ESM_LOADER_WITHOUT_PROJECT, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, - contextTsNodeUnderTest, + ctxTsNode, delay, EXPERIMENTAL_MODULES_FLAG, nodeSupportsEsmHooks, @@ -26,7 +26,7 @@ import * as expect from 'expect'; import type { NodeLoaderHooksAPI2 } from '../'; import { pathToFileURL } from 'url'; -const test = context(contextTsNodeUnderTest); +const test = context(ctxTsNode); const exec = createExec({ cwd: TEST_DIR, @@ -48,22 +48,24 @@ test.suite('esm', (test) => { expect(err).toBe(null); expect(stdout).toBe('foo bar baz biff libfoo\n'); }); - test('should use source maps', async () => { - const { err, stdout } = await exec( + test('should use source maps', async (t) => { + const { err, stdout, stderr } = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} "throw error.ts"`, { cwd: join(TEST_DIR, './esm'), } ); expect(err).not.toBe(null); + const expectedModuleUrl = pathToFileURL( + join(TEST_DIR, './esm/throw error.ts') + ).toString(); expect(err!.message).toMatch( [ - `${pathToFileURL(join(TEST_DIR, './esm/throw error.ts')) - .toString() - .replace(/%20/g, ' ')}:100`, + `${expectedModuleUrl}:100`, " bar() { throw new Error('this is a demo'); }", ' ^', 'Error: this is a demo', + ` at Foo.bar (${expectedModuleUrl}:100:17)`, ].join('\n') ); }); @@ -233,8 +235,8 @@ test.suite('esm', (test) => { }); }); - test.suite('unit test hooks', (_test) => { - const test = _test.context(async (t) => { + test.suite('unit test hooks', ({ context }) => { + const test = context(async (t) => { const service = t.context.tsNodeUnderTest.create({ cwd: TEST_DIR, }); diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index bf076647..c98c4c7b 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -100,6 +100,7 @@ export function createSpawn>( } const defaultExec = createExec(); +export { defaultExec as exec }; export interface ExecTesterOptions { cmd: string; diff --git a/src/test/fs-helpers.ts b/src/test/fs-helpers.ts new file mode 100644 index 00000000..e34d2d81 --- /dev/null +++ b/src/test/fs-helpers.ts @@ -0,0 +1,103 @@ +import { TEST_DIR } from './helpers'; +import * as fs from 'fs'; +import * as Path from 'path'; + +// Helpers to describe a bunch of files in a project programmatically, +// then write them to disk in a temp directory. + +export interface File { + path: string; + content: string; +} +export interface JsonFile extends File { + obj: T; +} +export interface DirectoryApi { + add(file: File): File; + addFile(...args: Parameters): File; + addJsonFile(...args: Parameters): JsonFile; + dir(dirPath: string, cb?: (dir: DirectoryApi) => void): DirectoryApi; +} + +export type ProjectAPI = ReturnType; + +export function file(path: string, content = '') { + return { path, content }; +} +export function jsonFile(path: string, obj: T) { + const file: JsonFile = { + path, + obj, + get content() { + return JSON.stringify(obj, null, 2); + }, + }; + return file; +} + +export function tempdirProject(name = '') { + const rootTmpDir = `${TEST_DIR}/tmp/`; + fs.mkdirSync(rootTmpDir, { recursive: true }); + const tmpdir = fs.mkdtempSync(`${TEST_DIR}/tmp/${name}`); + return projectInternal(tmpdir); +} + +export type Project = ReturnType; +export function project(name: string) { + return projectInternal(`${TEST_DIR}/tmp/${name}`); +} + +function projectInternal(cwd: string) { + const files: File[] = []; + function write() { + for (const file of files) { + fs.mkdirSync(Path.dirname(file.path), { recursive: true }); + fs.writeFileSync(file.path, file.content); + } + } + function rm() { + try { + fs.rmdirSync(cwd, { recursive: true }); + } catch (err) { + if (fs.existsSync(cwd)) throw err; + } + } + const { add, addFile, addJsonFile, dir } = createDirectory(cwd); + function createDirectory( + dirPath: string, + cb?: (dir: DirectoryApi) => void + ): DirectoryApi { + function add(file: File) { + file.path = Path.join(dirPath, file.path); + files.push(file); + return file; + } + function addFile(...args: Parameters) { + return add(file(...args)); + } + function addJsonFile(...args: Parameters) { + return add(jsonFile(...args)) as JsonFile; + } + function dir(path: string, cb?: (dir: DirectoryApi) => void) { + return createDirectory(Path.join(dirPath, path), cb); + } + const _dir: DirectoryApi = { + add, + addFile, + addJsonFile, + dir, + }; + cb?.(_dir); + return _dir; + } + return { + cwd, + files: [], + dir, + add, + addFile, + addJsonFile, + write, + rm, + }; +} diff --git a/src/test/helpers.ts b/src/test/helpers.ts index cddf7575..0a58c5a6 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -12,8 +12,9 @@ import type { Readable } from 'stream'; */ import type * as tsNodeTypes from '../index'; import type _createRequire from 'create-require'; -import { has, once } from 'lodash'; +import { has, mapValues, once, sortBy } from 'lodash'; import semver = require('semver'); +import type { ExecutionContext } from './testlib'; const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; @@ -22,6 +23,10 @@ export const ROOT_DIR = resolve(__dirname, '../..'); export const DIST_DIR = resolve(__dirname, '..'); export const TEST_DIR = join(__dirname, '../../tests'); export const PROJECT = join(TEST_DIR, 'tsconfig.json'); +export const PROJECT_TRANSPILE_ONLY = join( + TEST_DIR, + 'tsconfig-transpile-only.json' +); export const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node'); export const BIN_PATH_JS = join(TEST_DIR, 'node_modules/ts-node/dist/bin.js'); export const BIN_SCRIPT_PATH = join( @@ -30,11 +35,15 @@ export const BIN_SCRIPT_PATH = join( ); export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); + +process.chdir(TEST_DIR); //#endregion //#region command lines /** Default `ts-node --project` invocation */ export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; +/** Default `ts-node --project` invocation with transpile-only */ +export const CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG = `"${BIN_PATH}" --project "${PROJECT_TRANSPILE_ONLY}"`; /** Default `ts-node` invocation without `--project` */ export const CMD_TS_NODE_WITHOUT_PROJECT_FLAG = `"${BIN_PATH}"`; export const EXPERIMENTAL_MODULES_FLAG = semver.gte(process.version, '12.17.0') @@ -66,18 +75,27 @@ export const tsSupportsTsconfigInheritanceViaNodePackages = semver.gte( ); /** Supports --showConfig: >= v3.2.0 */ export const tsSupportsShowConfig = semver.gte(ts.version, '3.2.0'); +/** Supports module:nodenext and module:node16 as *stable* features */ +export const tsSupportsStableNodeNextNode16 = + ts.version.startsWith('4.7.') || semver.gte(ts.version, '4.7.0'); +// TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions +export const tsSupportsMtsCtsExtensions = semver.gte(ts.version, '4.5.0'); //#endregion export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ -export const contextTsNodeUnderTest = once(async () => { +export const ctxTsNode = once(async () => { await installTsNode(); const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node'); return { tsNodeUnderTest, }; }); +export namespace ctxTsNode { + export type Ctx = Awaited>; + export type T = ExecutionContext; +} //#region install ts-node tarball const ts_node_install_lock = process.env.ts_node_install_lock as string; @@ -189,7 +207,8 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { //#region Reset node environment const defaultRequireExtensions = captureObjectState(require.extensions); -const defaultProcess = captureObjectState(process); +// Avoid node deprecation warning for accessing _channel +const defaultProcess = captureObjectState(process, ['_channel']); const defaultModule = captureObjectState(require('module')); const defaultError = captureObjectState(Error); const defaultGlobal = captureObjectState(global); @@ -201,53 +220,100 @@ const defaultGlobal = captureObjectState(global); * Must also play nice with `nyc`'s environmental mutations. */ export function resetNodeEnvironment() { + const sms = + require('@cspotcode/source-map-support') as typeof import('@cspotcode/source-map-support'); // We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test. - require('@cspotcode/source-map-support').uninstall(); + sms.uninstall(); + // Must remove handlers to avoid a memory leak + sms.resetRetrieveHandlers(); // Modified by ts-node hooks - resetObject(require.extensions, defaultRequireExtensions); + resetObject( + require.extensions, + defaultRequireExtensions, + undefined, + undefined, + undefined, + true + ); // ts-node attaches a property when it registers an instance // source-map-support monkey-patches the emit function - resetObject(process, defaultProcess); + // Avoid node deprecation warnings for setting process.config or accessing _channel + resetObject(process, defaultProcess, undefined, ['_channel'], ['config']); // source-map-support swaps out the prepareStackTrace function resetObject(Error, defaultError); - // _resolveFilename is modified by tsconfig-paths, future versions of source-map-support, and maybe future versions of ts-node - resetObject(require('module'), defaultModule); + // _resolveFilename et.al. are modified by ts-node, tsconfig-paths, source-map-support, yarn, maybe other things? + resetObject(require('module'), defaultModule, undefined, ['wrap', 'wrapper']); // May be modified by REPL tests, since the REPL sets globals. - resetObject(global, defaultGlobal); + // Avoid deleting nyc's coverage data. + resetObject(global, defaultGlobal, ['__coverage__']); + + // Reset our ESM hooks + process.__test_setloader__?.(undefined); } -function captureObjectState(object: any) { +function captureObjectState(object: any, avoidGetters: string[] = []) { + const descriptors = Object.getOwnPropertyDescriptors(object); + const values = mapValues(descriptors, (_d, key) => { + if (avoidGetters.includes(key)) return descriptors[key].value; + return object[key]; + }); return { - descriptors: Object.getOwnPropertyDescriptors(object), - values: { ...object }, + descriptors, + values, }; } // Redefine all property descriptors and delete any new properties function resetObject( object: any, - state: ReturnType + state: ReturnType, + doNotDeleteTheseKeys: string[] = [], + doNotSetTheseKeys: true | string[] = [], + avoidSetterIfUnchanged: string[] = [], + reorderProperties = false ) { const currentDescriptors = Object.getOwnPropertyDescriptors(object); for (const key of Object.keys(currentDescriptors)) { - if (!has(state.descriptors, key)) { - delete object[key]; - } + if (doNotDeleteTheseKeys.includes(key)) continue; + if (has(state.descriptors, key)) continue; + delete object[key]; } // Trigger nyc's setter functions for (const [key, value] of Object.entries(state.values)) { try { - object[key] = value; + if (doNotSetTheseKeys === true || doNotSetTheseKeys.includes(key)) + continue; + if (avoidSetterIfUnchanged.includes(key) && object[key] === value) + continue; + state.descriptors[key].set?.call(object, value); } catch {} } // Reset descriptors Object.defineProperties(object, state.descriptors); + + if (reorderProperties) { + // Delete and re-define each property so that they are in original order + const originalOrder = Object.keys(state.descriptors); + const properties = Object.getOwnPropertyDescriptors(object); + const sortedKeys = sortBy(Object.keys(properties), (name) => + originalOrder.includes(name) ? originalOrder.indexOf(name) : 999 + ); + for (const key of sortedKeys) { + delete object[key]; + Object.defineProperty(object, key, properties[key]); + } + } } //#endregion export const delay = promisify(setTimeout); + +/** Essentially Array:includes, but with tweaked types for checks on enums */ +export function isOneOf(value: V, arrayOfPossibilities: ReadonlyArray) { + return arrayOfPossibilities.includes(value as any); +} diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 72faf86e..ca4c2cf8 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -1,19 +1,22 @@ -import { _test } from './testlib'; +import { context, ExecutionContext } from './testlib'; import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; import { tmpdir } from 'os'; import semver = require('semver'); import { BIN_PATH_JS, + CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, nodeSupportsEsmHooks, + nodeSupportsSpawningChildProcess, ts, + tsSupportsMtsCtsExtensions, tsSupportsShowConfig, + tsSupportsStableNodeNextNode16, tsSupportsTsconfigInheritanceViaNodePackages, } from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; -import { pathToFileURL } from 'url'; import { createExec } from './exec-helpers'; import { BIN_CWD_PATH, @@ -25,18 +28,18 @@ import { testsDirRequire, tsNodeTypes, xfs, - contextTsNodeUnderTest, + ctxTsNode, CMD_TS_NODE_WITH_PROJECT_FLAG, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, CMD_ESM_LOADER_WITHOUT_PROJECT, - EXPERIMENTAL_MODULES_FLAG, } from './helpers'; +import type { CreateOptions } from '..'; const exec = createExec({ cwd: TEST_DIR, }); -const test = _test.context(contextTsNodeUnderTest); +const test = context(ctxTsNode); test.suite('ts-node', (test) => { test('should export the correct version', (t) => { @@ -70,11 +73,13 @@ test.suite('ts-node', (test) => { testsDirRequire.resolve('ts-node/register/transpile-only'); testsDirRequire.resolve('ts-node/register/type-check'); - // `node --loader ts-node/esm` - testsDirRequire.resolve('ts-node/esm'); - testsDirRequire.resolve('ts-node/esm.mjs'); - testsDirRequire.resolve('ts-node/esm/transpile-only'); - testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); + if (semver.gte(process.version, '12.17.0')) { + // `node --loader ts-node/esm` + testsDirRequire.resolve('ts-node/esm'); + testsDirRequire.resolve('ts-node/esm.mjs'); + 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'); @@ -143,7 +148,10 @@ test.suite('ts-node', (test) => { test('should execute cli with absolute path', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} "${join(TEST_DIR, 'hello-world')}"` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} "${join( + TEST_DIR, + 'hello-world' + )}"` ); expect(err).toBe(null); expect(stdout).toBe('Hello, world!\n'); @@ -157,26 +165,18 @@ test.suite('ts-node', (test) => { expect(stdout).toBe('example\n'); }); - test('should provide registered information globally', async () => { + test("should expose ts-node Service as a symbol property on Node's `process` object", async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} env` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} env` ); expect(err).toBe(null); expect(stdout).toBe('object\n'); }); - test('should provide registered information on register', async () => { - const { err, stdout } = await exec(`node -r ts-node/register env.ts`, { - cwd: TEST_DIR, - }); - expect(err).toBe(null); - expect(stdout).toBe('object\n'); - }); - test('should allow js', async () => { const { err, stdout } = await exec( [ - CMD_TS_NODE_WITH_PROJECT_FLAG, + CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { main } from \'./allow-js/run\';main()"', ].join(' ') @@ -188,7 +188,7 @@ test.suite('ts-node', (test) => { test('should include jsx when `allow-js` true', async () => { const { err, stdout } = await exec( [ - CMD_TS_NODE_WITH_PROJECT_FLAG, + CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', ].join(' ') @@ -197,9 +197,42 @@ test.suite('ts-node', (test) => { expect(stdout).toBe('hello world\n'); }); + test.suite('should support cts when module = CommonJS', (test) => { + test.runIf(tsSupportsMtsCtsExtensions); + test('test', async (t) => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + '-pe "import { main } from \'./index.cjs\';main()"', + ].join(' '), + { + cwd: join(TEST_DIR, 'ts45-ext/ext-cts'), + } + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); + }); + + test.suite('should support mts when module = ESNext', (test) => { + test.runIf( + nodeSupportsSpawningChildProcess && tsSupportsMtsCtsExtensions + ); + test('test', async () => { + const { err, stdout } = await exec( + [CMD_TS_NODE_WITHOUT_PROJECT_FLAG, './entrypoint.mjs'].join(' '), + { + cwd: join(TEST_DIR, 'ts45-ext/ext-mts'), + } + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); + }); + test('should eval code', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import * as m from './module';console.log(m.example('test'))"` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -e "import * as m from './module';console.log(m.example('test'))"` ); expect(err).toBe(null); expect(stdout).toBe('TEST\n'); @@ -207,13 +240,13 @@ test.suite('ts-node', (test) => { test('should import empty files', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import './empty'"` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -e "import './empty'"` ); expect(err).toBe(null); expect(stdout).toBe(''); }); - test('should throw errors', async () => { + test('should throw typechecking errors', async () => { const { err } = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import * as m from './module';console.log(m.example(123))"` ); @@ -377,7 +410,7 @@ test.suite('ts-node', (test) => { } test('should pipe into `ts-node` and evaluate', async () => { - const execPromise = exec(CMD_TS_NODE_WITH_PROJECT_FLAG); + const execPromise = exec(CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG); execPromise.child.stdin!.end("console.log('hello')"); const { err, stdout } = await execPromise; expect(err).toBe(null); @@ -385,7 +418,9 @@ test.suite('ts-node', (test) => { }); test('should pipe into `ts-node`', async () => { - const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} -p`); + const execPromise = exec( + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -p` + ); execPromise.child.stdin!.end('true'); const { err, stdout } = await execPromise; expect(err).toBe(null); @@ -404,7 +439,7 @@ test.suite('ts-node', (test) => { test('should support require flags', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} -r ./hello-world -pe "console.log('success')"` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -r ./hello-world -pe "console.log('success')"` ); expect(err).toBe(null); expect(stdout).toBe('Hello, world!\nsuccess\nundefined\n'); @@ -412,7 +447,7 @@ test.suite('ts-node', (test) => { test('should support require from node modules', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} -r typescript -e "console.log('success')"` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -r typescript -e "console.log('success')"` ); expect(err).toBe(null); expect(stdout).toBe('success\n'); @@ -458,28 +493,17 @@ test.suite('ts-node', (test) => { ); }); - test('should preserve `ts-node` context with child process', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} child-process` - ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\n'); - }); - test('should import js before ts by default', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} import-order/compiled` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/compiled` ); expect(err).toBe(null); expect(stdout).toBe('Hello, JavaScript!\n'); }); - const preferTsExtsEntrypoint = semver.gte(process.version, '12.0.0') - ? 'import-order/compiled' - : 'import-order/require-compiled'; test('should import ts before js when --prefer-ts-exts flag is present', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} --prefer-ts-exts ${preferTsExtsEntrypoint}` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} --prefer-ts-exts import-order/compiled` ); expect(err).toBe(null); expect(stdout).toBe('Hello, TypeScript!\n'); @@ -487,7 +511,7 @@ test.suite('ts-node', (test) => { test('should import ts before js when TS_NODE_PREFER_TS_EXTS env is present', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} ${preferTsExtsEntrypoint}`, + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/compiled`, { env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' }, } @@ -498,7 +522,7 @@ test.suite('ts-node', (test) => { test('should ignore .d.ts files', async () => { const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} import-order/importer` + `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/importer` ); expect(err).toBe(null); expect(stdout).toBe('Hello, World!\n'); @@ -538,63 +562,60 @@ test.suite('ts-node', (test) => { }); }); - if (semver.gte(ts.version, '2.7.0')) { - test('should locate tsconfig relative to entry-point by default', async () => { - const { err, stdout } = await exec(`${BIN_PATH} ../a/index`, { - cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), - }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); + test('should locate tsconfig relative to entry-point by default', async () => { + const { err, stdout } = await exec(`${BIN_PATH} ../a/index`, { + cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - test('should locate tsconfig relative to entry-point via ts-node-script', async () => { - const { err, stdout } = await exec(`${BIN_SCRIPT_PATH} ../a/index`, { - cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), - }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); - }); - test('should locate tsconfig relative to entry-point with --script-mode', async () => { - const { err, stdout } = await exec( - `${BIN_PATH} --script-mode ../a/index`, - { - cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), - } - ); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); + }); + test('should locate tsconfig relative to entry-point via ts-node-script', async () => { + const { err, stdout } = await exec(`${BIN_SCRIPT_PATH} ../a/index`, { + cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - test('should locate tsconfig relative to cwd via ts-node-cwd', async () => { - const { err, stdout } = await exec(`${BIN_CWD_PATH} ../a/index`, { + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); + }); + test('should locate tsconfig relative to entry-point with --script-mode', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} --script-mode ../a/index`, + { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), - }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-b/); + } + ); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); + }); + test('should locate tsconfig relative to cwd via ts-node-cwd', async () => { + const { err, stdout } = await exec(`${BIN_CWD_PATH} ../a/index`, { + cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), + }); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-b/); + }); + test('should locate tsconfig relative to cwd in --cwd-mode', async () => { + const { err, stdout } = await exec(`${BIN_PATH} --cwd-mode ../a/index`, { + cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - test('should locate tsconfig relative to cwd in --cwd-mode', async () => { + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-b/); + }); + test('should locate tsconfig relative to realpath, not symlink, when entrypoint is a symlink', async (t) => { + if ( + lstatSync( + join(TEST_DIR, 'main-realpath/symlink/symlink.tsx') + ).isSymbolicLink() + ) { const { err, stdout } = await exec( - `${BIN_PATH} --cwd-mode ../a/index`, - { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') } + `${BIN_PATH} main-realpath/symlink/symlink.tsx` ); expect(err).toBe(null); - expect(stdout).toMatch(/plugin-b/); - }); - test('should locate tsconfig relative to realpath, not symlink, when entrypoint is a symlink', async (t) => { - if ( - lstatSync( - join(TEST_DIR, 'main-realpath/symlink/symlink.tsx') - ).isSymbolicLink() - ) { - const { err, stdout } = await exec( - `${BIN_PATH} main-realpath/symlink/symlink.tsx` - ); - expect(err).toBe(null); - expect(stdout).toBe(''); - } else { - t.log('Skipping'); - return; - } - }); - } + expect(stdout).toBe(''); + } else { + t.log('Skipping'); + return; + } + }); test.suite('should read ts-node options from tsconfig.json', (test) => { const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`; @@ -725,8 +746,8 @@ test.suite('ts-node', (test) => { test.suite( 'should use implicit @tsconfig/bases config when one is not loaded from disk', - (_test) => { - const test = _test.context(async (t) => ({ + ({ context }) => { + const test = context(async (t) => ({ tempDir: mkdtempSync(join(tmpdir(), 'ts-node-spec')), })); if ( @@ -957,8 +978,8 @@ test.suite('ts-node', (test) => { }); }); - test.suite('create', (_test) => { - const test = _test.context(async (t) => { + test.suite('create', ({ context }) => { + const test = context(async (t) => { return { service: t.context.tsNodeUnderTest.create({ compilerOptions: { target: 'es5' }, @@ -997,64 +1018,76 @@ test.suite('ts-node', (test) => { }); test.suite('issue #1098', (test) => { - function testIgnored( - ignored: tsNodeTypes.Service['ignored'], - allowed: string[], - disallowed: string[] + function testAllowedExtensions( + t: ExecutionContext, + compilerOptions: CreateOptions['compilerOptions'], + allowed: string[] ) { + const disallowed = allExtensions.filter((ext) => !allowed.includes(ext)); + const { ignored } = t.context.tsNodeUnderTest.create({ + compilerOptions, + skipProject: true, + }); for (const ext of allowed) { - // should accept ${ext} files + t.log(`Testing that ${ext} files are allowed`); expect(ignored(join(DIST_DIR, `index${ext}`))).toBe(false); } for (const ext of disallowed) { - // should ignore ${ext} files + t.log(`Testing that ${ext} files are ignored`); expect(ignored(join(DIST_DIR, `index${ext}`))).toBe(true); } } + const allExtensions = [ + '.ts', + '.js', + '.d.ts', + '.mts', + '.cts', + '.d.mts', + '.d.cts', + '.mjs', + '.cjs', + '.tsx', + '.jsx', + '.xyz', + '', + ]; + const mtsCts = tsSupportsMtsCtsExtensions + ? ['.mts', '.cts', '.d.mts', '.d.cts'] + : []; + const mjsCjs = tsSupportsMtsCtsExtensions ? ['.mjs', '.cjs'] : []; + test('correctly filters file extensions from the compiler when allowJs=false and jsx=false', (t) => { - const { ignored } = t.context.tsNodeUnderTest.create({ - compilerOptions: {}, - skipProject: true, - }); - testIgnored( - ignored, - ['.ts', '.d.ts'], - ['.js', '.tsx', '.jsx', '.mjs', '.cjs', '.xyz', ''] - ); + testAllowedExtensions(t, {}, ['.ts', '.d.ts', ...mtsCts]); }); test('correctly filters file extensions from the compiler when allowJs=true and jsx=false', (t) => { - const { ignored } = t.context.tsNodeUnderTest.create({ - compilerOptions: { allowJs: true }, - skipProject: true, - }); - testIgnored( - ignored, - ['.ts', '.js', '.d.ts'], - ['.tsx', '.jsx', '.mjs', '.cjs', '.xyz', ''] - ); + testAllowedExtensions(t, { allowJs: true }, [ + '.ts', + '.js', + '.d.ts', + ...mtsCts, + ...mjsCjs, + ]); }); test('correctly filters file extensions from the compiler when allowJs=false and jsx=true', (t) => { - const { ignored } = t.context.tsNodeUnderTest.create({ - compilerOptions: { allowJs: false, jsx: 'preserve' }, - skipProject: true, - }); - testIgnored( - ignored, - ['.ts', '.tsx', '.d.ts'], - ['.js', '.jsx', '.mjs', '.cjs', '.xyz', ''] - ); + testAllowedExtensions(t, { allowJs: false, jsx: 'preserve' }, [ + '.ts', + '.tsx', + '.d.ts', + ...mtsCts, + ]); }); test('correctly filters file extensions from the compiler when allowJs=true and jsx=true', (t) => { - const { ignored } = t.context.tsNodeUnderTest.create({ - compilerOptions: { allowJs: true, jsx: 'preserve' }, - skipProject: true, - }); - testIgnored( - ignored, - ['.ts', '.tsx', '.js', '.jsx', '.d.ts'], - ['.mjs', '.cjs', '.xyz', ''] - ); + testAllowedExtensions(t, { allowJs: true, jsx: 'preserve' }, [ + '.ts', + '.tsx', + '.js', + '.jsx', + '.d.ts', + ...mtsCts, + ...mjsCjs, + ]); }); }); }); @@ -1135,7 +1168,11 @@ test('Detect when typescript adds new ModuleKind values; flag as a failure so we expect(ts.ModuleKind[99]).toBeUndefined(); } check(7, 'ES2022', false); - check(100, 'Node12', false); + if (tsSupportsStableNodeNextNode16) { + check(100, 'Node16', true); + } else { + check(100, 'Node12', false); + } check(199, 'NodeNext', false); const actualKeys = Object.keys(ts.ModuleKind); actualKeys.sort(); diff --git a/src/test/module-node.spec.ts b/src/test/module-node.spec.ts new file mode 100644 index 00000000..b182444e --- /dev/null +++ b/src/test/module-node.spec.ts @@ -0,0 +1,366 @@ +import { expect, context } from './testlib'; +import { + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + isOneOf, + resetNodeEnvironment, + tsSupportsStableNodeNextNode16, +} from './helpers'; +import * as Path from 'path'; +import { ctxTsNode } from './helpers'; +import { exec } from './exec-helpers'; +import { file, project, ProjectAPI as ProjectAPI } from './fs-helpers'; + +const test = context(ctxTsNode); +test.beforeEach(async () => { + resetNodeEnvironment(); +}); +type Test = typeof test; + +// Declare one test case for each permutations of project configuration +test.suite('TypeScript module=NodeNext and Node16', (test) => { + test.runIf(tsSupportsStableNodeNextNode16); + + for (const allowJs of [true, false]) { + for (const typecheckMode of [ + 'typecheck', + 'transpileOnly', + 'swc', + ] as const) { + for (const packageJsonType of [ + undefined, + 'commonjs', + 'module', + ] as const) { + for (const tsModuleMode of ['NodeNext', 'Node16'] as const) { + declareTest(test, { + allowJs, + packageJsonType, + typecheckMode, + tsModuleMode, + }); + } + } + } + } +}); + +function declareTest(test: Test, testParams: TestParams) { + const name = `package-json-type=${testParams.packageJsonType} allowJs=${testParams.allowJs} ${testParams.typecheckMode} tsconfig-module=${testParams.tsModuleMode}`; + + test(name, async (t) => { + const proj = writeFixturesToFilesystem(name, testParams); + + t.log( + `Running this command: ( cd ${proj.cwd} ; ${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --esm ./index.mjs )` + ); + + // All assertions happen within the fixture scripts + // Zero exit code indicates a passing test + const { stdout, stderr, err } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --esm ./index.mjs`, + { cwd: proj.cwd } + ); + t.log(stdout); + t.log(stderr); + expect(err).toBe(null); + }); +} + +type PackageJsonType = typeof packageJsonTypes[number]; +const packageJsonTypes = [undefined, 'commonjs', 'module'] as const; +const typecheckModes = ['typecheck', 'transpileOnly', 'swc'] as const; +const importStyles = [ + 'static import', + 'require', + 'dynamic import', + 'import = require', +] as const; +const importExtension = ['js', 'ts', 'omitted'] as const; + +interface Extension { + ext: string; + jsEquivalentExt?: string; + forcesCjs?: boolean; + forcesEsm?: boolean; + isJs?: boolean; + supportsJsx?: boolean; + isJsxExt?: boolean; + cjsAllowsOmittingExt?: boolean; +} +const extensions: Extension[] = [ + { + ext: 'cts', + jsEquivalentExt: 'cjs', + forcesCjs: true, + }, + { + ext: 'cjs', + forcesCjs: true, + isJs: true, + }, + { + ext: 'mts', + jsEquivalentExt: 'mjs', + forcesEsm: true, + }, + { + ext: 'mjs', + forcesEsm: true, + isJs: true, + }, + { + ext: 'ts', + jsEquivalentExt: 'js', + cjsAllowsOmittingExt: true, + }, + { + ext: 'tsx', + jsEquivalentExt: 'js', + supportsJsx: true, + isJsxExt: true, + cjsAllowsOmittingExt: true, + }, + { + ext: 'jsx', + jsEquivalentExt: 'js', + isJs: true, + supportsJsx: true, + isJsxExt: true, + cjsAllowsOmittingExt: true, + }, + { + ext: 'js', + isJs: true, + cjsAllowsOmittingExt: true, + }, +]; +/** + * Describe how a given project config handles files with this extension. + * For example, projects with allowJs:false do not like .jsx + */ +function getExtensionTreatment(ext: Extension, testParams: TestParams) { + // JSX and any TS extensions get compiled. Everything is compiled in allowJs mode + const isCompiled = testParams.allowJs || !ext.isJs || ext.isJsxExt; + const isExecutedAsEsm = + ext.forcesEsm || + (!ext.forcesCjs && testParams.packageJsonType === 'module'); + const isExecutedAsCjs = !isExecutedAsEsm; + // if allowJs:false, then .jsx files are not allowed + const isAllowed = !ext.isJsxExt || !ext.isJs || testParams.allowJs; + const canHaveJsxSyntax = ext.isJsxExt || (ext.supportsJsx && isCompiled); + return { + isCompiled, + isExecutedAsCjs, + isExecutedAsEsm, + isAllowed, + canHaveJsxSyntax, + }; +} + +interface TestParams { + packageJsonType: PackageJsonType; + typecheckMode: typeof typecheckModes[number]; + allowJs: boolean; + tsModuleMode: 'NodeNext' | 'Node16'; +} + +interface ImporterParams { + importStyle: typeof importStyles[number]; + importerExtension: typeof extensions[number]; +} + +interface ImporteeParams { + importeeExtension: typeof extensions[number]; +} + +function writeFixturesToFilesystem(name: string, testParams: TestParams) { + const { packageJsonType, allowJs, typecheckMode, tsModuleMode } = testParams; + + const proj = project(name.replace(/ /g, '_')); + + proj.addJsonFile('package.json', { + type: packageJsonType, + }); + + proj.addJsonFile('tsconfig.json', { + compilerOptions: { + allowJs, + target: 'esnext', + module: tsModuleMode, + jsx: 'react', + }, + 'ts-node': { + transpileOnly: typecheckMode === 'transpileOnly' || undefined, + swc: typecheckMode === 'swc', + experimentalResolver: true, + }, + }); + + const indexFile = file('index.mjs', ``); + proj.add(indexFile); + + for (const importStyle of importStyles) { + for (const importerExtension of extensions) { + const importer = createImporter(proj, testParams, { + importStyle, + importerExtension, + }); + if (!importer) continue; + + let importSpecifier = `./${Path.relative(proj.cwd, importer.path)}`; + importSpecifier = replaceExtension( + importSpecifier, + importerExtension.jsEquivalentExt ?? importerExtension.ext + ); + indexFile.content += `await import('${importSpecifier}');\n`; + } + } + + proj.rm(); + proj.write(); + return proj; +} + +function createImporter( + proj: ProjectAPI, + testParams: TestParams, + importerParams: ImporterParams +) { + const { importStyle, importerExtension } = importerParams; + const name = `${importStyle} from ${importerExtension.ext}`; + + const importerTreatment = getExtensionTreatment( + importerExtension, + testParams + ); + + if (!importerTreatment.isAllowed) return; + // import = require only allowed in non-js files + if (importStyle === 'import = require' && importerExtension.isJs) return; + // const = require not allowed in ESM + if (importStyle === 'require' && importerTreatment.isExecutedAsEsm) return; + // swc bug: import = require will not work in ESM, because swc does not emit necessary `__require = createRequire()` + if ( + testParams.typecheckMode === 'swc' && + importStyle === 'import = require' && + importerTreatment.isExecutedAsEsm + ) + return; + + const importer = { + path: `${name.replace(/ /g, '_')}.${importerExtension.ext}`, + imports: '', + assertions: '', + get content() { + return ` + ${this.imports} + async function main() { + ${this.assertions} + } + main(); + `; + }, + }; + proj.add(importer); + + if (!importerExtension.isJs) importer.imports += `export {};\n`; + + for (const importeeExtension of extensions) { + const ci = createImportee(testParams, { importeeExtension }); + if (!ci) continue; + const { importee, treatment: importeeTreatment } = ci; + proj.add(importee); + + // dynamic import is the only way to import ESM from CJS + if ( + importeeTreatment.isExecutedAsEsm && + importerTreatment.isExecutedAsCjs && + importStyle !== 'dynamic import' + ) + continue; + // Cannot import = require an ESM file + if (importeeTreatment.isExecutedAsEsm && importStyle === 'import = require') + continue; + // Cannot use static imports in non-compiled non-ESM + if ( + importStyle === 'static import' && + !importerTreatment.isCompiled && + importerTreatment.isExecutedAsCjs + ) + continue; + + let importSpecifier = `./${importeeExtension.ext}`; + if ( + !importeeExtension.cjsAllowsOmittingExt || + isOneOf(importStyle, ['dynamic import', 'static import']) + ) + importSpecifier += + '.' + (importeeExtension.jsEquivalentExt ?? importeeExtension.ext); + + switch (importStyle) { + case 'dynamic import': + importer.assertions += `const ${importeeExtension.ext} = await import('${importSpecifier}');\n`; + break; + case 'import = require': + importer.imports += `import ${importeeExtension.ext} = require('${importSpecifier}');\n`; + break; + case 'require': + importer.imports += `const ${importeeExtension.ext} = require('${importSpecifier}');\n`; + break; + case 'static import': + importer.imports += `import * as ${importeeExtension.ext} from '${importSpecifier}';\n`; + break; + } + + // Check both namespace.ext and namespace.default.ext, because node can't detect named exports from files we transform + const namespaceAsAny = importerExtension.isJs + ? importeeExtension.ext + : `(${importeeExtension.ext} as any)`; + importer.assertions += `if((${importeeExtension.ext}.ext ?? ${namespaceAsAny}.default.ext) !== '${importeeExtension.ext}')\n`; + importer.assertions += ` throw new Error('Wrong export from importee: expected ${importeeExtension.ext} but got ' + ${importeeExtension.ext}.ext + '(importee has these keys: ' + Object.keys(${importeeExtension.ext}) + ')');\n`; + } + return importer; +} +function createImportee( + testParams: TestParams, + importeeParams: ImporteeParams +) { + const { importeeExtension } = importeeParams; + const importee = file(`${importeeExtension.ext}.${importeeExtension.ext}`); + const treatment = getExtensionTreatment(importeeExtension, testParams); + if (!treatment.isAllowed) return; + if (treatment.isCompiled || treatment.isExecutedAsEsm) { + importee.content += `export const ext = '${importeeExtension.ext}';\n`; + } else { + importee.content += `exports.ext = '${importeeExtension.ext}';\n`; + } + if (!importeeExtension.isJs) { + importee.content += `const testTsTypeSyntax: string = 'a string';\n`; + } + if (treatment.isExecutedAsCjs) { + importee.content += `if(typeof __filename !== 'string') throw new Error('expected file to be CJS but __filename is not declared');\n`; + } else { + importee.content += `if(typeof __filename !== 'undefined') throw new Error('expected file to be ESM but __filename is declared');\n`; + importee.content += `if(typeof import.meta.url !== 'string') throw new Error('expected file to be ESM but import.meta.url is not declared');\n`; + } + if (treatment.canHaveJsxSyntax) { + importee.content += ` + const React = { + createElement(tag, dunno, content) { + return {props: {children: [content]}}; + } + }; + const jsxTest = Hello World; + if(jsxTest?.props?.children[0] !== 'Hello World') throw new Error('Expected ${importeeExtension.ext} to support JSX but it did not.'); + `; + } + return { importee, treatment }; +} + +function replaceExtension(path: string, ext: string) { + return Path.posix.format({ + ...Path.parse(path), + ext: '.' + ext, + base: undefined, + }); +} diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts index 95504351..480a40bb 100644 --- a/src/test/pluggable-dep-resolution.spec.ts +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -1,13 +1,14 @@ import { context } from './testlib'; import { - contextTsNodeUnderTest, + ctxTsNode, resetNodeEnvironment, + TEST_DIR, tsSupportsTsconfigInheritanceViaNodePackages, } from './helpers'; import * as expect from 'expect'; import { resolve } from 'path'; -const test = context(contextTsNodeUnderTest); +const test = context(ctxTsNode); test.suite( 'Pluggable dependency (compiler, transpiler, swc backend) is require()d relative to the tsconfig file that declares it', @@ -25,36 +26,66 @@ test.suite( // ts-node should resolve ts-patch or @swc/core relative to the extended tsconfig // to ensure we use the known working versions. - const macro = _macro.bind(null, test); + const macro = test.macro((config: string, expected: string) => [ + `${config} uses ${expected}`, + async (t) => { + t.teardown(resetNodeEnvironment); + + // A bit hacky: we've monkey-patched the various dependencies to either: + // a) return transpiled output we expect + // b) throw an error that we expect + // Either way, we've proven that the correct dependency is used, which + // is our goal. + let output: string; + try { + output = t.context.tsNodeUnderTest + .create({ + project: resolve(TEST_DIR, 'pluggable-dep-resolution', config), + }) + .compile('', 'index.ts'); + } catch (e) { + expect(e).toBe(`emit from ${expected}`); + return; + } - macro('tsconfig-custom-compiler.json', 'root custom compiler'); - macro('tsconfig-custom-transpiler.json', 'root custom transpiler'); - macro('tsconfig-swc-custom-backend.json', 'root custom swc backend'); - macro('tsconfig-swc-core.json', 'root @swc/core'); - macro('tsconfig-swc-wasm.json', 'root @swc/wasm'); - macro('tsconfig-swc.json', 'root @swc/core'); + expect(output).toContain(`emit from ${expected}\n`); + }, + ]); - macro( + test(macro, 'tsconfig-custom-compiler.json', 'root custom compiler'); + test(macro, 'tsconfig-custom-transpiler.json', 'root custom transpiler'); + test(macro, 'tsconfig-swc-custom-backend.json', 'root custom swc backend'); + test(macro, 'tsconfig-swc-core.json', 'root @swc/core'); + test(macro, 'tsconfig-swc-wasm.json', 'root @swc/wasm'); + test(macro, 'tsconfig-swc.json', 'root @swc/core'); + + test( + macro, 'node_modules/shared-config/tsconfig-custom-compiler.json', 'shared-config custom compiler' ); - macro( + test( + macro, 'node_modules/shared-config/tsconfig-custom-transpiler.json', 'shared-config custom transpiler' ); - macro( + test( + macro, 'node_modules/shared-config/tsconfig-swc-custom-backend.json', 'shared-config custom swc backend' ); - macro( + test( + macro, 'node_modules/shared-config/tsconfig-swc-core.json', 'shared-config @swc/core' ); - macro( + test( + macro, 'node_modules/shared-config/tsconfig-swc-wasm.json', 'shared-config @swc/wasm' ); - macro( + test( + macro, 'node_modules/shared-config/tsconfig-swc.json', 'shared-config @swc/core' ); @@ -62,37 +93,24 @@ test.suite( test.suite('"extends"', (test) => { test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); - const macro = _macro.bind(null, test); - - macro( + test( + macro, 'tsconfig-extend-custom-compiler.json', 'shared-config custom compiler' ); - macro( + test( + macro, 'tsconfig-extend-custom-transpiler.json', 'shared-config custom transpiler' ); - macro( + test( + macro, 'tsconfig-extend-swc-custom-backend.json', 'shared-config custom swc backend' ); - macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); - macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); - macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); + test(macro, 'tsconfig-extend-swc-core.json', 'shared-config @swc/core'); + test(macro, 'tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); + test(macro, 'tsconfig-extend-swc.json', 'shared-config @swc/core'); }); - - function _macro(_test: typeof test, config: string, expected: string) { - _test(`${config} uses ${expected}`, async (t) => { - t.teardown(resetNodeEnvironment); - - const output = t.context.tsNodeUnderTest - .create({ - project: resolve('tests/pluggable-dep-resolution', config), - }) - .compile('', 'index.ts'); - - expect(output).toContain(`emit from ${expected}\n`); - }); - } } ); diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts index 97769283..721e6d03 100644 --- a/src/test/register.spec.ts +++ b/src/test/register.spec.ts @@ -1,13 +1,12 @@ import { once } from 'lodash'; import { - contextTsNodeUnderTest, - PROJECT, + ctxTsNode, + PROJECT_TRANSPILE_ONLY, resetNodeEnvironment, TEST_DIR, tsNodeTypes, } from './helpers'; -import { context } from './testlib'; -import { expect } from 'chai'; +import { context, expect } from './testlib'; import * as exp from 'expect'; import { join, resolve } from 'path'; import proxyquire = require('proxyquire'); @@ -16,13 +15,13 @@ const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; const createOptions: tsNodeTypes.CreateOptions = { - project: PROJECT, + project: PROJECT_TRANSPILE_ONLY, compilerOptions: { jsx: 'preserve', }, }; -const test = context(contextTsNodeUnderTest).context( +const test = context(ctxTsNode).context( once(async (t) => { return { moduleTestPath: resolve(__dirname, '../../tests/module.ts'), @@ -74,7 +73,7 @@ test.suite('register(create(options))', (test) => { }) => { const m = require(moduleTestPath); - expect(m.example('foo')).to.equal('FOO'); + expect(m.example('foo')).toBe('FOO'); }); test('should support dynamically disabling', ({ @@ -82,29 +81,29 @@ test.suite('register(create(options))', (test) => { }) => { delete require.cache[moduleTestPath]; - expect(service.enabled(false)).to.equal(false); - expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); + expect(service.enabled(false)).toBe(false); + expect(() => require(moduleTestPath)).toThrow(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(service.enabled()).to.equal(false); - expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); + expect(service.enabled()).toBe(false); + expect(() => require(moduleTestPath)).toThrow(/Unexpected token/); delete require.cache[moduleTestPath]; - expect(service.enabled(true)).to.equal(true); - expect(() => require(moduleTestPath)).to.not.throw(); + expect(service.enabled(true)).toBe(true); + expect(() => require(moduleTestPath)).not.toThrow(); delete require.cache[moduleTestPath]; - expect(service.enabled()).to.equal(true); - expect(() => require(moduleTestPath)).to.not.throw(); + expect(service.enabled()).toBe(true); + expect(() => require(moduleTestPath)).not.toThrow(); }); test('should compile through js and ts', () => { const m = require('../../tests/complex'); - expect(m.example()).to.equal('example'); + expect(m.example()).toBe('example'); }); test('should work with proxyquire', () => { @@ -112,13 +111,13 @@ test.suite('register(create(options))', (test) => { './example': 'hello', }); - expect(m.example()).to.equal('hello'); + expect(m.example()).toBe('hello'); }); test('should work with `require.cache`', () => { const { example1, example2 } = require('../../tests/require-cache'); - expect(example1).to.not.equal(example2); + expect(example1).not.toBe(example2); }); test('should use source maps', async () => { @@ -155,10 +154,10 @@ test.suite('register(create(options))', (test) => { try { require('../../tests/with-jsx.tsx'); } catch (error: any) { - expect(error.stack).to.contain('SyntaxError: Unexpected token'); + expect(error.stack).toMatch('SyntaxError: Unexpected token'); } - expect(compiled).to.match(SOURCE_MAP_REGEXP); + expect(compiled).toMatch(SOURCE_MAP_REGEXP); }); }); }); @@ -190,18 +189,18 @@ test('should support compiler scopes w/multiple registered compiler services at }); try { - expect(require('../../tests/scope/a').ext).to.equal('.ts'); - expect(require('../../tests/scope/b').ext).to.equal('.ts'); + expect(require('../../tests/scope/a').ext).toBe('.ts'); + expect(require('../../tests/scope/b').ext).toBe('.ts'); } finally { compilers.forEach((c) => c.enabled(false)); } - expect(calls).to.deep.equal([ + expect(calls).toEqual([ join(TEST_DIR, 'scope/a/index.ts'), join(TEST_DIR, 'scope/b/index.ts'), ]); delete require.cache[moduleTestPath]; - expect(() => require(moduleTestPath)).to.throw(); + expect(() => require(moduleTestPath)).toThrow(); }); diff --git a/src/test/regression.spec.ts b/src/test/regression.spec.ts index db3c4464..ddcf179e 100644 --- a/src/test/regression.spec.ts +++ b/src/test/regression.spec.ts @@ -5,12 +5,12 @@ import { join } from 'path'; import { createExec, createExecTester } from './exec-helpers'; import { CMD_TS_NODE_WITHOUT_PROJECT_FLAG, - contextTsNodeUnderTest, + ctxTsNode, TEST_DIR, } from './helpers'; -import { test as _test } from './testlib'; +import { context } from './testlib'; -const test = _test.context(contextTsNodeUnderTest); +const test = context(ctxTsNode); const exec = createExecTester({ exec: createExec({ cwd: TEST_DIR, diff --git a/src/test/repl/helpers.ts b/src/test/repl/helpers.ts index a085a266..4aad2373 100644 --- a/src/test/repl/helpers.ts +++ b/src/test/repl/helpers.ts @@ -1,27 +1,32 @@ -import * as promisify from 'util.promisify'; import { PassThrough } from 'stream'; -import { getStream, TEST_DIR, tsNodeTypes } from '../helpers'; +import { delay, getStream, TEST_DIR, tsNodeTypes, ctxTsNode } from '../helpers'; import type { ExecutionContext } from 'ava'; - -export interface ContextWithTsNodeUnderTest { - tsNodeUnderTest: Pick< - typeof tsNodeTypes, - 'create' | 'register' | 'createRepl' - >; -} +import { test, expect } from '../testlib'; export interface CreateReplViaApiOptions { - registerHooks: true; + registerHooks: boolean; createReplOpts?: Partial; createServiceOpts?: Partial; } +export interface ExecuteInReplOptions extends CreateReplViaApiOptions { + waitMs?: number; + waitPattern?: string | RegExp; + /** When specified, calls `startInternal` instead of `start` and passes options */ + startInternalOptions?: Parameters< + tsNodeTypes.ReplService['startInternal'] + >[0]; +} + +export namespace ctxRepl { + export type Ctx = ctxTsNode.Ctx & Awaited>; + export type T = ExecutionContext; +} + /** * pass to test.context() to get REPL testing helper functions */ -export async function contextReplHelpers( - t: ExecutionContext -) { +export async function ctxRepl(t: ctxTsNode.T) { const { tsNodeUnderTest } = t.context; return { createReplViaApi, executeInRepl }; @@ -55,18 +60,7 @@ export async function contextReplHelpers( return { stdin, stdout, stderr, replService, service }; } - // Todo combine with replApiMacro - async function executeInRepl( - input: string, - options: CreateReplViaApiOptions & { - waitMs?: number; - waitPattern?: string | RegExp; - /** When specified, calls `startInternal` instead of `start` and passes options */ - startInternalOptions?: Parameters< - tsNodeTypes.ReplService['startInternal'] - >[0]; - } - ) { + async function executeInRepl(input: string, options: ExecuteInReplOptions) { const { waitPattern, // Wait longer if there's a signal to end it early @@ -87,11 +81,7 @@ export async function contextReplHelpers( const stdoutPromise = getStream(stdout, waitPattern); const stderrPromise = getStream(stderr, waitPattern); // Wait for expected output pattern or timeout, whichever comes first - await Promise.race([ - promisify(setTimeout)(waitMs), - stdoutPromise, - stderrPromise, - ]); + await Promise.race([delay(waitMs), stdoutPromise, stderrPromise]); stdout.end(); stderr.end(); @@ -102,3 +92,46 @@ export async function contextReplHelpers( }; } } + +export const macroReplNoErrorsAndStdoutContains = test.macro( + (script: string, contains: string, options?: Partial) => + async (t: ctxRepl.T) => { + macroReplInternal(t, script, contains, undefined, contains, options); + } +); +export const macroReplStderrContains = test.macro( + ( + script: string, + errorContains: string, + options?: Partial + ) => + async (t: ctxRepl.T) => { + macroReplInternal( + t, + script, + undefined, + errorContains, + errorContains, + options + ); + } +); + +async function macroReplInternal( + t: ctxRepl.T, + script: string, + stdoutContains: string | undefined, + stderrContains: string | undefined, + waitPattern: string, + options?: Partial +) { + const { stdout, stderr } = await t.context.executeInRepl(script, { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern, + ...options, + }); + if (stderrContains) expect(stderr).toContain(stderrContains); + else expect(stderr).toBe(''); + if (stdoutContains) expect(stdout).toContain(stdoutContains); +} diff --git a/src/test/repl/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts index c3926566..2f30a06f 100644 --- a/src/test/repl/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -1,12 +1,12 @@ -import { expect } from 'chai'; +import { expect } from '../testlib'; import type { Key } from 'readline'; import { Stream } from 'stream'; import semver = require('semver'); import { ts } from '../helpers'; -import type { ContextWithTsNodeUnderTest } from './helpers'; +import type { ctxTsNode } from '../helpers'; import { nodeSupportsEsmHooks } from '../helpers'; -interface SharedObjects extends ContextWithTsNodeUnderTest { +interface SharedObjects extends ctxTsNode.Ctx { TEST_DIR: string; } @@ -64,7 +64,7 @@ export async function upstreamTopLevelAwaitTests({ return promise; } - runAndWait([ + await runAndWait([ 'function foo(x) { return x; }', 'function koo() { return Promise.resolve(4); }', ]); @@ -246,12 +246,12 @@ export async function upstreamTopLevelAwaitTests({ if (Array.isArray(expected)) { if (expected.length === 1) expected.push('undefined'); if (lines[0] === input) lines.shift(); - expect(lines).to.eqls([...expected, PROMPT]); + expect(lines).toEqual([...expected, PROMPT]); } else if ('line' in options) { - expect(lines[toBeRun.length + options.line!]).to.eqls(expected); + expect(lines[toBeRun.length + options.line!]).toEqual(expected); } else { const echoed = toBeRun.map((a, i) => `${i > 0 ? '... ' : ''}${a}\r`); - expect(lines).to.eqls([...echoed, expected, PROMPT]); + expect(lines).toEqual([...echoed, expected, PROMPT]); } } } diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts index f54d2030..d02341f1 100644 --- a/src/test/repl/repl-environment.spec.ts +++ b/src/test/repl/repl-environment.spec.ts @@ -3,21 +3,20 @@ * globals, __filename, builtin module accessors. */ -import { test as _test, expect } from '../testlib'; -import * as promisify from 'util.promisify'; +import { context, expect } from '../testlib'; import * as getStream from 'get-stream'; import { CMD_TS_NODE_WITH_PROJECT_FLAG, - contextTsNodeUnderTest, - ROOT_DIR, + ctxTsNode, + delay, TEST_DIR, } from '../helpers'; import { dirname, join } from 'path'; import { createExec, createExecTester } from '../exec-helpers'; import { homedir } from 'os'; -import { contextReplHelpers } from './helpers'; +import { ctxRepl } from './helpers'; -const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); +const test = context(ctxTsNode).context(ctxRepl); const exec = createExec({ cwd: TEST_DIR, @@ -75,10 +74,10 @@ test.suite( stdin.end(); let done = false; await Promise.race([ - promisify(setTimeout)(20e3), + delay(20e3), (async () => { while (!done && !waitFor?.()) { - await promisify(setTimeout)(1e3); + await delay(1e3); } })(), ]); @@ -137,7 +136,6 @@ test.suite( /** Every possible ./node_modules directory ascending upwards starting with ./tests/node_modules */ const modulePaths = createModulePaths(TEST_DIR); - const rootModulePaths = createModulePaths(ROOT_DIR); function createModulePaths(dir: string) { const modulePaths: string[] = []; for (let path = dir; ; path = dirname(path)) { @@ -430,7 +428,7 @@ test.suite( // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(ROOT_DIR, '.ts')}:1:` + ` at ${join(TEST_DIR, '.ts')}:1:` ), }, }); @@ -455,13 +453,13 @@ test.suite( modulePath: '.', moduleFilename: null, modulePaths: expect.objectContaining({ - ...[join(ROOT_DIR, `repl/node_modules`), ...rootModulePaths], + ...[join(TEST_DIR, `repl/node_modules`), ...modulePaths], }), // Note: vanilla node REPL does not set exports exportsTest: true, // Note: vanilla node uses different name. See #1360 stackTest: expect.stringContaining( - ` at ${join(ROOT_DIR, '.ts')}:1:` + ` at ${join(TEST_DIR, '.ts')}:1:` ), moduleAccessorsTest: true, }, diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 3a106a38..55f49cb6 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -1,18 +1,30 @@ -import { _test, expect } from '../testlib'; -import { ts } from '../helpers'; +import { context, expect } from '../testlib'; +import { delay, resetNodeEnvironment, ts } from '../helpers'; import semver = require('semver'); import { CMD_TS_NODE_WITH_PROJECT_FLAG, - contextTsNodeUnderTest, + ctxTsNode, getStream, TEST_DIR, } from '../helpers'; import { createExec, createExecTester } from '../exec-helpers'; import { upstreamTopLevelAwaitTests } from './node-repl-tla'; -import { contextReplHelpers } from './helpers'; -import { promisify } from 'util'; - -const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); +import { + ctxRepl, + macroReplNoErrorsAndStdoutContains, + macroReplStderrContains, +} from './helpers'; + +const test = context(ctxTsNode).context(ctxRepl); +test.runSerially(); +test.beforeEach(async (t) => { + t.teardown(() => { + resetNodeEnvironment(); + // Useful for debugging memory leaks. Leaving in case I need it again. + // global.gc(); // Requires adding nodeArguments: ['--expose-gc'] to ava config + // console.dir(process.memoryUsage().heapUsed / 1000 / 1000); + }); +}); const exec = createExec({ cwd: TEST_DIR, @@ -109,11 +121,11 @@ test.serial('REPL can be created via API', async (t) => { ); }); -test.suite('top level await', (_test) => { +test.suite('top level await', ({ context }) => { const compilerOptions = { target: 'es2018', }; - const test = _test.context(async (t) => { + const test = context(async (t) => { return { executeInTlaRepl }; function executeInTlaRepl(input: string, waitPattern?: string | RegExp) { @@ -241,13 +253,13 @@ test.suite('top level await', (_test) => { 'should error with typing information when importing a file with type errors', async (t) => { const { stdout, stderr } = await t.context.executeInTlaRepl( - `const {foo} = await import('./tests/repl/tla-import');`, + `const {foo} = await import('./repl/tla-import');`, 'error' ); expect(stdout).toBe('> > '); expect(stderr.replace(/\r\n/g, '\n')).toBe( - 'tests/repl/tla-import.ts(1,14): error TS2322: ' + + 'repl/tla-import.ts(1,14): error TS2322: ' + (semver.gte(ts.version, '4.0.0') ? `Type 'number' is not assignable to type 'string'.\n` : `Type '1' is not assignable to type 'string'.\n`) + @@ -258,7 +270,7 @@ test.suite('top level await', (_test) => { test('should pass upstream test cases', async (t) => { const { tsNodeUnderTest } = t.context; - upstreamTopLevelAwaitTests({ TEST_DIR, tsNodeUnderTest }); + await upstreamTopLevelAwaitTests({ TEST_DIR, tsNodeUnderTest }); }); } else { test('should throw error when attempting to use top level await on TS < 3.8', async (t) => { @@ -308,12 +320,12 @@ test.suite( test.suite( 'REPL inputs are syntactically independent of each other', (test) => { - // Serial because it's timing-sensitive - test.serial( - 'arithmetic operators are independent of previous values', - async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( - `9 + // Serial because they're timing-sensitive + test.runSerially(); + + test('arithmetic operators are independent of previous values', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `9 + 3 7 - 3 @@ -325,114 +337,127 @@ test.suite( ** 2\n.break console.log('done!') `, - { - registerHooks: true, - startInternalOptions: { useGlobal: false }, - waitPattern: 'done!\nundefined\n>', - } - ); - expect(stdout).not.toContain('12'); - expect(stdout).not.toContain('4'); - expect(stdout).not.toContain('21'); - expect(stdout).not.toContain('50'); - expect(stdout).not.toContain('25'); - expect(stdout).toContain('3'); - expect(stdout).toContain('-3'); - } - ); + { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: 'done!\nundefined\n>', + } + ); + expect(stdout).not.toContain('12'); + expect(stdout).not.toContain('4'); + expect(stdout).not.toContain('21'); + expect(stdout).not.toContain('50'); + expect(stdout).not.toContain('25'); + expect(stdout).toContain('3'); + expect(stdout).toContain('-3'); + }); - // Serial because it's timing-sensitive - test.serial( - 'automatically inserted semicolons do not appear in error messages at the end', - async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( - `( + test('automatically inserted semicolons do not appear in error messages at the end', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `( a console.log('done!')`, - { - registerHooks: true, - startInternalOptions: { useGlobal: false }, - waitPattern: 'done!\nundefined\n>', - } - ); - expect(stderr).toContain("error TS1005: ')' expected."); - expect(stderr).not.toContain(';'); - } - ); + { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: 'done!\nundefined\n>', + } + ); + expect(stderr).toContain("error TS1005: ')' expected."); + expect(stderr).not.toContain(';'); + }); - // Serial because it's timing-sensitive - test.serial( - 'automatically inserted semicolons do not appear in error messages at the start', - async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( - `) + test('automatically inserted semicolons do not appear in error messages at the start', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `) console.log('done!')`, - { - registerHooks: true, - startInternalOptions: { useGlobal: false }, - waitPattern: 'done!\nundefined\n>', - } - ); - expect(stderr).toContain( - 'error TS1128: Declaration or statement expected.' - ); - expect(stderr).toContain(')'); - expect(stderr).not.toContain(';'); - } - ); + { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: 'done!\nundefined\n>', + } + ); + expect(stderr).toContain( + 'error TS1128: Declaration or statement expected.' + ); + expect(stderr).toContain(')'); + expect(stderr).not.toContain(';'); + }); - // Serial because it's timing-sensitive - test.serial( - 'automatically inserted semicolons do not break function calls', - async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( - `function foo(a: number) { + test('automatically inserted semicolons do not break function calls', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `function foo(a: number) { return a + 1; } foo( 1 )`, - { - registerHooks: true, - startInternalOptions: { useGlobal: false }, - waitPattern: '2\n>', - } - ); - expect(stderr).toBe(''); - expect(stdout).toContain('2'); - } - ); + { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: '2\n>', + } + ); + expect(stderr).toBe(''); + expect(stdout).toContain('2'); + }); - // Serial because it's timing-sensitive - test.serial( - 'automatically inserted semicolons do not affect subsequent line numbers', - async (t) => { - // If first line of input ends in a semicolon, should not add a second semicolon. - // That will cause an extra blank line in the compiled output which will - // offset the stack line number. - const { stdout, stderr } = await t.context.executeInRepl( - `1; + test('automatically inserted semicolons do not affect subsequent line numbers', async (t) => { + // If first line of input ends in a semicolon, should not add a second semicolon. + // That will cause an extra blank line in the compiled output which will + // offset the stack line number. + const { stdout, stderr } = await t.context.executeInRepl( + `1; new Error().stack!.split('\\n')[1] console.log('done!')`, - { - registerHooks: true, - startInternalOptions: { useGlobal: false }, - waitPattern: 'done!', - } - ); - expect(stderr).toBe(''); - expect(stdout).toContain(":1:1'\n"); - } - ); + { + registerHooks: true, + startInternalOptions: { useGlobal: false }, + waitPattern: 'done!', + } + ); + expect(stderr).toBe(''); + expect(stdout).toContain(":1:1'\n"); + }); } ); +test.suite('Multiline inputs and RECOVERY_CODES', (test) => { + test.runSerially(); + test( + 'multiline function args declaration', + macroReplNoErrorsAndStdoutContains, + ` + function myFn( + a: string, + b: string + ) { + return a + ' ' + b + } + myFn('test', '!') + `, + 'test !' + ); + + test( + 'Conditional recovery codes: this one-liner *should* raise an error; should not be recoverable', + macroReplStderrContains, + ` + (a: any) => a = null; + `, + 'error TS', + { + createServiceOpts: { compilerOptions: { strictNullChecks: 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, + registerHooks: false, createServiceOpts: { compilerOptions: { traceResolution: true, @@ -444,7 +469,7 @@ test.suite('REPL works with traceResolution', (test) => { repl.stdin.end(); - await promisify(setTimeout)(3e3); + await delay(3e3); repl.stdout.end(); const stdout = await getStream(repl.stdout); @@ -519,3 +544,109 @@ test.suite('REPL declares types for node built-ins within REPL', (test) => { expect(stderr).toBe(''); }); }); + +test.suite('REPL treats object literals and block scopes correctly', (test) => { + test( + 'repl should treat { key: 123 } as object literal', + macroReplNoErrorsAndStdoutContains, + '{ key: 123 }', + '{ key: 123 }' + ); + test( + 'repl should treat ({ key: 123 }) as object literal', + macroReplNoErrorsAndStdoutContains, + '({ key: 123 })', + '{ key: 123 }' + ); + test( + 'repl should treat ({ let v = 0; v; }) as object literal and error', + macroReplStderrContains, + '({ let v = 0; v; })', + semver.satisfies(ts.version, '2.7') + ? 'error TS2304' + : 'No value exists in scope for the shorthand property' + ); + test( + 'repl should treat { let v = 0; v; } as block scope', + macroReplNoErrorsAndStdoutContains, + '{ let v = 0; v; }', + '0' + ); + test.suite('extra', (test) => { + test.skipIf(semver.satisfies(ts.version, '2.7')); + test( + 'repl should treat { key: 123 }; as block scope', + macroReplNoErrorsAndStdoutContains, + '{ key: 123 };', + '123' + ); + test( + 'repl should treat {\\nkey: 123\\n}; as block scope', + macroReplNoErrorsAndStdoutContains, + '{\nkey: 123\n};', + '123' + ); + test( + 'repl should treat { key: 123 }[] as block scope (edge case)', + macroReplNoErrorsAndStdoutContains, + '{ key: 123 }[]', + '[]' + ); + }); + test.suite('multiline', (test) => { + test( + 'repl should treat {\\nkey: 123\\n} as object literal', + macroReplNoErrorsAndStdoutContains, + '{\nkey: 123\n}', + '{ key: 123 }' + ); + test( + 'repl should treat ({\\nkey: 123\\n}) as object literal', + macroReplNoErrorsAndStdoutContains, + '({\nkey: 123\n})', + '{ key: 123 }' + ); + test( + 'repl should treat ({\\nlet v = 0;\\nv;\\n}) as object literal and error', + macroReplStderrContains, + '({\nlet v = 0;\nv;\n})', + semver.satisfies(ts.version, '2.7') + ? 'error TS2304' + : 'No value exists in scope for the shorthand property' + ); + test( + 'repl should treat {\\nlet v = 0;\\nv;\\n} as block scope', + macroReplNoErrorsAndStdoutContains, + '{\nlet v = 0;\nv;\n}', + '0' + ); + }); + test.suite('property access', (test) => { + test( + 'repl should treat { key: 123 }.key as object literal property access', + macroReplNoErrorsAndStdoutContains, + '{ key: 123 }.key', + '123' + ); + test( + 'repl should treat { key: 123 }["key"] as object literal indexed access', + macroReplNoErrorsAndStdoutContains, + '{ key: 123 }["key"]', + '123' + ); + test( + 'repl should treat { key: 123 }.foo as object literal non-existent property access', + macroReplStderrContains, + '{ key: 123 }.foo', + "Property 'foo' does not exist on type" + ); + test( + 'repl should treat { key: 123 }["foo"] as object literal non-existent indexed access', + macroReplStderrContains, + '{ key: 123 }["foo"]', + semver.satisfies(ts.version, '2.7') + ? 'error TS7017' + : "Property 'foo' does not exist on type" + ); + }); +}); diff --git a/src/test/resolver.spec.ts b/src/test/resolver.spec.ts new file mode 100755 index 00000000..96d6cd8c --- /dev/null +++ b/src/test/resolver.spec.ts @@ -0,0 +1,816 @@ +import { context, ExecutionContext, expect, TestInterface } from './testlib'; +import { + ctxTsNode, + isOneOf, + resetNodeEnvironment, + ts, + tsSupportsStableNodeNextNode16, +} from './helpers'; +import { project as fsProject, Project as FsProject } from './fs-helpers'; +import { join } from 'path'; +import * as semver from 'semver'; +import { padStart } from 'lodash'; +import _ = require('lodash'); +import { pathToFileURL } from 'url'; +import type { RegisterOptions } from '..'; +import * as fs from 'fs'; +import * as Path from 'path'; + +/* + * Each test case is a separate TS project, with a different permutation of + * project options. The project is written to disc, then ts-node is installed, + * then several entrypoint-* files are imported to test our resolver. + * + * High-level structure of these tests: + * package.json, tsconfig.json, src/, and out/ + * entrypoint-* files are the entrypoints + * they import a bunch of target files / directories / node_modules + * + * The heart of this test is every time an entrypoint imports a target. + * We are testing if the resolver figures out the correct target file to import. + * + * To better understand the emitted projects, run the tests, then look in `tests/tmp/resolver-*` + * + * Whenever a test fails, the error will log a command you can paste into your terminal to re-run + * that project *outside* of this test suite. This may be helpful in understanding and debugging + * these tests. + */ + +// Test a bunch of permutations of: + +// import permutations: + +// - [x] Relative import of file +// - [x] Relative import of index +// - [x] rootless library import of main +// - [x] rootless library import of index +// - [x] rootless library import of exports sub-path +// - [x] rootless self-import of main +// - [x] rootless self-import of index +// - [x] rootless self-import of exports sub-path + +// - [x] Require with extension +// - [x] Require without extension + +// - Require from dist to dist +// - Require from dist to src +// - Require from src to dist +// - [x] Require from src to src + +// lib permutations: + +// - [x] module exists in both src and dist (precompilation ran) +// - [x] module exists in only dist (came from elsewhere) +// - [x] module exists only in src (did not precompile) + +// - .ts / .js extension +// - .tsx / .js extension +// - .cts / .cjs extension +// - .mts / .mjs extension +// - .js / .js extension +// - .jsx / .js extension +// - .cjs / .cjs extension +// - .mjs / .mjs extension + +// Side-step compiler transformation of import() into require() +const dynamicImport = new Function('specifier', 'return import(specifier)'); +// For some reason `new Function` was triggering what *might* be a node bug, +// where `context.parentURL` passed into loader `resolve()` was wrong. +// eval works for unknown reasons. This may change in future node releases. +const declareDynamicImportFunction = `const dynamicImport = eval('(specifier) => import(specifier)');`; + +const test = context(ctxTsNode); +type Test = TestInterface; +type T = ExecutionContext; + +const projectSeq = seqGenerator(); +const entrypointSeq = seqGenerator(); +const targetSeq = seqGenerator(); + +interface Project { + identifier: string; + allowJs: boolean; + preferSrc: boolean; + typeModule: boolean; + /** Use TS's new module: `nodenext` option */ + useTsNodeNext: boolean; + experimentalSpecifierResolutionNode: boolean; + skipIgnore: boolean; +} +interface EntrypointPermutation { + entrypointExt: 'cjs' | 'mjs'; + withExt: boolean; + entrypointLocation: 'src' | 'out'; + entrypointTargetting: 'src' | 'out'; +} +type Entrypoint = string; +interface GenerateTargetOptions { + inSrc: boolean; + inOut: boolean; + srcExt: string; + /** If true, is an index.* file within a directory */ + isIndex: boolean; + targetPackageStyle: TargetPackageStyle; + packageTypeModule: boolean; +} +interface Target { + targetIdentifier: string; + outName: string; + srcName: string; + srcExt: string; + outExt: string; + inSrc: boolean; + inOut: boolean; + /** If true, is neither an index.* nor a package */ + isNamedFile: boolean; + /** If true, is an index.* file within a directory */ + isIndex: boolean; + /** If true, should be imported as an npm package, not relative import */ + isPackage: boolean; + packageStyle: TargetPackageStyle; + typeModule: boolean; +} + +/** When target is actually a mini node_modules package */ +type TargetPackageStyle = typeof targetPackageStyles[number]; +const targetPackageStyles = [ + false, + // test that the package contains /index.* + 'empty-manifest', + // "main": "src/target." + 'main-src-with-extension', + // "main": "src/target." + 'main-src-with-out-extension', + // "main": "out/target." + 'main-out-with-extension', + // "main": "src/target" + 'main-src-extensionless', + // "main": "out/target" + 'main-out-extensionless', + // "exports": {".": "src/target."} + 'exports-src-with-extension', + // "exports": {".": "src/target."} + 'exports-src-with-out-extension', + // "exports": {".": "out/target."} + 'exports-out-with-extension', +] as const; + +test.suite('Resolver hooks', (test) => { + test.runSerially(); + test.runIf( + semver.gte(process.version, '14.0.0') && + !semver.satisfies(ts.version, '2.7.x') + ); + + // + // Generate all permutations of projects + // + for (const preferSrc of [false, true]) { + for (const typeModule of [false, true]) { + for (const allowJs of [false, true]) { + for (const useTsNodeNext of [false, true]) { + // TODO test against skipIgnore: false, where imports of third-party deps in `node_modules` should not get our mapping behaviors + for (const skipIgnore of [/*false, */ true]) { + for (const experimentalSpecifierResolutionNode of [false, true]) { + let identifier = `resolver-${projectSeq()}`; + identifier += preferSrc ? '-preferSrc' : '-preferOut'; + identifier += typeModule ? '-typeModule' : '-typeCjs---'; + identifier += allowJs ? '-allowJs' : '--------'; + identifier += useTsNodeNext ? '-useTsNodenext' : '--------------'; + identifier += skipIgnore ? '-skipIgnore' : '-----------'; + identifier += experimentalSpecifierResolutionNode + ? '-experimentalSpecifierResolutionNode' + : ''; + + const project: Project = { + identifier, + allowJs, + preferSrc, + typeModule, + useTsNodeNext, + experimentalSpecifierResolutionNode, + skipIgnore, + }; + declareProject(test, project); + } + } + } + } + } + } +}); + +function declareProject(_test: Test, project: Project) { + const test = + project.useTsNodeNext && !tsSupportsStableNodeNextNode16 + ? _test.skip + : _test; + test(`${project.identifier}`, async (t) => { + t.teardown(() => { + resetNodeEnvironment(); + }); + + const p = fsProject(project.identifier); + p.rm(); + + p.addJsonFile('package.json', { + type: project.typeModule ? 'module' : undefined, + }); + p.addJsonFile('tsconfig.json', { + 'ts-node': { + experimentalResolver: true, + preferTsExts: project.preferSrc, + transpileOnly: true, + experimentalSpecifierResolution: + project.experimentalSpecifierResolutionNode ? 'node' : undefined, + skipIgnore: project.skipIgnore, + } as RegisterOptions, + compilerOptions: { + allowJs: project.allowJs, + skipLibCheck: true, + // TODO add nodenext permutation + module: project.useTsNodeNext + ? 'NodeNext' + : project.typeModule + ? 'esnext' + : 'commonjs', + jsx: 'react', + target: 'esnext', + }, + }); + + const targets = generateTargets(project, p); + const entrypoints = generateEntrypoints(project, p, targets); + p.write(); + await execute(t, p, entrypoints); + }); +} + +// +// Generate all target-* files +// +function generateTargets(project: Project, p: FsProject) { + /** Array of metadata about target files to be imported */ + const targets: Array = []; + // TODO does allowJs matter? + for (const inOut of [false, true]) { + for (const inSrc of [false, true]) { + for (const srcExt of [ + 'ts', + 'tsx', + 'cts', + 'mts', + 'jsx', + 'js', + 'cjs', + 'mjs', + ]) { + for (const targetPackageStyle of targetPackageStyles) { + const packageTypeModulePermutations = targetPackageStyle + ? [true, false] + : [project.typeModule]; + for (const packageTypeModule of packageTypeModulePermutations) { + const isIndexPermutations = targetPackageStyle + ? [false] + : [true, false]; + // TODO test main pointing to a directory containing an `index.` file? + for (const isIndex of isIndexPermutations) { + //#region SKIPPING + if (!inSrc && !inOut) continue; + + // Don't bother with jsx if we don't have allowJs enabled + // TODO Get rid of this? "Just work" in this case? + if (srcExt === 'jsx' && !project.allowJs) continue; + // Don't bother with src-only extensions when only emitting to `out` + if (!inSrc && ['ts', 'tsx', 'cts', 'mts', 'jsx'].includes(srcExt)) + continue; + + // TODO re-enable with src <-> out mapping + if ( + !inOut && + isOneOf(targetPackageStyle, [ + 'main-out-with-extension', + 'main-out-extensionless', + 'exports-out-with-extension', + ]) + ) + continue; + if ( + !inSrc && + isOneOf(targetPackageStyle, [ + 'main-src-with-extension', + 'main-src-extensionless', + 'exports-src-with-extension', + ]) + ) + continue; + if ( + isOneOf(targetPackageStyle, [ + 'main-out-with-extension', + 'main-out-extensionless', + 'exports-out-with-extension', + ]) + ) + continue; + //#endregion + + targets.push( + generateTarget(project, p, { + inSrc, + inOut, + srcExt, + targetPackageStyle, + packageTypeModule, + isIndex, + }) + ); + } + } + } + } + } + } + return targets; +} + +function generateTarget( + project: Project, + p: FsProject, + options: GenerateTargetOptions +) { + const { + inSrc, + inOut, + srcExt, + targetPackageStyle, + packageTypeModule, + isIndex, + } = options; + + const outExt = srcExt.replace('ts', 'js').replace('x', ''); + let targetIdentifier = `target-${targetSeq()}-${ + inOut && inSrc ? 'inboth' : inOut ? 'onlyout' : 'onlysrc' + }-${srcExt}`; + + if (targetPackageStyle) + targetIdentifier = `${targetIdentifier}-${targetPackageStyle}-${ + packageTypeModule ? 'module' : 'commonjs' + }`; + let prefix = targetPackageStyle ? `node_modules/${targetIdentifier}/` : ''; + let suffix = + targetPackageStyle === 'empty-manifest' + ? 'index' + : targetPackageStyle + ? 'target' + : targetIdentifier; + if (isIndex) suffix += '-dir/index'; + const srcDirInfix = targetPackageStyle === 'empty-manifest' ? '' : 'src/'; + const outDirInfix = targetPackageStyle === 'empty-manifest' ? '' : 'out/'; + const srcName = `${prefix}${srcDirInfix}${suffix}.${srcExt}`; + const srcDirOutExtName = `${prefix}${srcDirInfix}${suffix}.${outExt}`; + const outName = `${prefix}${outDirInfix}${suffix}.${outExt}`; + const selfImporterCjsName = `${prefix}self-import-cjs.cjs`; + const selfImporterMjsName = `${prefix}self-import-mjs.mjs`; + const target: Target = { + targetIdentifier, + srcName, + outName, + srcExt, + outExt, + inSrc, + inOut, + isNamedFile: !isIndex && !targetPackageStyle, + isIndex, + isPackage: !!targetPackageStyle, + packageStyle: targetPackageStyle, + typeModule: packageTypeModule, + }; + const { isMjs: targetIsMjs } = fileInfo( + '.' + srcExt, + packageTypeModule, + project.allowJs + ); + function targetContent(loc: string) { + let content = ''; + if (targetIsMjs) { + content += String.raw` + const {fileURLToPath} = await import('url'); + const filenameNative = fileURLToPath(import.meta.url); + export const directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/]/, '$1'); + export const filename = filenameNative.replace(/.*[\\\/]/, ''); + export const targetIdentifier = '${targetIdentifier}'; + export const ext = filenameNative.replace(/.*\./, ''); + export const loc = '${loc}'; + `; + } else { + content += String.raw` + const filenameNative = __filename; + exports.filename = filenameNative.replace(/.*[\\\/]/, ''); + exports.directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/].*/, '$1'); + exports.targetIdentifier = '${targetIdentifier}'; + exports.ext = filenameNative.replace(/.*\./, ''); + exports.loc = '${loc}'; + `; + } + return content; + } + if (inOut) { + p.addFile(outName, targetContent('out')); + // TODO so we can test multiple file extensions in a single directory, preferTsExt + p.addFile(srcDirOutExtName, targetContent('out')); + } + if (inSrc) { + p.addFile(srcName, targetContent('src')); + } + if (targetPackageStyle) { + const selfImporterIsCompiled = project.allowJs; + const cjsSelfImporterMustUseDynamicImportHack = + !project.useTsNodeNext && selfImporterIsCompiled && targetIsMjs; + p.addFile( + selfImporterCjsName, + targetIsMjs + ? cjsSelfImporterMustUseDynamicImportHack + ? `${declareDynamicImportFunction}\nmodule.exports = dynamicImport('${targetIdentifier}');` + : `module.exports = import("${targetIdentifier}");` + : `module.exports = require("${targetIdentifier}");` + ); + p.addFile( + selfImporterMjsName, + ` + export * from "${targetIdentifier}"; + ` + ); + function writePackageJson(obj: any) { + p.addJsonFile(`${prefix}/package.json`, { + name: targetIdentifier, + type: packageTypeModule ? 'module' : undefined, + ...obj, + }); + } + switch (targetPackageStyle) { + case 'empty-manifest': + writePackageJson({}); + break; + case 'exports-src-with-extension': + writePackageJson({ + exports: { + '.': `./src/${suffix}.${srcExt}`, + }, + }); + break; + case 'exports-src-with-out-extension': + writePackageJson({ + exports: { + '.': `./src/${suffix}.${outExt}`, + }, + }); + break; + case 'exports-out-with-extension': + writePackageJson({ + exports: { + '.': `./out/${suffix}.${outExt}`, + }, + }); + break; + case 'main-src-extensionless': + writePackageJson({ + main: `src/${suffix}`, + }); + break; + case 'main-out-extensionless': + writePackageJson({ + main: `out/${suffix}`, + }); + break; + case 'main-src-with-extension': + writePackageJson({ + main: `src/${suffix}.${srcExt}`, + }); + break; + case 'main-src-with-out-extension': + writePackageJson({ + main: `src/${suffix}.${outExt}`, + }); + break; + case 'main-out-with-extension': + writePackageJson({ + main: `src/${suffix}.${outExt}`, + }); + break; + default: + const _assert: never = targetPackageStyle; + } + } + return target; +} + +/** + * Generate all entrypoint-* files + */ +function generateEntrypoints( + project: Project, + p: FsProject, + targets: Target[] +) { + /** Array of entrypoint files to be imported during the test */ + let entrypoints: string[] = []; + for (const entrypointExt of ['cjs', 'mjs'] as const) { + // TODO consider removing this logic; deferring to conditionals in the generateEntrypoint which emit meaningful comments + const withExtPermutations = + entrypointExt == 'mjs' && + project.experimentalSpecifierResolutionNode === false + ? [true] + : [false, true]; + for (const withExt of withExtPermutations) { + // Location of the entrypoint + for (const entrypointLocation of ['src', 'out'] as const) { + // Target of the entrypoint's import statements + for (const entrypointTargetting of ['src', 'out'] as const) { + // TODO re-enable when we have out <-> src mapping + if (entrypointLocation !== 'src') continue; + if (entrypointTargetting !== 'src') continue; + + entrypoints.push( + generateEntrypoint(project, p, targets, { + entrypointExt, + withExt, + entrypointLocation, + entrypointTargetting, + }) + ); + } + } + } + } + return entrypoints; +} + +function generateEntrypoint( + project: Project, + p: FsProject, + targets: Target[], + opts: EntrypointPermutation +) { + const { entrypointExt, withExt, entrypointLocation, entrypointTargetting } = + opts; + const entrypointFilename = `entrypoint-${entrypointSeq()}-${entrypointLocation}-to-${entrypointTargetting}${ + withExt ? '-withext' : '' + }.${entrypointExt}`; + const { isMjs: entrypointIsMjs, isCompiled: entrypointIsCompiled } = fileInfo( + entrypointFilename, + project.typeModule, + project.allowJs + ); + let entrypointContent = 'let mod;\n'; + entrypointContent += 'let testsRun = 0;\n'; + if (entrypointIsMjs) { + entrypointContent += `import assert from 'assert';\n`; + } else { + entrypointContent += `const assert = require('assert');\n`; + entrypointContent += `${declareDynamicImportFunction}\n`; + } + entrypointContent += `async function main() {\n`; + + for (const target of targets) { + // TODO re-enable these when we have outDir <-> rootDir mapping + if (target.srcName.includes('onlyout') && entrypointTargetting === 'src') + continue; + if (target.srcName.includes('onlysrc') && entrypointTargetting === 'out') + continue; + + const { + ext: targetSrcExt, + isMjs: targetIsMjs, + isCompiled: targetIsCompiled, + } = fileInfo(target.srcName, target.typeModule, project.allowJs); + + let targetExtPermutations = ['']; + if (!target.isPackage) { + if (entrypointTargetting === 'out' && target.outExt !== target.srcExt) { + // TODO re-enable when we have out <-> src mapping + targetExtPermutations = [target.outExt]; + } else if (target.srcExt !== target.outExt) { + targetExtPermutations = [target.srcExt, target.outExt]; + } else { + targetExtPermutations = [target.srcExt]; + } + } + const externalPackageSelfImportPermutations = target.isPackage + ? [false, true] + : [false]; + for (const targetExt of targetExtPermutations) { + for (const externalPackageSelfImport of externalPackageSelfImportPermutations) { + entrypointContent += `\n// ${target.targetIdentifier}`; + if (target.isPackage) { + entrypointContent += ` node_modules package`; + if (externalPackageSelfImport) { + entrypointContent += ` self-import`; + } + } else { + entrypointContent += `.${targetExt}`; + } + entrypointContent += '\n'; + + // should specifier be relative or absolute? + let specifier: string; + if (externalPackageSelfImport) { + specifier = `../node_modules/${target.targetIdentifier}/self-import-${entrypointExt}.${entrypointExt}`; + } else if (target.isPackage) { + specifier = target.targetIdentifier; + } else { + if (entrypointTargetting === entrypointLocation) specifier = './'; + else specifier = `../${entrypointTargetting}/`; + specifier += target.targetIdentifier; + if (target.isIndex) specifier += '-dir'; + if (!target.isIndex && withExt) specifier += '.' + targetExt; + } + + //#region SKIPPING + if (target.isNamedFile && !withExt) { + // Do not try to import cjs/cts without extension; node always requires these extensions + if (target.outExt === 'cjs') { + entrypointContent += `// skipping ${specifier} because we cannot omit extension from cjs / cts files; node always requires them\n`; + continue; + } + // Do not try to import mjs/mts unless experimental-specifier-resolution is turned on + if ( + target.outExt === 'mjs' && + !project.experimentalSpecifierResolutionNode + ) { + entrypointContent += `// skipping ${specifier} because we cannot omit extension from mjs/mts unless experimental-specifier-resolution=node\n`; + continue; + } + // Do not try to import anything extensionless via ESM loader unless experimental-specifier-resolution is turned on + if ( + (targetIsMjs || entrypointIsMjs) && + !project.experimentalSpecifierResolutionNode + ) { + entrypointContent += `// skipping ${specifier} because we cannot omit extension via esm loader unless experimental-specifier-resolution=node\n`; + continue; + } + } + if ( + target.isPackage && + isOneOf(target.packageStyle, [ + 'empty-manifest', + 'main-out-extensionless', + 'main-src-extensionless', + ]) && + isOneOf(target.outExt, ['cjs', 'mjs']) + ) { + entrypointContent += `// skipping ${specifier} because it points to a node_modules package that tries to omit file extension, and node does not allow omitting cjs/mjs extension\n`; + continue; + } + + // Do not try to import a transpiled file if compiler options disagree with node's extension-based classification + if (!project.useTsNodeNext && targetIsCompiled) { + if (targetIsMjs && !project.typeModule) { + entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=commonjs\n`; + continue; + } + if (!targetIsMjs && project.typeModule) { + entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=esnext\n`; + continue; + } + } + + // Do not try to import index from a directory if is forbidden by node's ESM resolver + if (target.isIndex) { + if ( + (targetIsMjs || entrypointIsMjs) && + !project.experimentalSpecifierResolutionNode + ) { + entrypointContent += `// skipping ${specifier} because esm loader does not allow directory ./index imports unless experimental-specifier-resolution=node\n`; + continue; + } + if (target.outExt === 'cjs') { + entrypointContent += `// skipping ${specifier} because it relies on node automatically resolving a directory to index.cjs/cts , but node does not support those extensions for index.* files, only .js (and equivalents), .node, .json\n`; + continue; + } + } + //#endregion + + // NOTE: if you try to explicitly import foo.ts, we will load foo.ts, EVEN IF you have `preferTsExts` off + const assertIsSrcOrOut = !target.inSrc + ? 'out' + : !target.inOut + ? 'src' + : project.preferSrc || + (!target.isIndex && targetExt === target.srcExt && withExt) || + target.srcExt === target.outExt || // <-- TODO re-enable when we have src <-> out mapping + (target.isPackage && + isOneOf(target.packageStyle, [ + 'main-src-with-extension', + 'exports-src-with-extension', + ])) + ? 'src' + : 'out'; + const assertHasExt = + assertIsSrcOrOut === 'src' ? target.srcExt : target.outExt; + + // If entrypoint is compiled as CJS, and *not* with TS's nodenext, then TS transforms `import` into `require`, + // so we must hack around the compiler to get a true `import`. + const entrypointMustUseDynamicImportHack = + !project.useTsNodeNext && + entrypointIsCompiled && + !entrypointIsMjs && + !externalPackageSelfImport; + entrypointContent += + entrypointExt === 'cjs' && (externalPackageSelfImport || !targetIsMjs) + ? ` mod = await require('${specifier}');\n` + : entrypointMustUseDynamicImportHack + ? ` mod = await dynamicImport('${specifier}');\n` + : ` mod = await import('${specifier}');\n`; + entrypointContent += ` assert.equal(mod.loc, '${assertIsSrcOrOut}');\n`; + entrypointContent += ` assert.equal(mod.targetIdentifier, '${target.targetIdentifier}');\n`; + entrypointContent += ` assert.equal(mod.ext, '${assertHasExt}');\n`; + entrypointContent += ` testsRun++;\n`; + } + } + } + entrypointContent += `}\n`; + entrypointContent += `const result = main().then(() => {return testsRun});\n`; + entrypointContent += `result.mark = 'marked';\n`; + if (entrypointIsMjs) { + entrypointContent += `export {result};\n`; + } else { + entrypointContent += `exports.result = result;\n`; + } + p.dir(entrypointLocation).addFile(entrypointFilename, entrypointContent); + return entrypointLocation + '/' + entrypointFilename; +} + +/** + * Assertions happen here + */ +async function execute(t: T, p: FsProject, entrypoints: Entrypoint[]) { + // + // Install ts-node and try to import all the index-* files + // + + const service = t.context.tsNodeUnderTest.register({ + projectSearchDir: p.cwd, + }); + process.__test_setloader__(t.context.tsNodeUnderTest.createEsmHooks(service)); + + for (const entrypoint of entrypoints) { + t.log(`Importing ${join(p.cwd, entrypoint)}`); + try { + const { result } = await dynamicImport( + pathToFileURL(join(p.cwd, entrypoint)) + ); + expect(result).toBeInstanceOf(Promise); + expect(result.mark).toBe('marked'); + const testsRun = await result; + t.log(`Entrypoint ran ${testsRun} tests.`); + } catch (e) { + try { + const launchJsonPath = Path.resolve( + __dirname, + '../../.vscode/launch.json' + ); + const launchJson = JSON.parse(fs.readFileSync(launchJsonPath, 'utf8')); + const config = launchJson.configurations.find( + (c: any) => c.name === 'Debug resolver test' + ); + config.cwd = Path.join( + '${workspaceFolder}', + Path.relative(Path.resolve(__dirname, '../..'), p.cwd) + ); + config.program = `./${entrypoint}`; + fs.writeFileSync(launchJsonPath, JSON.stringify(launchJson, null, 2)); + } catch {} + throw new Error( + [ + (e as Error).message, + (e as Error).stack, + '', + 'This is an error in a resolver test. It might be easier to investigate by running outside of the test suite.', + 'To do that, try pasting this into your bash shell (windows invocation will be similar but maybe not identical):', + ` ( cd ${p.cwd} ; node --loader ../../../esm.mjs ./${entrypoint} )`, + ].join('\n') + ); + } + } +} + +function fileInfo(filename: string, typeModule: boolean, allowJs: boolean) { + const ext = filename.match(/\.(.*)$/)?.[1] ?? filename; + // ['ts', 'tsx', 'cts', 'mts', 'js', 'jsx', 'cjs', 'mjs'] + return { + ext, + isMjs: ['mts', 'mjs'].includes(ext) + ? true + : ['cts', 'cjs'].includes(ext) + ? false + : typeModule, + isCompiled: allowJs || ['ts', 'tsx', 'jsx', 'mts', 'cts'].includes(ext), + }; +} + +function seqGenerator() { + let next = 0; + return function () { + return padStart('' + next++, 4, '0'); + }; +} diff --git a/src/test/sourcemaps.spec.ts b/src/test/sourcemaps.spec.ts index 505107c8..26a0cb04 100644 --- a/src/test/sourcemaps.spec.ts +++ b/src/test/sourcemaps.spec.ts @@ -1,12 +1,8 @@ import * as expect from 'expect'; import { createExec, createExecTester } from './exec-helpers'; -import { - CMD_TS_NODE_WITH_PROJECT_FLAG, - contextTsNodeUnderTest, - TEST_DIR, -} from './helpers'; -import { test as _test } from './testlib'; -const test = _test.context(contextTsNodeUnderTest); +import { CMD_TS_NODE_WITH_PROJECT_FLAG, ctxTsNode, TEST_DIR } from './helpers'; +import { context } from './testlib'; +const test = context(ctxTsNode); const exec = createExecTester({ cmd: CMD_TS_NODE_WITH_PROJECT_FLAG, diff --git a/src/test/test-loader.d.ts b/src/test/test-loader.d.ts new file mode 100644 index 00000000..1d83a997 --- /dev/null +++ b/src/test/test-loader.d.ts @@ -0,0 +1,8 @@ +export {}; +declare global { + namespace NodeJS { + interface Process { + __test_setloader__(hooks: any): void; + } + } +} diff --git a/src/test/test-loader.mjs b/src/test/test-loader.mjs new file mode 100644 index 00000000..b7fd7bbc --- /dev/null +++ b/src/test/test-loader.mjs @@ -0,0 +1,21 @@ +// Grant ourselves the ability to install ESM loader behaviors in-process during tests +import semver from 'semver'; + +const newHooksAPI = semver.gte(process.versions.node, '16.12.0'); + +let hooks = undefined; +process.__test_setloader__ = function (_hooks) { + hooks = _hooks; +}; +function createHook(name) { + return function (a, b, c) { + const target = (hooks && hooks[name]) || c; + return target(...arguments); + }; +} +export const resolve = createHook('resolve'); +export const load = newHooksAPI ? createHook('load') : null; +export const getFormat = !newHooksAPI ? createHook('getFormat') : null; +export const transformSource = !newHooksAPI + ? createHook('transformSource') + : null; diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 6304164b..780979e8 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -15,6 +15,9 @@ import * as expect from 'expect'; export { ExecutionContext, expect }; +// HACK ensure ts-node-specific bootstrapping is executed +import './helpers'; + // NOTE: this limits concurrency within a single process, but AVA launches // each .spec file in its own process, so actual concurrency is higher. const concurrencyLimiter = throat(16); @@ -52,9 +55,7 @@ export const test = createTestInterface({ separator: ' \u203a ', titlePrefix: undefined, }); -// In case someone wants to `const test = _test.context()` -export { test as _test }; -// Or import `context` +// In case someone wants to `const test = context()` export const context = test.context; export interface TestInterface< @@ -85,22 +86,26 @@ export interface TestInterface< macros: OneOrMoreMacros, ...rest: T ): void; + skip(title: string, implementation: Implementation): void; + /** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */ + skip( + title: string, + macros: OneOrMoreMacros, + ...rest: T + ): void; + /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ + skip(macros: OneOrMoreMacros, ...rest: T): void; - macro( + macro( cb: ( ...args: Args ) => | [ - (title: string | undefined) => string | undefined, - (t: ExecutionContext) => Promise + ((title: string | undefined) => string | undefined) | string, + (t: ExecutionContext) => Promise ] - | ((t: ExecutionContext) => Promise) - ): ( - test: ExecutionContext, - ...args: Args - ) => Promise & { - title(givenTitle: string | undefined, ...args: Args): string; - }; + | ((t: ExecutionContext) => Promise) + ): AvaMacro; beforeAll(cb: (t: ExecutionContext) => Promise): void; beforeEach(cb: (t: ExecutionContext) => Promise): void; @@ -120,6 +125,11 @@ export interface TestInterface< // TODO add teardownEach } +export interface AvaMacro { + (test: ExecutionContext, ...args: Args): Promise; + title?(givenTitle: string | undefined, ...args: Args): string; +} + function createTestInterface(opts: { titlePrefix: string | undefined; separator: string | undefined; @@ -133,7 +143,16 @@ function createTestInterface(opts: { let { mustDoSerial, automaticallyDoSerial, automaticallySkip } = opts; let hookDeclared = false; let suiteOrTestDeclared = false; - function computeTitle(title: string | undefined) { + function computeTitle( + title: string | undefined, + macros?: AvaMacro[], + ...args: Args + ) { + for (const macro of macros ?? []) { + if (macro.title) { + title = macro.title(title, ...args); + } + } assert(title); // return `${ titlePrefix }${ separator }${ title }`; if (titlePrefix != null && title != null) { @@ -148,9 +167,9 @@ function createTestInterface(opts: { typeof args[0] === 'string' ? (args.shift() as string) : undefined; const macros = typeof args[0] === 'function' - ? [args.shift() as Function] + ? [args.shift() as AvaMacro] : Array.isArray(args[0]) - ? (args.shift() as Function[]) + ? (args.shift() as AvaMacro[]) : []; return { title, macros, args }; } @@ -177,9 +196,10 @@ function createTestInterface(opts: { */ function declareTest( title: string | undefined, - macros: Function[], + macros: AvaMacro[], avaDeclareFunction: Function & { skip: Function }, - args: any[] + args: any[], + skip = false ) { const wrappedMacros = macros.map((macro) => { return async function (t: ExecutionContext, ...args: any[]) { @@ -195,8 +215,8 @@ function createTestInterface(opts: { ); }; }); - const computedTitle = computeTitle(title); - (automaticallySkip ? avaDeclareFunction.skip : avaDeclareFunction)( + const computedTitle = computeTitle(title, macros, ...args); + (automaticallySkip || skip ? avaDeclareFunction.skip : avaDeclareFunction)( computedTitle, wrappedMacros, ...args @@ -222,6 +242,11 @@ function createTestInterface(opts: { const { args, macros, title } = parseArgs(inputArgs); return declareTest(title, macros, avaTest.serial, args); }; + test.skip = function (...inputArgs: any[]) { + assertOrderingForDeclaringTest(); + const { args, macros, title } = parseArgs(inputArgs); + return declareTest(title, macros, avaTest, args, true); + }; test.beforeEach = function ( cb: (test: ExecutionContext) => Promise ) { @@ -250,7 +275,7 @@ function createTestInterface(opts: { ...args: Args ) => | [ - (title: string | undefined) => string, + ((title: string | undefined) => string | undefined) | string, (t: ExecutionContext) => Promise ] | ((t: ExecutionContext) => Promise) @@ -262,7 +287,11 @@ function createTestInterface(opts: { } macro.title = function (givenTitle: string | undefined, ...args: Args) { const ret = cb(...args); - return Array.isArray(ret) ? ret[0](givenTitle) : givenTitle; + return Array.isArray(ret) + ? typeof ret[0] === 'string' + ? ret[0] + : ret[0](givenTitle) + : givenTitle; }; return macro; }; diff --git a/src/test/transpilers.spec.ts b/src/test/transpilers.spec.ts index f1d30172..57174f5e 100644 --- a/src/test/transpilers.spec.ts +++ b/src/test/transpilers.spec.ts @@ -3,10 +3,10 @@ // Should consolidate them here. import { context } from './testlib'; -import { contextTsNodeUnderTest, testsDirRequire } from './helpers'; +import { ctxTsNode, testsDirRequire } from './helpers'; import * as expect from 'expect'; -const test = context(contextTsNodeUnderTest); +const test = context(ctxTsNode); test.suite('swc', (test) => { test('verify that TS->SWC target mappings suppport all possible values from both TS and SWC', async (t) => { diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 1d8d1c44..246b70f4 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -17,6 +17,7 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { swc, service: { config, projectLocalResolveHelper }, transpilerConfigLocalResolveHelper, + nodeModuleEmitKind, } = createOptions; // Load swc compiler @@ -78,6 +79,8 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } swcTarget = swcTargets[swcTargetIndex]; const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; + const isNodeModuleKind = + module === ModuleKind.Node12 || module === ModuleKind.NodeNext; // swc only supports these 4x module options [MUST_UPDATE_FOR_NEW_MODULEKIND] const moduleType = module === ModuleKind.CommonJS @@ -86,6 +89,10 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { ? 'amd' : module === ModuleKind.UMD ? 'umd' + : isNodeModuleKind && nodeModuleEmitKind === 'nodecjs' + ? 'commonjs' + : isNodeModuleKind && nodeModuleEmitKind === 'nodeesm' + ? 'es6' : '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. @@ -111,6 +118,8 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { noInterop: !esModuleInterop, type: moduleType, strictMode, + // For NodeNext and Node12, emit as CJS but do not transform dynamic imports + ignoreDynamic: nodeModuleEmitKind === 'nodecjs', } as swcTypes.ModuleConfig) : undefined, swcrc: false, @@ -198,4 +207,6 @@ const ModuleKind = { ES2015: 5, ES2020: 6, ESNext: 99, + Node12: 100, + NodeNext: 199, } as const; diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index 641deb4c..70faf491 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -1,20 +1,25 @@ import type * as ts from 'typescript'; -import type { Service } from '../index'; +import type { NodeModuleEmitKind, Service } from '../index'; import type { ProjectLocalResolveHelper } from '../util'; /** * Third-party transpilers are implemented as a CommonJS module with a * named export "create" + * + * @category Transpiler */ export interface TranspilerModule { create: TranspilerFactory; } /** * Called by ts-node to create a custom transpiler. + * + * @category Transpiler */ export type TranspilerFactory = ( options: CreateTranspilerOptions ) => Transpiler; +/** @category Transpiler */ export interface CreateTranspilerOptions { // TODO this is confusing because its only a partial Service. Rename? // Careful: must avoid stripInternal breakage by guarding with Extract<> @@ -29,16 +34,26 @@ export interface CreateTranspilerOptions { * @internal */ transpilerConfigLocalResolveHelper: ProjectLocalResolveHelper; + /** + * When using `module: nodenext` or `module: node12`, there are two possible styles of emit: + * - CommonJS with dynamic imports preserved (not transformed into `require()` calls) + * - ECMAScript modules with `import foo = require()` transformed into `require = createRequire(); const foo = require()` + * @internal + */ + nodeModuleEmitKind?: NodeModuleEmitKind; } +/** @category Transpiler */ export interface Transpiler { // TODOs // Create spec for returning diagnostics? Currently transpilers are allowed to // throw an error but that's it. transpile(input: string, options: TranspileOptions): TranspileOutput; } +/** @category Transpiler */ export interface TranspileOptions { fileName: string; } +/** @category Transpiler */ export interface TranspileOutput { outputText: string; diagnostics?: ts.Diagnostic[]; diff --git a/src/ts-compiler-types.ts b/src/ts-compiler-types.ts index 0abaaec2..2f961b85 100644 --- a/src/ts-compiler-types.ts +++ b/src/ts-compiler-types.ts @@ -19,7 +19,7 @@ export interface TSCommon { getPreEmitDiagnostics: typeof _ts.getPreEmitDiagnostics; flattenDiagnosticMessageText: typeof _ts.flattenDiagnosticMessageText; transpileModule: typeof _ts.transpileModule; - ModuleKind: typeof _ts.ModuleKind; + ModuleKind: TSCommon.ModuleKindEnum; ScriptTarget: typeof _ts.ScriptTarget; findConfigFile: typeof _ts.findConfigFile; readConfigFile: typeof _ts.readConfigFile; @@ -73,6 +73,12 @@ export namespace TSCommon { _ts.ResolvedModuleWithFailedLookupLocations; export type FileReference = _ts.FileReference; export type SourceFile = _ts.SourceFile; + // Hack until we start building against TS >= 4.7.0 + export type ModuleKindEnum = typeof _ts.ModuleKind & { + Node16: typeof _ts.ModuleKind extends { Node16: any } + ? typeof _ts.ModuleKind['Node16'] + : 100; + }; } /** @@ -112,6 +118,17 @@ export interface TSInternal { ref: _ts.FileReference | string, containingFileMode: _ts.SourceFile['impliedNodeFormat'] ) => _ts.SourceFile['impliedNodeFormat']; + // TODO do we need these? Which TS version adds them? + getPatternFromSpec( + spec: string, + basePath: string, + usage: 'files' | 'directories' | 'exclude' + ): string | undefined; + getRegularExpressionForWildcard( + specs: readonly string[] | undefined, + basePath: string, + usage: 'files' | 'directories' | 'exclude' + ): string | undefined; } /** @internal */ export namespace TSInternal { diff --git a/src/ts-transpile-module.ts b/src/ts-transpile-module.ts new file mode 100644 index 00000000..0744dc4d --- /dev/null +++ b/src/ts-transpile-module.ts @@ -0,0 +1,177 @@ +import type { + CompilerHost, + CompilerOptions, + Diagnostic, + SourceFile, + TranspileOptions, + TranspileOutput, +} from 'typescript'; +import type { TSCommon } from './ts-compiler-types'; + +/** @internal */ +export function createTsTranspileModule( + ts: TSCommon, + transpileOptions: Pick< + TranspileOptions, + 'compilerOptions' | 'reportDiagnostics' | 'transformers' + > +) { + const { + createProgram, + createSourceFile, + getDefaultCompilerOptions, + getImpliedNodeFormatForFile, + fixupCompilerOptions, + transpileOptionValueCompilerOptions, + getNewLineCharacter, + fileExtensionIs, + normalizePath, + Debug, + toPath, + getSetExternalModuleIndicator, + getEntries, + addRange, + hasProperty, + getEmitScriptTarget, + getDirectoryPath, + } = ts as any; + + const compilerOptionsDiagnostics: Diagnostic[] = []; + + const options: CompilerOptions = transpileOptions.compilerOptions + ? fixupCompilerOptions( + transpileOptions.compilerOptions, + compilerOptionsDiagnostics + ) + : {}; + + // mix in default options + const defaultOptions = getDefaultCompilerOptions(); + for (const key in defaultOptions) { + if (hasProperty(defaultOptions, key) && options[key] === undefined) { + options[key] = defaultOptions[key]; + } + } + + for (const option of transpileOptionValueCompilerOptions) { + options[option.name] = option.transpileOptionValue; + } + + // transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between input and output paths. + options.suppressOutputPathCheck = true; + + // Filename can be non-ts file. + options.allowNonTsExtensions = true; + + const newLine = getNewLineCharacter(options); + // Create a compilerHost object to allow the compiler to read and write files + const compilerHost: CompilerHost = { + getSourceFile: (fileName) => + fileName === normalizePath(inputFileName) ? sourceFile : undefined, + writeFile: (name, text) => { + if (fileExtensionIs(name, '.map')) { + Debug.assertEqual( + sourceMapText, + undefined, + 'Unexpected multiple source map outputs, file:', + name + ); + sourceMapText = text; + } else { + Debug.assertEqual( + outputText, + undefined, + 'Unexpected multiple outputs, file:', + name + ); + outputText = text; + } + }, + getDefaultLibFileName: () => 'lib.d.ts', + useCaseSensitiveFileNames: () => true, + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => '', + getNewLine: () => newLine, + fileExists: (fileName): boolean => + fileName === inputFileName || fileName === packageJsonFileName, + readFile: (fileName) => + fileName === packageJsonFileName ? `{"type": "${_packageJsonType}"}` : '', + directoryExists: () => true, + getDirectories: () => [], + }; + + let inputFileName: string; + let packageJsonFileName: string; + let _packageJsonType: 'module' | 'commonjs'; + let sourceFile: SourceFile; + let outputText: string | undefined; + let sourceMapText: string | undefined; + + return transpileModule; + + function transpileModule( + input: string, + transpileOptions2: Pick< + TranspileOptions, + 'fileName' | 'moduleName' | 'renamedDependencies' + >, + packageJsonType: 'module' | 'commonjs' = 'commonjs' + ): TranspileOutput { + // if jsx is specified then treat file as .tsx + inputFileName = + transpileOptions2.fileName || + (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx + ? 'module.tsx' + : 'module.ts'); + packageJsonFileName = getDirectoryPath(inputFileName) + '/package.json'; + _packageJsonType = packageJsonType; + + sourceFile = createSourceFile(inputFileName, input, { + languageVersion: getEmitScriptTarget(options), + impliedNodeFormat: getImpliedNodeFormatForFile( + toPath(inputFileName, '', compilerHost.getCanonicalFileName), + /*cache*/ undefined, + compilerHost, + options + ), + setExternalModuleIndicator: getSetExternalModuleIndicator(options), + }); + if (transpileOptions2.moduleName) { + sourceFile.moduleName = transpileOptions2.moduleName; + } + + if (transpileOptions2.renamedDependencies) { + (sourceFile as any).renamedDependencies = new Map( + getEntries(transpileOptions2.renamedDependencies) + ); + } + + // Output + outputText = undefined; + sourceMapText = undefined; + + const program = createProgram([inputFileName], options, compilerHost); + + const diagnostics = compilerOptionsDiagnostics.slice(); + + if (transpileOptions.reportDiagnostics) { + addRange( + /*to*/ diagnostics, + /*from*/ program.getSyntacticDiagnostics(sourceFile) + ); + addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); + } + // Emit + program.emit( + /*targetSourceFile*/ undefined, + /*writeFile*/ undefined, + /*cancellationToken*/ undefined, + /*emitOnlyDtsFiles*/ undefined, + transpileOptions.transformers + ); + + if (outputText === undefined) return Debug.fail('Output generation failed'); + + return { outputText, diagnostics, sourceMapText }; + } +} diff --git a/src/util.ts b/src/util.ts index adc5eaca..ec7f3485 100644 --- a/src/util.ts +++ b/src/util.ts @@ -156,3 +156,43 @@ export function getBasePathForProjectLocalDependencyResolution( // and we attempt to resolve relative specifiers. By the time we resolve relative specifiers, // should have configFilePath, so not reach this codepath. } + +/** @internal */ +export function once any>(fn: Fn) { + let value: ReturnType; + let ran = false; + function onceFn(...args: Parameters): ReturnType { + if (ran) return value; + value = fn(...args); + ran = true; + return value; + } + return onceFn; +} + +/** @internal */ +export function versionGteLt( + version: string, + gteRequirement: string, + ltRequirement?: string +) { + const [major, minor, patch, extra] = parse(version); + const [gteMajor, gteMinor, gtePatch] = parse(gteRequirement); + const isGte = + major > gteMajor || + (major === gteMajor && + (minor > gteMinor || (minor === gteMinor && patch >= gtePatch))); + let isLt = true; + if (ltRequirement) { + const [ltMajor, ltMinor, ltPatch] = parse(ltRequirement); + isLt = + major < ltMajor || + (major === ltMajor && + (minor < ltMinor || (minor === ltMinor && patch < ltPatch))); + } + return isGte && isLt; + + function parse(requirement: string) { + return requirement.split(/[\.-]/).map((s) => parseInt(s, 10)); + } +} diff --git a/tests/.gitignore b/tests/.gitignore index 1f16711a..33ac13ea 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,4 @@ !from-node-modules/node_modules/ package-lock.json ts-node-packed.tgz +/tmp diff --git a/tests/cwd-and-script-mode/a/tsconfig.json b/tests/cwd-and-script-mode/a/tsconfig.json index 196e2b44..377a8601 100644 --- a/tests/cwd-and-script-mode/a/tsconfig.json +++ b/tests/cwd-and-script-mode/a/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "transpileOnly": true + }, "compilerOptions": { "plugins": [ { diff --git a/tests/cwd-and-script-mode/b/tsconfig.json b/tests/cwd-and-script-mode/b/tsconfig.json index 3b22d468..dd98865c 100644 --- a/tests/cwd-and-script-mode/b/tsconfig.json +++ b/tests/cwd-and-script-mode/b/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "transpileOnly": true + }, "compilerOptions": { "plugins": [ { diff --git a/tests/esm-import-assertions/tsconfig.json b/tests/esm-import-assertions/tsconfig.json index d626b927..731778e9 100644 --- a/tests/esm-import-assertions/tsconfig.json +++ b/tests/esm-import-assertions/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "transpileOnly": true + }, "compilerOptions": { "module": "ESNext", "target": "ESNext", diff --git a/tests/import-order/require-compiled.js b/tests/import-order/require-compiled.js deleted file mode 100644 index b73d5a37..00000000 --- a/tests/import-order/require-compiled.js +++ /dev/null @@ -1,2 +0,0 @@ -// indirectly load ./compiled in node < 12 (soon to be end-of-life'd) -require('./compiled'); diff --git a/tests/main-realpath/target/tsconfig.json b/tests/main-realpath/target/tsconfig.json index 986627de..93d434ff 100644 --- a/tests/main-realpath/target/tsconfig.json +++ b/tests/main-realpath/target/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "transpileOnly": true + }, "compilerOptions": { "jsx": "react" } diff --git a/tests/package.json b/tests/package.json index 371b9008..1db06d54 100644 --- a/tests/package.json +++ b/tests/package.json @@ -3,5 +3,8 @@ "@swc/core": "latest", "ts-node": "file:ts-node-packed.tgz", "tslog": "3.2.2" + }, + "volta": { + "extends": "../package.json" } } diff --git a/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js index 806376ab..2b57fb73 100644 --- a/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js +++ b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js @@ -6,4 +6,7 @@ module.exports = { sourceMapText: '{}', }; }, + createProgram() { + throw 'emit from root custom compiler'; + } }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js index b1a45e62..389b3f04 100644 --- a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js @@ -6,4 +6,7 @@ module.exports = { sourceMapText: '{}', }; }, + createProgram() { + throw 'emit from shared-config custom compiler'; + } }; diff --git a/tests/recursive-fork/index.ts b/tests/recursive-fork/index.ts index ed68a208..b426d29b 100644 --- a/tests/recursive-fork/index.ts +++ b/tests/recursive-fork/index.ts @@ -1,5 +1,9 @@ import { fork } from 'child_process'; +// Type syntax to prove its compiled, though the import above should also +// prove the same +const a = null as any; + console.log(JSON.stringify({ execArgv: process.execArgv, argv: process.argv })); if (process.env.generation !== 'grandchild') { const nextGeneration = diff --git a/tests/resolver/README.md b/tests/resolver/README.md new file mode 100644 index 00000000..50ec7424 --- /dev/null +++ b/tests/resolver/README.md @@ -0,0 +1,46 @@ +TODO must require explicit rootDir; do not allow to be inferred. +TODO resolve JSON if resolveJsonModules?? + +Test a bunch of permutations of: + +config permutations: + +- allowJs +- not allowJs + +- preferSrc +- not preferSrc + +import permutations: + +- Relative import of file +- Relative import of index +- rootless library import of main +- rootless library import of index +- rootless library import of exports sub-path +- rootless self-import of main +- rootless self-import of index +- rootless self-import of exports sub-path + + - Require with extension + - Require without extension + + - Require from dist to dist + - Require from dist to src + - Require from src to dist + - Require from src to src + +lib permutations: + +- module exists in both src and dist (precompilation ran) +- module exists in only dist (came from elsewhere) +- module exists only in src (did not precompile) + +- .ts / .js extension +- .tsx / .js extension +- .cts / .cjs extension +- .mts / .mjs extension +- .js / .js extension +- .jsx / .js extension +- .cjs / .cjs extension +- .mjs / .mjs extension diff --git a/tests/resolver/package.json b/tests/resolver/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/resolver/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/resolver/test.sh b/tests/resolver/test.sh new file mode 100644 index 00000000..a30080e3 --- /dev/null +++ b/tests/resolver/test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd project-preferSrc-typeModule diff --git a/tests/ts45-ext/ext-cts/index.cts b/tests/ts45-ext/ext-cts/index.cts new file mode 100644 index 00000000..9001625a --- /dev/null +++ b/tests/ts45-ext/ext-cts/index.cts @@ -0,0 +1,3 @@ +export function main() { + return 'hello world'; +} diff --git a/tests/ts45-ext/ext-cts/tsconfig.json b/tests/ts45-ext/ext-cts/tsconfig.json new file mode 100644 index 00000000..0ff9a8a6 --- /dev/null +++ b/tests/ts45-ext/ext-cts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "ts-node": { + "experimentalResolver": true + }, + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/tests/ts45-ext/ext-mts/entrypoint.mjs b/tests/ts45-ext/ext-mts/entrypoint.mjs new file mode 100644 index 00000000..13d90bee --- /dev/null +++ b/tests/ts45-ext/ext-mts/entrypoint.mjs @@ -0,0 +1,2 @@ +import { main } from './index.mjs'; +console.log(main()); diff --git a/tests/ts45-ext/ext-mts/index.mts b/tests/ts45-ext/ext-mts/index.mts new file mode 100644 index 00000000..9001625a --- /dev/null +++ b/tests/ts45-ext/ext-mts/index.mts @@ -0,0 +1,3 @@ +export function main() { + return 'hello world'; +} diff --git a/tests/ts45-ext/ext-mts/tsconfig.json b/tests/ts45-ext/ext-mts/tsconfig.json new file mode 100644 index 00000000..c414ea58 --- /dev/null +++ b/tests/ts45-ext/ext-mts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "ts-node": { + "esm": true, + "experimentalResolver": true + }, + "compilerOptions": { + "module": "ESNext" + } +} diff --git a/tests/tsconfig-transpile-only.json b/tests/tsconfig-transpile-only.json new file mode 100644 index 00000000..1a80ab28 --- /dev/null +++ b/tests/tsconfig-transpile-only.json @@ -0,0 +1,13 @@ +{ + "ts-node": { + "transpileOnly": true + }, + "compilerOptions": { + "target": "es2015", + "jsx": "react", + "noEmit": true, + "strict": true, + // Global type definitions. + "typeRoots": ["./typings", "../node_modules/@types"] + } +} diff --git a/tsconfig.build-dist-raw.json b/tsconfig.build-dist-raw.json new file mode 100644 index 00000000..82ee4b18 --- /dev/null +++ b/tsconfig.build-dist-raw.json @@ -0,0 +1,18 @@ +// This tsconfig.json is referenced by tsconfig.build-dist.json, a little trick to keep tsc happy. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "allowJs": true, + "checkJs": false, + "noResolve": true, + "declaration": true, + "noEmit": false, + "emitDeclarationOnly": true, + "rootDir": "dist-raw", + "outDir": "temp/dist-raw" + }, + "include": [ + "dist-raw/**/*" + ] +} diff --git a/tsconfig.build-dist.json b/tsconfig.build-dist.json new file mode 100644 index 00000000..8889d3d9 --- /dev/null +++ b/tsconfig.build-dist.json @@ -0,0 +1,18 @@ +// This tsconfig.json drives compiling the src/ directory into the dist/ directory +{ + "extends": "./tsconfig.json", + "references": [{ + "path": "./tsconfig.build-dist-raw.json" + }], + "compilerOptions": { + "noEmit": false, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "temp/tsconfig.build-dist.tsbuildinfo", + "allowJs": false, + "rootDirs": ["temp", "."] + }, + "include": [ + "src/**/*" + ] +} diff --git a/tsconfig.build-schema.json b/tsconfig.build-schema.json index adf48b4c..f3f762f5 100644 --- a/tsconfig.build-schema.json +++ b/tsconfig.build-schema.json @@ -1,6 +1,11 @@ +// This tsconfig.json is used when we generate the tsconfig JSON schema { "extends": "./tsconfig.json", "compilerOptions": { - "incremental": false + "incremental": false, + "tsBuildInfoFile": null, + "noEmit": false, + "allowJs": true, + "checkJs": false, } } diff --git a/tsconfig.json b/tsconfig.json index 158695d2..0b879a94 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,14 @@ +// This tsconfig.json drives Intellisense { "$schema": "./tsconfig.schemastore-schema.json", "compilerOptions": { // `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", + "rootDir": ".", + "outDir": "temp", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "noEmit": true, "module": "commonjs", "moduleResolution": "node", "strict": true, @@ -16,7 +19,12 @@ "stripInternal": true, "incremental": true, "skipLibCheck": true, - "importsNotUsedAsValues": "error" + "importsNotUsedAsValues": "error", + "allowJs": true, + + // Enable to assist in sanity-checking your changes to JS files, but note you will see many unrelated type errors! + // "checkJs": true, + // "noImplicitAny": false }, "include": [ "src/**/*" @@ -24,6 +32,10 @@ "typedocOptions": { "entryPoints": ["./src/index.ts"], "readme": "none", - "out": "website/static/api" + "out": "website/static/api", + "excludeTags": ["allof"], + "categorizeByGroup": false, + "categoryOrder": ["Basic", "REPL", "Transpiler", "ESM Loader", "Other"], + "defaultCategory": "Other" } } diff --git a/tsdoc.json b/tsdoc.json index 78b2de6d..0316a4fc 100644 --- a/tsdoc.json +++ b/tsdoc.json @@ -7,7 +7,8 @@ "@default": true, "@allOf": true, "@see": true, - "@deprecated": true + "@deprecated": true, + "@category": true }, "tagDefinitions": [ diff --git a/website/docs/api.md b/website/docs/api.md new file mode 100644 index 00000000..182e845b --- /dev/null +++ b/website/docs/api.md @@ -0,0 +1,14 @@ +--- +title: API +--- + +ts-node's complete API is documented here: [API Docs](https://typestrong.org/ts-node/api/) + +Here are a few highlights of what you can accomplish: + +- [`create()`](https://typestrong.org/ts-node/api/index.html#create) creates ts-node's compiler service without +registering any hooks. +- [`createRepl()`](https://typestrong.org/ts-node/api/index.html#createRepl) creates an instance of our REPL service, so +you can create your own TypeScript-powered REPLs. +- [`createEsmHooks()`](https://typestrong.org/ts-node/api/index.html#createEsmHooks) creates our ESM loader hooks, +suitable for composing with other loaders or augmenting with additional features. diff --git a/website/docs/imports.md b/website/docs/commonjs-vs-native-ecmascript-modules.md similarity index 99% rename from website/docs/imports.md rename to website/docs/commonjs-vs-native-ecmascript-modules.md index 4a0ea5c7..b433438d 100644 --- a/website/docs/imports.md +++ b/website/docs/commonjs-vs-native-ecmascript-modules.md @@ -1,5 +1,6 @@ --- title: "CommonJS vs native ECMAScript modules" +slug: imports --- TypeScript is almost always written using modern `import` syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native `import` syntax, using node's native ESM support. Configuration is different for each. diff --git a/website/docs/how-it-works.md b/website/docs/how-it-works.md index f75f32c3..d9f2f700 100644 --- a/website/docs/how-it-works.md +++ b/website/docs/how-it-works.md @@ -1,31 +1,9 @@ --- -title: How It Works +title: How it works --- ts-node works by registering hooks for `.ts`, `.tsx`, `.js`, and/or `.jsx` extensions. Vanilla `node` loads `.js` by reading code from disk and executing it. Our hook runs in the middle, transforming code from TypeScript to JavaScript and passing the result to `node` for execution. This transformation will respect your `tsconfig.json` as if you had compiled via `tsc`. -`.js` and `.jsx` are only transformed when [`allowJs`](https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) is enabled. - -`.tsx` and `.jsx` are only transformed when [`jsx`](https://www.typescriptlang.org/docs/handbook/jsx.html) is enabled. - -> **Warning:** if a file is ignored or its file extension is not registered, node will either fail to resolve the file or will attempt to execute it as JavaScript without any transformation. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. - -> **Warning:** When ts-node is used with `allowJs`, all non-ignored JavaScript files are transformed using the TypeScript compiler. - -## Skipping `node_modules` - -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). +We also register a few other hooks to apply sourcemaps to stack traces and remap from `.js` imports to `.ts`. diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index 05222ce2..c8701a93 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -2,18 +2,22 @@ title: Module type overrides --- -When deciding between CommonJS and native ECMAScript modules, ts-node defaults to matching vanilla `node` and `tsc` -behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` option and executed -according to node's rules for the `package.json` `"type"` field. - -In some projects you may need to override this behavior for some files. For example, in a webpack -project, you may have `package.json` configured with `"type": "module"` and `tsconfig.json` with -`"module": "esnext"`. However, webpack uses our CommonJS hook to execute your `webpack.config.ts`, -so you need to force your webpack config and any supporting scripts to execute as CommonJS. - -In these situations, our `moduleTypes` option lets you override certain files, forcing execution as -CommonJS or ESM. Node supports similar overriding via `.cjs` and `.mjs` file extensions, but `.ts` files cannot use them. -`moduleTypes` achieves the same effect, and *also* overrides your `tsconfig.json` `"module"` config appropriately. +> Wherever possible, it is recommended to use TypeScript's [`NodeNext` or `Node16` mode](https://www.typescriptlang.org/docs/handbook/esm-node.html) instead of the options described +in this section. Setting `"module": "NodeNext"` and using the `.cts` file extension should work well for most projects. + +When deciding how a file should be compiled and executed -- as either CommonJS or native ECMAScript module -- ts-node matches +`node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` +option and executed according to node's rules for the `package.json` `"type"` field. Set `"module": "NodeNext"` and everything should work. + +In rare cases, you may need to override this behavior for some files. For example, some tools read a `name-of-tool.config.ts` +and require that file to execute as CommonJS. If you have `package.json` configured with `"type": "module"` and `tsconfig.json` with +`"module": "esnext"`, the config is native ECMAScript by default and will raise an error. You will need to force the config and +any supporting scripts to execute as CommonJS. + +In these situations, our `moduleTypes` option can override certain files to be +CommonJS or ESM. Similar overriding is possible by using `.mts`, `.cts`, `.cjs` and `.mjs` file extensions. +`moduleTypes` achieves the same effect for `.ts` and `.js` files, and *also* overrides your `tsconfig.json` `"module"` +config appropriately. The following example tells ts-node to execute a webpack config as CommonJS: diff --git a/website/docs/options.md b/website/docs/options.md index 96e49c1e..2ddf09fb 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -2,64 +2,405 @@ 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`. +Most options can be declared in your tsconfig.json: [Configuration via tsconfig.json](./configuration.md#via-tsconfigjson-recommended) + +`ts-node` supports `--print` (`-p`), `--eval` (`-e`), `--require` (`-r`) and `--interactive` (`-i`) similar to the [node.js CLI](https://nodejs.org/api/cli.html). + +`ts-node` supports `--project` and `--showConfig` similar to the [tsc CLI](https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options). + _Environment variables, where available, are in `ALL_CAPS`_ -## Shell +## CLI Options + +### help + +```shell +ts-node --help +``` + +Prints the help text + +### version + +```shell +ts-node -v +ts-node -vvv +``` + +Prints the version. `-vv` includes node and typescript compiler versions. `-vvv` includes absolute paths to ts-node and +typescript installations. + +### eval + +```shell +ts-node -e +# Example +ts-node -e 'console.log("Hello world!")' +``` + +Evaluate code + +### print + +```shell +ts-node -p -e +# Example +ts-node -p -e '"Hello world!"' +``` + +Print result of `--eval` + +### interactive + +```shell +ts-node -i +``` + +Opens the REPL even if stdin does not appear to be a terminal + +### esm + +```shell +ts-node --esm +ts-node-esm +``` + +Bootstrap with the ESM loader, enabling full ESM support + + +## TSConfig Options + +### project + +```shell +ts-node -P +ts-node --project +``` + +Path to tsconfig file. + +*Note the uppercase `-P`. This is different from `tsc`'s `-p/--project` option.* + +*Environment:* `TS_NODE_PROJECT` + +### skipProject + +```shell +ts-node --skipProject +``` + +Skip project config resolution and loading + +*Default:* `false`
+*Environment:* `TS_NODE_SKIP_PROJECT` + +### cwdMode -- `-h, --help` Prints the help text -- `-v, --version` Prints the version. `-vv` prints node and typescript compiler versions, too -- `-e, --eval` Evaluate code -- `-p, --print` Print result of `--eval` -- `-i, --interactive` Opens the REPL even if stdin does not appear to be a terminal -- `--esm` Bootstrap with the ESM loader, enabling full ESM support +```shell +ts-node -c +ts-node --cwdMode +ts-node-cwd +``` -## TSConfig +Resolve config relative to the current directory instead of the directory of the entrypoint script -- `-P, --project [path]` Path to TypeScript JSON project file
*Environment:* `TS_NODE_PROJECT` -- `--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 +### compilerOptions + +```shell +ts-node -O +ts-node --compilerOptions +``` + +JSON object to merge with compiler options + +*Environment:* `TS_NODE_COMPILER_OPTIONS` + +### showConfig + +```shell +ts-node --showConfig +``` + +Print resolved `tsconfig.json`, including `ts-node` options, and exit ## Typechecking -- `-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, --ignoreDiagnostics [code]` Ignore TypeScript warnings by diagnostic code
*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` +### transpileOnly + +```shell +ts-node -T +ts-node --transpileOnly +``` + +Use TypeScript's faster `transpileModule` + +*Default:* `false`
+*Environment:* `TS_NODE_TRANSPILE_ONLY` + +### typeCheck + +```shell +ts-node --typeCheck +``` + +Opposite of `--transpileOnly` + +*Default:* `true`
+*Environment:* `TS_NODE_TYPE_CHECK` + +### compilerHost + +```shell +ts-node -H +ts-node --compilerHost +``` + +Use TypeScript's compiler host API + +*Default:* `false`
+*Environment:* `TS_NODE_COMPILER_HOST` + +### files + +```shell +ts-node --files +``` + +Load `files`, `include` and `exclude` from `tsconfig.json` on startup. This may +avoid certain typechecking failures. See [Missing types](./troubleshooting.md#missing-types) for details. + +*Default:* `false`
+*Environment:* `TS_NODE_FILES` + +### ignoreDiagnostics + +```shell +ts-node -D +ts-node --ignoreDiagnostics +``` + +Ignore TypeScript warnings by diagnostic code + +*Environment:* `TS_NODE_IGNORE_DIAGNOSTICS` + + +## Transpilation Options + +### ignore + +```shell +ts-node -I +ts-node --ignore +``` + +Override the path patterns to skip compilation + +*Default:* `/node_modules/`
+*Environment:* `TS_NODE_IGNORE` + +### skipIgnore + +```shell +ts-node --skipIgnore +``` + +Skip ignore checks + +*Default:* `false`
+*Environment:* `TS_NODE_SKIP_IGNORE` + +### compiler + +```shell +ts-node -C +ts-node --compiler +``` + +Specify a custom TypeScript compiler + +*Default:* `typescript`
+*Environment:* `TS_NODE_COMPILER` + +### swc + +```shell +ts-node --swc +``` + +Transpile with [swc](./swc.md). Implies `--transpileOnly` + +*Default:* `false` + +### transpiler + +```shell +ts-node --transpiler +# Example +ts-node --transpiler ts-node/transpilers/swc +``` + +Use a third-party, non-typechecking transpiler + +### preferTsExts + +```shell +ts-node --preferTsExts +``` + +Re-order file extensions so that TypeScript imports are preferred + +*Default:* `false`
+*Environment:* `TS_NODE_PREFER_TS_EXTS` + + +## Diagnostic Options + +### logError + +```shell +ts-node --logError +``` + +Logs TypeScript errors to stderr instead of throwing exceptions + +*Default:* `false`
+*Environment:* `TS_NODE_LOG_ERROR` + +### pretty + +```shell +ts-node --pretty +``` + +Use pretty diagnostic formatter + +*Default:* `false`
+*Environment:* `TS_NODE_PRETTY` + +### TS_NODE_DEBUG + +```shell +TS_NODE_DEBUG=true ts-node +``` + +Enable debug logging + +## Advanced Options + +### require + +```shell +ts-node -r +ts-node --require +``` + +Require a node module before execution + +### cwd + +```shell +ts-node --cwd +``` + +Behave as if invoked in this working directory + +*Default:* `process.cwd()`
+*Environment:* `TS_NODE_CWD` + +### emit + +```shell +ts-node --emit +``` + +Emit output files into `.ts-node` directory. Requires `--compilerHost` + +*Default:* `false`
+*Environment:* `TS_NODE_EMIT` + +### scope + +```shell +ts-node --scope +``` + +Scope compiler to files within `scopeDir`. Anything outside this directory is ignored. + +*Default: `false`
+*Environment:* `TS_NODE_SCOPE` + +### scopeDir + +```shell +ts-node --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` + +### 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 + +```shell +TS_NODE_HISTORY= ts-node +``` + +Path to history file for REPL + +*Default:* `~/.ts_node_repl_history` + +### noExperimentalReplAwait + +```shell +ts-node --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 + +### experimentalResolver + +Enable experimental hooks that re-map imports and require calls to support: + +* resolves `.js` to `.ts`, so that `import "./foo.js"` will execute `foo.ts` +* resolves `.cjs` to `.cts` +* resolves `.mjs` to `.mts` +* allows including file extensions in CommonJS, for consistency with ESM where this is often mandatory + +In the future, this hook will also support: + +* `baseUrl`, `paths` +* `rootDirs` +* `outDir` to `rootDir` mappings for composite projects and monorepos -## Transpilation +For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514). -- `-I, --ignore [pattern]` Override the path patterns to skip compilation
*Default:* `/node_modules/`
*Environment:* `TS_NODE_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 -- `--preferTsExts` Re-order file extensions so that TypeScript imports are preferred
*Default:* `false`
*Environment:* `TS_NODE_PREFER_TS_EXTS` +*Default:* `false`, but will likely be enabled by default in a future version
+*Can only be specified via `tsconfig.json` or API.* -## Diagnostics +### experimentalSpecifierResolution -- `--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
+```shell +ts-node --experimentalSpecifierResolution node +``` -## Advanced +Like node's [`--experimental-specifier-resolution`](https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm), but can also be set in your `tsconfig.json` for convenience. +Requires `esm` to be enabled. -- `-r, --require [path]` Require a node module before execution -- `--cwd` Behave as if invoked in this working directory
*Default:* `process.cwd()`
*Environment:* `TS_NODE_CWD` -- `--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` -- `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`
-- `--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.* +*Default:* `explicit`
-## API +## API Options The API includes [additional options](https://typestrong.org/ts-node/api/interfaces/RegisterOptions.html) not shown here. diff --git a/website/docs/performance.md b/website/docs/performance.md index 69b2c6cb..aebc50de 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -1,16 +1,16 @@ --- -title: Make it fast +title: Performance --- These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck once before your tests run or as a lint step. In these cases, ts-node can skip typechecking. +It is often better to use `tsc --noEmit` to typecheck as part of your tests or linting. In these cases, ts-node can skip typechecking. -* Enable [`transpileOnly`](./options.md) to skip typechecking -* Use our [`swc` integration](./transpilers.md#swc) +* Enable [swc](./transpilers.md#swc) * This is by far the fastest option +* Enable [`transpileOnly`](./options.md) to skip typechecking without swc ## With typechecking diff --git a/website/docs/recipes/npx-and-yarn-dlx.md b/website/docs/recipes/npx-and-yarn-dlx.md new file mode 100644 index 00000000..effff389 --- /dev/null +++ b/website/docs/recipes/npx-and-yarn-dlx.md @@ -0,0 +1,53 @@ +--- +title: npx and yarn dlx +--- + +Using [`npx`](https://docs.npmjs.com/cli/v8/commands/npx) or [`yarn dlx`](https://yarnpkg.com/cli/dlx) is a great ways to publish reusable TypeScript tools to GitHub without precompiling or packaging. + +Check out our working example: [TypeStrong/ts-node-npx-example](https://github.com/TypeStrong/ts-node-npx-example) + +```shell +npx typestrong/ts-node-npx-example --help +npx typestrong/ts-node-npx-example --first Arthur --last Dent +``` + +TODO publish demo and link to it +TODO test demo: + - uninstall global ts-node + - try running demo + - does ts-node need to be installed globally? + +This boilerplate is a good starting point: + +```json title="package.json" +{ + "bin": "./cli.ts", + "dependencies": { + "ts-node": "latest", + "@swc/core": "latest", + "@swc/helpers": "latest", + "@tsconfig/node16": "latest" + } +} +``` + +```json title="tsconfig.json" +{ + "extends": "@tsconfig/node16/tsconfig.json", + "ts-node": { + "swc": true + } +} +``` + +```typescript twoslash title="cli.ts" +#!/usr/bin/env ts-node + +console.log("Hello world!") +``` + +If you require native ESM support, use `ts-node-esm` in your shebang and follow the configuration instructions for ESM: [Native ECMAScript modules](../commonjs-vs-native-ecmascript-modules.md#native-ecmascript-modules) + +```typescript twoslash title="cli.ts" +#!/usr/bin/env ts-node-esm +``` diff --git a/website/docs/recipes/watching-and-restarting.md b/website/docs/recipes/watching-and-restarting.md index 82f0d909..85504b3e 100644 --- a/website/docs/recipes/watching-and-restarting.md +++ b/website/docs/recipes/watching-and-restarting.md @@ -1,7 +1,9 @@ --- -title: Watching and Restarting +title: Watching and restarting --- -**TypeScript Node** compiles source code via `require()`, watching files and code reloads are out of scope for the project. If you want to restart the `ts-node` process on file change, existing node.js tools such as [nodemon](https://github.com/remy/nodemon), [onchange](https://github.com/Qard/onchange) and [node-dev](https://github.com/fgnass/node-dev) work. +ts-node focuses on adding first-class TypeScript support to node. Watching files and code reloads are out of scope for the project. -There's also [`ts-node-dev`](https://github.com/whitecolor/ts-node-dev), a modified version of [`node-dev`](https://github.com/fgnass/node-dev) using `ts-node` for compilation that will restart the process on file change. +If you want to restart the `ts-node` process on file change, existing node.js tools such as [nodemon](https://github.com/remy/nodemon), [onchange](https://github.com/Qard/onchange) and [node-dev](https://github.com/fgnass/node-dev) work. + +There's also [`ts-node-dev`](https://github.com/whitecolor/ts-node-dev), a modified version of [`node-dev`](https://github.com/fgnass/node-dev) using `ts-node` for compilation that will restart the process on file change. Note that `ts-node-dev` is incompatible with our native ESM loader. diff --git a/website/docs/scope.md b/website/docs/scope.md new file mode 100644 index 00000000..4b267821 --- /dev/null +++ b/website/docs/scope.md @@ -0,0 +1,47 @@ +--- +title: Ignored files +--- + +ts-node transforms certain files and ignores others. We refer to this mechanism as "scoping." There are various +options to configure scoping, so that ts-node transforms only the files in your project. + +> **Warning:** +> +> An ignored file can still be executed by node.js. Ignoring a file means we do not transform it from TypeScript into JavaScript, but it does not prevent execution. +> +> If a file requires transformation but is ignored, node may either fail to resolve it or attempt to execute it as vanilla JavaScript. This may cause syntax errors or other failures, because node does not understand TypeScript type syntax nor bleeding-edge ECMAScript features. + +## File extensions + +`.js` and `.jsx` are only transformed when [`allowJs`](https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) is enabled. + +`.tsx` and `.jsx` are only transformed when [`jsx`](https://www.typescriptlang.org/docs/handbook/jsx.html) is enabled. + +> **Warning:** +> +> When ts-node is used with `allowJs`, _all_ non-ignored JavaScript files are transformed by ts-node. + +## Skipping `node_modules` + +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#skipignore) or [`TS_NODE_SKIP_IGNORE`](./options#skipignore) 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#prefertsexts). + +## Scope by directory + +Our [`scope`](./options.md#scope) and [`scopeDir`](./options.md#scopedir) options will limit transformation to files +within a directory. + +## Ignore by regexp + +Our [`ignore`](./options.md#ignore) option will ignore files matching one or more regular expressions. diff --git a/website/docs/swc.md b/website/docs/swc.md new file mode 100644 index 00000000..e64646b1 --- /dev/null +++ b/website/docs/swc.md @@ -0,0 +1,25 @@ +--- +title: SWC +--- + +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 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 `async`/`await` or generator functions, also install `regenerator-runtime`. + +```shell +npm i -D @swc/core @swc/helpers regenerator-runtime +``` + +Then add the following to your `tsconfig.json`. + +```json title="tsconfig.json" +{ + "ts-node": { + "swc": true + } +} +``` + +> SWC uses `@swc/helpers` instead of `tslib`. If you have enabled `importHelpers`, you must also install `@swc/helpers`. diff --git a/website/docs/transpilers.md b/website/docs/transpilers.md index 6d6f3709..2274bf55 100644 --- a/website/docs/transpilers.md +++ b/website/docs/transpilers.md @@ -2,45 +2,20 @@ title: Transpilers --- -In transpile-only mode, we skip typechecking to speed up execution time. You can go a step further and use a -third-party transpiler to transform TypeScript into JavaScript even faster. You will still benefit from -ts-node's automatic `tsconfig.json` discovery, sourcemap support, and global ts-node CLI. Integrations -can automatically derive an appropriate configuration from your existing `tsconfig.json` which simplifies project -boilerplate. +ts-node supports third-party transpilers as plugins. Transpilers such as swc can transform TypeScript into JavaScript +much faster than the TypeScript compiler. You will still benefit from ts-node's automatic `tsconfig.json` discovery, +sourcemap support, and global ts-node CLI. Plugins automatically derive an appropriate configuration from your existing +`tsconfig.json` which simplifies project boilerplate. > **What is the difference between a compiler and a transpiler?** > > For our purposes, a compiler implements TypeScript's API and can perform typechecking. > A third-party transpiler does not. Both transform TypeScript into JavaScript. -## swc +## Third-party plugins -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 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`. - -```shell -npm i -D @swc/core @swc/helpers regenerator-runtime -``` - -Then add the following to your `tsconfig.json`. - -```json title="tsconfig.json" -{ - "ts-node": { - "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`. +The `transpiler` option allows using third-party transpiler plugins with ts-node. `transpiler` must be given the +name of a module which can be `require()`d. The built-in `swc` plugin 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` @@ -55,10 +30,14 @@ Then add the following to your tsconfig: } ``` -## Writing your own integration +## Write your own plugin + +To write your own transpiler plugin, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). -To write your own transpiler integration, check our [API docs](https://typestrong.org/ts-node/api/interfaces/TranspilerModule.html). +Plugins are `require()`d by ts-node, so they can be a local script or a node module published to npm. 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 one or more transpiler instances. The instances are used to transform +TypeScript into JavaScript. -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. +For a working example, check out out our bundled swc plugin: https://github.com/TypeStrong/ts-node/blob/main/src/transpilers/swc.ts diff --git a/website/docs/troubleshooting.md b/website/docs/troubleshooting.md index d007bcac..0db2c96a 100644 --- a/website/docs/troubleshooting.md +++ b/website/docs/troubleshooting.md @@ -2,7 +2,7 @@ title: Troubleshooting --- -## Understanding configuration +## 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 --showConfig`. This is similar to @@ -52,7 +52,7 @@ $ ts-node --showConfig } ``` -## Understanding Errors +## Common errors It is important to differentiate between errors from ts-node, errors from the TypeScript compiler, and errors from `node`. It is also important to understand when errors are caused by a type error in your code, a bug in your code, or a flaw in your configuration. @@ -85,3 +85,98 @@ const a = foo?.bar; ``` When you try to run this code, node 12 will throw a `SyntaxError`. To fix this, you must switch to `"target": "es2019"` or lower so TypeScript transforms `?.` into something `node` can understand. + +### `ERR_REQUIRE_ESM` + +This error is thrown by node when a module is `require()`d, but node believes it should execute as native ESM. This can happen for a few reasons: + +- You have installed an ESM dependency but your own code compiles to CommonJS. + - Solution: configure your project to compile and execute as native ESM. [Docs](./commonjs-vs-native-ecmascript-modules.md#native-ecmascript-modules) + - Solution: downgrade the dependency to an older, CommonJS version. +- You have moved your project to ESM but still have a config file, such as `webpack.config.ts`, which must be executed as CommonJS + - Solution: if supported by the relevant tool, rename your config file to `.cts` + - Solution: Configure a module type override. [Docs](./module-type-overrides.md) +- You have a mix of CommonJS and native ESM in your project + - Solution: double-check all package.json "type" and tsconfig.json "module" configuration [Docs](./commonjs-vs-native-ecmascript-modules.md) + - Solution: consider simplifying by making your project entirely CommonJS or entirely native ESM + +### `ERR_UNKNOWN_FILE_EXTENSION` + +This error is thrown by node when a module has an unrecognized file extension, or no extension at all, and is being executed as native ESM. This can happen for a few reasons: + +- You are using a tool which has an extensionless binary, such as `mocha`. + - CommonJS supports extensionless files but native ESM does not. + - Solution: upgrade to ts-node >=[v10.6.0](https://github.com/TypeStrong/ts-node/releases/tag/v10.6.0), which implements a workaround. +- Our ESM loader is not installed. + - Solution: Use `ts-node-esm`, `ts-node --esm`, or add `"ts-node": {"esm": true}` to your tsconfig.json. [Docs](./commonjs-vs-native-ecmascript-modules.md#native-ecmascript-modules) +- You have moved your project to ESM but still have a config file, such as `webpack.config.ts`, which must be executed as CommonJS + - Solution: if supported by the relevant tool, rename your config file to `.cts` + - Solution: Configure a module type override. [Docs](./module-type-overrides.md) + +## Missing Types + +ts-node does _not_ eagerly load `files`, `include` or `exclude` by default. This is because a large majority of projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. + +Occasionally, this optimization leads to missing types. Fortunately, there are other ways to include them in typechecking. + +For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). + +Example `tsconfig.json`: + +```json +{ + "compilerOptions": { + "typeRoots" : ["./node_modules/@types", "./typings"] + } +} +``` + +Example project structure: + +```text +/ +-- tsconfig.json +-- typings/ + -- / + -- index.d.ts +``` + +Example module declaration file: + +```typescript twoslash +declare module '' { + // module definitions go here +} +``` + +For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): + +```json title="tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "custom-module-type": ["types/custom-module-type"] + } + } +} +``` + +Another option is [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your `compilerOptions` or structure your type definitions for `typeRoots`. Below is an example of a triple-slash directive as a relative path within your project: + +```typescript twoslash +/// +import {Greeter} from "lib_greeter" +const g = new Greeter(); +g.sayHello(); +``` + +If none of the above work, and you _must_ use `files`, `include`, or `exclude`, enable our [`files`](./options.md#files) option. + +## npx, yarn dlx, and node_modules + +When executing TypeScript with `npx` or `yarn dlx`, the code resides within a temporary `node_modules` directory. + +The contents of `node_modules` are ignored by default. If execution fails, enable [`skipIgnore`](./options.md#skipignore). + + diff --git a/website/docs/types.md b/website/docs/types.md index cb1df0be..e69de29b 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -1,59 +0,0 @@ ---- -title: "Help! My Types Are Missing!" ---- - -ts-node does _not_ use `files`, `include` or `exclude`, by default. This is because a large majority projects do not use all of the files in a project directory (e.g. `Gulpfile.ts`, runtime vs tests) and parsing every file for types slows startup time. Instead, ts-node starts with the script file (e.g. `ts-node index.ts`) and TypeScript resolves dependencies based on imports and references. - -For global definitions, you can use the `typeRoots` compiler option. This requires that your type definitions be structured as type packages (not loose TypeScript definition files). More details on how this works can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types). - -Example `tsconfig.json`: - -```json -{ - "compilerOptions": { - "typeRoots" : ["./node_modules/@types", "./typings"] - } -} -``` - -Example project structure: - -```text -/ --- tsconfig.json --- typings/ - -- / - -- index.d.ts -``` - -Example module declaration file: - -```typescript twoslash -declare module '' { - // module definitions go here -} -``` - -For module definitions, you can use [`paths`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping): - -```json title="tsconfig.json" -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "custom-module-type": ["types/custom-module-type"] - } - } -} -``` - -An alternative approach for definitions of third-party libraries are [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). This may be helpful if you prefer not to change your TypeScript `compilerOptions` or structure your custom type definitions when using `typeRoots`. Below is an example of the triple-slash directive as a relative path within your project: - -```typescript twoslash -/// -import {Greeter} from "untyped_js_lib" -const g = new Greeter(); -g.sayHello(); -``` - -**Tip:** If you _must_ use `files`, `include`, or `exclude`, enable `--files` flags or set `TS_NODE_FILES=true`. diff --git a/website/docs/usage.md b/website/docs/usage.md index 9b62841f..084c8b39 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -2,7 +2,7 @@ title: Usage --- -## Shell +## Command Line ```shell # Execute a script as `node` + `tsc`. @@ -32,13 +32,17 @@ ts-node-esm script.ts ## Shebang +To write scripts with maximum portability, [specify options in your `tsconfig.json`](./configuration#via-tsconfigjson-recommended) and omit them from the shebang. + ```typescript twoslash #!/usr/bin/env ts-node +// ts-node options are read from tsconfig.json + console.log("Hello, world!") ``` -Passing options via shebang requires the [`env -S` flag](https://manpages.debian.org/bullseye/coreutils/env.1.en.html#S), which is available on recent versions of `env`. ([compatibility](https://github.com/TypeStrong/ts-node/pull/1448#issuecomment-913895766)) +Including options within the 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 twoslash #!/usr/bin/env -S ts-node --files @@ -46,26 +50,39 @@ Passing options via shebang requires the [`env -S` flag](https://manpages.debian // Technically, Mac allows omitting `-S`, but Linux requires it ``` -To write scripts with maximum portability, [specify all options in your `tsconfig.json`](./configuration#via-tsconfigjson-recommended) and omit them from the shebang. +To test your version of `env` for compatibility with `-S`: -```typescript twoslash -#!/usr/bin/env ts-node -// This shebang works everywhere +```shell +# Note that these unusual quotes are necessary +/usr/bin/env --debug '-S echo foo bar' ``` -To test your version of `env` for compatibility: +## node flags and other tools + +You can register ts-node without using our CLI: `node -r ts-node/register` and `node --loader ts-node/esm` + +In many cases, setting [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#cli_node_options_options) will enable `ts-node` within other node tools, child processes, and worker threads. This can be combined with other node flags. ```shell -# Note that these unusual quotes are necessary -/usr/bin/env --debug '-S echo foo bar' +NODE_OPTIONS="-r ts-node/register --no-warnings" node ./index.ts ``` -## Programmatic +Or, if you require native ESM support: + +```shell +NODE_OPTIONS="--loader ts-node/esm" +``` -You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. You can also use file shortcuts - `node -r ts-node/register` or `node -r ts-node/register/transpile-only` - depending on your preferences. +This tells any node processes which receive this environment variable to install `ts-node`'s hooks before executing other code. -**Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of ts-node's CLI. +If you are invoking node directly, you can avoid the environment variable and pass those flags to node. + +```shell +node --loader ts-node/esm --inspect ./index.ts +``` + +## Programmatic -### Developers +You can require ts-node and register the loader for future requires by using `require('ts-node').register({ /* options */ })`. -ts-node exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. +Check out our [API](./api.md) for more features. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 8c33ffcb..7b909fb4 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -50,7 +50,7 @@ module.exports = { position: 'right', }, { - href: 'https://discord.gg/typescript', + href: 'https://discord.gg/3rBctmf3dP', label: 'Chat', position: 'right', }, diff --git a/website/scripts/build-readme.mjs b/website/scripts/build-readme.mjs index 50842776..2919795d 100755 --- a/website/scripts/build-readme.mjs +++ b/website/scripts/build-readme.mjs @@ -100,6 +100,9 @@ async function main() { contents: '', }) ); + + fs.writeFileSync(readmePath, renderedReadme.contents); + console.error(vfileReporter(renderedReadme)); if (renderedReadme.messages.length) throw new Error('Aborting on diagnostics.'); @@ -109,8 +112,6 @@ async function main() { .process(renderedReadme); console.error(vfileReporter(lintResults)); if (lintResults.messages.length) throw new Error('Aborting on diagnostics.'); - - fs.writeFileSync(readmePath, renderedReadme.contents); } function parseFrontmatter() { @@ -134,7 +135,10 @@ function rewritePageLinksToAnchorLinks() { visit(ast, 'link', (node) => { if (node.url?.match?.(/^https?\:\/\//)) return; // TODO take page title into account - node.url = node.url.replace(/^[\.\/]*(?:([^#]+)|.*#(.*))$/, '#$1$2'); + node.url = node.url.replace( + /^[\.\/]*(?:recipes\/)?(?:([^#]+)|.*#(.*))$/, + '#$1$2' + ); node.url = node.url.replace(/\.md$/, ''); }); }; diff --git a/website/sidebars.js b/website/sidebars.js index 4e3dbd9f..6e5de11e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -10,7 +10,8 @@ module.exports = { 'usage', 'configuration', 'options', - 'imports', + 'swc', + 'commonjs-vs-native-ecmascript-modules', 'troubleshooting', 'performance', ], @@ -21,11 +22,12 @@ module.exports = { collapsed: false, items: [ 'how-it-works', + 'scope', 'paths', - 'types', 'compilers', 'transpilers', 'module-type-overrides', + 'api', ], }, { @@ -34,6 +36,7 @@ module.exports = { collapsed: false, items: [ 'recipes/watching-and-restarting', + // 'recipes/npx-and-yarn-dlx', 'recipes/ava', 'recipes/gulp', 'recipes/intellij', diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 49c27214..743c1fe8 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -7,7 +7,7 @@ /* You can override the default Infima variables here. */ :root { - /* Generating by pasting #3178C6, the shade of blue from typescriptlang.org's navbar, into the palette generator at https://v2.docusaurus.io/docs/styling-layout/ */ + /* Generating by pasting #3178C6, the shade of blue from typescriptlang.org's navbar, into the palette generator at https://docusaurus.io/docs/styling-layout/ */ --ifm-color-primary: #3178c6; --ifm-color-primary-dark: #2c6cb2; --ifm-color-primary-darker: #2a66a8; @@ -27,6 +27,18 @@ --ifm-code-font-size: 95%; */ } +[data-theme='dark'] { + /* Generating by pasting #4084d0, into the palette generator at https://docusaurus.io/docs/styling-layout/ */ + --ifm-color-primary: #4084d0; + --ifm-color-primary-dark: #3076c4; + --ifm-color-primary-darker: #2e70ba; + --ifm-color-primary-darkest: #265c99; + --ifm-color-primary-light: #5692d5; + --ifm-color-primary-lighter: #6199d8; + --ifm-color-primary-lightest: #81aee0; + --ifm-color-primary-lighter: #4a8bd2; +} + .docusaurus-highlight-code-line { background-color: rgb(72, 77, 91); display: block; diff --git a/website/types/untyped_js_lib.d.ts b/website/types/lib_greeter.ts similarity index 83% rename from website/types/untyped_js_lib.d.ts rename to website/types/lib_greeter.ts index d6c8f55c..39d28b1c 100644 --- a/website/types/untyped_js_lib.d.ts +++ b/website/types/lib_greeter.ts @@ -1,4 +1,4 @@ -declare module 'untyped_js_lib' { +declare module 'lib_greeter' { /** API for saying hello to everyone. */ export class Greeter { /** Returns a friendly salutation and a remark about the weather */