Skip to content

Commit

Permalink
vm: add vm.stripTypeScriptTypes(code, options)
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Oct 7, 2024
1 parent 20d8b85 commit 9ab643f
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 31 deletions.
61 changes: 61 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,65 @@ local scope, so the value `localVar` is changed. In this way
`vm.runInThisContext()` is much like an [indirect `eval()` call][], e.g.
`(0,eval)('code')`.
## `vm.stripTypeScriptTypes(code[, options])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
* `code` {string} The code to strip type annotations from.
* `options` {Object}
* `mode` {string} **Default:** `'strip-only'`. Possible values are:
* `'strip-only'` Only strip type annotations without performing the transformation of TypeScript features.
* `'transform'` Strip type annotations and transform TypeScript features to JavaScript.
* `sourceMap` {boolean} **Default:** `false`. Only when `mode` is `'transform'`, if `true`, a source map
will be generated for the transformed code.
* `filename` {string} Only when `mode` is `'transform'`, specifies the filename used in the source map.
* Returns: {string} The code with type annotations stripped.
`vm.stripTypeScriptTypes()` removes type annotations from TypeScript code. It
can be used to strip type annotations from TypeScript code before running it
with `vm.runInContext()` or `vm.compileFunction()`.
By default, it will throw an error if the code contains TypeScript features
that require transformation such as `Enums`,
see [type-stripping][] for more information.
When mode is `'transform'`, it also transforms TypeScript features to JavaScript,
see [transform TypeScript features][] for more information.
When mode is `'strip-only'`, source maps are not generated, because locations are preserved.
If `sourceMap` or `filename` is provided, when mode is `'strip-only'`, an error will be thrown.
```js
const vm = require('node:vm');
const code = `const a: number = 1;`;
const strippedCode = vm.stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
```
When `mode` is `'transform'`, the code is transformed to JavaScript:
```js
const vm = require('node:vm');
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = vm.stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
console.log(strippedCode);
// Prints:
// var MathUtil;
// (function(MathUtil) {
// MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
//# sourceMappingURL=data:application/json;base64, ...
```
## Example: Running an HTTP server within a VM
When using either [`script.runInThisContext()`][] or
Expand Down Expand Up @@ -1982,3 +2041,5 @@ const { Script, SyntheticModule } = require('node:vm');
[global object]: https://es5.github.io/#x15.1
[indirect `eval()` call]: https://es5.github.io/#x10.4.2
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
[transform TypeScript features]: typescript.md#typescript-features
[type-stripping]: typescript.md#type-stripping
51 changes: 20 additions & 31 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,44 +313,33 @@ function getBuiltinModule(id) {
return normalizedId ? require(normalizedId) : undefined;
}

