'use strict';
const {
ObjectPrototypeHasOwnProperty,
} = primordials;
const {
validateOneOf,
validateObject,
validateString,
} = require('internal/validators');
const { assertTypeScript,
emitExperimentalWarning,
getLazy,
isUnderNodeModules,
kEmptyObject } = require('internal/util');
const {
ERR_INVALID_TYPESCRIPT_SYNTAX,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const assert = require('internal/assert');
const {
getCompileCacheEntry,
saveCompileCacheEntry,
cachedCodeTypes: { kStrippedTypeScript },
} = internalBinding('modules');
/**
* 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/internal');
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) {
/**
* Amaro v0.3.0 (from SWC v1.10.7) throws an object with `message` and `code` properties.
* It allows us to distinguish between invalid syntax and unsupported syntax.
*/
switch (error?.code) {
case 'UnsupportedSyntax': {
const unsupportedSyntaxError = new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message);
throw decorateErrorWithSnippet(unsupportedSyntaxError, error); /* node-do-not-add-exception-line */
}
case 'InvalidSyntax': {
const invalidSyntaxError = new ERR_INVALID_TYPESCRIPT_SYNTAX(error.message);
throw decorateErrorWithSnippet(invalidSyntaxError, error); /* node-do-not-add-exception-line */
}
default:
// SWC may throw strings when something goes wrong.
if (typeof error === 'string') { assert.fail(error); }
assert(error != null && ObjectPrototypeHasOwnProperty(error, 'message'));
assert.fail(error.message);
}
}
}
/**
*
* @param {Error} error the error to decorate: ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
* @param {object} amaroError the error object from amaro
* @returns {Error} the decorated error
*/
function decorateErrorWithSnippet(error, amaroError) {
const errorHints = `${amaroError.filename}:${amaroError.startLine}\n${amaroError.snippet}`;
error.stack = `${errorHints}\n${error.stack}`;
return error;
}
/**
* Performs type-stripping to TypeScript source code.
* @param {string} code TypeScript code to parse.
* @param {TransformOptions} options The configuration for type stripping.
* @returns {string} The stripped TypeScript code.
*/
function stripTypeScriptTypes(code, options = kEmptyObject) {
emitExperimentalWarning('stripTypeScriptTypes');
validateString(code, 'code');
validateObject(options, 'options');
const {
sourceMap = false,
sourceUrl = '',
} = options;
let { mode = 'strip' } = options;
validateOneOf(mode, 'options.mode', ['strip']);
validateString(sourceUrl, 'options.sourceUrl');
if (mode === 'strip') {
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
// Rename mode from 'strip' to 'strip-only'.
// The reason is to match `process.features.typescript` which returns `strip`,
// but the parser expects `strip-only`.
mode = 'strip-only';
}
return processTypeScriptCode(code, {
mode,
filename: sourceUrl,
});
}
/**
* @typedef {object} TypeScriptOptions
* @property {string|undefined} filename Filename.
*/
/**
* Processes TypeScript code by stripping types.
* @param {string} code TypeScript code to process.
* @param {TypeScriptOptions} options The configuration object.
* @returns {string} The processed code.
*/
function processTypeScriptCode(code, options) {
const { code: transformedCode } = parseTypeScript(code, options);
if (options.filename) {
return `${transformedCode}\n\n//# sourceURL=${options.filename}`;
}
return transformedCode;
}
function stripTypeScriptTypesForCoverage(code) {
validateString(code, 'code');
return processTypeScriptCode(code, { mode: 'strip-only' });
}
/**
* Whether type-stripping is allowed for files under node_modules belonging
* to packages whose package.json contains `"private": true`.
* @returns {boolean}
*/
const isStripPrivateModulesEnabled = getLazy(
() => getOptionValue('--experimental-strip-private-modules'),
);
/**
* Checks whether a file under node_modules is owned by a private package, in
* which case type stripping is allowed under `--experimental-strip-private-modules`.
*
* Every package enclosing the file inside `node_modules` must be marked
* `"private": true`. npm refuses to publish a private package, so requiring
* the package root (not a nested package.json, which is published verbatim)
* and rejecting any non-private enclosing package (e.g. a publishable package
* bundling a private dependency) prevents published packages from shipping
* strippable TypeScript. See {@link getRootPackageJSON}.
* @param {string} filename The filename (path or file URL) of the source code.
* @returns {boolean} Whether the file belongs to a package with `"private": true`.
*/
function isInsidePrivatePackage(filename) {
const { getRootPackageJSON } = require('internal/modules/package_json_reader');
const { urlToFilename } = require('internal/modules/helpers');
const packageJSON = getRootPackageJSON(urlToFilename(filename));
return packageJSON?.data.private === true;
}
/**
* Performs type-stripping to TypeScript source code internally.
* It is used by internal loaders.
* @param {string} source TypeScript code to parse.
* @param {string} filename The filename of the source code.
* @param {string} [sourceURL] The source URL of the source code. If not specified, use filename.
* @returns {TransformOutput} The stripped TypeScript code.
*/
function stripTypeScriptModuleTypes(source, filename, sourceURL) {
assert(typeof source === 'string');
if (isUnderNodeModules(filename)) {
if (!isStripPrivateModulesEnabled() || !isInsidePrivatePackage(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
emitExperimentalWarning('Type stripping in node_modules');
}
// Get a compile cache entry into the native compile cache store,
// keyed by the filename. If the cache can already be loaded on disk,
// cached.transpiled contains the cached string. Otherwise we should do
// the transpilation and save it in the native store later using
// saveCompileCacheEntry().
const cached = (filename ? getCompileCacheEntry(source, filename, kStrippedTypeScript) : undefined);
if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here.
return cached.transpiled;
}
const options = {
mode: 'strip-only',
filename: sourceURL ?? filename,
};
const transpiled = processTypeScriptCode(source, options);
if (cached) {
// cached.external contains a pointer to the native cache entry.
// The cached object would be unreachable once it's out of scope,
// but the pointer inside cached.external would stay around for reuse until
// environment shutdown or when the cache is manually flushed
// to disk. Unwrap it in JS before passing into C++ since it's faster.
saveCompileCacheEntry(cached.external, transpiled);
}
return transpiled;
}
module.exports = {
stripTypeScriptModuleTypes,
stripTypeScriptTypes,
stripTypeScriptTypesForCoverage,
};