Skip to content

Commit 50afe56

Browse files
vm: add vm.stripTypeScriptTypes(code, options)
1 parent 20d8b85 commit 50afe56

File tree

4 files changed

+200
-31
lines changed

4 files changed

+200
-31
lines changed

doc/api/vm.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,65 @@ local scope, so the value `localVar` is changed. In this way
15641564
`vm.runInThisContext()` is much like an [indirect `eval()` call][], e.g.
15651565
`(0,eval)('code')`.
15661566
1567+
## `vm.stripTypeScriptTypes(code[, options])`
1568+
1569+
<!-- YAML
1570+
added: REPLACEME
1571+
-->
1572+
1573+
> Stability: 1.0 - Early development
1574+
1575+
* `code` {string} The code to strip type annotations from.
1576+
* `options` {Object}
1577+
* `mode` {string} **Default:** `'strip-only'`. Possible values are:
1578+
* `'strip-only'` Only strip type annotations without performing the transformation of TypeScript features.
1579+
* `'transform'` Strip type annotations and transform TypeScript features to JavaScript.
1580+
* `sourceMap` {boolean} **Default:** `false`. Only when `mode` is `'transform'`, if `true`, a source map
1581+
will be generated for the transformed code.
1582+
* `filename` {string} Only when `mode` is `'transform'`, specifies the filename used in the source map.
1583+
* Returns: {string} The code with type annotations stripped.
1584+
1585+
`vm.stripTypeScriptTypes()` removes type annotations from TypeScript code. It
1586+
can be used to strip type annotations from TypeScript code before running it
1587+
with `vm.runInContext()` or `vm.compileFunction()`.
1588+
By default, it will throw an error if the code contains TypeScript features
1589+
that require transformation such as `Enums`,
1590+
see [type-stripping][] for more information.
1591+
When mode is `'transform'`, it also transforms TypeScript features to JavaScript,
1592+
see [transform TypeScript features][] for more information.
1593+
When mode is `'strip-only'`, source maps are not generated, because locations are preserved.
1594+
If `sourceMap` or `filename` is provided, when mode is `'strip-only'`, an error will be thrown.
1595+
1596+
```js
1597+
const vm = require('node:vm');
1598+
1599+
const code = `const a: number = 1;`;
1600+
const strippedCode = vm.stripTypeScriptTypes(code);
1601+
console.log(strippedCode);
1602+
// Prints: const a = 1;
1603+
```
1604+
1605+
When `mode` is `'transform'`, the code is transformed to JavaScript:
1606+
1607+
```js
1608+
const vm = require('node:vm');
1609+
1610+
const code = `
1611+
namespace MathUtil {
1612+
export const add = (a: number, b: number) => a + b;
1613+
}`;
1614+
const strippedCode = vm.stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
1615+
console.log(strippedCode);
1616+
1617+
// Prints:
1618+
// var MathUtil;
1619+
// (function(MathUtil) {
1620+
// MathUtil.add = (a, b)=>a + b;
1621+
// })(MathUtil || (MathUtil = {}));
1622+
1623+
//# sourceMappingURL=data:application/json;base64, ...
1624+
```
1625+
15671626
## Example: Running an HTTP server within a VM
15681627
15691628
When using either [`script.runInThisContext()`][] or
@@ -1982,3 +2041,5 @@ const { Script, SyntheticModule } = require('node:vm');
19822041
[global object]: https://es5.github.io/#x15.1
19832042
[indirect `eval()` call]: https://es5.github.io/#x10.4.2
19842043
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
2044+
[transform TypeScript features]: typescript.md#typescript-features
2045+
[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/vm.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const {
3838
const {
3939
ERR_CONTEXT_NOT_INITIALIZED,
4040
ERR_INVALID_ARG_TYPE,
41+
ERR_INVALID_ARG_VALUE,
4142
} = require('internal/errors').codes;
4243
const {
4344
validateArray,
@@ -67,6 +68,8 @@ const {
6768
vm_dynamic_import_main_context_default,
6869
vm_context_no_contextify,
6970
} = internalBinding('symbols');
71+
const { parseTypeScript } = require('internal/modules/helpers');
72+
const { Buffer } = require('buffer');
7073
const kParsingContext = Symbol('script parsing context');
7174

7275
/**
@@ -400,6 +403,35 @@ const vmConstants = {
400403

401404
ObjectFreeze(vmConstants);
402405

406+
function stripTypeScriptTypes(code, options = kEmptyObject) {
407+
emitExperimentalWarning('vm.stripTypeScriptTypes');
408+
validateObject(options, 'options');
409+
const { mode = 'strip-only', sourceMap = false, filename = '' } = options;
410+
validateOneOf(mode, 'options.mode', ['strip-only', 'transform']);
411+
if (mode === 'strip-only') {
412+
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
413+
validateOneOf(filename, 'options.filename', ['', undefined]);
414+
}
415+
validateBoolean(sourceMap, 'options.sourceMap');
416+
validateString(filename, 'options.filename');
417+
418+
const transformOptions = {
419+
__proto__: null,
420+
mode,
421+
sourceMap,
422+
filename,
423+
};
424+
425+
const { code: transformed, map } = parseTypeScript(code, transformOptions);
426+
if (map) {
427+
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
428+
// base64 transformation, we should change this line.
429+
const base64SourceMap = Buffer.from(map).toString('base64');
430+
return `${transformed}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
431+
}
432+
return transformed;
433+
}
434+
403435
module.exports = {
404436
Script,
405437
createContext,
@@ -411,6 +443,7 @@ module.exports = {
411443
compileFunction,
412444
measureMemory,
413445
constants: vmConstants,
446+
stripTypeScriptTypes,
414447
};
415448

416449
// The vm module is patched to include vm.Module, vm.SourceTextModule

test/parallel/test-vm-strip-types.js

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

0 commit comments

Comments
 (0)