Skip to content

Commit 945b1ca

Browse files
marco-ippolitotargos
authored andcommitted
module: add --experimental-transform-types flag
PR-URL: #54283 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Zeyu "Alex" Yang <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Paolo Insogna <[email protected]>
1 parent 67f7574 commit 945b1ca

19 files changed

+317
-37
lines changed

doc/api/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,17 @@ CommonJS. This includes the following:
956956
* Lexical redeclarations of the CommonJS wrapper variables (`require`, `module`,
957957
`exports`, `__dirname`, `__filename`).
958958

959+
### `--experimental-transform-types`
960+
961+
<!-- YAML
962+
added: REPLACEME
963+
-->
964+
965+
> Stability: 1.0 - Early development
966+
967+
Enables the transformation of TypeScript-only syntax into JavaScript code.
968+
Implies `--experimental-strip-types` and `--enable-source-maps`.
969+
959970
### `--experimental-eventsource`
960971

961972
<!-- YAML
@@ -2973,6 +2984,7 @@ one is included in the list below.
29732984
* `--experimental-sqlite`
29742985
* `--experimental-strip-types`
29752986
* `--experimental-top-level-await`
2987+
* `--experimental-transform-types`
29762988
* `--experimental-vm-modules`
29772989
* `--experimental-wasi-unstable-preview1`
29782990
* `--experimental-wasm-modules`

doc/api/typescript.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Modules: TypeScript
22

3+
<!-- YAML
4+
changes:
5+
- version: REPLACEME
6+
pr-url: https://github.com/nodejs/node/pull/54283
7+
description: Added `--experimental-transform-types` flag.
8+
-->
9+
10+
> Stability: 1.0 - Early development
11+
312
## Enabling
413

514
There are two ways to enable runtime TypeScript support in Node.js:
@@ -44,13 +53,15 @@ added: v22.6.0
4453
> Stability: 1.0 - Early development
4554
4655
The flag [`--experimental-strip-types`][] enables Node.js to run TypeScript
47-
files that contain only type annotations. Such files contain no TypeScript
48-
features that require transformation, such as enums or namespaces. Node.js will
49-
replace inline type annotations with whitespace, and no type checking is
50-
performed. TypeScript features that depend on settings within `tsconfig.json`,
56+
files. By default Node.js will execute only files that contain no
57+
TypeScript features that require transformation, such as enums or namespaces.
58+
Node.js will replace inline type annotations with whitespace,
59+
and no type checking is performed.
60+
To enable the transformation of such features
61+
use the flag [`--experimental-transform-types`][].
62+
TypeScript features that depend on settings within `tsconfig.json`,
5163
such as paths or converting newer JavaScript syntax to older standards, are
52-
intentionally unsupported. To get fuller TypeScript support, including support
53-
for enums and namespaces and paths, see [Full TypeScript support][].
64+
intentionally unsupported. To get full TypeScript support, see [Full TypeScript support][].
5465

5566
The type stripping feature is designed to be lightweight.
5667
By intentionally not supporting syntaxes that require JavaScript code
@@ -82,20 +93,24 @@ The `tsconfig.json` option `allowImportingTsExtensions` will allow the
8293
TypeScript compiler `tsc` to type-check files with `import` specifiers that
8394
include the `.ts` extension.
8495

85-
### Unsupported TypeScript features
96+
### TypeScript features
8697

8798
Since Node.js is only removing inline types, any TypeScript features that
88-
involve _replacing_ TypeScript syntax with new JavaScript syntax will error.
89-
This is by design. To run TypeScript with such features, see
90-
[Full TypeScript support][].
99+
involve _replacing_ TypeScript syntax with new JavaScript syntax will error,
100+
unless the flag [`--experimental-transform-types`][] is passed.
91101

92-
The most prominent unsupported features that require transformation are:
102+
The most prominent features that require transformation are:
93103

94104
* `Enum`
95-
* `experimentalDecorators`
96105
* `namespaces`
106+
* `legacy module`
97107
* parameter properties
98108

