Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: add module.stripTypeScriptTypes #55282

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,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 @@ -1252,3 +1351,5 @@ returned object contains the following keys:
[realm]: https://tc39.es/ecma262/#realm
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
[transferable 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 @@ -151,8 +151,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 @@ -1357,7 +1357,7 @@ let hasPausedEntry = 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 @@ -1599,7 +1599,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 @@ -1611,7 +1611,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 @@ -1631,7 +1631,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 @@ -31,10 +31,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 @@ -248,7 +248,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 @@ -463,7 +463,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 @@ -472,7 +472,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);
});
74 changes: 1 addition & 73 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,74 +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}`;
}

/** @type {import('internal/util/types')} */
let _TYPES = null;
/**
Expand Down Expand Up @@ -485,7 +414,6 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
stripTypeScriptTypes,
stringify,
stripBOM,
toRealPath,
Expand Down
Loading
Loading