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

[v22.x] Backport: "module: add --experimental-transform-types flag" #54373

Closed
Closed
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
12 changes: 12 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,17 @@ CommonJS. This includes the following:
* Lexical redeclarations of the CommonJS wrapper variables (`require`, `module`,
`exports`, `__dirname`, `__filename`).

### `--experimental-transform-types`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

Enables the transformation of TypeScript-only syntax into JavaScript code.
Implies `--experimental-strip-types` and `--enable-source-maps`.

### `--experimental-eventsource`

<!-- YAML
Expand Down Expand Up @@ -2973,6 +2984,7 @@ one is included in the list below.
* `--experimental-sqlite`
* `--experimental-strip-types`
* `--experimental-top-level-await`
* `--experimental-transform-types`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
* `--experimental-wasm-modules`
Expand Down
45 changes: 31 additions & 14 deletions doc/api/typescript.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Modules: TypeScript

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/54283

Check warning on line 6 in doc/api/typescript.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Added `--experimental-transform-types` flag.
-->

> Stability: 1.0 - Early development

## Enabling

There are two ways to enable runtime TypeScript support in Node.js:
Expand Down Expand Up @@ -44,13 +53,15 @@
> Stability: 1.0 - Early development

The flag [`--experimental-strip-types`][] enables Node.js to run TypeScript
files that contain only type annotations. Such files contain no TypeScript
features that require transformation, such as enums or namespaces. Node.js will
replace inline type annotations with whitespace, and no type checking is
performed. TypeScript features that depend on settings within `tsconfig.json`,
files. By default Node.js will execute only files that contain no
TypeScript features that require transformation, such as enums or namespaces.
Node.js will replace inline type annotations with whitespace,
and no type checking is performed.
To enable the transformation of such features
use the flag [`--experimental-transform-types`][].
TypeScript features that depend on settings within `tsconfig.json`,
such as paths or converting newer JavaScript syntax to older standards, are
intentionally unsupported. To get fuller TypeScript support, including support
for enums and namespaces and paths, see [Full TypeScript support][].
intentionally unsupported. To get full TypeScript support, see [Full TypeScript support][].

The type stripping feature is designed to be lightweight.
By intentionally not supporting syntaxes that require JavaScript code
Expand Down Expand Up @@ -82,20 +93,24 @@
TypeScript compiler `tsc` to type-check files with `import` specifiers that
include the `.ts` extension.

### Unsupported TypeScript features
### TypeScript features

Since Node.js is only removing inline types, any TypeScript features that
involve _replacing_ TypeScript syntax with new JavaScript syntax will error.
This is by design. To run TypeScript with such features, see
[Full TypeScript support][].
involve _replacing_ TypeScript syntax with new JavaScript syntax will error,
unless the flag [`--experimental-transform-types`][] is passed.

The most prominent unsupported features that require transformation are:
The most prominent features that require transformation are:

* `Enum`
* `experimentalDecorators`
* `namespaces`
* `legacy module`
* parameter properties

