@ 261110.9
current
'use strict';

// Build-time counterpart of the SEA virtual file system: run by --build-sea
// (src/node_sea.cc) in a regular Node.js instance to compile every module of
// the virtual file system and collect their compile cache, which the
// executable then carries as assets. The assets are mounted the way the
// executable mounts them at startup, so the modules are compiled from the
// same paths and sources that the loaders compile them from in the
// executable, and the cache is keyed and hashed exactly as the loaders look
// it up there.

const {
  ArrayPrototypeIncludes,
  StringPrototypeEndsWith,
  StringPrototypeReplaceAll,
} = primordials;

const { Buffer } = require('buffer');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('internal/url');
const { getOptionValue } = require('internal/options');
const {
  enableCompileCache: enableNativeCompileCache,
} = internalBinding('modules');
const {
  constants: { compileCacheStatus },
  stringify,
} = require('internal/modules/helpers');
const {
  codes: {
    ERR_INVALID_STATE,
  },
} = require('internal/errors');

let debug = require('internal/util/debuglog').debuglog('sea', (fn) => {
  debug = fn;
});

// Only files the module loaders compile are cached. Everything else in the
// mount is data.
const kScriptExtensions = ['.js', '.cjs', '.mjs'];
const kTypeScriptExtensions = ['.ts', '.cts', '.mts'];

/**
 * Mounts the assets of the SEA configuration the way the executable mounts
 * them: the main script at the mount point root, at the layer reserved for
 * --vfs-load when they come from an archive (see lib/internal/vfs/sea.js).
 * @param {object} options The SEA configuration, see buildSeaCompileCache()
 * @param {string} mainName The name of the main script at the mount root
 * @returns {string} The mount point
 */
function mountAssets(options, mainName) {
  const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
  const isArchive = options.vfsArchive !== undefined;

  let provider;
  if (isArchive) {
    const { ZipBuffer } = require('internal/zip');
    const { ZipProvider } = require('internal/vfs/providers/ziparchive');
    const zip = new ZipBuffer(fs.readFileSync(options.vfsArchive));
    zip.addSync(mainName, Buffer.from(options.mainSource),
                { __proto__: null, method: 'store' });
    provider = new ZipProvider(zip);
  } else {
    const { MemoryProvider } = require('internal/vfs/providers/memory');
    provider = new MemoryProvider();
  }

  const vfs = new VirtualFileSystem(provider, {
    __proto__: null,
    emitExperimentalWarning: false,
    [kLoadLayer]: isArchive,
  });

  if (!isArchive) {
    // Every asset is a file at its key, as the SEAProvider serves them.
    const { assetKeys, assetPaths } = options;
    for (let i = 0; i < assetKeys.length; i++) {
      const target = path.posix.normalize(
        '/' + StringPrototypeReplaceAll(assetKeys[i], '\\', '/'));
      const dir = path.posix.dirname(target);
      if (dir !== '/') {
        vfs.mkdirSync(dir, { __proto__: null, recursive: true });
      }
      vfs.writeFileSync(target, fs.readFileSync(assetPaths[i]));
    }
    vfs.writeFileSync('/' + mainName, options.mainSource);
    provider.setReadOnly();
  }

  return vfs.mount();
}

/**
 * Compiles a module without running it, the way the loaders compile it when
 * it is loaded from the mount, so that its compile cache entry is created.
 * @param {string} filename The path of the module inside the mount
 * @returns {boolean} Whether the file is a module the loaders compile
 */
