Skip to content

Commit 6b155bd

Browse files
module: add --experimental-enable-transformation for strip-types
1 parent 3cbeed8 commit 6b155bd

19 files changed

+347
-38
lines changed

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,15 @@ files with no extension will be treated as WebAssembly if they begin with the
933933
WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module
934934
JavaScript.
935935

936+
### `--experimental-enable-type-transform`
937+
938+
<!-- YAML
939+
added: REPLACEME
940+
-->
941+
942+
Enables the transformation of TypeScript-only syntax into JavaScript code.
943+
Implies `--experimental-strip-types`.
944+
936945
### `--experimental-eventsource`
937946

938947
<!-- YAML
@@ -2911,6 +2920,7 @@ one is included in the list below.
29112920
* `--experimental-async-context-frame`
29122921
* `--experimental-default-type`
29132922
* `--experimental-detect-module`
2923+
* `--experimental-enable-type-transform`
29142924
* `--experimental-eventsource`
29152925
* `--experimental-import-meta-resolve`
29162926
* `--experimental-json-modules`

doc/api/typescript.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ added: v22.6.0
4444
> Stability: 1.0 - Early development
4545
4646
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`,
47+
files. By default Node.js will execute only files that contain no
48+
TypeScript features that require transformation, such as enums or namespaces.
49+
Node.js will replace inline type annotations with whitespace,
50+
and no type checking is performed.
51+
To enable the transformation of such features
52+
use the flag [`--experimental-enable-type-transform`][].
53+
TypeScript features that depend on settings within `tsconfig.json`,
5154
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][].
55+
intentionally unsupported. To get fuller TypeScript support, see [Full TypeScript support][].
5456

5557
The type stripping feature is designed to be lightweight.
5658
By intentionally not supporting syntaxes that require JavaScript code
@@ -82,20 +84,24 @@ The `tsconfig.json` option `allowImportingTsExtensions` will allow the
8284
TypeScript compiler `tsc` to type-check files with `import` specifiers that
8385
include the `.ts` extension.
8486

85-
### Unsupported TypeScript features
87+
### TypeScript features
8688

8789
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][].
90+
involve _replacing_ TypeScript syntax with new JavaScript syntax will error,
91+
unless the flag [`--experimental-enable-type-transform`][] is passed.
9192

92-
The most prominent unsupported features that require transformation are:
93+
The most prominent features that require transformation are:
9394

9495
* `Enum`
95-
* `experimentalDecorators`
9696
* `namespaces`
97+
* `legacy module`
9798
* parameter properties
9899

100+
Since Decorators are currently a [TC39 ECMA402 stage 3 proposal](https://github.com/tc39/proposal-decorators)
101+
and will soon be supported by the JavaScript engine,
102+
they are not transformed and will result in a runtime error.
103+
This is a temporary limitation and will be resolved in the future.
104+
99105
In addition, Node.js does not read `tsconfig.json` files and does not support
100106
features that depend on settings within `tsconfig.json`, such as paths or
101107
converting newer JavaScript syntax into older standards.
@@ -132,8 +138,11 @@ TypeScript syntax is unsupported in the REPL, STDIN input, `--print`, `--check`,
132138
### Source maps
133139

134140
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][].
141+
correct line numbers in stack traces; and Node.js does not generate them.
142+
When [`--experimental-enable-type-transform`][] is enabled, by passing the flag
143+
`--enable-source-maps`, source maps will be generated for the transformed code.
144+
If `--enable-source-maps` is not passed the stack traces might not have correct
145+
locations.
137146

138147
### Type stripping in dependencies
139148

@@ -144,6 +153,7 @@ a `node_modules` path.
144153
[CommonJS]: modules.md
145154
[ES Modules]: esm.md
146155
[Full TypeScript support]: #full-typescript-support
156+
[`--experimental-enable-type-transform`]: cli.md#--experimental-enable-type-transform
147157
[`--experimental-strip-types`]: cli.md#--experimental-strip-types
148158
[`tsx`]: https://tsx.is/
149159
[`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax

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-enable-type-transform
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, stripTypes } = 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+
stripTypes(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 { stripTypes } = require('internal/modules/helpers');
1366+
source = stripTypes(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 { stripTypes } = require('internal/modules/helpers');
1580+
const code = stripTypes(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 { stripTypes } = require('internal/modules/helpers');
1596+
const content = stripTypes(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 = stripTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
16171617
} catch {
16181618
// Continue regardless of error.
16191619
}

lib/internal/modules/esm/get_format.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
161161
default: { // The user did not pass `--experimental-default-type`.
162162
// `source` is undefined when this is called from `defaultResolve`;
163163
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
164-
const { tsParse } = require('internal/modules/helpers');
165-
const parsedSource = tsParse(source);
166-
const detectedFormat = detectModuleFormat(parsedSource, url);
164+
const { stripTypes } = require('internal/modules/helpers');
165+
const code = stripTypes(source, url);
166+
const detectedFormat = detectModuleFormat(code, url);
167167
// When source is undefined, default to module-typescript.
168168
const format = detectedFormat ? `${detectedFormat}-typescript` : 'module-typescript';
169169
if (format === 'module-typescript' && foundPackageJson) {

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+
stripTypes,
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 = stripTypes(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 = stripTypes(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 = stripTypes(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: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const internalFS = require('internal/fs/utils');
2424
const path = require('path');
2525
const { pathToFileURL, fileURLToPath } = require('internal/url');
2626
const assert = require('internal/assert');
27-
27+
const { Buffer } = require('buffer');
2828
const { getOptionValue } = require('internal/options');
2929
const { setOwnProperty } = require('internal/util');
3030
const { inspect } = require('internal/util/inspect');
@@ -300,7 +300,21 @@ function getBuiltinModule(id) {
300300
return normalizedId ? require(normalizedId) : undefined;
301301
}
302302

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

305319
/**
306320
* Load the TypeScript parser.
@@ -318,23 +332,34 @@ function loadTypeScriptParser(parser) {
318332
} else {
319333
const amaro = require('internal/deps/amaro/dist/index');
320334
// Default option for Amaro is to perform Type Stripping only.
321-
const defaultOptions = { __proto__: null, mode: 'strip-only' };
335+
typeScriptParsingMode = getOptionValue('--experimental-enable-type-transform') ? 'transform' : 'strip-only';
336+
sourceMapEnabled = getOptionValue('--enable-source-maps');
322337
// Curry the transformSync function with the default options.
323-
typeScriptParser = (source) => amaro.transformSync(source, defaultOptions);
338+
typeScriptParser = amaro.transformSync;
324339
}
325340
return typeScriptParser;
326341
}
327342

328343
/**
344+
* @typedef {object} TransformOutput
345+
* @property {string} code - The compiled code.
346+
* @property {string} [map] - The source maps (optional).
347+
*
329348
* Performs type-stripping to TypeScript source code.
330349
* @param {string} source TypeScript code to parse.
331-
* @returns {string} JavaScript code.
350+
* @param {string} filename The filename of the source code.
351+
* @returns {TransformOutput} The stripped TypeScript code.
332352
*/
333-
function tsParse(source) {
353+
function stripTypes(source, filename) {
334354
// TODO(@marco-ippolito) Checking empty string or non string input should be handled in Amaro.
335355
if (!source || typeof source !== 'string') { return ''; }
336356
const parse = loadTypeScriptParser();
337-
const { code } = parse(source);
357+
const options = { __proto__: null, mode: typeScriptParsingMode, sourceMap: sourceMapEnabled, filename };
358+
const { code, map } = parse(source, options);
359+
if (map) {
360+
const base64SourceMap = Buffer.from(map).toString('base64');
361+
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
362+
}
338363
return code;
339364
}
340365

@@ -354,7 +379,7 @@ module.exports = {
354379
loadBuiltinModule,
355380
makeRequireFunction,
356381
normalizeReferrerURL,
357-
tsParse,
382+
stripTypes,
358383
stripBOM,
359384
toRealPath,
360385
hasStartedUserCJSExecution() {

src/node_options.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
789789
"Experimental type-stripping for TypeScript files.",
790790
&EnvironmentOptions::experimental_strip_types,
791791
kAllowedInEnvvar);
792+
AddOption("--experimental-enable-type-transform",
793+
"enable transformation of TypeScript-only"
794+
"syntax in JavaScript code",
795+
&EnvironmentOptions::experimental_enable_transformation,
796+
kAllowedInEnvvar);
797+
Implies("--experimental-enable-type-transform", "--experimental-strip-types");
792798
AddOption("--interactive",
793799
"always enter the REPL even if stdin does not appear "
794800
"to be a terminal",

src/node_options.h

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

241241
bool experimental_strip_types = false;
242+
bool experimental_enable_transformation = false;
242243

243244
std::vector<std::string> user_argv;
244245

0 commit comments

Comments
 (0)