Skip to content

Commit c5da92e

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

File tree

13 files changed

+217
-24
lines changed

13 files changed

+217
-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+
Impiles `--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) :
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-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);
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-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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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, /test-enum-stacktrace\.ts:4:7/);
42+
strictEqual(result.stdout, '');
43+
strictEqual(result.code, 1);
44+
});
45+
46+
test('reconstruct error of a complex TypeScript file with transformation enabled and sourcemaps', async () => {
47+
const result = await spawnPromisified(process.execPath, [
48+
'--experimental-enable-transformation',
49+
'--enable-source-maps',
50+
'--no-warnings',
51+
fixtures.path('typescript/ts/transformation/test-complex-stacktrace.ts'),
52+
]);
53+
54+
match(result.stderr, /Calculation failed: Division by zero!/);
55+
match(result.stderr, /test-complex-stacktrace\.ts:64/);
56+
match(result.stderr, /test-complex-stacktrace\.ts:64:19/);
57+
strictEqual(result.stdout, '');
58+
strictEqual(result.code, 1);
59+
});
60+
61+
test('reconstruct error of a complex TypeScript file with transformation enabled without sourcemaps', async () => {
62+
const result = await spawnPromisified(process.execPath, [
63+
'--experimental-enable-transformation',
64+
'--no-warnings',
65+
fixtures.path('typescript/ts/transformation/test-complex-stacktrace.ts'),
66+
]);
67+
// The stack trace is not reconstructed without sourcemaps.
68+
match(result.stderr, /Calculation failed: Division by zero!/);
69+
match(result.stderr, /test-complex-stacktrace\.ts:50/);
70+
match(result.stderr, /test-complex-stacktrace\.ts:50:19/);
71+
strictEqual(result.stdout, '');
72+
strictEqual(result.code, 1);
73+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Namespace
2+
namespace Mathematics {
3+
// Enum
4+
export enum Operation {
5+
Add,
6+
Subtract,
7+
Multiply,
8+
Divide
9+
}
10+
11+
// Interface
12+
export interface MathOperation {
13+
perform(a: number, b: number): number;
14+
}
15+
16+
// Generic function
17+
export function executeOperation<T extends MathOperation>(op: T, x: number, y: number): number {
18+
return op.perform(x, y);
19+
}
20+
21+
// Class implementing interface
22+
export class Calculator implements MathOperation {
23+
constructor(private op: Operation) { }
24+
25+
perform(a: number, b: number): number {
26+
switch (this.op) {
27+
case Operation.Add: return a + b;
28+
case Operation.Subtract: return a - b;
29+
case Operation.Multiply: return a * b;
30+
case Operation.Divide:
31+
if (b === 0) throw new Error("Division by zero!");
32+
return a / b;
33+
default:
34+
throw new Error("Unknown operation");
35+
}
36+
}
37+
}
38+
}
39+
40+
// Using the namespace
41+
const calc = new Mathematics.Calculator(Mathematics.Operation.Divide);
42+
43+
// Union type
44+
type Result = number | string;
45+
46+
// Function with rest parameters and arrow syntax
47+
const processResults = (...results: Result[]): string => {
48+
return results.map(r => r.toString()).join(", ");
49+
};
50+
51+
// Async function with await
52+
async function performAsyncCalculation(a: number, b: number): Promise<number> {
53+
const result = await Promise.resolve(Mathematics.executeOperation(calc, a, b));
54+
return result;
55+
}
56+
57+
// IIFE to create a block scope
58+
(async () => {
59+
try {
60+
const result = await performAsyncCalculation(10, 0);
61+
} catch (error) {
62+
if (error instanceof Error) {
63+
// This line will throw an error
64+
throw new Error("Calculation failed: " + error.message);
65+
}
66+
}
67+
})();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
enum Foo {
2+
A = "Hello, TypeScript!",
3+
}
4+
throw new Error(Foo.A);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
enum Foo {
2+
A = "Hello, TypeScript!",
3+
}
4+
console.log(Foo.A);

0 commit comments

Comments
 (0)