Skip to content

Commit

Permalink
module: add --experimental-enable-transformation for strip-types
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Aug 9, 2024
1 parent 3cbeed8 commit 6b155bd
Show file tree
Hide file tree
Showing 19 changed files with 347 additions and 38 deletions.
10 changes: 10 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,15 @@ files with no extension will be treated as WebAssembly if they begin with the
WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module
JavaScript.

### `--experimental-enable-type-transform`

<!-- YAML
added: REPLACEME
-->

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

### `--experimental-eventsource`

<!-- YAML
Expand Down Expand Up @@ -2911,6 +2920,7 @@ one is included in the list below.
* `--experimental-async-context-frame`
* `--experimental-default-type`
* `--experimental-detect-module`
* `--experimental-enable-type-transform`
* `--experimental-eventsource`
* `--experimental-import-meta-resolve`
* `--experimental-json-modules`
Expand Down
38 changes: 24 additions & 14 deletions doc/api/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ added: v22.6.0
> 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-enable-type-transform`][].
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 fuller 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 +84,24 @@ The `tsconfig.json` option `allowImportingTsExtensions` will allow the
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-enable-type-transform`][] 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 ECMA402 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 runtime 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 +138,11 @@ TypeScript syntax is unsupported in the REPL, STDIN input, `--print`, `--check`,
### 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-enable-type-transform`][] is enabled, by passing the flag
`--enable-source-maps`, source maps will be generated for the transformed code.
If `--enable-source-maps` is not passed the stack traces might not have correct
locations.
### Type stripping in dependencies
Expand All @@ -144,6 +153,7 @@ a `node_modules` path.
[CommonJS]: modules.md
[ES Modules]: esm.md
[Full TypeScript support]: #full-typescript-support
[`--experimental-enable-type-transform`]: cli.md#--experimental-enable-type-transform
[`--experimental-strip-types`]: cli.md#--experimental-strip-types
[`tsx`]: https://tsx.is/
[`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax
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-enable-type-transform
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, stripTypes } = 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) :
stripTypes(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 { stripTypes } = require('internal/modules/helpers');
source = stripTypes(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 { stripTypes } = require('internal/modules/helpers');
const code = stripTypes(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 { stripTypes } = require('internal/modules/helpers');
const content = stripTypes(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 = stripTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
} catch {
// Continue regardless of error.
}
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
default: { // The user did not pass `--experimental-default-type`.
// `source` is undefined when this is called from `defaultResolve`;
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
const { tsParse } = require('internal/modules/helpers');
const parsedSource = tsParse(source);
const detectedFormat = detectModuleFormat(parsedSource, url);
const { stripTypes } = require('internal/modules/helpers');
const code = stripTypes(source, url);
const detectedFormat = detectModuleFormat(code, url);
// When source is undefined, default to module-typescript.
const format = detectedFormat ? `${detectedFormat}-typescript` : 'module-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 @@ -38,7 +38,7 @@ const { readFileSync } = require('fs');
const { dirname, extname, isAbsolute } = require('path');
const {
loadBuiltinModule,
tsParse,
stripTypes,
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 = stripTypes(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 = stripTypes(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 = stripTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
});
39 changes: 32 additions & 7 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const internalFS = require('internal/fs/utils');
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 +300,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,23 +332,34 @@ 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-enable-type-transform') ? '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 stripTypes(source, filename) {
// TODO(@marco-ippolito) Checking empty string or non string input should be handled in Amaro.
if (!source || typeof source !== 'string') { return ''; }
const parse = loadTypeScriptParser();
const { code } = parse(source);
const options = { __proto__: null, mode: typeScriptParsingMode, sourceMap: sourceMapEnabled, filename };
const { code, map } = parse(source, options);
if (map) {
const base64SourceMap = Buffer.from(map).toString('base64');
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
}
return code;
}

Expand All @@ -354,7 +379,7 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
tsParse,
stripTypes,
stripBOM,
toRealPath,
hasStartedUserCJSExecution() {
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"Experimental type-stripping for TypeScript files.",
&EnvironmentOptions::experimental_strip_types,
kAllowedInEnvvar);
AddOption("--experimental-enable-type-transform",
"enable transformation of TypeScript-only"
"syntax in JavaScript code",
&EnvironmentOptions::experimental_enable_transformation,
kAllowedInEnvvar);
Implies("--experimental-enable-type-transform", "--experimental-strip-types");
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 @@ -239,6 +239,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> preload_esm_modules;

bool experimental_strip_types = false;
bool experimental_enable_transformation = false;

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

Expand Down
Loading

0 comments on commit 6b155bd

Please sign in to comment.