Source: lib/compiler/sourceMapTrace.js

/**
 * @file sourceMapTrace.js
 * @description Builds the source-location sidecar that `avenx trace view` uses.
 *
 * A trace records the *source text* of every action and computed it ran, which
 * is already more than a compiled framework can offer. What it cannot know at
 * runtime is where that text came from — the bundle is one concatenated script
 * with no file boundaries left in it.
 *
 * The compiler does know, so it writes the answer beside the bundle rather
 * than inside it. `bundle.trace.json` is emitted next to `bundle.js` and is not
 * referenced by it: an application that never records a trace downloads
 * nothing extra, and a deployment that does not want the file simply does not
 * upload it.
 * @module lib/compiler/sourceMapTrace
 */

import path from 'path';
import { TRACE_VERSION } from '../core/trace/schema.js';

/**
 * The file name the sidecar is written under, given a bundle name.
 * @param {string} outputName - The configured bundle name, e.g. `bundle`.
 * @returns {string} The sidecar file name.
 */
export function sidecarFileName(outputName) {
  return `${outputName}.trace.json`;
}

/**
 * Finds the 1-based line a pattern first occurs on.
 * @param {string} source - The file contents.
 * @param {RegExp} pattern - What to look for.
 * @returns {number|null} The line number, or null when absent.
 */
function lineOf(source, pattern) {
  const match = pattern.exec(source);
  if (!match) {
    return null;
  }
  return source.slice(0, match.index).split('\n').length;
}

/**
 * Escapes a name for use inside a regular expression.
 * @param {string} name - The declared name.
 * @returns {string} The escaped name.
 */
function escape(name) {
  return String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Collects the source locations of one component's declarations.
 * @param {object} unit - What the parser extracted.
 * @param {string} unit.name - The component class name.
 * @param {string} unit.filePath - Absolute path to the source file.
 * @param {string} unit.rootDir - The project root, for relative paths.
 * @param {string} unit.content - The file contents.
 * @param {object} [unit.computed] - Computed definitions by name.
 * @param {object} [unit.methods] - Action bodies by name.
 * @param {object} [unit.resources] - Resource definitions by name.
 * @param {Set<string>} [unit.contracts] - Declared contracts.
 * @returns {object} The component's sidecar entry.
 */
export function collectLocations(unit) {
  const relative = path.relative(unit.rootDir, unit.filePath).split(path.sep).join('/');
  const entry = { file: relative, actions: {}, computed: {}, resources: {} };

  if (unit.contracts && unit.contracts.size > 0) {
    entry.contracts = Array.from(unit.contracts);
  }

  for (const name of Object.keys(unit.methods || {})) {
    const line = lineOf(unit.content, new RegExp(`<action\\s+[^>]*name\\s*=\\s*["']${escape(name)}["']`));
    if (line !== null) {
      entry.actions[name] = { line };
    }
  }

  for (const name of Object.keys(unit.computed || {})) {
    const line = lineOf(unit.content, new RegExp(`<computed\\s+[^>]*name\\s*=\\s*["']${escape(name)}["']`));
    if (line !== null) {
      entry.computed[name] = { line, expression: unit.computed[name] };
    }
  }

  for (const name of Object.keys(unit.resources || {})) {
    const line = lineOf(unit.content, new RegExp(`<resource\\s+[^>]*name\\s*=\\s*["']${escape(name)}["']`));
    if (line !== null) {
      entry.resources[name] = { line };
    }
  }

  return entry;
}

/**
 * Assembles the sidecar document.
 * @param {Map<string, object>} components - Entries keyed by class name.
 * @param {Map<string, object>} [bridges] - Bridge descriptors. The compiler keys
 *   these by absolute path, so the descriptor's own `name` is used for the
 *   sidecar rather than the map key — a trace records bridge names, not paths.
 * @param {string} [rootDir] - The project root, for relative bridge paths.
 * @returns {object} The sidecar, ready to serialize.
 */
export function buildSidecar(components, bridges = new Map(), rootDir = process.cwd()) {
  const sidecar = {
    traceVersion: TRACE_VERSION,
    generatedAt: new Date().toISOString(),
    components: {},
    bridges: {},
  };

  for (const [name, entry] of components) {
    sidecar.components[name] = entry;
  }

  for (const [key, descriptor] of bridges) {
    if (!descriptor || !descriptor.filePath) {
      continue;
    }
    const name = descriptor.name || key;
    sidecar.bridges[name] = {
      file: path.relative(rootDir, descriptor.filePath).split(path.sep).join('/'),
      actions: descriptor.actions || [],
      getters: descriptor.getters || [],
      state: descriptor.stateKeys || [],
    };
  }

  return sidecar;
}

/**
 * Annotates a trace's nodes with the source locations the sidecar holds.
 *
 * Applied when a trace is read, not when it is recorded: a trace stays a
 * record of what happened, and the mapping from that to a file and a line is a
 * property of the build it came from.
 * @param {object} trace - The trace to annotate. Mutated in place.
 * @param {object|null} sidecar - The sidecar, or null to leave the trace alone.
 * @returns {object} The same trace.
 */
export function annotateTrace(trace, sidecar) {
  if (!sidecar || !trace || !Array.isArray(trace.nodes)) {
    return trace;
  }

  for (const node of trace.nodes) {
    const owner = node.component || node.bridge;
    if (!owner) {
      continue;
    }

    const component = sidecar.components && sidecar.components[owner];
    if (component) {
      if (node.type === 'action' && component.actions[node.name]) {
        node.loc = { file: component.file, line: component.actions[node.name].line };
      } else if (node.type === 'computed' && component.computed[node.name]) {
        node.loc = { file: component.file, line: component.computed[node.name].line };
      } else if (node.type === 'resource' && component.resources[node.name]) {
        node.loc = { file: component.file, line: component.resources[node.name].line };
      } else if (!node.loc) {
        node.loc = { file: component.file };
      }
      continue;
    }

    const bridge = sidecar.bridges && sidecar.bridges[owner];
    if (bridge) {
      node.loc = { file: bridge.file };
    }
  }

  return trace;
}