Initial commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { join } from "node:path";
|
||||
import { backfillLazyBackgrounds, buildIR, writeIR, type IR } from "../normalize/ir.js";
|
||||
import { detectSections, type Section } from "../infer/sections.js";
|
||||
import { extractTokens, tokensToCss, buildTokenResolver, type Tokens } from "../infer/tokens.js";
|
||||
import { buildColorPalette } from "../infer/semanticTokens.js";
|
||||
import { recognizePrimitives, inventoryOf } from "../infer/primitives.js";
|
||||
import { buildAssetGraph, materializeAssets, type AssetGraph } from "../infer/assets.js";
|
||||
import { buildFontGraph, type FontGraph } from "../infer/fonts.js";
|
||||
import { buildRecipeReport, recipeReportToMarkdown, type RecipeReport } from "../infer/recipes.js";
|
||||
import { buildInteractionRecipeReport, interactionRecipeReportToMarkdown, type InteractionRecipeReport } from "../infer/interactionRecipes.js";
|
||||
import { generateApp, type AppFramework } from "./app.js";
|
||||
import { buildCodeQualityReport, codeQualityReportToMarkdown, type CodeQualityReport } from "./codeQuality.js";
|
||||
import { interactionRejectedSet } from "./interactive.js";
|
||||
import { buildManifest } from "./manifest.js";
|
||||
import { buildSeoInventory, seoInventoryToMarkdown, type SeoInventory } from "./seo.js";
|
||||
import { writeJSON, writeText, readJSON, fileExists } from "../util/fsx.js";
|
||||
import type { CaptureResult } from "../capture/capture.js";
|
||||
|
||||
export type GenerateAllResult = {
|
||||
ir: IR;
|
||||
sections: Section[];
|
||||
tokens: Tokens;
|
||||
assetGraph: AssetGraph;
|
||||
fontGraph: FontGraph;
|
||||
recipeReport: RecipeReport;
|
||||
interactionRecipeReport: InteractionRecipeReport;
|
||||
seoInventory: SeoInventory;
|
||||
codeQuality: CodeQualityReport;
|
||||
manifest: Record<string, unknown>;
|
||||
assetsCopied: number;
|
||||
assetsMissing: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure deterministic generation from a frozen capture: IR → infer → generate app
|
||||
* → emit artifacts. Called by the CLI and twice by the determinism gate; given
|
||||
* the same sourceDir it must produce byte-identical output (rubric Gate 6).
|
||||
*/
|
||||
export function generateAll(opts: {
|
||||
sourceDir: string;
|
||||
capture: CaptureResult;
|
||||
viewports: number[]; // band/gate widths (standard breakpoints)
|
||||
sampleViewports?: number[]; // dense set for size inference (defaults to viewports)
|
||||
url: string;
|
||||
outDir: string; // the generated/ dir
|
||||
ignoreRejectedInteractions?: boolean; // validation retest mode: wire everything before pruning
|
||||
}): GenerateAllResult {
|
||||
const { sourceDir, capture, viewports, url, outDir } = opts;
|
||||
const sampleViewports = opts.sampleViewports ?? viewports;
|
||||
const appDir = join(outDir, "app");
|
||||
|
||||
// Stage 5: carry @keyframes only when motion capture ran (motion on by default;
|
||||
// `--no-motion` / the plain static benchmark leave `capture.motion` unset), so
|
||||
// `--no-motion` output is byte-identical to the pre-Stage-5 frozen clone. `!!capture.motion`
|
||||
// is stable across processes (persisted in capture-result.json) ⇒ determinism holds.
|
||||
const ir = buildIR(sourceDir, sampleViewports, { motion: !!capture.motion, bandViewports: viewports });
|
||||
backfillLazyBackgrounds(ir);
|
||||
writeIR(ir, sourceDir);
|
||||
|
||||
const sections = detectSections(ir);
|
||||
const tokens = extractTokens(ir);
|
||||
const assetGraph = buildAssetGraph(capture);
|
||||
const fontGraph = buildFontGraph(capture.fontFaces, assetGraph, url);
|
||||
const seoInventory = buildSeoInventory(ir, assetGraph, capture);
|
||||
|
||||
// Stage 3.5: semantic color tokens (load-bearing). The palette's :root supersedes
|
||||
// the decorative color group; ditto.css references var(--token) for colors.
|
||||
const palette = buildColorPalette(ir);
|
||||
const tokensCss = (palette.css ? palette.css + "\n" : "") + tokensToCss(tokens, true);
|
||||
const tokenResolver = buildTokenResolver(tokens);
|
||||
const primitives = recognizePrimitives(ir);
|
||||
const recipeReport = buildRecipeReport(ir, sections, primitives);
|
||||
const interactionRecipeReport = buildInteractionRecipeReport(ir, sections, capture.interaction);
|
||||
// Patterns the interaction gate previously rejected (don't reproduce) → left static.
|
||||
const rejPath = join(sourceDir, "interaction-rejected.json");
|
||||
const rejectedSpecs = !opts.ignoreRejectedInteractions && fileExists(rejPath)
|
||||
? interactionRejectedSet(readJSON<unknown>(rejPath))
|
||||
: undefined;
|
||||
// Stage 4.5: component extraction is opt-in, persisted in the source dir at clone
|
||||
// time so every generateAll for this run (deliverable, determinism gate, prune
|
||||
// regen) makes the SAME choice — keeping output deterministic across processes.
|
||||
const optPath = join(sourceDir, "clone-options.json");
|
||||
const cloneOpts = fileExists(optPath) ? readJSON<{ components?: boolean; humanizeMode?: "tailwind" | "css"; framework?: AppFramework; reflow?: boolean }>(optPath) : {};
|
||||
const components = !!cloneOpts.components;
|
||||
const humanizeMode = cloneOpts.humanizeMode; // undefined → generateApp default ("tailwind")
|
||||
const gen = generateApp({ ir, assetGraph, fontGraph, appDir, sourceDir, sourceUrl: url, seoInventory, colorVar: palette.varForColor, tokenResolver, primitives, recipeReport, interaction: capture.interaction, rejectedSpecs, components, humanizeMode, framework: cloneOpts.framework, motion: capture.motion, reflow: !!cloneOpts.reflow }, tokensCss);
|
||||
const mat = materializeAssets(assetGraph, sourceDir, join(appDir, "public"));
|
||||
|
||||
// Stage 4.5: record promoted components (empty when extraction is off).
|
||||
writeJSON(join(outDir, "extracted-components.json"), gen.components);
|
||||
|
||||
writeJSON(join(outDir, "sections.json"), sections);
|
||||
writeJSON(join(outDir, "tokens.json"), tokens);
|
||||
writeJSON(join(outDir, "assets.json"), assetGraph.entries);
|
||||
writeJSON(join(outDir, "fonts.json"), fontGraph.entries);
|
||||
const inventory = inventoryOf(ir, primitives);
|
||||
writeJSON(join(outDir, "components.json"), inventory);
|
||||
writeJSON(join(outDir, "recipes.json"), recipeReport);
|
||||
writeText(join(outDir, "recipes.md"), recipeReportToMarkdown(recipeReport));
|
||||
writeJSON(join(outDir, "interaction-recipes.json"), interactionRecipeReport);
|
||||
writeText(join(outDir, "interaction-recipes.md"), interactionRecipeReportToMarkdown(interactionRecipeReport));
|
||||
writeJSON(join(outDir, "seo.json"), seoInventory);
|
||||
writeText(join(outDir, "seo.md"), seoInventoryToMarkdown(seoInventory));
|
||||
const codeQuality = buildCodeQualityReport(appDir, recipeReport);
|
||||
writeJSON(join(outDir, "code-quality.json"), codeQuality);
|
||||
writeText(join(outDir, "code-quality.md"), codeQualityReportToMarkdown(codeQuality));
|
||||
const manifest = buildManifest({ ir, sections, tokens, assetGraph, fontGraph, capture, componentCount: inventory.count });
|
||||
writeJSON(join(outDir, "manifest.json"), manifest);
|
||||
|
||||
return { ir, sections, tokens, assetGraph, fontGraph, recipeReport, interactionRecipeReport, seoInventory, codeQuality, manifest, assetsCopied: mat.copied, assetsMissing: mat.missing };
|
||||
}
|
||||
Reference in New Issue
Block a user