Source: lib/compiler/atlas/emit.js

/**
 * @file emit.js
 * @description Serializing the Atlas beside the bundle.
 *
 * The artifact follows the convention `bundle.trace.json` already established:
 * it is written next to `bundle.js` and is **never referenced by it**. An
 * application that never runs `avenx impact` downloads nothing extra, a
 * deployment that does not want the file simply does not upload it, and Atlas
 * adds zero bytes to the runtime.
 * @module lib/compiler/atlas/emit
 */

import { ATLAS_VERSION } from './AppModel.js';

/**
 * The file name the Atlas is written under, given a bundle name.
 * @param {string} outputName - The configured bundle name, e.g. `bundle`.
 * @returns {string} The artifact's file name.
 */
export function atlasFileName(outputName) {
  return `${outputName}.atlas.json`;
}

/**
 * Assembles the artifact.
 *
 * `generatedAt` is the only field that changes between two builds of unchanged
 * sources. Everything below it is sorted, so the artifact diffs cleanly in
 * review and a golden test can compare it after dropping the timestamp.
 * @param {AppModel} model - The model to serialize.
 * @param {object} [meta] - Extra envelope fields, such as the source directory.
 * @returns {object} The serializable artifact.
 */
export function buildAtlas(model, meta = {}) {
  const { nodes, edges, unresolved, errors } = model.toJSON();
  return {
    atlasVersion: ATLAS_VERSION,
    generatedAt: new Date().toISOString(),
    ...meta,
    summary: {
      nodes: nodes.length,
      edges: edges.length,
      unresolved: unresolved.length,
      counts: model.counts(),
    },
    nodes,
    edges,
    unresolved,
    // Present only when a phase failed. A reader that finds this field is
    // holding a partial model and should say so rather than treating an
    // absence in it as an absence in the application.
    ...(errors ? { errors } : {}),
  };
}

/**
 * Serializes the artifact to JSON text.
 * @param {AppModel} model - The model.
 * @param {object} [meta] - Extra envelope fields.
 * @returns {string} Pretty-printed JSON.
 */
export function serializeAtlas(model, meta = {}) {
  return JSON.stringify(buildAtlas(model, meta), null, 2);
}

/**
 * Reads an artifact back, rejecting a format this build cannot interpret.
 *
 * A newer major version may have changed what a field means, and answering an
 * impact query from a model this build misreads would be worse than refusing.
 * Unknown *fields* are tolerated; an unknown *version* is not.
 * @param {string} text - The artifact's contents.
 * @returns {object} The parsed artifact.
 * @throws {Error} When the document is not a readable Atlas.
 */
export function parseAtlas(text) {
  const parsed = JSON.parse(text);
  if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.nodes)) {
    throw new Error('Not an Avenx Atlas document.');
  }
  if (typeof parsed.atlasVersion !== 'number' || parsed.atlasVersion > ATLAS_VERSION) {
    throw new Error(
      `Atlas format version ${parsed.atlasVersion} cannot be read by this build (supports up to ${ATLAS_VERSION}).`,
    );
  }
  return parsed;
}

export default { atlasFileName, buildAtlas, serializeAtlas, parseAtlas };