diff --git a/compiler/src/generate/app.ts b/compiler/src/generate/app.ts index 13db98b..7b19c03 100644 --- a/compiler/src/generate/app.ts +++ b/compiler/src/generate/app.ts @@ -14,6 +14,7 @@ import type { FontGraph } from "../infer/fonts.js"; import { SYSTEM_FALLBACK } from "../infer/fonts.js"; import { detectComponents, type ComponentPlan, type ComponentCluster } from "../infer/components.js"; import { buildClassMap } from "./classMap.js"; +import { generatePreviewHtml } from "./preview.js"; import { buildTailwind, tailwindGlobalsCss } from "./tailwind.js"; import { planSections, type SectionPlan } from "./sectionSplit.js"; import type { RecipeReport } from "../infer/recipes.js"; @@ -2659,7 +2660,7 @@ export function recipeResponsiveClassCleaner(ir: IR, recipes: RecipeReport | und }; } -export function generateApp(input: GenerateInput, tokensCss: string): { pageTsx: string; cloneCss: string; components: ExtractedComponent[] } { +export function generateApp(input: GenerateInput, tokensCss: string): { pageTsx: string; cloneCss: string; components: ExtractedComponent[]; previewHtml: string } { const { ir, assetGraph, fontGraph, appDir, sourceUrl } = input; const assetMap = buildAssetMap(assetGraph); const framework = input.framework ?? "next"; @@ -2789,6 +2790,16 @@ export function generateApp(input: GenerateInput, tokensCss: string): { pageTsx: writeText(join(rootDir, "globals.css"), framework === "vite" ? viteGlobalsCss(globals) : globals); } writeText(join(rootDir, "ditto.css"), cloneCss); + // Fast, self-contained static preview (runtime-free single HTML file) the service can + // render within seconds of `generate`, BEFORE the Next build + deploy completes. Emitted + // at generated/app/preview.html (assets relative to public/); shares the same IR + css.ts + // rule/banding collectors and the same propsList/resolveTag decision code as the app, + // so it shows what the built app will show without a second maintained emission path. + const previewPath = "preview.html"; + writeText(join(appDir, previewPath), generatePreviewHtml({ + ir, assetMap, fontGraph, tokensCss, sourceUrl, + colorVar: input.colorVar, tokenResolver: input.tokenResolver, + })); writeText(join(appDir, "src", "lib", "utils.ts"), CN_UTILS_MODULE); // SITE_ORIGIN constant for SEO/metadata routes (Next only — Vite ships static SEO files). if (framework === "next") writeText(join(appDir, "src", "lib", "site.ts"), SITE_ORIGIN_MODULE); @@ -2830,7 +2841,7 @@ export function generateApp(input: GenerateInput, tokensCss: string): { pageTsx: ], }); - return { pageTsx, cloneCss, components: components ? summarizeComponents(components) : [] }; + return { pageTsx, cloneCss, components: components ? summarizeComponents(components) : [], previewHtml: previewPath }; } /** Add lottie-web to a generated package.json's dependencies — DittoLottie imports it at diff --git a/compiler/src/generate/manifest.ts b/compiler/src/generate/manifest.ts index 57ee6b2..e18b9b6 100644 --- a/compiler/src/generate/manifest.ts +++ b/compiler/src/generate/manifest.ts @@ -18,8 +18,12 @@ export function buildManifest(args: { capture: CaptureResult; componentCount: number; patternHints?: PatternHints; + // Relative path (within generated/app) of the static preview artifact, when emitted. + // Lets the service/client show the cloned page from the file map BEFORE the Next build + // + deploy finishes, then swap to the deployed app. A fixed string — no timestamps. + previewHtml?: string; }): Record { - const { ir, sections, tokens, assetGraph, fontGraph, capture, componentCount, patternHints } = args; + const { ir, sections, tokens, assetGraph, fontGraph, capture, componentCount, patternHints, previewHtml } = args; const byType: Record = {}; let downloaded = 0, skipped = 0; @@ -58,6 +62,9 @@ export function buildManifest(args: { fallback: fontGraph.entries.filter((f) => f.status === "fallback").length, }, components: { count: componentCount }, + // Static preview artifact (runtime-free, single HTML file) the client can render + // immediately from the file map. Omitted when not emitted. + ...(previewHtml ? { preview_html: previewHtml } : {}), // Frozen-catalog pattern evidence (hint-only, additive): library/platform fingerprints // detected in the IR, with the node cids that carried each signature (bounded pre-order // sample). Deterministic — matches are id-sorted and cids are pre-order, so the same diff --git a/compiler/src/generate/pipeline.ts b/compiler/src/generate/pipeline.ts index 8411a45..5706ac7 100644 --- a/compiler/src/generate/pipeline.ts +++ b/compiler/src/generate/pipeline.ts @@ -111,7 +111,7 @@ export function generateAll(opts: { 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, patternHints }); + const manifest = buildManifest({ ir, sections, tokens, assetGraph, fontGraph, capture, componentCount: inventory.count, patternHints, previewHtml: gen.previewHtml }); writeJSON(join(outDir, "manifest.json"), manifest); return { ir, sections, tokens, assetGraph, fontGraph, recipeReport, interactionRecipeReport, patternHints, seoInventory, codeQuality, manifest, assetsCopied: mat.copied, assetsMissing: mat.missing }; diff --git a/compiler/src/generate/preview.ts b/compiler/src/generate/preview.ts new file mode 100644 index 0000000..f1a78ab --- /dev/null +++ b/compiler/src/generate/preview.ts @@ -0,0 +1,243 @@ +import { isTextChild, type IR, type IRNode, type IRChild } from "../normalize/ir.js"; +import type { FontGraph } from "../infer/fonts.js"; +import { SYSTEM_FALLBACK } from "../infer/fonts.js"; +import { collectNodeRules, keyframesCss, computeBands, assembleCss, RESET_CSS } from "./css.js"; +import type { TokenResolver } from "../infer/tokens.js"; +import { propsList, resolveTag, resolveHtmlBg, htmlBgRule } from "./app.js"; + +/** + * Fast, self-contained static preview artifact (`preview.html`). + * + * Product moment: the service shows the cloned page within seconds of `generate` + * finishing — BEFORE the Next.js build + deploy completes — then swaps to the + * deployed app. This artifact renders WITHOUT any build toolchain (no next / tailwind + * / esbuild): it is pure string building over the same frozen IR + rules the JSX + * emitter consumes. + * + * SHARED DECISION CODE (not a parallel emission path): + * - CSS: `collectNodeRules` + `keyframesCss` + `computeBands` + `assembleCss` from + * css.ts — the identical per-node rule collection + media-query banding the semantic + * class-map emitter (classMap.ts) and the legacy per-node emitter (generateCss) use. + * We emit `.c` selectors, exactly like `generateCss`, so the preview's computed + * styles are byte-for-byte the same rules the built app resolves to. Crucially this + * holds even though the app's DEFAULT output is Tailwind (utilities in the JSX, no + * `.c` rules) — the preview reuses the css-mode rule collector directly. + * - HTML: `propsList` + `resolveTag` from app.ts — the SAME tag resolution, attribute + * filtering, asset-URL mapping (src/poster/srcset), video/lottie poster stills, and + * class assignment the JSX renderer uses. We translate the JSX-flavoured attribute + * pairs to raw HTML attributes; we do NOT re-derive which attrs/assets to keep. + * + * Runtime-free: no DittoWire / DittoLottie / DittoMotion scripts. Lottie mounts and + *