Skip to content

Commit 76ff249

Browse files
lib: add parser.stripTypeScriptTypes
1 parent 8af1382 commit 76ff249

File tree

8 files changed

+229
-31
lines changed

8 files changed

+229
-31
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
/doc/api/typescript.md @nodejs/typescript
190190
/test/fixtures/typescript/ @nodejs/typescript
191191
/tools/dep_updaters/update-amaro.sh @nodejs/typescript
192+
/lib/parser.js @nodejs/typescript
192193

193194
# Performance
194195
/benchmark/* @nodejs/performance

.github/label-pr-config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ subSystemLabels:
102102
/^lib\/internal\/modules/: module
103103
/^lib\/internal\/webstreams/: web streams
104104
/^lib\/internal\/test_runner/: test_runner
105+
/^lib\/parser.js$/: strip-types
105106

106107
# All other lib/ files map directly
107108
/^lib\/_(\w+)_\w+\.js?$/: $1 # e.g. _(stream)_wrap
@@ -183,6 +184,7 @@ allJsSubSystems:
183184
- module
184185
- net
185186
- os
187+
- parser
186188
- path
187189
- perf_hooks
188190
- process

doc/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* [Modules: TypeScript](typescript.md)
4545
* [Net](net.md)
4646
* [OS](os.md)
47+
* [Parser](parser.md)
4748
* [Path](path.md)
4849
* [Performance hooks](perf_hooks.md)
4950
* [Permissions](permissions.md)

doc/api/parser.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Path
2+
3+
<!--introduced_in=REPLACEME-->
4+
5+
> Stability: 1.0 - Early Development
6+
7+
<!-- source_link=lib/parser.js -->
8+
9+
The `node:parser` module provides utilities for working with source code. It can be accessed using:
10+
11+
```js
12+
const parser = require('node:parser');
13+
```
14+
15+
## `parser.stripTypeScriptTypes(code[, options])`
16+
17+
<!-- YAML
18+
added: REPLACEME
19+
-->
20+
21+
> Stability: 1.0 - Early development
22+
23+
* `code` {string} The code to strip type annotations from.
24+
* `options` {Object}
25+
* `mode` {string} **Default:** `'strip-only'`. Possible values are:
26+
* `'strip-only'` Only strip type annotations without performing the transformation of TypeScript features.
27+
* `'transform'` Strip type annotations and transform TypeScript features to JavaScript.
28+
* `sourceMap` {boolean} **Default:** `false`. Only when `mode` is `'transform'`, if `true`, a source map
29+
will be generated for the transformed code.
30+
* `filename` {string} Only when `mode` is `'transform'`, specifies the filename used in the source map.
31+
* Returns: {string} The code with type annotations stripped.
32+
`parser.stripTypeScriptTypes()` removes type annotations from TypeScript code. It
33+
can be used to strip type annotations from TypeScript code before running it
34+
with `vm.runInContext()` or `vm.compileFunction()`.
35+
By default, it will throw an error if the code contains TypeScript features
36+
that require transformation such as `Enums`,
37+
see [type-stripping][] for more information.
38+
When mode is `'transform'`, it also transforms TypeScript features to JavaScript,
39+
see [transform TypeScript features][] for more information.
40+
When mode is `'strip-only'`, source maps are not generated, because locations are preserved.
41+
If `sourceMap` or `filename` is provided, when mode is `'strip-only'`, an error will be thrown.
42+
43+
```js
44+
const parser = require('node:parser');
45+
const code = `const a: number = 1;`;
46+
const strippedCode = parser.stripTypeScriptTypes(code);
47+
console.log(strippedCode);
48+
// Prints: const a = 1;
49+
```
50+
51+
When `mode` is `'transform'`, the code is transformed to JavaScript:
52+
53+
```js
54+
const parser = require('node:parser');
55+
const code = `
56+
namespace MathUtil {
57+
export const add = (a: number, b: number) => a + b;
58+
}`;
59+
const strippedCode = parser.stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
60+
console.log(strippedCode);
61+
// Prints:
62+
// var MathUtil;
63+
// (function(MathUtil) {
64+
// MathUtil.add = (a, b)=>a + b;
65+
// })(MathUtil || (MathUtil = {}));
66+
//# sourceMappingURL=data:application/json;base64, ...
67+
```
68+
69+
[transform TypeScript features]: typescript.md#typescript-features
70+
[type-stripping]: typescript.md#type-stripping

lib/internal/modules/helpers.js

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -313,44 +313,33 @@ function getBuiltinModule(id) {
313313
return normalizedId ? require(normalizedId) : undefined;
314314
}
315315

316-
/**
317-
* TypeScript parsing function, by default Amaro.transformSync.
318-
* @type {Function}
319-
*/
320-
let typeScriptParser;
321316
/**
322317
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
323318
* @type {string}
324319
*/
325-
let typeScriptParsingMode;
326-
/**
327-
* Whether source maps are enabled for TypeScript parsing.
328-
* @type {boolean}
329-
*/
330-
let sourceMapEnabled;
320+
const getTypeScriptParsingMode = getLazy(() =>
321+
(getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
322+
);
331323

332324
/**
333325
* Load the TypeScript parser.
334-
* @param {Function} parser - A function that takes a string of TypeScript code
335326
* and returns an object with a `code` property.
336327
* @returns {Function} The TypeScript parser function.
337328
*/
338-
function loadTypeScriptParser(parser) {
339-
if (typeScriptParser) {
340-
return typeScriptParser;
341-
}
329+
const loadTypeScriptParser = getLazy(() => {
330+
const amaro = require('internal/deps/amaro/dist/index');
331+
return amaro.transformSync;
332+
});
342333

343-
if (parser) {
344-
typeScriptParser = parser;
345-
} else {
346-
const amaro = require('internal/deps/amaro/dist/index');
347-
// Default option for Amaro is to perform Type Stripping only.
348-
typeScriptParsingMode = getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only';
349-
sourceMapEnabled = getOptionValue('--enable-source-maps');
350-
// Curry the transformSync function with the default options.
351-
typeScriptParser = amaro.transformSync;
352-
}
353-
return typeScriptParser;
334+
/**
335+
*
336+
* @param {string} source the source code
337+
* @param {object} options the options to pass to the parser
338+
* @returns {TransformOutput} an object with a `code` property.
339+
*/
340+
function parseTypeScript(source, options) {
341+
const parse = loadTypeScriptParser();
342+
return parse(source, options);
354343
}
355344

356345
/**
@@ -365,14 +354,13 @@ function loadTypeScriptParser(parser) {
365354
*/
366355
function stripTypeScriptTypes(source, filename) {
367356
assert(typeof source === 'string');
368-
const parse = loadTypeScriptParser();
369357
const options = {
370358
__proto__: null,
371-
mode: typeScriptParsingMode,
372-
sourceMap: sourceMapEnabled,
359+
mode: getTypeScriptParsingMode(),
360+
sourceMap: getOptionValue('--enable-source-maps'),
373361
filename,
374362
};
375-
const { code, map } = parse(source, options);
363+
const { code, map } = parseTypeScript(source, options);
376364
if (map) {
377365
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
378366
// base64 transformation, we should change this line.
@@ -488,6 +476,7 @@ module.exports = {
488476
loadBuiltinModule,
489477
makeRequireFunction,
490478
normalizeReferrerURL,
479+
parseTypeScript,
491480
stripTypeScriptTypes,
492481
stringify,
493482
stripBOM,

lib/parser.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const {
4+
validateBoolean,
5+
validateOneOf,
6+
validateObject,
7+
validateString,
8+
} = require('internal/validators');
9+
const {
10+
emitExperimentalWarning,
11+
kEmptyObject,
12+
} = require('internal/util');
13+
const { parseTypeScript } = require('internal/modules/helpers');
14+
const { Buffer } = require('buffer');
15+
16+
function stripTypeScriptTypes(code, options = kEmptyObject) {
17+
emitExperimentalWarning('parser.stripTypeScriptTypes');
18+
validateObject(options, 'options');
19+
const { mode = 'strip-only', sourceMap = false, filename = '' } = options;
20+
validateOneOf(mode, 'options.mode', ['strip-only', 'transform']);
21+
if (mode === 'strip-only') {
22+
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
23+
validateOneOf(filename, 'options.filename', ['', undefined]);
24+
}
25+
validateBoolean(sourceMap, 'options.sourceMap');
26+
validateString(filename, 'options.filename');
27+
28+
const transformOptions = {
29+
__proto__: null,
30+
mode,
31+
sourceMap,
32+
filename,
33+
};
34+
35+
const { code: transformed, map } = parseTypeScript(code, transformOptions);
36+
if (map) {
37+
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
38+
// base64 transformation, we should change this line.
39+
const base64SourceMap = Buffer.from(map).toString('base64');
40+
return `${transformed}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
41+
}
42+
return transformed;
43+
}
44+
45+
module.exports = {
46+
stripTypeScriptTypes,
47+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const parser = require('node:parser');
6+
const vm = require('node:vm');
7+
const { test } = require('node:test');
8+
9+
common.expectWarning(
10+
'ExperimentalWarning',
11+
'parser.stripTypeScriptTypes is an experimental feature and might change at any time',
12+
);
13+
14+
test('parser.stripTypeScriptTypes', () => {
15+
const source = 'const x: number = 1;';
16+
const result = parser.stripTypeScriptTypes(source);
17+
assert.strictEqual(result, 'const x = 1;');
18+
});
19+
20+
test('parser.stripTypeScriptTypes explicit', () => {
21+
const source = 'const x: number = 1;';
22+
const result = parser.stripTypeScriptTypes(source, { mode: 'strip-only' });
23+
assert.strictEqual(result, 'const x = 1;');
24+
});
25+
26+
test('parser.stripTypeScriptTypes invalid mode', () => {
27+
const source = 'const x: number = 1;';
28+
assert.throws(() => parser.stripTypeScriptTypes(source, { mode: 'invalid' }), { code: 'ERR_INVALID_ARG_VALUE' });
29+
});
30+
31+
test('parser.stripTypeScriptTypes sourceMap throws when mode is strip-only', () => {
32+
const source = 'const x: number = 1;';
33+
assert.throws(() => parser.stripTypeScriptTypes(source,
34+
{ mode: 'strip-only', sourceMap: true }),
35+
{ code: 'ERR_INVALID_ARG_VALUE' });
36+
});
37+
38+
test('parser.stripTypeScriptTypes filename throws when mode is strip-only', () => {
39+
const source = 'const x: number = 1;';
40+
assert.throws(() => parser.stripTypeScriptTypes(source,
41+
{ mode: 'strip-only', filename: 'foo.ts' }),
42+
{ code: 'ERR_INVALID_ARG_VALUE' });
43+
});
44+
45+
test('parser.stripTypeScriptTypes source map when mode is transform', () => {
46+
const source = `
47+
namespace MathUtil {
48+
export const add = (a: number, b: number) => a + b;
49+
}`;
50+
const result = parser.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true });
51+
const script = new vm.Script(result);
52+
const sourceMap =
53+
{
54+
version: 3,
55+
sources: [
56+
'<anon>',
57+
],
58+
sourcesContent: [
59+
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
60+
],
61+
names: [],
62+
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
63+
};
64+
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
65+
});
66+
67+
test('parser.stripTypeScriptTypes source map when mode is transform and filename', () => {
68+
const source = `
69+
namespace MathUtil {
70+
export const add = (a: number, b: number) => a + b;
71+
}`;
72+
const result = parser.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true, filename: 'test.ts' });
73+
const script = new vm.Script(result);
74+
const sourceMap =
75+
{
76+
version: 3,
77+
sources: [
78+
'test.ts',
79+
],
80+
sourcesContent: [
81+
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
82+
],
83+
names: [],
84+
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
85+
};
86+
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
87+
});

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"module": ["./lib/module.js"],
5454
"net": ["./lib/net.js"],
5555
"os": ["./lib/os.js"],
56+
"parser": ["./lib/parser.js"],
5657
"path": ["./lib/path.js"],
5758
"path/posix": ["./lib/path/posix.js"],
5859
"path/win32": ["./lib/path/win32.js"],

0 commit comments

Comments
 (0)