Skip to content

Commit 29040a6

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

File tree

12 files changed

+118
-24
lines changed

12 files changed

+118
-24
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-transformation`
937+
938+
<!-- YAML
939+
added: REPLACEME
940+
-->
941+
942+
Enables the transformation of TypeScript only features.
943+
Imples `--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-transformation`
29142924
* `--experimental-eventsource`
29152925
* `--experimental-import-meta-resolve`
29162926
* `--experimental-json-modules`

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-transformation
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).code :
2828
code;
2929

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

lib/internal/modules/cjs/loader.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,8 +1362,9 @@ 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+
const { code } = stripTypes(source, filename);
1367+
source = code;
13671368
}
13681369
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13691370
const isMain = mod[kIsMainSymbol];
@@ -1576,9 +1577,9 @@ function loadCTS(module, filename) {
15761577
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
15771578
}
15781579
const source = getMaybeCachedSource(module, filename);
1579-
const { tsParse } = require('internal/modules/helpers');
1580-
const content = tsParse(source);
1581-
module._compile(content, filename, 'commonjs');
1580+
const { stripTypes } = require('internal/modules/helpers');
1581+
const { code } = stripTypes(source, filename);
1582+
module._compile(code, filename, 'commonjs');
15821583
}
15831584

15841585
/**
@@ -1592,8 +1593,8 @@ function loadTS(module, filename) {
15921593
}
15931594
// If already analyzed the source, then it will be cached.
15941595
const source = getMaybeCachedSource(module, filename);
1595-
const { tsParse } = require('internal/modules/helpers');
1596-
const content = tsParse(source);
1596+
const { stripTypes } = require('internal/modules/helpers');
1597+
const { code: content } = stripTypes(source, filename);
15971598
let format;
15981599
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
15991600
// Function require shouldn't be used in ES modules.
@@ -1613,7 +1614,8 @@ function loadTS(module, filename) {
16131614
if (Module._cache[parentPath]) {
16141615
let parentSource;
16151616
try {
1616-
parentSource = tsParse(fs.readFileSync(parentPath, 'utf8'));
1617+
const { code } = stripTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
1618+
parentSource = code;
16171619
} catch {
16181620
// Continue regardless of error.
16191621
}

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: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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,24 +332,30 @@ 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-transformation') ? '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);
338-
return code;
357+
const options = { __proto__: null, mode: typeScriptParsingMode, sourceMap: sourceMapEnabled, filename };
358+
return parse(source, options);
339359
}
340360

341361
function isUnderNodeModules(filename) {
@@ -354,7 +374,7 @@ module.exports = {
354374
loadBuiltinModule,
355375
makeRequireFunction,
356376
normalizeReferrerURL,
357-
tsParse,
377+
stripTypes,
358378
stripBOM,
359379
toRealPath,
360380
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-transformation",
793+
"enable transformation of TypeScript-only"
794+
"syntax in JavaScript code",
795+
&EnvironmentOptions::experimental_enable_transformation,
796+
kAllowedInEnvvar);
797+
Implies("--experimental-enable-transformation", "--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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { skip, spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { match, strictEqual } from 'node:assert';
4+
import { test } from 'node:test';
5+
6+
if (!process.config.variables.node_use_amaro) skip('Requires Amaro');
7+
8+
test('execute a TypeScript file with transformation enabled', async () => {
9+
const result = await spawnPromisified(process.execPath, [
10+
'--experimental-enable-transformation',
11+
'--no-warnings',
12+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
13+
]);
14+
15+
strictEqual(result.stderr, '');
16+
match(result.stdout, /Hello, TypeScript!/);
17+
strictEqual(result.code, 0);
18+
});
19+
20+
test('execute a TypeScript file with transformation enabled and sourcemaps', async () => {
21+
const result = await spawnPromisified(process.execPath, [
22+
'--experimental-enable-transformation',
23+
'--enable-source-maps',
24+
'--no-warnings',
25+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
26+
]);
27+
28+
strictEqual(result.stderr, '');
29+
match(result.stdout, /Hello, TypeScript!/);
30+
strictEqual(result.code, 0);
31+
});
32+
33+
test('reconstruct error of a TypeScript file with transformation enabled and sourcemaps', async () => {
34+
const result = await spawnPromisified(process.execPath, [
35+
'--experimental-enable-transformation',
36+
'--enable-source-maps',
37+
'--no-warnings',
38+
fixtures.path('typescript/ts/transformation/test-enum-stacktrace.ts'),
39+
]);
40+
41+
match(result.stderr, match(result.stderr, /test-enum-stacktrace\.ts:4:7/));
42+
strictEqual(result.stdout, '');
43+
strictEqual(result.code, 0);
44+
});

0 commit comments

Comments
 (0)