Since Decorators are currently a [TC39 Stage 3 proposal](https://github.com/tc39/proposal-decorators)
and will soon be supported by the JavaScript engine,
they are not transformed and will result in a parser error.
This is a temporary limitation and will be resolved in the future.

In addition, Node.js does not read `tsconfig.json` files and does not support
features that depend on settings within `tsconfig.json`, such as paths or
converting newer JavaScript syntax into older standards.
Expand Down Expand Up @@ -132,8 +147,9 @@
### Source maps

Since inline types are replaced by whitespace, source maps are unnecessary for
correct line numbers in stack traces; and Node.js does not generate them. For
source maps support, see [Full TypeScript support][].
correct line numbers in stack traces; and Node.js does not generate them.
When [`--experimental-transform-types`][] is enabled, source-maps
are enabled by default.

### Type stripping in dependencies

Expand All @@ -145,6 +161,7 @@
[ES Modules]: esm.md
[Full TypeScript support]: #full-typescript-support
[`--experimental-strip-types`]: cli.md#--experimental-strip-types
[`--experimental-transform-types`]: cli.md#--experimental-transform-types
[`tsx`]: https://tsx.is/
[`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax
[file extensions are mandatory]: esm.md#mandatory-file-extensions
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ Enable snapshot testing in the test runner.
.It Fl -experimental-strip-types
Enable experimental type-stripping for TypeScript files.
.
.It Fl -experimental-transform-types
Enable transformation of TypeScript-only syntax into JavaScript code.
.
.It Fl -experimental-eventsource
Enable experimental support for the EventSource Web API.
.
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
const { addBuiltinLibsToObject, tsParse } = require('internal/modules/helpers');
const { addBuiltinLibsToObject, stripTypeScriptTypes } = require('internal/modules/helpers');

const { getOptionValue } = require('internal/options');

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

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

const print = getOptionValue('--print');
Expand Down
16 changes: 8 additions & 8 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1362,8 +1362,8 @@ function loadESMFromCJS(mod, filename) {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const { tsParse } = require('internal/modules/helpers');
source = tsParse(source);
const { stripTypeScriptTypes } = require('internal/modules/helpers');
source = stripTypeScriptTypes(source, filename);
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const isMain = mod[kIsMainSymbol];
Expand Down Expand Up @@ -1576,9 +1576,9 @@ function loadCTS(module, filename) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const source = getMaybeCachedSource(module, filename);
const { tsParse } = require('internal/modules/helpers');
const content = tsParse(source);
module._compile(content, filename, 'commonjs');
const { stripTypeScriptTypes } = require('internal/modules/helpers');
const code = stripTypeScriptTypes(source, filename);
module._compile(code, filename, 'commonjs');
}

/**
Expand All @@ -1592,8 +1592,8 @@ function loadTS(module, filename) {
}
// If already analyzed the source, then it will be cached.
const source = getMaybeCachedSource(module, filename);
const { tsParse } = require('internal/modules/helpers');
const content = tsParse(source);
const { stripTypeScriptTypes } = require('internal/modules/helpers');
const content = stripTypeScriptTypes(source, filename);
let format;
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
Expand All @@ -1613,7 +1613,7 @@ function loadTS(module, filename) {
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = tsParse(fs.readFileSync(parentPath, 'utf8'));
parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
} catch {
// Continue regardless of error.
}
Expand Down
5 changes: 2 additions & 3 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,8 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
let parsedSource;
if (source) {
// We do the type stripping only if `source` is not falsy.
const { tsParse } = require('internal/modules/helpers');
parsedSource = tsParse(source);
const { stripTypeScriptTypes } = require('internal/modules/helpers');
parsedSource = stripTypeScriptTypes(source, url);
}
const detectedFormat = detectModuleFormat(parsedSource, url);
// When source is undefined, default to module-typescript.
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 @@ -38,7 +38,7 @@ const { readFileSync } = require('fs');
const { dirname, extname, isAbsolute } = require('path');
const {
loadBuiltinModule,
tsParse,
stripTypeScriptTypes,
stripBOM,
urlToFilename,
} = require('internal/modules/helpers');
Expand Down Expand Up @@ -309,7 +309,7 @@ translators.set('require-commonjs', (url, source, isMain) => {
translators.set('require-commonjs-typescript', (url, source, isMain) => {
emitExperimentalWarning('Type Stripping');
assert(cjsParse);
const code = tsParse(stringify(source));
const code = stripTypeScriptTypes(stringify(source), url);
return createCJSModuleWrap(url, code);
});

Expand Down Expand Up @@ -526,7 +526,7 @@ translators.set('wasm', async function(url, source) {
translators.set('commonjs-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, false, 'load');
const code = tsParse(stringify(source));
const code = stripTypeScriptTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
});
Expand All @@ -535,7 +535,7 @@ translators.set('commonjs-typescript', function(url, source) {
translators.set('module-typescript', function(url, source) {
emitExperimentalWarning('Type Stripping');
assertBufferSource(source, false, 'load');
const code = tsParse(stringify(source));
const code = stripTypeScriptTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
});
49 changes: 43 additions & 6 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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 { setOwnProperty } = require('internal/util');
const { inspect } = require('internal/util/inspect');
Expand Down Expand Up @@ -300,7 +301,21 @@ 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;

/**
* Load the TypeScript parser.
Expand All @@ -318,22 +333,44 @@ function loadTypeScriptParser(parser) {
} else {
const amaro = require('internal/deps/amaro/dist/index');
// Default option for Amaro is to perform Type Stripping only.
const defaultOptions = { __proto__: null, mode: 'strip-only' };
typeScriptParsingMode = getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only';
sourceMapEnabled = getOptionValue('--enable-source-maps');
// Curry the transformSync function with the default options.
typeScriptParser = (source) => amaro.transformSync(source, defaultOptions);
typeScriptParser = amaro.transformSync;
}
return typeScriptParser;
}

/**
* @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.
* @returns {string} JavaScript code.
* @param {string} filename The filename of the source code.
* @returns {TransformOutput} The stripped TypeScript code.
*/
function tsParse(source) {
function stripTypeScriptTypes(source, filename) {
assert(typeof source === 'string');
const parse = loadTypeScriptParser();
const { code } = parse(source);
const options = {
__proto__: null,
mode: typeScriptParsingMode,
sourceMap: sourceMapEnabled,
filename,
// Transform option is only applied in transform mode.
transform: {
verbatimModuleSyntax: true,
},
};
const { code, map } = parse(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}`;
}
return code;
}

Expand All @@ -353,7 +390,7 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
tsParse,
stripTypeScriptTypes,
stripBOM,
toRealPath,
hasStartedUserCJSExecution() {
Expand Down
8 changes: 8 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::experimental_strip_types,
kAllowedInEnvvar);
Implies("--experimental-strip-types", "--experimental-detect-module");
AddOption("--experimental-transform-types",
"enable transformation of TypeScript-only"
"syntax into JavaScript code",
&EnvironmentOptions::experimental_transform_types,
kAllowedInEnvvar);
Implies("--experimental-transform-types", "--experimental-strip-types");
Implies("--experimental-transform-types", "--experimental-detect-module");
Implies("--experimental-transform-types", "--enable-source-maps");
AddOption("--interactive",
"always enter the REPL even if stdin does not appear "
"to be a terminal",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> preload_esm_modules;

bool experimental_strip_types = false;
bool experimental_transform_types = false;

std::vector<std::string> user_argv;

Expand Down
Loading
Loading