Skip to content

Commit 80fa828

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

File tree

4 files changed

+189
-31
lines changed

4 files changed

+189
-31
lines changed

doc/api/vm.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,61 @@ 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`. If `true`, a source map will be generated for the transformed code when `mode` is `'transform'`.
1581+
* `filename` {string} Specifies the filename used in the source map.
1582+
* Returns: {string} The code with type annotations stripped.
1583+
1584+
`vm.stripTypeScriptTypes()` removes type annotations from TypeScript code. It
1585+
can be used to strip type annotations from TypeScript code before running it
1586+
with `vm.runInContext()` or `vm.compileFunction()`.
1587+
By default, it will throw an error if the code contains TypeScript features
1588+
that require transformation such as `Enums`, see [type-stripping][] for more information.
1589+
When mode is `'transform'`, it also transforms TypeScript features to JavaScript, see [transform TypeScript features][] for more information.
1590+
When mode is `'strip-only'`, source maps are not generated, because locations are preserved.
1591+
1592+
```js
1593+
const vm = require('node:vm');
1594+
1595+
const code = `const a: number = 1;`;
1596+
const strippedCode = vm.stripTypeScriptTypes(code);
1597+
console.log(strippedCode);
1598+
// Prints: const a = 1;
1599+
```
1600+
1601+
When `mode` is `'transform'`, the code is transformed to JavaScript:
1602+
1603+
```js
1604+
const vm = require('node:vm');
1605+
1606+
const code = `
1607+
namespace MathUtil {
1608+
export const add = (a: number, b: number) => a + b;
1609+
}`;
1610+
const strippedCode = vm.stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
1611+
console.log(strippedCode);
1612+
1613+
// Prints:
1614+
// var MathUtil;
1615+
// (function(MathUtil) {
1616+
// MathUtil.add = (a, b)=>a + b;
1617+
// })(MathUtil || (MathUtil = {}));
1618+
1619+
//# sourceMappingURL=data:application/json;base64, ...
1620+
```
1621+
15671622
## Example: Running an HTTP server within a VM
15681623
15691624
When using either [`script.runInThisContext()`][] or
@@ -1982,3 +2037,5 @@ const { Script, SyntheticModule } = require('node:vm');
19822037
[global object]: https://es5.github.io/#x15.1
19832038
[indirect `eval()` call]: https://es5.github.io/#x10.4.2
19842039
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
2040+
[transform TypeScript features]: typescript.md#typescript-features
2041+
[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 = {}) {
407+
emitExperimentalWarning('vm.stripTypeScriptTypes');
408+
const { mode = 'strip-only', sourceMap = false, filename } = options;
409+
validateObject(options, 'options');
410+
validateString(mode, 'options.mode');
411+
if (mode !== 'strip-only' && mode !== 'transform') {
412+
throw new ERR_INVALID_ARG_VALUE('options.mode', mode, 'must be either ' +
413+
'"strip-only" or "transform"');
414+
}
415+
416+
validateBoolean(sourceMap, 'options.sourceMap');
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 sourceMap are ignored when mode is strip-only', () => {
26+
const source = 'const x: number = 1;';
27+
const result = vm.stripTypeScriptTypes(source, { mode: 'strip-only', sourceMap: true });
28+
assert.strictEqual(result, 'const x = 1;');
29+
});
30+
31+
test('vm.stripTypeScriptTypes source map is ignored when mode is strip-only', () => {
32+
const source = 'const x: number = 1;';
33+
const result = vm.stripTypeScriptTypes(source, { mode: 'strip-only', sourceMap: true });
34+
assert.strictEqual(result, 'const x = 1;');
35+
});
36+
37+
test('vm.stripTypeScriptTypes source map when mode is transform', () => {
38+
const source = `
39+
namespace MathUtil {
40+
export const add = (a: number, b: number) => a + b;
41+
}`;
42+
const result = vm.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true });
43+
const script = new vm.Script(result);
44+
const sourceMap =
45+
{
46+
version: 3,
47+
sources: [
48+
'<anon>',
49+
],
50+
sourcesContent: [
51+
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
52+
],
53+
names: [],
54+
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
55+
};
56+
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
57+
});
58+
59+
test('vm.stripTypeScriptTypes source map when mode is transform and filename', () => {
60+
const source = `
61+
namespace MathUtil {
62+
export const add = (a: number, b: number) => a + b;
63+
}`;
64+
const result = vm.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true, filename: 'test.ts' });
65+
const script = new vm.Script(result);
66+
const sourceMap =
67+
{
68+
version: 3,
69+
sources: [
70+
'test.ts',
71+
],
72+
sourcesContent: [
73+
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
74+
],
75+
names: [],
76+
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
77+
};
78+
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
79+
});

0 commit comments

Comments
 (0)