@ 261105.16
current
'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,
};