Skip to content

Commit

Permalink
lib: add parser.stripTypeScriptTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Oct 7, 2024
1 parent 8af1382 commit 76ff249
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 31 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
/doc/api/typescript.md @nodejs/typescript
/test/fixtures/typescript/ @nodejs/typescript
/tools/dep_updaters/update-amaro.sh @nodejs/typescript
/lib/parser.js @nodejs/typescript

# Performance
/benchmark/* @nodejs/performance
2 changes: 2 additions & 0 deletions .github/label-pr-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ subSystemLabels:
/^lib\/internal\/modules/: module
/^lib\/internal\/webstreams/: web streams
/^lib\/internal\/test_runner/: test_runner
/^lib\/parser.js$/: strip-types

# All other lib/ files map directly
/^lib\/_(\w+)_\w+\.js?$/: $1 # e.g. _(stream)_wrap
Expand Down Expand Up @@ -183,6 +184,7 @@ allJsSubSystems:
- module
- net
- os
- parser
- path
- perf_hooks
- process
Expand Down
1 change: 1 addition & 0 deletions doc/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* [Modules: TypeScript](typescript.md)
* [Net](net.md)
* [OS](os.md)
* [Parser](parser.md)
* [Path](path.md)
* [Performance hooks](perf_hooks.md)
* [Permissions](permissions.md)
Expand Down
70 changes: 70 additions & 0 deletions doc/api/parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Path

<!--introduced_in=REPLACEME-->

> Stability: 1.0 - Early Development
<!-- source_link=lib/parser.js -->

The `node:parser` module provides utilities for working with source code. It can be accessed using:

```js
const parser = require('node:parser');
```

## `parser.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.
`parser.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 parser = require('node:parser');
const code = `const a: number = 1;`;
const strippedCode = parser.stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
```

When `mode` is `'transform'`, the code is transformed to JavaScript:

```js
const parser = require('node:parser');
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = parser.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, ...
```

[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
47 changes: 47 additions & 0 deletions lib/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const {
validateBoolean,
validateOneOf,
validateObject,
validateString,
} = require('internal/validators');
const {
emitExperimentalWarning,
kEmptyObject,
} = require('internal/util');
const { parseTypeScript } = require('internal/modules/helpers');
const { Buffer } = require('buffer');

function stripTypeScriptTypes(code, options = kEmptyObject) {
emitExperimentalWarning('parser.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 = {
stripTypeScriptTypes,
};
87 changes: 87 additions & 0 deletions test/parallel/test-parser-strip-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

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

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

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

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

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

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

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

test('parser.stripTypeScriptTypes source map when mode is transform', () => {
const source = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const result = parser.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('parser.stripTypeScriptTypes source map when mode is transform and filename', () => {
const source = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const result = parser.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)}`);
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"module": ["./lib/module.js"],
"net": ["./lib/net.js"],
"os": ["./lib/os.js"],
"parser": ["./lib/parser.js"],
"path": ["./lib/path.js"],
"path/posix": ["./lib/path/posix.js"],
"path/win32": ["./lib/path/win32.js"],
Expand Down

0 comments on commit 76ff249

Please sign in to comment.