All files / lib/compiler/atlas emit.js

87.64% Statements 78/89
100% Branches 6/6
75% Functions 3/4
87.64% Lines 78/89

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 103x 103x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 100x 277x 277x 277x 277x 277x 277x 277x 277x 91x 91x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x 277x                       277x 277x  
/**
 * @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 };