Source: lib/compiler/bundle/validate.js

/**
 * @file validate.js
 * @description Proves that emitted JavaScript parses before the build is
 * allowed to call itself successful.
 *
 * ## Why this is a separate, unconditional step
 *
 * The compiler could previously emit a bundle containing
 *
 * ```js
 * const { AvenxGuard } = Avenx;
 * const { AvenxGuard } = Avenx;
 * ```
 *
 * and report `Build successful`. The generated file was not JavaScript; the
 * application did not start; nothing in the pipeline noticed, because nothing
 * in the pipeline ever asked whether the output it had just written could be
 * parsed. That specific collision cannot happen any more -- every module is its
 * own scope now, so two files declaring the same name simply coexist -- but a
 * code generator that is only correct because its known failure modes have been
 * fixed one at a time is a generator whose next failure ships silently.
 *
 * So this check is not about guards. It establishes the invariant:
 *
 * ```text
 * avenx build reports success  =>  the emitted JavaScript parses
 * ```
 *
 * ## How
 *
 * `node:vm` compiles the source without running it. That is a real parse by the
 * same engine that will parse the file in production, it needs no dependency,
 * and it costs a few milliseconds on a bundle of this size.
 *
 * Validation happens against the staging directory, before promotion, so a
 * bundle that fails never reaches `dist/` at all — the previous build's output
 * is left intact rather than being replaced by something broken.
 * @module lib/compiler/bundle/validate
 */

import vm from 'vm';
import { AvenxErrorCodes } from '../../core/runtime/AvenxError.js';
import { BuildError } from '../errors/index.js';

/**
 * Extracts the offending line from a source, for the error message.
 *
 * A syntax error in a 190 KB single-line minified bundle is unreadable without
 * an excerpt, and the line number alone points at a file the developer did not
 * write. Showing the text is what makes the diagnostic actionable.
 * @param {string} source - The emitted source.
 * @param {number} lineNumber - 1-based line number.
 * @returns {string} A trimmed excerpt, or an empty string.
 */
function excerpt(source, lineNumber) {
  if (!Number.isInteger(lineNumber) || lineNumber < 1) return '';
  const line = source.split(/\r?\n/)[lineNumber - 1];
  if (typeof line !== 'string') return '';
  const trimmed = line.trim();
  return trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed;
}

/**
 * Reads the line number out of a V8 syntax error's stack.
 *
 * V8 reports the position as `<filename>:<line>` on the stack's first line. It
 * is not exposed as a structured property, so it has to be read back out.
 * @param {Error} error - The thrown SyntaxError.
 * @param {string} filename - The filename passed to the compiler.
 * @returns {number} The 1-based line, or 0 when it cannot be determined.
 */
function lineFromStack(error, filename) {
  const stack = typeof error.stack === 'string' ? error.stack : '';
  const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const match = stack.match(new RegExp(`${escaped}:(\\d+)`));
  return match ? Number(match[1]) : 0;
}

/**
 * Asserts that a string of emitted JavaScript parses as a classic script.
 *
 * The bundle is a classic script, not a module: it is loaded with a plain
 * `<script src>` tag, and every specifier in it has been resolved into the
 * graph rather than left for a module loader. Parsing it as a script is
 * therefore the same parse the browser will perform.
 * @param {string} code - The emitted JavaScript.
 * @param {string} artifact - The artifact name, used in the diagnostic.
 * @throws {BuildError} AVX_C15 when the source does not parse.
 */
export function assertValidJavaScript(code, artifact) {
  try {
    // Compiling is enough: the script is never run, so no application code
    // executes during a build.
    new vm.Script(code, { filename: artifact });
  } catch (error) {
    if (!(error instanceof SyntaxError)) {
      throw error;
    }
    const line = lineFromStack(error, artifact);
    throw new BuildError(
      AvenxErrorCodes.COMPILER_INVALID_OUTPUT,
      artifact,
      error.message,
      line > 0 ? String(line) : 'unknown',
      excerpt(code, line) || '(source line unavailable)',
    );
  }
}

/**
 * Asserts that the bundle carries the runtime capabilities the build linked.
 *
 * The compiler decides what a bundle needs from what it saw while compiling:
 * a template the IR refused puts the string renderer into the entry graph, and
 * the build then tells the developer, through AVX_W47, that their component
 * renders through it. That sentence has to be true of the artifact, not just of
 * the intent.
 *
 * It stopped being true once. The string renderer registers itself into a
 * module-scoped registry, and a build that linked the same runtime file twice
 * -- which a symlinked `avenx-core` used to cause -- filled one copy of that
 * registry while the component consulted the other. The build reported success
 * and the application rendered nothing.
 *
 * Resolution no longer produces two copies of a file, so that specific cause is
 * gone. This check is here because the property is worth asserting on the
 * output regardless of which defect might break it next: a capability the build
 * committed to is either in the bundle or the build fails.
 * @param {Map<string, string>} outputs - Artifact name to contents.
 * @param {Array<{capability: string, reason: string, evidence: string}>} required
 *   - Capabilities the build linked. `evidence` is a pattern source that can
 *   only match if the capability was linked *and* executed, never merely
 *   defined.
 * @throws {BuildError} AVX_C23 when a linked capability is absent.
 */
export function assertRuntimeCapabilities(outputs, required) {
  if (!Array.isArray(required) || required.length === 0) {
    return;
  }

  let bundle = null;
  for (const [name, contents] of outputs) {
    if (name.endsWith('.js') && !name.endsWith('.map') && typeof contents === 'string') {
      bundle = contents;
      break;
    }
  }
  if (bundle === null) {
    return;
  }

  for (const { capability, reason, evidence } of required) {
    // A pattern rather than a substring, because the obvious substring is the
    // wrong test: `installStringRenderer(` matches the function's own
    // declaration, which survives in the bundle whenever anything imports the
    // registry -- and `AvenxComponent` always does, to read from it. The
    // evidence has to distinguish the call from the definition.
    if (!new RegExp(evidence).test(bundle)) {
      throw new BuildError(
        AvenxErrorCodes.COMPILER_MISSING_RUNTIME_CAPABILITY,
        capability,
        reason,
        evidence,
      );
    }
  }
}

/**
 * Validates every JavaScript artifact in a build's output set.
 * @param {Map<string, string>} outputs - Artifact name to contents.
 * @throws {BuildError} When any `.js` artifact does not parse.
 */
export function assertValidOutputs(outputs) {
  for (const [name, contents] of outputs) {
    if (name.endsWith('.js') && typeof contents === 'string') {
      assertValidJavaScript(contents, name);
    }
  }
}