function compileModule(filename) {
  const { Module, wrapSafe } = require('internal/modules/cjs/loader');
  const {
    defaultGetFormatWithoutErrors,
  } = require('internal/modules/esm/get_format');
  const {
    compileSourceTextModule,
    SourceTextModuleTypes: { kUser },
  } = require('internal/modules/esm/utils');

  const url = pathToFileURL(filename);
  const buffer = fs.readFileSync(filename);
  let format = defaultGetFormatWithoutErrors(url, {
    __proto__: null,
    source: buffer,
  });

  let source;
  switch (format) {
    case 'module':
      // The ESM loader decodes the source it loads.
      source = stringify(buffer);
      break;
    case 'commonjs':
    case 'commonjs-typescript':
    case 'module-typescript':
    case 'typescript':
      // The CommonJS loader reads the source as a UTF-8 string.
      source = buffer.toString('utf8');
      break;
    default:
      return false;
  }

  if (StringPrototypeEndsWith(format, 'typescript')) {
    const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
    source = stripTypeScriptModuleTypes(source, filename, url.href);
    switch (format) {
      case 'commonjs-typescript':
        format = 'commonjs';
        break;
      case 'module-typescript':
        format = 'module';
        break;
      default:
        // Detected from the type-stripped source by wrapSafe(), like
        // Module.prototype._compile() does.
        format = undefined;
        break;
    }
  }

  if (format !== 'module') {
    const result = wrapSafe(filename, source, new Module(filename, null), format);
    if (!result.canParseAsESM) {
      return true;
    }
  }
  compileSourceTextModule(url.href, source, kUser);
  return true;
}

/**
 * Compiles every module under a directory of the mount.
 * @param {string} dir The directory
 * @param {boolean} stripTypes Whether TypeScript files are modules too
 * @returns {number} The number of modules compiled
 */
function compileTree(dir, stripTypes) {
  let compiled = 0;
  const dirents = fs.readdirSync(dir, { __proto__: null, withFileTypes: true });
  for (let i = 0; i < dirents.length; i++) {
    const dirent = dirents[i];
    const filename = path.join(dir, dirent.name);
    if (dirent.isDirectory()) {
      compiled += compileTree(filename, stripTypes);
      continue;
    }
    if (!dirent.isFile()) continue;
    const ext = path.extname(filename);
    if (!ArrayPrototypeIncludes(kScriptExtensions, ext) &&
        !(stripTypes && ArrayPrototypeIncludes(kTypeScriptExtensions, ext))) {
      continue;
    }
    // A module that does not compile is left out of the cache: it fails the
    // same way at run time if the application ever loads it.
    try {
      if (compileModule(filename)) compiled++;
    } catch (err) {
      debug('not caching %s: %s', filename, err?.message);
    }
  }
  return compiled;
}

/**
 * Compiles the modules of the SEA virtual file system into the compile cache
 * of this Node.js instance, from which --build-sea collects them. The cache
 * is keyed relative to the mount point, which is the same in the executable.
 * @param {object} options
 * @param {string} options.main The "main" of the SEA configuration
 * @param {string} options.mainSource The source of the main script
 * @param {string} [options.vfsArchive] The "vfsArchive" of the configuration
 * @param {string[]} [options.assetKeys] The keys of the "assets"
 * @param {string[]} [options.assetPaths] The paths of the "assets"
 * @returns {number} The number of modules compiled
 */
function buildSeaCompileCache(options) {
  const { getSeaMainName } = require('internal/vfs/sea');
  const mountPoint = mountAssets(options, getSeaMainName(options.main));

  // Enabling the compile cache takes a directory, but nothing is persisted
  // to it: the entries are collected from memory by --build-sea.
  const cacheDir = fs.mkdtempSync(
    path.join(require('os').tmpdir(), 'node-sea-compile-cache-'));
  try {
    const { 0: status, 1: message } = enableNativeCompileCache(
      cacheDir, true /* portable */, false /* readOnly */, mountPoint);
    if (status !== compileCacheStatus.ENABLED) {
      throw new ERR_INVALID_STATE(
        `cannot enable the compile cache for ${mountPoint}: ${message}`);
    }
    const compiled = compileTree(mountPoint, getOptionValue('--strip-types'));
    debug('compiled %d modules in %s for the compile cache',
          compiled, mountPoint);
    return compiled;
  } finally {
    fs.rmSync(cacheDir, { __proto__: null, recursive: true, force: true });
  }
}

module.exports = {
  buildSeaCompileCache,
};