109+
Since Decorators are currently a [TC39 Stage 3 proposal](https://github.com/tc39/proposal-decorators)
110+
and will soon be supported by the JavaScript engine,
111+
they are not transformed and will result in a parser error.
112+
This is a temporary limitation and will be resolved in the future.
113+
99114
In addition, Node.js does not read `tsconfig.json` files and does not support
100115
features that depend on settings within `tsconfig.json`, such as paths or
101116
converting newer JavaScript syntax into older standards.
@@ -132,8 +147,9 @@ TypeScript syntax is unsupported in the REPL, STDIN input, `--print`, `--check`,
132147
### Source maps
133148

134149
Since inline types are replaced by whitespace, source maps are unnecessary for
135-
correct line numbers in stack traces; and Node.js does not generate them. For
136-
source maps support, see [Full TypeScript support][].
150+
correct line numbers in stack traces; and Node.js does not generate them.
151+
When [`--experimental-transform-types`][] is enabled, source-maps
152+
are enabled by default.
137153

138154
### Type stripping in dependencies
139155

@@ -145,6 +161,7 @@ a `node_modules` path.
145161
[ES Modules]: esm.md
146162
[Full TypeScript support]: #full-typescript-support
147163
[`--experimental-strip-types`]: cli.md#--experimental-strip-types
164+
[`--experimental-transform-types`]: cli.md#--experimental-transform-types
148165
[`tsx`]: https://tsx.is/
149166
[`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax
150167
[file extensions are mandatory]: esm.md#mandatory-file-extensions

doc/node.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ Enable snapshot testing in the test runner.
194194
.It Fl -experimental-strip-types
195195
Enable experimental type-stripping for TypeScript files.
196196
.
197+
.It Fl -experimental-transform-types
198+
Enable transformation of TypeScript-only syntax into JavaScript code.
199+
.
197200
.It Fl -experimental-eventsource
198201
Enable experimental support for the EventSource Web API.
199202
.

lib/internal/main/eval_string.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const {
1414
markBootstrapComplete,
1515
} = require('internal/process/pre_execution');
1616
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
17-
const { addBuiltinLibsToObject, tsParse } = require('internal/modules/helpers');
17+
const { addBuiltinLibsToObject, stripTypeScriptTypes } = require('internal/modules/helpers');
1818

1919
const { getOptionValue } = require('internal/options');
2020

@@ -24,7 +24,7 @@ markBootstrapComplete();
2424

2525
const code = getOptionValue('--eval');
2626
const source = getOptionValue('--experimental-strip-types') ?
27-
tsParse(code) :
27+
stripTypeScriptTypes(code) :
2828
code;
2929

3030
const print = getOptionValue('--print');

lib/internal/modules/cjs/loader.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,8 +1362,8 @@ function loadESMFromCJS(mod, filename) {
13621362
if (isUnderNodeModules(filename)) {
13631363
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
13641364
}
1365-
const { tsParse } = require('internal/modules/helpers');
1366-
source = tsParse(source);
1365+
const { stripTypeScriptTypes } = require('internal/modules/helpers');
1366+
source = stripTypeScriptTypes(source, filename);
13671367
}
13681368
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13691369
const isMain = mod[kIsMainSymbol];
@@ -1576,9 +1576,9 @@ function loadCTS(module, filename) {
15761576
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
15771577
}
15781578
const source = getMaybeCachedSource(module, filename);
1579-
const { tsParse } = require('internal/modules/helpers');
1580-
const content = tsParse(source);
1581-
module._compile(content, filename, 'commonjs');
1579+
const { stripTypeScriptTypes } = require('internal/modules/helpers');
1580+
const code = stripTypeScriptTypes(source, filename);
1581+
module._compile(code, filename, 'commonjs');
15821582
}
15831583

15841584
/**
@@ -1592,8 +1592,8 @@ function loadTS(module, filename) {
15921592
}
15931593
// If already analyzed the source, then it will be cached.
15941594
const source = getMaybeCachedSource(module, filename);
1595-
const { tsParse } = require('internal/modules/helpers');
1596-
const content = tsParse(source);
1595+
const { stripTypeScriptTypes } = require('internal/modules/helpers');
1596+
const content = stripTypeScriptTypes(source, filename);
15971597
let format;
15981598
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
15991599
// Function require shouldn't be used in ES modules.
@@ -1613,7 +1613,7 @@ function loadTS(module, filename) {
16131613
if (Module._cache[parentPath]) {
16141614
let parentSource;
16151615
try {
1616-
parentSource = tsParse(fs.readFileSync(parentPath, 'utf8'));
1616+
parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
16171617
} catch {
16181618
// Continue regardless of error.
16191619
}

lib/internal/modules/esm/get_format.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,8 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
162162
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
163163
let parsedSource;
164164
if (source) {
165-
// We do the type stripping only if `source` is not falsy.
166-
const { tsParse } = require('internal/modules/helpers');
167-
parsedSource = tsParse(source);
165+
const { stripTypeScriptTypes } = require('internal/modules/helpers');
166+
parsedSource = stripTypeScriptTypes(source, url);
168167
}
169168
const detectedFormat = detectModuleFormat(parsedSource, url);
170169
// When source is undefined, default to module-typescript.

lib/internal/modules/esm/translators.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const { readFileSync } = require('fs');
3838
const { dirname, extname, isAbsolute } = require('path');
3939
const {
4040
loadBuiltinModule,
41-
tsParse,
41+
stripTypeScriptTypes,
4242
stripBOM,
4343
urlToFilename,
4444
} = require('internal/modules/helpers');
@@ -309,7 +309,7 @@ translators.set('require-commonjs', (url, source, isMain) => {
309309
translators.set('require-commonjs-typescript', (url, source, isMain) => {
310310
emitExperimentalWarning('Type Stripping');
311311
assert(cjsParse);
312-
const code = tsParse(stringify(source));
312+
const code = stripTypeScriptTypes(stringify(source), url);
313313
return createCJSModuleWrap(url, code);
314314
});
315315

@@ -526,7 +526,7 @@ translators.set('wasm', async function(url, source) {
526526
translators.set('commonjs-typescript', function(url, source) {
527527
emitExperimentalWarning('Type Stripping');
528528
assertBufferSource(source, false, 'load');
529-
const code = tsParse(stringify(source));
529+
const code = stripTypeScriptTypes(stringify(source), url);
530530
debug(`Translating TypeScript ${url}`);
531531
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
532532
});
@@ -535,7 +535,7 @@ translators.set('commonjs-typescript', function(url, source) {
535535
translators.set('module-typescript', function(url, source) {
536536
emitExperimentalWarning('Type Stripping');
537537
assertBufferSource(source, false, 'load');
538-
const code = tsParse(stringify(source));
538+
const code = stripTypeScriptTypes(stringify(source), url);
539539
debug(`Translating TypeScript ${url}`);
540540
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
541541
});

lib/internal/modules/helpers.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const path = require('path');
2525
const { pathToFileURL, fileURLToPath } = require('internal/url');
2626
const assert = require('internal/assert');
2727

28+
const { Buffer } = require('buffer');
2829
const { getOptionValue } = require('internal/options');
2930
const { setOwnProperty } = require('internal/util');
3031
const { inspect } = require('internal/util/inspect');
@@ -300,7 +301,21 @@ function getBuiltinModule(id) {
300301
return normalizedId ? require(normalizedId) : undefined;
301302
}
302303

304+
/**
305+
* TypeScript parsing function, by default Amaro.transformSync.
306+
* @type {Function}
307+
*/
303308
let typeScriptParser;
309+
/**
310+
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
311+
* @type {string}
312+
*/
313+
let typeScriptParsingMode;
314+
/**
315+
* Whether source maps are enabled for TypeScript parsing.
316+
* @type {boolean}
317+
*/
318+
let sourceMapEnabled;
304319

305320
/**
306321
* Load the TypeScript parser.
@@ -318,22 +333,44 @@ function loadTypeScriptParser(parser) {
318333
} else {
319334
const amaro = require('internal/deps/amaro/dist/index');
320335
// Default option for Amaro is to perform Type Stripping only.
321-
const defaultOptions = { __proto__: null, mode: 'strip-only' };
336+
typeScriptParsingMode = getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only';
337+
sourceMapEnabled = getOptionValue('--enable-source-maps');
322338
// Curry the transformSync function with the default options.
323-
typeScriptParser = (source) => amaro.transformSync(source, defaultOptions);
339+
typeScriptParser = amaro.transformSync;
324340
}
325341
return typeScriptParser;
326342
}
327343

328344
/**
345+
* @typedef {object} TransformOutput
346+
* @property {string} code The compiled code.
347+
* @property {string} [map] The source maps (optional).
348+
*
329349
* Performs type-stripping to TypeScript source code.
330350
* @param {string} source TypeScript code to parse.
331-
* @returns {string} JavaScript code.
351+
* @param {string} filename The filename of the source code.
352+
* @returns {TransformOutput} The stripped TypeScript code.
332353
*/
333-
function tsParse(source) {
354+
function stripTypeScriptTypes(source, filename) {
334355
assert(typeof source === 'string');
335356
const parse = loadTypeScriptParser();
336-
const { code } = parse(source);
357+
const options = {
358+
__proto__: null,
359+
mode: typeScriptParsingMode,
360+
sourceMap: sourceMapEnabled,
361+
filename,
362+
// Transform option is only applied in transform mode.
363+
transform: {
364+
verbatimModuleSyntax: true,
365+
},
366+
};
367+
const { code, map } = parse(source, options);
368+
if (map) {
369+
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
370+
// base64 transformation, we should change this line.
371+
const base64SourceMap = Buffer.from(map).toString('base64');
372+
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
373+
}
337374
return code;
338375
}
339376

@@ -353,7 +390,7 @@ module.exports = {
353390
loadBuiltinModule,
354391
makeRequireFunction,
355392
normalizeReferrerURL,
356-
tsParse,
393+
stripTypeScriptTypes,
357394
stripBOM,
358395
toRealPath,
359396
hasStartedUserCJSExecution() {

src/node_options.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,13 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
805805
&EnvironmentOptions::experimental_strip_types,
806806
kAllowedInEnvvar);
807807
Implies("--experimental-strip-types", "--experimental-detect-module");
808+
AddOption("--experimental-transform-types",
809+
"enable transformation of TypeScript-only"
810+
"syntax into JavaScript code",
811+
&EnvironmentOptions::experimental_transform_types,
812+
kAllowedInEnvvar);
813+
Implies("--experimental-transform-types", "--experimental-strip-types");
814+
Implies("--experimental-transform-types", "--enable-source-maps");
808815
AddOption("--interactive",
809816
"always enter the REPL even if stdin does not appear "
810817
"to be a terminal",

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ class EnvironmentOptions : public Options {
241241
std::vector<std::string> preload_esm_modules;
242242

243243
bool experimental_strip_types = false;
244+
bool experimental_transform_types = false;
244245

245246
std::vector<std::string> user_argv;
246247

0 commit comments

Comments
 (0)