Skip to content

Commit

Permalink
module: add module.stripTypeScriptTypes
Browse files Browse the repository at this point in the history
PR-URL: #55282
Fixes: #54300
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Joyee Cheung <[email protected]>
Reviewed-By: Chemi Atlow <[email protected]>
Reviewed-By: Paolo Insogna <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Richard Lau <[email protected]>
  • Loading branch information
marco-ippolito committed Dec 18, 2024
1 parent ec6a002 commit 2ee876c
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 89 deletions.
101 changes: 101 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,105 @@ changes:
Register a module that exports [hooks][] that customize Node.js module
resolution and loading behavior. See [Customization hooks][].
## `module.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'`. Possible values are:
* `'strip'` 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.
* `sourceUrl` {string} Specifies the source url used in the source map.
* Returns: {string} The code with type annotations stripped.
`module.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'`, source maps are not generated, because locations are preserved.
If `sourceMap` is provided, when mode is `'strip'`, an error will be thrown.
_WARNING_: The output of this function should not be considered stable across Node.js versions,
due to changes in the TypeScript parser.
```mjs
import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
```
```cjs
const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
```
If `sourceUrl` is provided, it will be used appended as a comment at the end of the output:
```mjs
import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a = 1\n\n//# sourceURL=source.ts;
```
```cjs
const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a = 1\n\n//# sourceURL=source.ts;
```
When `mode` is `'transform'`, the code is transformed to JavaScript:
```mjs
import { stripTypeScriptTypes } from 'node:module';
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = 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, ...
```
```cjs
const { stripTypeScriptTypes } = require('node:module');
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = 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, ...
```
### `module.syncBuiltinESMExports()`
<!-- YAML
Expand Down Expand Up @@ -1333,3 +1432,5 @@ returned object contains the following keys:
[realm]: https://tc39.es/ecma262/#realm
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
[transferrable objects]: worker_threads.md#portpostmessagevalue-transferlist
[transform TypeScript features]: typescript.md#typescript-features
[type-stripping]: typescript.md#type-stripping
6 changes: 3 additions & 3 deletions lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const {
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
const { addBuiltinLibsToObject, stripTypeScriptTypes } = require('internal/modules/helpers');

const { addBuiltinLibsToObject } = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const { getOptionValue } = require('internal/options');

prepareMainThreadExecution();
Expand All @@ -24,7 +24,7 @@ markBootstrapComplete();

const code = getOptionValue('--eval');
const source = getOptionValue('--experimental-strip-types') ?
stripTypeScriptTypes(code) :
stripTypeScriptModuleTypes(code) :
code;

const print = getOptionValue('--print');
Expand Down
10 changes: 5 additions & 5 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ const {
setHasStartedUserCJSExecution,
stripBOM,
toRealPath,
stripTypeScriptTypes,
} = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const packageJsonReader = require('internal/modules/package_json_reader');
const { getOptionValue, getEmbedderOptions } = require('internal/options');
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
Expand Down Expand Up @@ -1347,7 +1347,7 @@ let emittedRequireModuleWarning = false;
function loadESMFromCJS(mod, filename) {
let source = getMaybeCachedSource(mod, filename);
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
source = stripTypeScriptTypes(source, filename);
source = stripTypeScriptModuleTypes(source, filename);
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const isMain = mod[kIsMainSymbol];
Expand Down Expand Up @@ -1584,7 +1584,7 @@ function getMaybeCachedSource(mod, filename) {

function loadCTS(module, filename) {
const source = getMaybeCachedSource(module, filename);
const code = stripTypeScriptTypes(source, filename);
const code = stripTypeScriptModuleTypes(source, filename);
module._compile(code, filename, 'commonjs');
}

Expand All @@ -1596,7 +1596,7 @@ function loadCTS(module, filename) {
function loadTS(module, filename) {
// If already analyzed the source, then it will be cached.
const source = getMaybeCachedSource(module, filename);
const content = stripTypeScriptTypes(source, filename);
const content = stripTypeScriptModuleTypes(source, filename);
let format;
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
Expand All @@ -1616,7 +1616,7 @@ function loadTS(module, filename) {
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
parentSource = stripTypeScriptModuleTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
} catch {
// Continue regardless of error.
}
Expand Down
5 changes: 3 additions & 2 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,10 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
// Since experimental-strip-types depends on detect-module, we always return null
// if source is undefined.
if (!source) { return null; }
const { stripTypeScriptTypes, stringify } = require('internal/modules/helpers');
const { stringify } = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const stringifiedSource = stringify(source);
const parsedSource = stripTypeScriptTypes(stringifiedSource, fileURLToPath(url));
const parsedSource = stripTypeScriptModuleTypes(stringifiedSource, fileURLToPath(url));
const detectedFormat = detectModuleFormat(parsedSource, url);
const format = `${detectedFormat}-typescript`;
if (format === 'module-typescript' && foundPackageJson) {
Expand Down
8 changes: 4 additions & 4 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const {
assertBufferSource,
loadBuiltinModule,
stringify,
stripTypeScriptTypes,
stripBOM,
urlToFilename,
} = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const {
kIsCachedByESMLoader,
Module: CJSModule,
Expand Down Expand Up @@ -244,7 +244,7 @@ translators.set('require-commonjs', (url, source, isMain) => {
translators.set('require-commonjs-typescript', (url, source, isMain) => {
emitExperimentalWarning('Type Stripping');
assert(cjsParse);
const code = stripTypeScriptTypes(stringify(source), url);
const code = stripTypeScriptModuleTypes(stringify(source), url);
return createCJSModuleWrap(url, code);
});

Expand Down Expand Up @@ -459,7 +459,7 @@ translators.set('wasm', async function(url, source) {
translators.set('commonjs-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, true, 'load');
const code = stripTypeScriptTypes(stringify(source), url);
const code = stripTypeScriptModuleTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
});
Expand All @@ -468,7 +468,7 @@ translators.set('commonjs-typescript', function(url, source) {
translators.set('module-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, true, 'load');
const code = stripTypeScriptTypes(stringify(source), url);
const code = stripTypeScriptModuleTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
});
75 changes: 1 addition & 74 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const {
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_TYPESCRIPT_SYNTAX,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;
const { BuiltinModule } = require('internal/bootstrap/realm');

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

const { Buffer } = require('buffer');
const { getOptionValue } = require('internal/options');
const { assertTypeScript, setOwnProperty, getLazy, isUnderNodeModules } = require('internal/util');
const { setOwnProperty, getLazy } = require('internal/util');
const { inspect } = require('internal/util/inspect');

const lazyTmpdir = getLazy(() => require('os').tmpdir());
Expand Down Expand Up @@ -314,75 +311,6 @@ function getBuiltinModule(id) {
return normalizedId ? require(normalizedId) : undefined;
}

/**
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
* @type {string}
*/
const getTypeScriptParsingMode = getLazy(() =>
(getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
);

/**
* Load the TypeScript parser.
* and returns an object with a `code` property.
* @returns {Function} The TypeScript parser function.
*/
const loadTypeScriptParser = getLazy(() => {
assertTypeScript();
const amaro = require('internal/deps/amaro/dist/index');
return amaro.transformSync;
});

/**
*
* @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();
try {
return parse(source, options);
} catch (error) {
throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error);
}
}

/**
* @typedef {object} TransformOutput
* @property {string} code The compiled code.
* @property {string} [map] The source maps (optional).
*
* Performs type-stripping to TypeScript source code.
* @param {string} source TypeScript code to parse.
* @param {string} filename The filename of the source code.
* @returns {TransformOutput} The stripped TypeScript code.
*/
function stripTypeScriptTypes(source, filename) {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
assert(typeof source === 'string');
const options = {
__proto__: null,
mode: getTypeScriptParsingMode(),
sourceMap: getOptionValue('--enable-source-maps'),
filename,
};
const { code, map } = parseTypeScript(source, options);
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 `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
}
// Source map is not necessary in strip-only mode. However, to map the source
// file in debuggers to the original TypeScript source, add a sourceURL magic
// comment to hint that it is a generated source.
return `${code}\n\n//# sourceURL=${filename}`;
}


/**
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
* after this method is called.
Expand Down Expand Up @@ -485,7 +413,6 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
stripTypeScriptTypes,
stringify,
stripBOM,
toRealPath,
Expand Down
Loading

0 comments on commit 2ee876c

Please sign in to comment.