Skip to content

Commit 69dd1e1

Browse files
marco-ippolitoRafaelGSS
authored andcommitted
module: add module.stripTypeScriptTypes
PR-URL: #55282 Fixes: #54300 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Richard Lau <rlau@redhat.com>
1 parent 0ea74f3 commit 69dd1e1

File tree

10 files changed

+358
-88
lines changed

10 files changed

+358
-88
lines changed

doc/api/module.md

+101
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,105 @@ changes:
270270
Register a module that exports [hooks][] that customize Node.js module
271271
resolution and loading behavior. See [Customization hooks][].
272272
273+
## `module.stripTypeScriptTypes(code[, options])`
274+
275+
<!-- YAML
276+
added: REPLACEME
277+
-->
278+
279+
> Stability: 1.0 - Early development
280+
281+
* `code` {string} The code to strip type annotations from.
282+
* `options` {Object}
283+
* `mode` {string} **Default:** `'strip'`. Possible values are:
284+
* `'strip'` Only strip type annotations without performing the transformation of TypeScript features.
285+
* `'transform'` Strip type annotations and transform TypeScript features to JavaScript.
286+
* `sourceMap` {boolean} **Default:** `false`. Only when `mode` is `'transform'`, if `true`, a source map
287+
will be generated for the transformed code.
288+
* `sourceUrl` {string} Specifies the source url used in the source map.
289+
* Returns: {string} The code with type annotations stripped.
290+
`module.stripTypeScriptTypes()` removes type annotations from TypeScript code. It
291+
can be used to strip type annotations from TypeScript code before running it
292+
with `vm.runInContext()` or `vm.compileFunction()`.
293+
By default, it will throw an error if the code contains TypeScript features
294+
that require transformation such as `Enums`,
295+
see [type-stripping][] for more information.
296+
When mode is `'transform'`, it also transforms TypeScript features to JavaScript,
297+
see [transform TypeScript features][] for more information.
298+
When mode is `'strip'`, source maps are not generated, because locations are preserved.
299+
If `sourceMap` is provided, when mode is `'strip'`, an error will be thrown.
300+
301+
_WARNING_: The output of this function should not be considered stable across Node.js versions,
302+
due to changes in the TypeScript parser.
303+
304+
```mjs
305+
import { stripTypeScriptTypes } from 'node:module';
306+
const code = 'const a: number = 1;';
307+
const strippedCode = stripTypeScriptTypes(code);
308+
console.log(strippedCode);
309+
// Prints: const a = 1;
310+
```
311+
312+
```cjs
313+
const { stripTypeScriptTypes } = require('node:module');
314+
const code = 'const a: number = 1;';
315+
const strippedCode = stripTypeScriptTypes(code);
316+
console.log(strippedCode);
317+
// Prints: const a = 1;
318+
```
319+
320+
If `sourceUrl` is provided, it will be used appended as a comment at the end of the output:
321+
322+
```mjs
323+
import { stripTypeScriptTypes } from 'node:module';
324+
const code = 'const a: number = 1;';
325+
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
326+
console.log(strippedCode);
327+
// Prints: const a = 1\n\n//# sourceURL=source.ts;
328+
```
329+
330+
```cjs
331+
const { stripTypeScriptTypes } = require('node:module');
332+
const code = 'const a: number = 1;';
333+
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
334+
console.log(strippedCode);
335+
// Prints: const a = 1\n\n//# sourceURL=source.ts;
336+
```
337+
338+
When `mode` is `'transform'`, the code is transformed to JavaScript:
339+
340+
```mjs
341+
import { stripTypeScriptTypes } from 'node:module';
342+
const code = `
343+
namespace MathUtil {
344+
export const add = (a: number, b: number) => a + b;
345+
}`;
346+
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
347+
console.log(strippedCode);
348+
// Prints:
349+
// var MathUtil;
350+
// (function(MathUtil) {
351+
// MathUtil.add = (a, b)=>a + b;
352+
// })(MathUtil || (MathUtil = {}));
353+
// # sourceMappingURL=data:application/json;base64, ...
354+
```
355+
356+
```cjs
357+
const { stripTypeScriptTypes } = require('node:module');
358+
const code = `
359+
namespace MathUtil {
360+
export const add = (a: number, b: number) => a + b;
361+
}`;
362+
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
363+
console.log(strippedCode);
364+
// Prints:
365+
// var MathUtil;
366+
// (function(MathUtil) {
367+
// MathUtil.add = (a, b)=>a + b;
368+
// })(MathUtil || (MathUtil = {}));
369+
// # sourceMappingURL=data:application/json;base64, ...
370+
```
371+
273372
### `module.syncBuiltinESMExports()`
274373
275374
<!-- YAML
@@ -1251,3 +1350,5 @@ returned object contains the following keys:
12511350
[realm]: https://tc39.es/ecma262/#realm
12521351
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
12531352
[transferable objects]: worker_threads.md#portpostmessagevalue-transferlist
1353+
[transform TypeScript features]: typescript.md#typescript-features
1354+
[type-stripping]: typescript.md#type-stripping