/**
* TypeScript parsing function, by default Amaro.transformSync.
* @type {Function}
*/
let typeScriptParser;
/**
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
* @type {string}
*/
let typeScriptParsingMode;
/**
* Whether source maps are enabled for TypeScript parsing.
* @type {boolean}
*/
let sourceMapEnabled;
const getTypeScriptParsingMode = getLazy(() =>
(getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
);

/**
* Load the TypeScript parser.
* @param {Function} parser - A function that takes a string of TypeScript code
* and returns an object with a `code` property.
* @returns {Function} The TypeScript parser function.
*/
function loadTypeScriptParser(parser) {
if (typeScriptParser) {
return typeScriptParser;
}
const loadTypeScriptParser = getLazy(() => {
const amaro = require('internal/deps/amaro/dist/index');
return amaro.transformSync;
});

if (parser) {
typeScriptParser = parser;
} else {
const amaro = require('internal/deps/amaro/dist/index');
// Default option for Amaro is to perform Type Stripping only.
typeScriptParsingMode = getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only';
sourceMapEnabled = getOptionValue('--enable-source-maps');
// Curry the transformSync function with the default options.
typeScriptParser = amaro.transformSync;
}
return typeScriptParser;
/**
*
* @param {string} source the source code
* @param {object} options the options to pass to the parser
* @returns {TransformOutput} an object with a `code` property.
*/
function parseTypeScript(source, options) {
const parse = loadTypeScriptParser();
return parse(source, options);
}

/**
Expand All @@ -365,14 +354,13 @@ function loadTypeScriptParser(parser) {
*/
function stripTypeScriptTypes(source, filename) {
assert(typeof source === 'string');
const parse = loadTypeScriptParser();
const options = {
__proto__: null,
mode: typeScriptParsingMode,
sourceMap: sourceMapEnabled,
mode: getTypeScriptParsingMode(),
sourceMap: getOptionValue('--enable-source-maps'),
filename,
};
const { code, map } = parse(source, options);
const { code, map } = parseTypeScript(source, options);
if (map) {
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
// base64 transformation, we should change this line.
Expand Down Expand Up @@ -488,6 +476,7 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
parseTypeScript,
stripTypeScriptTypes,
stringify,
stripBOM,
Expand Down
32 changes: 32 additions & 0 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const {
vm_dynamic_import_main_context_default,
vm_context_no_contextify,
} = internalBinding('symbols');
const { parseTypeScript } = require('internal/modules/helpers');
const { Buffer } = require('buffer');
const kParsingContext = Symbol('script parsing context');

/**
Expand Down Expand Up @@ -400,6 +402,35 @@ const vmConstants = {

ObjectFreeze(vmConstants);

function stripTypeScriptTypes(code, options = kEmptyObject) {
emitExperimentalWarning('vm.stripTypeScriptTypes');
validateObject(options, 'options');
const { mode = 'strip-only', sourceMap = false, filename = '' } = options;
validateOneOf(mode, 'options.mode', ['strip-only', 'transform']);
if (mode === 'strip-only') {
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
validateOneOf(filename, 'options.filename', ['', undefined]);
}
validateBoolean(sourceMap, 'options.sourceMap');
validateString(filename, 'options.filename');

const transformOptions = {
__proto__: null,
mode,
sourceMap,
filename,
};

const { code: transformed, map } = parseTypeScript(code, transformOptions);
if (map) {
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
// base64 transformation, we should change this line.
const base64SourceMap = Buffer.from(map).toString('base64');
return `${transformed}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
}
return transformed;
}

module.exports = {
Script,
createContext,
Expand All @@ -411,6 +442,7 @@ module.exports = {
compileFunction,
measureMemory,
constants: vmConstants,
stripTypeScriptTypes,
};

// The vm module is patched to include vm.Module, vm.SourceTextModule
Expand Down
86 changes: 86 additions & 0 deletions test/parallel/test-vm-strip-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const vm = require('vm');
const { test } = require('node:test');

common.expectWarning(
'ExperimentalWarning',
'vm.stripTypeScriptTypes is an experimental feature and might change at any time',
);

test('vm.stripTypeScriptTypes', () => {
const source = 'const x: number = 1;';
const result = vm.stripTypeScriptTypes(source);
assert.strictEqual(result, 'const x = 1;');
});

test('vm.stripTypeScriptTypes explicit', () => {
const source = 'const x: number = 1;';
const result = vm.stripTypeScriptTypes(source, { mode: 'strip-only' });
assert.strictEqual(result, 'const x = 1;');
});

test('vm.stripTypeScriptTypes invalid mode', () => {
const source = 'const x: number = 1;';
assert.throws(() => vm.stripTypeScriptTypes(source, { mode: 'invalid' }), { code: 'ERR_INVALID_ARG_VALUE' });
});

test('vm.stripTypeScriptTypes sourceMap throws when mode is strip-only', () => {
const source = 'const x: number = 1;';
assert.throws(() => vm.stripTypeScriptTypes(source,
{ mode: 'strip-only', sourceMap: true }),
{ code: 'ERR_INVALID_ARG_VALUE' });
});

test('vm.stripTypeScriptTypes filename throws when mode is strip-only', () => {
const source = 'const x: number = 1;';
assert.throws(() => vm.stripTypeScriptTypes(source,
{ mode: 'strip-only', filename: 'foo.ts' }),
{ code: 'ERR_INVALID_ARG_VALUE' });
});

test('vm.stripTypeScriptTypes source map when mode is transform', () => {
const source = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const result = vm.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true });
const script = new vm.Script(result);
const sourceMap =
{
version: 3,
sources: [
'<anon>',
],
sourcesContent: [
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
],
names: [],
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
};
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
});

test('vm.stripTypeScriptTypes source map when mode is transform and filename', () => {
const source = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const result = vm.stripTypeScriptTypes(source, { mode: 'transform', sourceMap: true, filename: 'test.ts' });
const script = new vm.Script(result);
const sourceMap =
{
version: 3,
sources: [
'test.ts',
],
sourcesContent: [
'\n namespace MathUtil {\n export const add = (a: number, b: number) => a + b;\n }',
],
names: [],
mappings: ';UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'
};
assert(script.sourceMapURL, `sourceMappingURL=data:application/json;base64,${JSON.stringify(sourceMap)}`);
});

0 comments on commit 9ab643f

Please sign in to comment.