noxid-cli 0.2.1

The Noxid compiler command line: check, build, test, adapt, and the agent surface
import { diagnostics as initialDiagnostics } from "./noxid-diagnostics.js";

const modules = new Map();
const instances = new Set();
const restoredState = new Map();
const pending = new Map();
let updateSequence = 0;
let flushTimer = null;

function activeInstances(component = null) {
  const active = [];
  for (const instance of instances) {
    if (instance.owner?.disposed) {
      instances.delete(instance);
    } else if (component === null || instance.component === component) {
      active.push(instance);
    }
  }
  return active;
}

function classify(previous, next) {
  if (!previous || !next || previous.component !== next.component) return "reload";
  if (previous.interfaceHash !== next.interfaceHash) return "reload";
  if (previous.stateSchemaHash !== next.stateSchemaHash) return "remount-reset-state";
  if (
    previous.templateHash !== next.templateHash
    || previous.bindingHash !== next.bindingHash
    || previous.reactiveHash !== next.reactiveHash
  ) return "remount-preserve-state";
  if (previous.actionHash !== next.actionHash) return "actions";
  if (previous.styleHash !== next.styleHash) return "style";
  return "none";
}

function snapshotState() {
  restoredState.clear();
  for (const instance of activeInstances()) {
    for (const [semanticId, source] of Object.entries(instance.state ?? {})) {
      const values = restoredState.get(semanticId) ?? [];
      values.push(source.get());
      restoredState.set(semanticId, values);
    }
  }
}

function refreshStyles(revision) {
  for (const link of document.querySelectorAll("link[data-noxid-route-style]")) {
    const url = new URL(link.href, location.href);
    url.searchParams.set("noxid-hmr", String(revision));
    const replacement = link.cloneNode();
    replacement.href = url.href;
    replacement.addEventListener("load", () => link.remove(), { once: true });
    link.after(replacement);
  }
}

function renderDiagnostics(values) {
  document.querySelector("[data-noxid-diagnostics]")?.remove();
  if (!values?.length) return;
  const panel = document.createElement("section");
  panel.dataset.noxidDiagnostics = "";
  panel.setAttribute("role", "alert");
  panel.style.cssText = "position:fixed;inset:auto 1rem 1rem 1rem;z-index:2147483647;max-height:45vh;overflow:auto;padding:1rem;border:1px solid #ef4444;border-radius:.5rem;background:#1c1010;color:#fee2e2;font:13px/1.45 ui-monospace,monospace;white-space:pre-wrap;box-shadow:0 1rem 3rem #0008";
  const heading = document.createElement("strong");
  heading.textContent = "Noxid compiler diagnostics";
  panel.appendChild(heading);
  for (const diagnostic of values) {
    const item = document.createElement("pre");
    item.textContent = `${diagnostic.code}: ${diagnostic.message}`;
    panel.appendChild(item);
  }
  document.body.appendChild(panel);
}

async function flush() {
  flushTimer = null;
  const changes = [...pending.values()];
  pending.clear();
  const revision = ++updateSequence;
  const kinds = new Set(changes.map((change) => change.kind));
  if (kinds.has("reload")) return location.reload();
  if (kinds.has("remount-reset-state") || kinds.has("remount-preserve-state")) {
    if (kinds.has("remount-preserve-state") && !kinds.has("remount-reset-state")) snapshotState();
    else restoredState.clear();
    const refreshed = await globalThis.__NOXID_APP__?.router?.refresh?.(revision);
    if (!refreshed) location.reload();
    return;
  }
  for (const change of changes) {
    if (change.kind === "actions") {
      for (const instance of activeInstances(change.component)) {
        instance.patchActions?.(change.nextModule);
      }
    }
  }
  if (kinds.has("style")) refreshStyles(revision);
}

const api = Object.freeze({
  registerModule(component, signature) {
    modules.set(component, signature);
  },
  registerInstance(instance) {
    instances.add(instance);
  },
  initialState(semanticId, fallback) {
    const values = restoredState.get(semanticId);
    if (!values?.length) return fallback;
    const value = values.shift();
    if (values.length === 0) restoredState.delete(semanticId);
    return value;
  },
  acceptComponent(component, nextModule, nextSignature) {
    const previous = modules.get(component);
    modules.set(component, nextSignature);
    pending.set(component, {
      component,
      nextModule,
      kind: classify(previous, nextSignature),
    });
    if (flushTimer === null) flushTimer = setTimeout(() => void flush(), 25);
  },
  inspect() {
    return Object.freeze({
      modules: Object.freeze(Object.fromEntries(modules)),
      activeInstances: activeInstances().length,
      pending: pending.size,
    });
  },
});

globalThis.__NOXID_HMR__ = api;
renderDiagnostics(initialDiagnostics);

if (import.meta.hot) {
  import.meta.hot.accept("./noxid-diagnostics.js", (nextModule) => {
    renderDiagnostics(nextModule?.diagnostics ?? []);
  });
}