module: add --experimental-strip-types

PR-URL: https://github.com/nodejs/node/pull/53725
Refs: https://github.com/nodejs/loaders/issues/217
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruy Adorno <ruy@vlt.sh>
This commit is contained in:
Marco Ippolito 2024-07-24 18:30:06 +02:00 committed by GitHub
parent c79a6741e0
commit 35f92d953c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 2190 additions and 25 deletions

View file

@ -16,6 +16,7 @@ on:
- acorn
- acorn-walk
- ada
- amaro
- brotli
- c-ares
- cjs-module-lexer
@ -82,6 +83,14 @@ jobs:
cat temp-output
tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true
rm temp-output
- id: amaro
subsystem: deps
label: dependencies
run: |
./tools/dep_updaters/update-amaro.sh > temp-output
cat temp-output
tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true
rm temp-output
- id: brotli
subsystem: deps
label: dependencies

25
LICENSE
View file

@ -130,6 +130,31 @@ The externally maintained libraries used by Node.js are:
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
- amaro, located at deps/amaro, is licensed as follows:
"""
MIT License
Copyright (c) Marco Ippolito and Amaro 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.
"""
- ICU, located at deps/icu-small, is licensed as follows:
"""
UNICODE LICENSE V3

21
deps/amaro/LICENSE.md vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Marco Ippolito and Amaro 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.

30
deps/amaro/README.md vendored Normal file
View file

@ -0,0 +1,30 @@
# Amaro
Amaro is a wrapper around `@swc/wasm-typescript`, a WebAssembly port of the SWC TypeScript parser.
It's currently used as an internal in Node.js for [Type Stripping](https://github.com/nodejs/loaders/issues/208), but in the future it will be possible to be upgraded separately by users.
The main goal of this package is to provide a stable API for TypeScript parser, which is unstable and subject to change.
> Amaro means "bitter" in Italian. It's a reference to [Mount Amaro](https://en.wikipedia.org/wiki/Monte_Amaro_(Abruzzo)) on whose slopes this package was conceived.
## How to Install
To install Amaro, run:
```shell
npm install amaro
```
## How to Use
By default Amaro exports a `transformSync` function that performs type stripping.
Stack traces are preserved, by replacing removed types with white spaces.
```javascript
const amaro = require('amaro');
const { code } = amaro.transformSync("const foo: string = 'bar';");
console.log(code); // "const foo = 'bar';"
```
## License (MIT)
See [`LICENSE.md`](./LICENSE.md).

617
deps/amaro/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

38
deps/amaro/package.json vendored Normal file
View file

@ -0,0 +1,38 @@
{
"name": "amaro",
"version": "0.0.4",
"description": "Node.js TypeScript wrapper",
"license": "MIT",
"type": "commonjs",
"main": "dist/index.js",
"homepage": "https://github.com/nodejs/amaro#readme",
"bugs": {
"url": "https://github.com/nodejs/amaro/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/nodejs/amaro.git"
},
"scripts": {
"clean": "rimraf dist",
"lint": "biome lint --write",
"format": "biome format --write",
"prepack": "npm run build",
"postpack": "npm run clean",
"build": "rspack build",
"typecheck": "tsc --noEmit",
"test": "node --test ./test"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@rspack/cli": "^0.7.5",
"@rspack/core": "^0.7.5",
"@types/node": "^20.14.11",
"rimraf": "^6.0.1",
"typescript": "^5.5.3"
},
"exports": {
"./package.json": "./package.json"
},
"files": ["dist", "LICENSE.md"]
}

View file

@ -868,6 +868,9 @@ export USERNAME="nodejs" # will result in `nodejs` as the value.
<!-- YAML
added: v0.5.2
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/53725
description: Eval now supports experimental type-stripping.
- version: v5.11.0
pr-url: https://github.com/nodejs/node/pull/5348
description: Built-in libraries are now available as predefined variables.
@ -880,6 +883,9 @@ On Windows, using `cmd.exe` a single quote will not work correctly because it
only recognizes double `"` for quoting. In Powershell or Git bash, both `'`
and `"` are usable.
It is possible to run code containing inline types by passing
[`--experimental-strip-types`][].
### `--experimental-default-type=type`
<!-- YAML
@ -1041,6 +1047,17 @@ added: v22.5.0
Enable the experimental [`node:sqlite`][] module.
### `--experimental-strip-types`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
Enable experimental type-stripping for TypeScript files.
For more information, see the [TypeScript type-stripping][] documentation.
### `--experimental-test-coverage`
<!-- YAML
@ -2902,6 +2919,7 @@ one is included in the list below.
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-sqlite`
* `--experimental-strip-types`
* `--experimental-top-level-await`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
@ -3427,6 +3445,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm
[Source Map]: https://sourcemaps.info/spec.html
[TypeScript type-stripping]: typescript.md#type-stripping
[V8 Inspector integration for Node.js]: debugger.md#v8-inspector-integration-for-nodejs
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
[V8 code cache]: https://v8.dev/blog/code-caching-for-devs
@ -3441,6 +3460,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
[`--experimental-default-type=module`]: #--experimental-default-typetype
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
[`--experimental-strip-types`]: #--experimental-strip-types
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule

View file

@ -4032,6 +4032,16 @@ The public key in the certificate SubjectPublicKeyInfo could not be read.
An error occurred trying to allocate memory. This should never happen.
<a id="ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING"></a>
#### `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`
<!-- YAML
added: REPLACEME
-->
Type stripping is not supported for files descendent of a `node_modules` directory.
[ES Module]: esm.md
[ICU]: intl.md#internationalization-support
[JSON Web Key Elliptic Curve Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve

View file

@ -41,6 +41,7 @@
* [Modules: ECMAScript modules](esm.md)
* [Modules: `node:module` API](module.md)
* [Modules: Packages](packages.md)
* [Modules: TypeScript](typescript.md)
* [Net](net.md)
* [OS](os.md)
* [Path](path.md)

153
doc/api/typescript.md Normal file
View file

@ -0,0 +1,153 @@
# Modules: TypeScript
## Enabling
There are two ways to enable runtime TypeScript support in Node.js:
1. For [full support][] of all of TypeScript's syntax and features, including
using any version of TypeScript, use a third-party package.
2. For lightweight support, you can use the built-in support for
[type stripping][].
## Full TypeScript support
To use TypeScript with full support for all TypeScript features, including
`tsconfig.json`, you can use a third-party package. These instructions use
[`tsx`][] as an example but there are many other similar libraries available.
1. Install the package as a development dependency using whatever package
manager you're using for your project. For example, with `npm`:
```bash
npm install --save-dev tsx
```
2. Then you can run your TypeScript code via:
```bash
npx tsx your-file.ts
```
Or alternatively, you can run with `node` via:
```bash
node --import=tsx your-file.ts
```
## Type stripping
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
The flag [`--experimental-strip-types`][] enables Node.js to run TypeScript
files that contain only type annotations. Such files contain no TypeScript
features that require transformation, such as enums or namespaces. Node.js will
replace inline type annotations with whitespace, and no type checking is
performed. TypeScript features that depend on settings within `tsconfig.json`,
such as paths or converting newer JavaScript syntax to older standards, are
intentionally unsupported. To get fuller TypeScript support, including support
for enums and namespaces and paths, see [Full TypeScript support][].
The type stripping feature is designed to be lightweight.
By intentionally not supporting syntaxes that require JavaScript code
generation, and by replacing inline types with whitespace, Node.js can run
TypeScript code without the need for source maps.
### Determining module system
Node.js supports both [CommonJS][] and [ES Modules][] syntax in TypeScript
files. Node.js will not convert from one module system to another; if you want
your code to run as an ES module, you must use `import` and `export` syntax, and
if you want your code to run as CommonJS you must use `require` and
`module.exports`.
* `.ts` files will have their module system determined [the same way as `.js`
files.][] To use `import` and `export` syntax, add `"type": "module"` to the
nearest parent `package.json`.
* `.mts` files will always be run as ES modules, similar to `.mjs` files.
* `.cts` files will always be run as CommonJS modules, similar to `.cjs` files.
* `.tsx` files are unsupported.
As in JavaScript files, [file extensions are mandatory][] in `import` statements
and `import()` expressions: `import './file.ts'`, not `import './file'`. Because
of backward compatibility, file extensions are also mandatory in `require()`
calls: `require('./file.ts')`, not `require('./file')`, similar to how the
`.cjs` extension is mandatory in `require` calls in CommonJS files.
The `tsconfig.json` option `allowImportingTsExtensions` will allow the
TypeScript compiler `tsc` to type-check files with `import` specifiers that
include the `.ts` extension.
### Unsupported TypeScript features
Since Node.js is only removing inline types, any TypeScript features that
involve _replacing_ TypeScript syntax with new JavaScript syntax will error.
This is by design. To run TypeScript with such features, see
[Full TypeScript support][].
The most prominent unsupported features that require transformation are:
* `Enum`
* `experimentalDecorators`
* `namespaces`
* parameter properties
In addition, Node.js does not read `tsconfig.json` files and does not support
features that depend on settings within `tsconfig.json`, such as paths or
converting newer JavaScript syntax into older standards.
### Importing types without `type` keyword
Due to the nature of type stripping, the `type` keyword is necessary to
correctly strip type imports. Without the `type` keyword, Node.js will treat the
import as a value import, which will result in a runtime error. The tsconfig
option [`verbatimModuleSyntax`][] can be used to match this behavior.
This example will work correctly:
```ts
import type { Type1, Type2 } from './module.ts';
import { fn, type FnParams } from './fn.ts';
```
This will result in a runtime error:
```ts
import { Type1, Type2 } from './module.ts';
import { fn, FnParams } from './fn.ts';
```
### Non-file forms of input
Type stripping can be enabled for `--eval` and STDIN input. The module system
will be determined by `--input-type`, as it is for JavaScript.
TypeScript syntax is unsupported in the REPL, `--print`, `--check`, and
`inspect`.
### Source maps
Since inline types are replaced by whitespace, source maps are unnecessary for
correct line numbers in stack traces; and Node.js does not generate them. For
source maps support, see [Full TypeScript support][].
### Type stripping in dependencies
To discourage package authors from publishing packages written in TypeScript,
Node.js will by default refuse to handle TypeScript files inside folders under
a `node_modules` path.
[CommonJS]: modules.md
[ES Modules]: esm.md
[Full TypeScript support]: #full-typescript-support
[`--experimental-strip-types`]: cli.md#--experimental-strip-types
[`tsx`]: https://tsx.is/
[`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax
[file extensions are mandatory]: esm.md#mandatory-file-extensions
[full support]: #full-typescript-support
[the same way as `.js` files.]: packages.md#determining-module-system
[type stripping]: #type-stripping

View file

@ -10,6 +10,7 @@ This a list of all the dependencies:
* [acorn][]
* [ada][]
* [amaro][]
* [base64][]
* [brotli][]
* [c-ares][]
@ -168,6 +169,11 @@ an abstract syntax tree walker for the ESTree format.
The [ada](https://github.com/ada-url/ada) dependency is a
fast and spec-compliant URL parser written in C++.
### amaro
The [amaro](https://www.npmjs.com/package/amaro) dependency is a wrapper around the
WebAssembly version of the SWC JavaScript/TypeScript parser.
### brotli
The [brotli](https://github.com/google/brotli) dependency is
@ -336,6 +342,7 @@ performance improvements not currently available in standard zlib.
[acorn]: #acorn
[ada]: #ada
[amaro]: #amaro
[base64]: #base64
[brotli]: #brotli
[c-ares]: #c-ares

View file

@ -194,6 +194,9 @@ Enable module mocking in the test runner.
.It Fl -experimental-test-snapshots
Enable snapshot testing in the test runner.
.
.It Fl -experimental-strip-types
Enable experimental type-stripping for TypeScript files.
.
.It Fl -experimental-eventsource
Enable experimental support for the EventSource Web API.
.

View file

@ -1834,6 +1834,9 @@ E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => {
msg += `. Received protocol '${url.protocol}'`;
return msg;
}, Error);
E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING',
'Stripping types is currently unsupported for files under node_modules, for "%s"',
Error);
E('ERR_UNSUPPORTED_RESOLVE_REQUEST',
'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.',
TypeError);

View file

@ -14,7 +14,7 @@ const {
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
const { addBuiltinLibsToObject } = require('internal/modules/helpers');
const { addBuiltinLibsToObject, tsParse } = require('internal/modules/helpers');
const { getOptionValue } = require('internal/options');
@ -22,7 +22,11 @@ prepareMainThreadExecution();
addBuiltinLibsToObject(globalThis, '<eval>');
markBootstrapComplete();
const source = getOptionValue('--eval');
const code = getOptionValue('--eval');
const source = getOptionValue('--experimental-strip-types') ?
tsParse(code) :
code;
const print = getOptionValue('--print');
const shouldLoadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
if (getOptionValue('--input-type') === 'module' ||

View file

@ -146,6 +146,7 @@ const { safeGetenv } = internalBinding('credentials');
const {
getCjsConditions,
initializeCjsConditions,
isUnderNodeModules,
loadBuiltinModule,
makeRequireFunction,
setHasStartedUserCJSExecution,
@ -168,6 +169,7 @@ const {
ERR_REQUIRE_CYCLE_MODULE,
ERR_REQUIRE_ESM,
ERR_UNKNOWN_BUILTIN_MODULE,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
},
setArrowMessage,
} = require('internal/errors');
@ -428,9 +430,18 @@ function initializeCJS() {
Module.runMain =
require('internal/modules/run_main').executeUserEntryPoint;
const tsEnabled = getOptionValue('--experimental-strip-types');
if (tsEnabled) {
emitExperimentalWarning('Type Stripping');
Module._extensions['.cts'] = loadCTS;
Module._extensions['.ts'] = loadTS;
}
if (getOptionValue('--experimental-require-module')) {
emitExperimentalWarning('Support for loading ES Module in require()');
Module._extensions['.mjs'] = loadESMFromCJS;
if (tsEnabled) {
Module._extensions['.mts'] = loadESMFromCJS;
}
}
}
@ -639,10 +650,24 @@ function resolveExports(nmPath, request) {
// We don't cache this in case user extends the extensions.
function getDefaultExtensions() {
const extensions = ObjectKeys(Module._extensions);
let extensions = ObjectKeys(Module._extensions);
const tsEnabled = getOptionValue('--experimental-strip-types');
if (tsEnabled) {
extensions = ArrayPrototypeFilter(extensions, (ext) =>
ext !== '.ts' || Module._extensions['.ts'] !== loadTS ||
ext !== '.cts' || Module._extensions['.ts'] !== loadCTS,
);
}
if (!getOptionValue('--experimental-require-module')) {
return extensions;
}
if (tsEnabled) {
extensions = ArrayPrototypeFilter(extensions, (ext) =>
ext !== '.mts' || Module._extensions['.mts'] !== loadESMFromCJS,
);
}
// If the .mjs extension is added by --experimental-require-module,
// remove it from the supported default extensions to maintain
// compatibility.
@ -1279,6 +1304,12 @@ Module.prototype.load = function(filename) {
throw new ERR_REQUIRE_ESM(filename, true);
}
if (getOptionValue('--experimental-strip-types')) {
if (StringPrototypeEndsWith(filename, '.mts') && !Module._extensions['.mts']) {
throw new ERR_REQUIRE_ESM(filename, true);
}
}
Module._extensions[extension](this, filename);
this.loaded = true;
@ -1322,7 +1353,14 @@ let hasPausedEntry = false;
* @param {string} filename Absolute path of the file.
*/
function loadESMFromCJS(mod, filename) {
const source = getMaybeCachedSource(mod, filename);
let source = getMaybeCachedSource(mod, filename);
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const { tsParse } = require('internal/modules/helpers');
source = tsParse(source);
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const isMain = mod[kIsMainSymbol];
if (isMain) {
@ -1529,6 +1567,77 @@ function getMaybeCachedSource(mod, filename) {
return content;
}
function loadCTS(module, filename) {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const source = getMaybeCachedSource(module, filename);
const { tsParse } = require('internal/modules/helpers');
const content = tsParse(source);
module._compile(content, filename, 'commonjs');
}
/**
* Built-in handler for `.ts` files.
* @param {Module} module The module to compile
* @param {string} filename The file path of the module
*/
function loadTS(module, filename) {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
// If already analyzed the source, then it will be cached.
const source = getMaybeCachedSource(module, filename);
const { tsParse } = require('internal/modules/helpers');
const content = tsParse(source);
let format;
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
if (getOptionValue('--experimental-require-module')) {
module._compile(content, filename, 'module');
return;
}
const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const usesEsm = containsModuleSyntax(content, filename);
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 = tsParse(fs.readFileSync(parentPath, 'utf8'));
} catch {
// Continue regardless of error.
}
if (parentSource) {
reconstructErrorStack(err, parentPath, parentSource);
}
}
throw err;
} else if (pkg?.data.type === 'commonjs') {
format = 'commonjs';
}
module._compile(content, filename, format);
};
function reconstructErrorStack(err, parentPath, 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);
}
}
/**
* Built-in handler for `.js` files.
* @param {Module} module The module to compile
@ -1564,17 +1673,7 @@ Module._extensions['.js'] = function(module, filename) {
// Continue regardless of error.
}
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);
}
reconstructErrorStack(err, parentPath, parentSource);
}
}
throw err;

View file

@ -23,6 +23,12 @@ if (experimentalWasmModules) {
extensionFormatMap['.wasm'] = 'wasm';
}
if (getOptionValue('--experimental-strip-types')) {
extensionFormatMap['.ts'] = 'module-typescript';
extensionFormatMap['.mts'] = 'module-typescript';
extensionFormatMap['.cts'] = 'commonjs-typescript';
}
/**
* @param {string} mime
* @returns {string | null}

View file

@ -95,6 +95,18 @@ function underNodeModules(url) {
}
let typelessPackageJsonFilesWarnedAbout;
function warnTypelessPackageJsonFile(pjsonPath, url) {
typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
const warning = `${url} parsed as an ES module because module syntax was detected;` +
` to avoid the performance penalty of syntax detection, add "type": "module" to ${pjsonPath}`;
process.emitWarning(warning, {
code: 'MODULE_TYPELESS_PACKAGE_JSON',
});
typelessPackageJsonFilesWarnedAbout.add(pjsonPath);
}
}
/**
* @param {URL} url
* @param {{parentURL: string; source?: Buffer}} context
@ -130,15 +142,38 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
if (format === 'module') {
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
const warning = `${url} parsed as an ES module because module syntax was detected;` +
` to avoid the performance penalty of syntax detection, add "type": "module" to ${pjsonPath}`;
process.emitWarning(warning, {
code: 'MODULE_TYPELESS_PACKAGE_JSON',
});
typelessPackageJsonFilesWarnedAbout.add(pjsonPath);
}
warnTypelessPackageJsonFile(pjsonPath, url);
}
return format;
}
}
}
if (ext === '.ts' && getOptionValue('--experimental-strip-types')) {
const { type: packageType, pjsonPath } = getPackageScopeConfig(url);
if (packageType !== 'none') {
return `${packageType}-typescript`;
}
// The controlling `package.json` file has no `type` field.
switch (getOptionValue('--experimental-default-type')) {
case 'module': { // The user explicitly passed `--experimental-default-type=module`.
// An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules`
// should retain the assumption that a lack of a `type` field means CommonJS.
return underNodeModules(url) ? 'commonjs-typescript' : 'module-typescript';
}
case 'commonjs': { // The user explicitly passed `--experimental-default-type=commonjs`.
return 'commonjs-typescript';
}
default: { // The user did not pass `--experimental-default-type`.
// `source` is undefined when this is called from `defaultResolve`;
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
const { tsParse } = require('internal/modules/helpers');
const parsedSource = tsParse(source);
const detectedFormat = detectModuleFormat(parsedSource, url);
const format = detectedFormat ? `${detectedFormat}-typescript` : 'commonjs-typescript';
if (format === 'module-typescript') {
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
warnTypelessPackageJsonFile(pjsonPath, url);
}
return format;
}

View file

@ -18,12 +18,16 @@ const defaultType =
getOptionValue('--experimental-default-type');
const { Buffer: { from: BufferFrom } } = require('buffer');
const {
isUnderNodeModules,
} = require('internal/modules/helpers');
const { URL } = require('internal/url');
const {
ERR_INVALID_URL,
ERR_UNKNOWN_MODULE_FORMAT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;
const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
@ -147,6 +151,12 @@ async function defaultLoad(url, context = kEmptyObject) {
format = 'commonjs-sync';
}
if (getOptionValue('--experimental-strip-types') &&
(format === 'module-typescript' || format === 'commonjs-typescript') &&
isUnderNodeModules(url)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(url);
}
return {
__proto__: null,
format,

View file

@ -579,13 +579,18 @@ class ModuleLoader {
this.#customizations.loadSync(url, context) :
defaultLoadSync(url, context);
let format = result?.format;
if (format === 'module') {
if (format === 'module' || format === 'module-typescript') {
throw new ERR_REQUIRE_ESM(url, true);
}
if (format === 'commonjs') {
format = 'require-commonjs';
result = { __proto__: result, format };
}
if (format === 'commonjs-typescript') {
format = 'require-commonjs-typescript';
result = { __proto__: result, format };
}
this.validateLoadResult(url, format);
return result;
}

View file

@ -3,6 +3,7 @@
const {
ArrayPrototypeMap,
Boolean,
FunctionPrototypeCall,
JSONParse,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
@ -37,6 +38,7 @@ const { readFileSync } = require('fs');
const { dirname, extname, isAbsolute } = require('path');
const {
loadBuiltinModule,
tsParse,
stripBOM,
urlToFilename,
} = require('internal/modules/helpers');
@ -302,6 +304,15 @@ translators.set('require-commonjs', (url, source, isMain) => {
return createCJSModuleWrap(url, source);
});
// Handle CommonJS modules referenced by `require` calls.
// This translator function must be sync, as `require` is sync.
translators.set('require-commonjs-typescript', (url, source, isMain) => {
emitExperimentalWarning('Type Stripping');
assert(cjsParse);
const code = tsParse(stringify(source));
return createCJSModuleWrap(url, code);
});
// Handle CommonJS modules referenced by `import` statements or expressions,
// or as the initial entry point when the ESM loader handles a CommonJS entry.
translators.set('commonjs', async function commonjsStrategy(url, source,
@ -510,3 +521,21 @@ translators.set('wasm', async function(url, source) {
}
}).module;
});
// Strategy for loading a commonjs TypeScript module
translators.set('commonjs-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, false, 'load');
const code = tsParse(stringify(source));
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
});
// Strategy for loading an esm TypeScript module
translators.set('module-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, false, 'load');
const code = tsParse(stringify(source));
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
});

View file

@ -2,6 +2,7 @@
const {
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ObjectDefineProperty,
ObjectPrototypeHasOwnProperty,
SafeMap,
@ -9,6 +10,7 @@ const {
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const {
@ -298,14 +300,37 @@ function getBuiltinModule(id) {
return normalizedId ? require(normalizedId) : undefined;
}
let parseTS;
function lazyLoadTSParser() {
parseTS ??= require('internal/deps/amaro/dist/index').transformSync;
return parseTS;
}
function tsParse(source) {
if (!source || typeof source !== 'string') { return; }
const transformSync = lazyLoadTSParser();
const { code } = transformSync(source);
return code;
}
function isUnderNodeModules(filename) {
const resolvedPath = path.resolve(filename);
const normalizedPath = path.normalize(resolvedPath);
const splitPath = StringPrototypeSplit(normalizedPath, path.sep);
return ArrayPrototypeIncludes(splitPath, 'node_modules');
}
module.exports = {
addBuiltinLibsToObject,
getBuiltinModule,
getCjsConditions,
initializeCjsConditions,
isUnderNodeModules,
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
tsParse,
stripBOM,
toRealPath,
hasStartedUserCJSExecution() {

View file

@ -81,6 +81,14 @@ function shouldUseESMLoader(mainPath) {
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
if (getOptionValue('--experimental-strip-types')) {
// This ensures that --experimental-default-type=commonjs and .mts files are treated as commonjs
if (getOptionValue('--experimental-default-type') === 'commonjs') { return false; }
if (mainPath && StringPrototypeEndsWith(mainPath, '.cts')) { return false; }
// This will likely change in the future to start with commonjs loader by default
if (mainPath && StringPrototypeEndsWith(mainPath, '.mts')) { return true; }
}
const type = getNearestParentPackageJSONType(mainPath);
// No package.json or no `type` field.

View file

@ -56,6 +56,7 @@
'deps/acorn/acorn/dist/acorn.js',
'deps/acorn/acorn-walk/dist/walk.js',
'deps/minimatch/index.js',
'deps/amaro/dist/index.js',
'<@(node_builtin_shareable_builtins)',
],
'node_sources': [

6
src/amaro_version.h Normal file
View file

@ -0,0 +1,6 @@
// This is an auto generated file, please do not edit.
// Refer to tools/dep_updaters/update-amaro.sh
#ifndef SRC_AMARO_VERSION_H_
#define SRC_AMARO_VERSION_H_
#define AMARO_VERSION "0.0.4"
#endif // SRC_AMARO_VERSION_H_

View file

@ -1,6 +1,7 @@
#include "node_metadata.h"
#include "acorn_version.h"
#include "ada.h"
#include "amaro_version.h"
#include "ares.h"
#include "brotli/encode.h"
#include "cjs_module_lexer_version.h"
@ -116,6 +117,7 @@ Metadata::Versions::Versions() {
acorn = ACORN_VERSION;
cjs_module_lexer = CJS_MODULE_LEXER_VERSION;
uvwasi = UVWASI_VERSION_STRING;
amaro = AMARO_VERSION;
#if HAVE_OPENSSL
openssl = GetOpenSSLVersion();

View file

@ -51,6 +51,7 @@ namespace node {
V(sqlite) \
V(ada) \
V(nbytes) \
V(amaro) \
NODE_VERSIONS_KEY_UNDICI(V) \
V(cjs_module_lexer)

View file

@ -785,6 +785,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"ES module to preload (option can be repeated)",
&EnvironmentOptions::preload_esm_modules,
kAllowedInEnvvar);
AddOption("--experimental-strip-types",
"Experimental type-stripping for TypeScript files.",
&EnvironmentOptions::experimental_strip_types,
kAllowedInEnvvar);
AddOption("--interactive",
"always enter the REPL even if stdin does not appear "
"to be a terminal",

View file

@ -233,6 +233,8 @@ class EnvironmentOptions : public Options {
std::vector<std::string> preload_esm_modules;
bool experimental_strip_types = false;
std::vector<std::string> user_argv;
bool report_exclude_network = false;

View file

@ -0,0 +1,166 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { match, strictEqual } from 'node:assert';
import { test } from 'node:test';
test('require a .ts file with explicit extension succeeds', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--eval',
'require("./test-typescript.ts")',
'--no-warnings',
], {
cwd: fixtures.path('typescript/ts'),
});
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'Hello, TypeScript!\n');
strictEqual(result.code, 0);
});
// TODO(marco-ippolito) This test should fail because extensionless require
// but it's behaving like a .js file
test('eval require a .ts file with implicit extension fails', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--eval',
'require("./test-typescript")',
'--no-warnings',
], {
cwd: fixtures.path('typescript/ts'),
});
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
// TODO(marco-ippolito) This test should fail because extensionless require
// but it's behaving like a .js file
test('require a .ts file with implicit extension fails', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/cts/test-extensionless-require.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect failure of an .mts file with CommonJS syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/cts/test-cts-but-module-syntax.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /To load an ES module, set "type": "module" in the package\.json or use the \.mjs extension\./);
strictEqual(result.code, 1);
});
test('execute a .cts file importing a .cts file', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/cts/test-require-commonjs.cts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a .cts file importing a .ts file export', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/cts/test-require-ts-file.cts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a .cts file importing a .mts file export', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/cts/test-require-mts-module.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /Error \[ERR_REQUIRE_ESM\]: require\(\) of ES Module/);
strictEqual(result.code, 1);
});
test('execute a .cts file importing a .mts file export', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-require-module',
fixtures.path('typescript/cts/test-require-mts-module.cts'),
]);
match(result.stderr, /Support for loading ES Module in require\(\) is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect failure of a .cts file with default type module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module', // Keeps working with commonjs
'--no-warnings',
fixtures.path('typescript/cts/test-require-commonjs.cts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect failure of a .cts file in node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/cts/test-cts-node_modules.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.code, 1);
});
test('expect failure of a .ts file in node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/cts/test-ts-node_modules.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.code, 1);
});
test('expect failure of a .cts requiring esm without default type module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/cts/test-mts-node_modules.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /ERR_REQUIRE_ESM/);
strictEqual(result.code, 1);
});
test('expect failure of a .cts file requiring esm in node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-require-module',
fixtures.path('typescript/cts/test-mts-node_modules.cts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.code, 1);
});

View file

@ -0,0 +1,84 @@
import { spawnPromisified } from '../common/index.mjs';
import { match, strictEqual } from 'node:assert';
import { test } from 'node:test';
test('eval TypeScript ESM syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--input-type=module',
'--experimental-strip-types',
'--eval',
`import util from 'node:util'
const text: string = 'Hello, TypeScript!'
console.log(util.styleText('red', text));`]);
match(result.stderr, /Type Stripping is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('eval TypeScript CommonJS syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--input-type=commonjs',
'--experimental-strip-types',
'--eval',
`const util = require('node:util');
const text: string = 'Hello, TypeScript!'
console.log(util.styleText('red', text));`,
'--no-warnings']);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.stderr, '');
strictEqual(result.code, 0);
});
test('eval TypeScript CommonJS syntax by default', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--eval',
`const util = require('node:util');
const text: string = 'Hello, TypeScript!'
console.log(util.styleText('red', text));`,
'--no-warnings']);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('TypeScript ESM syntax not specified', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--eval',
`import util from 'node:util'
const text: string = 'Hello, TypeScript!'
console.log(text);`]);
match(result.stderr, /ExperimentalWarning: Type Stripping is an experimental/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect fail eval TypeScript CommonJS syntax with input-type module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--input-type=module',
'--eval',
`const util = require('node:util');
const text: string = 'Hello, TypeScript!'
console.log(util.styleText('red', text));`]);
strictEqual(result.stdout, '');
match(result.stderr, /require is not defined in ES module scope, you can use import instead/);
strictEqual(result.code, 1);
});
test('expect fail eval TypeScript CommonJS syntax with input-type module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--input-type=commonjs',
'--eval',
`import util from 'node:util'
const text: string = 'Hello, TypeScript!'
console.log(util.styleText('red', text));`]);
strictEqual(result.stdout, '');
match(result.stderr, /Cannot use import statement outside a module/);
strictEqual(result.code, 1);
});

View file

@ -0,0 +1,97 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { match, strictEqual } from 'node:assert';
import { test } from 'node:test';
test('expect failure of a .mts file with CommonJS syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/mts/test-mts-but-commonjs-syntax.mts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /require is not defined in ES module scope, you can use import instead/);
strictEqual(result.code, 1);
});
test('execute an .mts file importing an .mts file', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/mts/test-import-module.mts'),
]);
match(result.stderr, /Type Stripping is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute an .mts file importing a .ts file', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module', // this should fail
'--no-warnings',
fixtures.path('typescript/mts/test-import-ts-file.mts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute an .mts file importing a .cts file', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
'--no-warnings',
fixtures.path('typescript/mts/test-import-commonjs.mts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute an .mts file with wrong default module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=commonjs',
fixtures.path('typescript/mts/test-import-module.mts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /Error \[ERR_REQUIRE_ESM\]: require\(\) of ES Module/);
strictEqual(result.code, 1);
});
test('execute an .mts file from node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/mts/test-mts-node_modules.mts'),
]);
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('execute a .cts file from node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/mts/test-cts-node_modules.mts'),
]);
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('execute a .ts file from node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/mts/test-ts-node_modules.mts'),
]);
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});

View file

@ -0,0 +1,229 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { match, strictEqual } from 'node:assert';
import { test } from 'node:test';
test('execute a TypeScript file', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-typescript.ts'),
]);
match(result.stderr, /Type Stripping is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with imports', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
'--no-warnings',
fixtures.path('typescript/ts/test-import-foo.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with node_modules', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
'--no-warnings',
fixtures.path('typescript/ts/test-typescript-node-modules.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect error when executing a TypeScript file with imports with no extensions', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
fixtures.path('typescript/ts/test-import-no-extension.ts'),
]);
match(result.stderr, /Error \[ERR_MODULE_NOT_FOUND\]:/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('expect error when executing a TypeScript file with enum', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-enums.ts'),
]);
// This error should be thrown during transformation
match(result.stderr, /TypeScript enum is not supported in strip-only mode/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('expect error when executing a TypeScript file with experimental decorators', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-experimental-decorators.ts'),
]);
// This error should be thrown at runtime
match(result.stderr, /Invalid or unexpected token/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('expect error when executing a TypeScript file with namespaces', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-namespaces.ts'),
]);
// This error should be thrown during transformation
match(result.stderr, /TypeScript namespace declaration is not supported in strip-only mode/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('execute a TypeScript file with type definition', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/ts/test-import-types.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with type definition but no type keyword', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
fixtures.path('typescript/ts/test-import-no-type-keyword.ts'),
]);
match(result.stderr, /does not provide an export named 'MyType'/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('execute a TypeScript file with CommonJS syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/ts/test-commonjs-parsing.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with ES module syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
'--no-warnings',
fixtures.path('typescript/ts/test-module-typescript.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect failure of a TypeScript file requiring ES module syntax', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-require-module',
fixtures.path('typescript/ts/test-require-module.ts'),
]);
match(result.stderr, /Support for loading ES Module in require\(\) is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('expect stack trace of a TypeScript file to be correct', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-whitespacing.ts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /test-whitespacing\.ts:5:7/);
strictEqual(result.code, 1);
});
test('execute CommonJS TypeScript file from node_modules with require-module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--experimental-strip-types',
fixtures.path('typescript/ts/test-import-ts-node-modules.ts'),
]);
match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
test('execute a TypeScript file with CommonJS syntax but default type module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=module',
fixtures.path('typescript/ts/test-commonjs-parsing.ts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /require is not defined in ES module scope, you can use import instead/);
strictEqual(result.code, 1);
});
test('execute a TypeScript file with CommonJS syntax requiring .cts', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--no-warnings',
fixtures.path('typescript/ts/test-require-cts.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with CommonJS syntax requiring .mts', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
fixtures.path('typescript/ts/test-require-mts.ts'),
]);
strictEqual(result.stdout, '');
match(result.stderr, /Error \[ERR_REQUIRE_ESM\]: require\(\) of ES Module/);
strictEqual(result.code, 1);
});
test('execute a TypeScript file with CommonJS syntax requiring .mts with require-module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-require-module',
fixtures.path('typescript/ts/test-require-mts.ts'),
]);
match(result.stderr, /Support for loading ES Module in require\(\) is an experimental feature and might change at any time/);
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});
test('execute a TypeScript file with CommonJS syntax requiring .mts with require-module', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-strip-types',
'--experimental-default-type=commonjs',
'--no-warnings',
fixtures.path('typescript/ts/test-require-cts.ts'),
]);
strictEqual(result.stderr, '');
match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0);
});

5
test/fixtures/typescript/cts/node_modules/bar/bar.ts generated vendored Normal file
View file

@ -0,0 +1,5 @@
const bar: string = "Hello, TypeScript!";
module.exports = {
bar,
};

View file

@ -0,0 +1,13 @@
{
"name": "bar",
"version": "1.0.0",
"main": "bar.ts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1 @@
export const baz: string = 'Hello, TypeScript!';

View file

@ -0,0 +1,14 @@
{
"name": "baz",
"version": "1.0.0",
"type": "module",
"main": "baz.mts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,5 @@
const foo: string = 'Hello, TypeScript!';
module.exports = {
foo
};

View file

@ -0,0 +1,14 @@
{
"name": "foo",
"version": "1.0.0",
"type": "commonjs",
"main": "foo.cts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,3 @@
const foo: string = 'Hello, TypeScript!';
module.exports = { foo };

View file

@ -0,0 +1,5 @@
import util from 'node:util';
export const text: string = 'Hello, TypeScript!';
console.log(util.styleText(['bold', 'red'], text));

View file

@ -0,0 +1,3 @@
const foo: string = 'Hello, TypeScript!';
module.exports = { foo };

View file

@ -0,0 +1,5 @@
const { foo } = require('foo');
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,3 @@
const { foo } = require('./test-commonjs-export');
console.log(foo);

View file

@ -0,0 +1,5 @@
const { baz } = require('baz');
interface Foo { };
console.log(baz);

View file

@ -0,0 +1,5 @@
const { foo } = require('./test-cts-export-foo.cts');
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
const { foo } = require('../mts/test-mts-export-foo.mts');
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
const { foo } = require('./test-commonjs-export.ts');
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
const { bar } = require('bar');
interface Foo { };
console.log(bar);

5
test/fixtures/typescript/mts/node_modules/bar/bar.ts generated vendored Normal file
View file

@ -0,0 +1,5 @@
const bar: string = 'Hello, TypeScript!'
module.exports = {
bar
};

View file

@ -0,0 +1,13 @@
{
"name": "bar",
"version": "1.0.0",
"main": "bar.ts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1 @@
export const baz: string = 'Hello, TypeScript!';

View file

@ -0,0 +1,13 @@
{
"name": "baz",
"version": "1.0.0",
"main": "baz.mts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,5 @@
const foo: string = 'Hello, TypeScript!';
module.exports = {
foo
};

View file

@ -0,0 +1,14 @@
{
"name": "foo",
"version": "1.0.0",
"type": "commonjs",
"main": "foo.cts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,5 @@
import { foo } from 'foo';
interface Foo { };
console.log(foo);

View file

@ -0,0 +1,5 @@
import { foo } from '../cts/test-cts-export-foo.cts';
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
import { foo } from './test-mts-export-foo.mts';
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
import { foo } from './test-module-export.ts';
interface Foo {};
console.log(foo);

View file

@ -0,0 +1 @@
export const foo: string = 'Hello, TypeScript!';

View file

@ -0,0 +1,9 @@
const util = require('node:util');
const text: string = 'Hello, TypeScript!';
console.log(util.styleText(['bold', 'red'], text));
module.exports = {
text
};

View file

@ -0,0 +1 @@
export const foo: string = 'Hello, TypeScript!';

View file

@ -0,0 +1,5 @@
import { baz } from 'baz';
interface Foo {};
console.log(baz);

View file

@ -0,0 +1,5 @@
import { bar } from 'bar';
interface Foo {};
console.log(bar);

3
test/fixtures/typescript/ts/node_modules/bar/bar.ts generated vendored Normal file
View file

@ -0,0 +1,3 @@
const bar: string = 'Hello, TypeScript!';
module.exports = { bar };

View file

@ -0,0 +1,13 @@
{
"name": "bar",
"version": "1.0.0",
"main": "bar.ts",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

1
test/fixtures/typescript/ts/node_modules/foo/foo.js generated vendored Normal file
View file

@ -0,0 +1 @@
export const foo = "Hello, TypeScript!"

View file

@ -0,0 +1,14 @@
{
"name": "foo",
"version": "1.0.0",
"type": "module",
"main": "foo.js",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,9 @@
const util = require('node:util');
const text: string = 'Hello, TypeScript!';
console.log(util.styleText(['bold', 'red'], text));
module.exports = {
text
};

View file

@ -0,0 +1,13 @@
enum Color {
Red,
Green,
Blue,
}
console.log(Color.Red);
console.log(Color.Green);
console.log(Color.Blue);
console.log(Color[0]);
console.log(Color[1]);
console.log(Color[2]);

View file

@ -0,0 +1,14 @@
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}

View file

@ -0,0 +1 @@
export const foo: string = "Hello, TypeScript!";

View file

@ -0,0 +1,5 @@
import { foo } from './test-export-foo.ts';
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,5 @@
import { foo } from './test-no-extensions';
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,7 @@
import { MyType } from './test-types.d.ts';
const myVar: MyType = {
foo: 'Hello, TypeScript!'
};
console.log(myVar.foo);

View file

@ -0,0 +1,5 @@
import { bar } from 'bar';
interface Bar {};
console.log(bar);

View file

@ -0,0 +1,7 @@
import type { MyType } from './test-types.d.ts';
const myVar: MyType = {
foo: 'Hello, TypeScript!'
};
console.log(myVar.foo);

View file

@ -0,0 +1,5 @@
import util from 'node:util';
export const text: string = 'Hello, TypeScript!';
console.log(util.styleText("red", text));

View file

@ -0,0 +1,9 @@
/// <reference path="a.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}

View file

@ -0,0 +1 @@
export const foo: string = 'Hello, TypeScript!';

View file

@ -0,0 +1,5 @@
const { foo } = require('../cts/test-cts-export-foo.cts');
interface Foo {};
console.log(foo);

View file

@ -0,0 +1,3 @@
const { foo } = require('../mts/test-mts-export-foo.mts');
console.log(foo);

View file

@ -0,0 +1,5 @@
const { foo } = require('../mts/test-mts-export-foo.mts');
interface Foo { };
console.log(foo);

View file

@ -0,0 +1,3 @@
export type MyType = {
foo: string;
};

View file

@ -0,0 +1,3 @@
import { foo } from 'foo';
console.log(foo);

View file

@ -0,0 +1,5 @@
const str: string = "Hello, TypeScript!";
interface Foo {
bar: string;
}
console.log(str);

View file

@ -0,0 +1,5 @@
interface Foo {
bar: string;
}
throw new Error("Whitespacing");

View file

@ -24,6 +24,7 @@ const expected_keys = [
'ada',
'cjs_module_lexer',
'nbytes',
'amaro',
];
const hasUndici = process.config.variables.node_builtin_shareable_builtins.includes('deps/undici/undici.js');

View file

@ -0,0 +1,83 @@
#!/bin/sh
# Shell script to update amaro in the source tree to the latest release.
# This script must be in the tools directory when it runs because it uses the
# script source file path to determine directories to work in.
set -ex
BASE_DIR=$(cd "$(dirname "$0")/../.." && pwd)
[ -z "$NODE" ] && NODE="$BASE_DIR/out/Release/node"
[ -x "$NODE" ] || NODE=$(command -v node)
DEPS_DIR="$BASE_DIR/deps"
NPM="$DEPS_DIR/npm/bin/npm-cli.js"
# shellcheck disable=SC1091
. "$BASE_DIR/tools/dep_updaters/utils.sh"
NEW_VERSION=$("$NODE" "$NPM" view amaro dist-tags.latest)
CURRENT_VERSION=$("$NODE" -p "require('./deps/amaro/package.json').version")
# This function exit with 0 if new version and current version are the same
compare_dependency_version "amaro" "$NEW_VERSION" "$CURRENT_VERSION"
cd "$( dirname "$0" )/../.." || exit
echo "Making temporary workspace..."
WORKSPACE=$(mktemp -d 2> /dev/null || mktemp -d -t 'tmp')
cleanup () {
EXIT_CODE=$?
[ -d "$WORKSPACE" ] && rm -rf "$WORKSPACE"
exit $EXIT_CODE
}
trap cleanup INT TERM EXIT
cd "$WORKSPACE"
echo "Fetching amaro source archive..."
"$NODE" "$NPM" pack "amaro@$NEW_VERSION"
amaro_TGZ="amaro-$NEW_VERSION.tgz"
log_and_verify_sha256sum "amaro" "$amaro_TGZ"
cp ./* "$DEPS_DIR/amaro/LICENSE"
rm -r "$DEPS_DIR/amaro"/*
tar -xf "$amaro_TGZ"
cd package
rm -rf node_modules
mv ./* "$DEPS_DIR/amaro"
# update version information in src/undici_version.h
cat > "$ROOT/src/amaro_version.h" <<EOF
// This is an auto generated file, please do not edit.
// Refer to tools/dep_updaters/update-amaro.sh
#ifndef SRC_AMARO_VERSION_H_
#define SRC_AMARO_VERSION_H_
#define AMARO_VERSION "$NEW_VERSION"
#endif // SRC_AMARO_VERSION_H_
EOF
echo "All done!"
echo ""
echo "Please git add amaro, commit the new version:"
echo ""
echo "$ git add -A deps/amaro"
echo "$ git commit -m \"deps: update amaro to $NEW_VERSION\""
echo ""
# Update the version number on maintaining-dependencies.md
# and print the new version as the last line of the script as we need
# to add it to $GITHUB_ENV variable
finalize_version_update "amaro" "$NEW_VERSION" "src/amaro_version.h"

View file

@ -38,6 +38,8 @@ licenseText="$(cat "${rootdir}/deps/cjs-module-lexer/LICENSE")"
addlicense "cjs-module-lexer" "deps/cjs-module-lexer" "$licenseText"
licenseText="$(cat "${rootdir}/deps/v8/third_party/ittapi/LICENSES/BSD-3-Clause.txt")"
addlicense "ittapi" "deps/v8/third_party/ittapi" "$licenseText"
licenseText="$(cat "${rootdir}/deps/amaro/LICENSE.md")"
addlicense "amaro" "deps/amaro" "$licenseText"
if [ -f "${rootdir}/deps/icu/LICENSE" ]; then
# ICU 57 and following. Drop the BOM
licenseText="$(sed -e '1s/^[^a-zA-Z ]*ICU/ICU/' -e :a -e 's/<[^>]*>//g;s/ / /g;s/ +$//;/</N;//ba' "${rootdir}/deps/icu/LICENSE")"