lib/internal/main/eval_string.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const {
1414
markBootstrapComplete,
1515
} = require('internal/process/pre_execution');
1616
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
17-
const { addBuiltinLibsToObject, stripTypeScriptTypes } = require('internal/modules/helpers');
18-
17+
const { addBuiltinLibsToObject } = require('internal/modules/helpers');
18+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
1919
const { getOptionValue } = require('internal/options');
2020

2121
prepareMainThreadExecution();
@@ -24,7 +24,7 @@ markBootstrapComplete();
2424

2525
const code = getOptionValue('--eval');
2626
const source = getOptionValue('--experimental-strip-types') ?
27-
stripTypeScriptTypes(code) :
27+
stripTypeScriptModuleTypes(code) :
2828
code;
2929

3030
const print = getOptionValue('--print');

lib/internal/modules/cjs/loader.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ const {
151151
setHasStartedUserCJSExecution,
152152
stripBOM,
153153
toRealPath,
154-
stripTypeScriptTypes,
155154
} = require('internal/modules/helpers');
155+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
156156
const packageJsonReader = require('internal/modules/package_json_reader');
157157
const { getOptionValue, getEmbedderOptions } = require('internal/options');
158158
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
@@ -1357,7 +1357,7 @@ let hasPausedEntry = false;
13571357
function loadESMFromCJS(mod, filename) {
13581358
let source = getMaybeCachedSource(mod, filename);
13591359
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
1360-
source = stripTypeScriptTypes(source, filename);
1360+
source = stripTypeScriptModuleTypes(source, filename);
13611361
}
13621362
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13631363
const isMain = mod[kIsMainSymbol];
@@ -1599,7 +1599,7 @@ function getMaybeCachedSource(mod, filename) {
15991599

16001600
function loadCTS(module, filename) {
16011601
const source = getMaybeCachedSource(module, filename);
1602-
const code = stripTypeScriptTypes(source, filename);
1602+
const code = stripTypeScriptModuleTypes(source, filename);
16031603
module._compile(code, filename, 'commonjs');
16041604
}
16051605

@@ -1611,7 +1611,7 @@ function loadCTS(module, filename) {
16111611
function loadTS(module, filename) {
16121612
// If already analyzed the source, then it will be cached.
16131613
const source = getMaybeCachedSource(module, filename);
1614-
const content = stripTypeScriptTypes(source, filename);
1614+
const content = stripTypeScriptModuleTypes(source, filename);
16151615
let format;
16161616
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
16171617
// Function require shouldn't be used in ES modules.
@@ -1631,7 +1631,7 @@ function loadTS(module, filename) {
16311631
if (Module._cache[parentPath]) {
16321632
let parentSource;
16331633
try {
1634-
parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
1634+
parentSource = stripTypeScriptModuleTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
16351635
} catch {
16361636
// Continue regardless of error.
16371637
}

lib/internal/modules/esm/get_format.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
164164
// Since experimental-strip-types depends on detect-module, we always return null
165165
// if source is undefined.
166166
if (!source) { return null; }
167-
const { stripTypeScriptTypes, stringify } = require('internal/modules/helpers');
167+
const { stringify } = require('internal/modules/helpers');
168+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
168169
const stringifiedSource = stringify(source);
169-
const parsedSource = stripTypeScriptTypes(stringifiedSource, fileURLToPath(url));
170+
const parsedSource = stripTypeScriptModuleTypes(stringifiedSource, fileURLToPath(url));
170171
const detectedFormat = detectModuleFormat(parsedSource, url);
171172
const format = `${detectedFormat}-typescript`;
172173
if (format === 'module-typescript' && foundPackageJson) {

lib/internal/modules/esm/translators.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ const {
3131
assertBufferSource,
3232
loadBuiltinModule,
3333
stringify,
34-
stripTypeScriptTypes,
3534
stripBOM,
3635
urlToFilename,
3736
} = require('internal/modules/helpers');
37+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
3838
const {
3939
kIsCachedByESMLoader,
4040
Module: CJSModule,
@@ -248,7 +248,7 @@ translators.set('require-commonjs', (url, source, isMain) => {
248248
translators.set('require-commonjs-typescript', (url, source, isMain) => {
249249
emitExperimentalWarning('Type Stripping');
250250
assert(cjsParse);
251-
const code = stripTypeScriptTypes(stringify(source), url);
251+
const code = stripTypeScriptModuleTypes(stringify(source), url);
252252
return createCJSModuleWrap(url, code);
253253
});
254254

@@ -463,7 +463,7 @@ translators.set('wasm', async function(url, source) {
463463
translators.set('commonjs-typescript', function(url, source) {
464464
emitExperimentalWarning('Type Stripping');
465465
assertBufferSource(source, true, 'load');
466-
const code = stripTypeScriptTypes(stringify(source), url);
466+
const code = stripTypeScriptModuleTypes(stringify(source), url);
467467
debug(`Translating TypeScript ${url}`);
468468
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
469469
});
@@ -472,7 +472,7 @@ translators.set('commonjs-typescript', function(url, source) {
472472
translators.set('module-typescript', function(url, source) {
473473
emitExperimentalWarning('Type Stripping');
474474
assertBufferSource(source, true, 'load');
475-
const code = stripTypeScriptTypes(stringify(source), url);
475+
const code = stripTypeScriptModuleTypes(stringify(source), url);
476476
debug(`Translating TypeScript ${url}`);
477477
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
478478
});

lib/internal/modules/helpers.js

+1-73
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ const {
1515
const {
1616
ERR_INVALID_ARG_TYPE,
1717
ERR_INVALID_RETURN_PROPERTY_VALUE,
18-
ERR_INVALID_TYPESCRIPT_SYNTAX,
19-
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
2018
} = require('internal/errors').codes;
2119
const { BuiltinModule } = require('internal/bootstrap/realm');
2220

@@ -27,9 +25,8 @@ const path = require('path');
2725
const { pathToFileURL, fileURLToPath } = require('internal/url');
2826
const assert = require('internal/assert');
2927

30-
const { Buffer } = require('buffer');
3128
const { getOptionValue } = require('internal/options');
32-
const { assertTypeScript, setOwnProperty, getLazy, isUnderNodeModules } = require('internal/util');
29+
const { setOwnProperty, getLazy } = require('internal/util');
3330
const { inspect } = require('internal/util/inspect');
3431

3532
const lazyTmpdir = getLazy(() => require('os').tmpdir());
@@ -314,74 +311,6 @@ function getBuiltinModule(id) {
314311
return normalizedId ? require(normalizedId) : undefined;
315312
}
316313

317-
/**
318-
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
319-
* @type {string}
320-
*/
321-
const getTypeScriptParsingMode = getLazy(() =>
322-
(getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
323-
);
324-
325-
/**
326-
* Load the TypeScript parser.
327-
* and returns an object with a `code` property.
328-
* @returns {Function} The TypeScript parser function.
329-
*/
330-
const loadTypeScriptParser = getLazy(() => {
331-
assertTypeScript();
332-
const amaro = require('internal/deps/amaro/dist/index');
333-
return amaro.transformSync;
334-
});
335-
336-
/**
337-
*
338-
* @param {string} source the source code
339-
* @param {object} options the options to pass to the parser
340-
* @returns {TransformOutput} an object with a `code` property.
341-
*/
342-
function parseTypeScript(source, options) {
343-
const parse = loadTypeScriptParser();
344-
try {
345-
return parse(source, options);
346-
} catch (error) {
347-
throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error);
348-
}
349-
}
350-
351-
/**
352-
* @typedef {object} TransformOutput
353-
* @property {string} code The compiled code.
354-
* @property {string} [map] The source maps (optional).
355-
*
356-
* Performs type-stripping to TypeScript source code.
357-
* @param {string} source TypeScript code to parse.
358-
* @param {string} filename The filename of the source code.
359-
* @returns {TransformOutput} The stripped TypeScript code.
360-
*/
361-
function stripTypeScriptTypes(source, filename) {
362-
if (isUnderNodeModules(filename)) {
363-
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
364-
}
365-
assert(typeof source === 'string');
366-
const options = {
367-
__proto__: null,
368-
mode: getTypeScriptParsingMode(),
369-
sourceMap: getOptionValue('--enable-source-maps'),
370-
filename,
371-
};
372-
const { code, map } = parseTypeScript(source, options);
373-
if (map) {
374-
// TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
375-
// base64 transformation, we should change this line.
376-
const base64SourceMap = Buffer.from(map).toString('base64');
377-
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
378-
}
379-
// Source map is not necessary in strip-only mode. However, to map the source
380-
// file in debuggers to the original TypeScript source, add a sourceURL magic
381-
// comment to hint that it is a generated source.
382-
return `${code}\n\n//# sourceURL=${filename}`;
383-
}
384-
385314
/** @type {import('internal/util/types')} */
386315
let _TYPES = null;
387316
/**
@@ -485,7 +414,6 @@ module.exports = {
485414
loadBuiltinModule,
486415
makeRequireFunction,
487416
normalizeReferrerURL,
488-
stripTypeScriptTypes,
489417
stringify,
490418
stripBOM,
491419
toRealPath,

0 commit comments

Comments
 (0)