feat(compiler): Lottie in multi-page output + materialize inline animationData
Two follow-ups to the single-page Lottie support: 1. Multi-page (clone-site). generateSiteApp emitted only DittoWire; it now wires DittoLottie per route exactly like wires — builds the route's lottie spec, emits the import + JSX, writes DittoLottie.tsx once, and injects lottie-web into the site package.json when any route replays an animation (the write moved after the route loop so the dep reflects actual usage). cloneSite now captures motion on full route captures (motion: full) so lotties are observed; light sibling probes skip it. Per-route gating verified: a route with no animation gets no DittoLottie. 2. Inline animationData. When a lottie is only in memory (data-driven init, no fetchable URL), capture now writes the JSON to the assets store as a real, content-hashed .json so it materializes to /assets/cloned/lottie like any source — instead of embedding the blob in the page spec. buildLottieSpec then resolves it to a local path; the spec carries path, not animationData. Verified end-to-end: a 2-page fixture emits DittoLottie only on the animated route with lottie-web added; a data-driven inline fixture materializes fd7c…json and the spec references it by path. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
co-authored by
claude-flow
parent
048fd8cdad
commit
9a89fed365
@@ -829,7 +829,20 @@ export async function captureSite(opts: {
|
||||
// Register lottie source JSONs as assets so the asset stage downloads + materializes
|
||||
// them (the in-page detector only records the URL; the fallback fetch grabs the file).
|
||||
for (const l of motion?.lotties ?? []) {
|
||||
if (l.src) recordAsset(l.src, "lottie", null, null, "lottie");
|
||||
if (l.src) { recordAsset(l.src, "lottie", null, null, "lottie"); continue; }
|
||||
// Inline animationData (no fetchable URL): store it as a real .json asset so it
|
||||
// materializes to /assets/cloned/lottie like any other source, instead of being
|
||||
// embedded in the page spec. Content-hashed so output stays deterministic.
|
||||
if (l.inlineKey && motion?.lottieInline) {
|
||||
const data = motion.lottieInline[l.inlineKey];
|
||||
if (data === undefined) continue;
|
||||
const buf = Buffer.from(JSON.stringify(data), "utf8");
|
||||
const synthUrl = `ditto-inline:/lottie/${sha1_12(buf.toString("utf8"))}.json`;
|
||||
recordAsset(synthUrl, "lottie", "application/json", 200, "lottie-inline");
|
||||
storeBytes(synthUrl, "lottie", buf);
|
||||
l.src = synthUrl; // now resolves to a local path through the asset graph
|
||||
l.inlineKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export async function runCloneSite(opts: CloneSiteOptions): Promise<CloneSiteRes
|
||||
// Only capture interactions on full (all-viewport) route captures, never the
|
||||
// light single-viewport sibling probes used for collection confirmation.
|
||||
const full = viewports.length === REQUIRED_VIEWPORTS.length;
|
||||
const capture = await captureSite({ url, outDir: sourceDir, viewports, interactions: opts.interactions && full, screenshots, log: () => {} });
|
||||
const capture = await captureSite({ url, outDir: sourceDir, viewports, interactions: opts.interactions && full, motion: full, screenshots, log: () => {} });
|
||||
const ir = buildIR(sourceDir, viewports);
|
||||
const assetGraph = buildAssetGraph(capture);
|
||||
const fontGraph = buildFontGraph(capture.fontFaces, assetGraph, url);
|
||||
|
||||
@@ -14,7 +14,8 @@ import { writeText, readJSON, fileExists } from "../util/fsx.js";
|
||||
import { generateCss, RESET_CSS } from "../generate/css.js";
|
||||
import { generateInteractionCss } from "../generate/interactionCss.js";
|
||||
import { buildRuntimeSpecs, wiresJsx, dittoWireImportPath, DITTO_WIRE_TSX, interactionRejectedSet } from "../generate/interactive.js";
|
||||
import { renderChildrenJsx, renderAttrs, buildComponentRegistry, componentPreamble, componentFiles, componentImports, componentDataDecls, summarizeComponents, fileBase, generateViteConfig, generateViteIndexHtml, viteGlobalsCss, PACKAGE_JSON, PACKAGE_JSON_TW, PACKAGE_JSON_VITE, PACKAGE_JSON_VITE_TW, TSCONFIG_JSON, TSCONFIG_JSON_VITE, NEXT_CONFIG, type AppFramework, type LinkRewrite, type ExtractedComponent, type RenderCtx } from "../generate/app.js";
|
||||
import { buildLottieSpec, lottieHasContent, lottieWireJsx, dittoLottieImportPath, DITTO_LOTTIE_TSX } from "../generate/lottie.js";
|
||||
import { renderChildrenJsx, renderAttrs, buildComponentRegistry, componentPreamble, componentFiles, componentImports, componentDataDecls, summarizeComponents, fileBase, generateViteConfig, generateViteIndexHtml, viteGlobalsCss, PACKAGE_JSON, PACKAGE_JSON_TW, PACKAGE_JSON_VITE, PACKAGE_JSON_VITE_TW, TSCONFIG_JSON, TSCONFIG_JSON_VITE, NEXT_CONFIG, injectLottieDep, type AppFramework, type LinkRewrite, type ExtractedComponent, type RenderCtx } from "../generate/app.js";
|
||||
import { buildTailwind, tailwindGlobalsCss, createColorInterner, colorDefsCssOf, type TailwindOutput } from "../generate/tailwind.js";
|
||||
import type { InteractionCapture } from "../capture/interactions.js";
|
||||
import type { IRChild } from "../normalize/ir.js";
|
||||
@@ -267,7 +268,8 @@ export function generateSiteApp(opts: {
|
||||
};
|
||||
|
||||
// Shared scaffold (written once).
|
||||
writeText(join(appDir, "package.json"), isVite ? (tw ? PACKAGE_JSON_VITE_TW : PACKAGE_JSON_VITE) : (tw ? PACKAGE_JSON_TW : PACKAGE_JSON));
|
||||
// package.json is written after the route loop (below) so the lottie-web dependency can be
|
||||
// added when any route actually replays a Lottie animation.
|
||||
writeText(join(appDir, "tsconfig.json"), isVite ? TSCONFIG_JSON_VITE : TSCONFIG_JSON);
|
||||
writeText(join(appDir, ".gitignore"), "node_modules\n.next\nout\ndist\n");
|
||||
if (isVite) {
|
||||
@@ -325,6 +327,7 @@ export function generateSiteApp(opts: {
|
||||
let assetsCopied = 0;
|
||||
let assetsMissing = 0;
|
||||
let anyWires = false;
|
||||
let anyLottie = false;
|
||||
const outRoutes: SiteGenResult["routes"] = [];
|
||||
const components = { count: 0, byType: {} as Record<string, number> };
|
||||
const materializedAssets = new Set<string>();
|
||||
@@ -368,6 +371,12 @@ export function generateSiteApp(opts: {
|
||||
const wireImport = wires.length ? `import DittoWire from "${dittoWireImportPath(depth)}";\n` : "";
|
||||
const wireBody = wires.length ? "\n" + wiresJsx(wires, 3) : "";
|
||||
if (wires.length) anyWires = true;
|
||||
// Lottie replay for this route (third-party JSON animations). Same per-route shape as wires.
|
||||
const lottieSpec = buildLottieSpec(route.ir, route.capture?.motion, route.assetGraph, include);
|
||||
const hasLottie = lottieHasContent(lottieSpec);
|
||||
const lottieImport = hasLottie ? `import DittoLottie from "${dittoLottieImportPath(depth)}";\n` : "";
|
||||
const lottieBody = hasLottie ? "\n" + lottieWireJsx(lottieSpec, 3) : "";
|
||||
if (hasLottie) anyLottie = true;
|
||||
const rKey = routeKey(route.routePath);
|
||||
const routeDir = isVite
|
||||
? join(appDir, "src", "routes", rKey)
|
||||
@@ -379,8 +388,8 @@ export function generateSiteApp(opts: {
|
||||
const compImport = componentImports(routeReg, 0); // route-local → ./components/Name
|
||||
const dataDecls = componentDataDecls(routeReg);
|
||||
const preBlock = dataDecls ? dataDecls + "\n\n" : "";
|
||||
const importLines = [wireImport.trimEnd(), compImport].filter(Boolean).join("\n");
|
||||
const pageTsx = `${isVite ? "" : 'import "./ditto.css";\n'}${importLines ? importLines + "\n" : ""}// Generated by clone-site. Do not edit by hand.\n${preBlock}export default function Page() {\n return (\n <>\n${bodyJsx}${wireBody}
|
||||
const importLines = [wireImport.trimEnd(), lottieImport.trimEnd(), compImport].filter(Boolean).join("\n");
|
||||
const pageTsx = `${isVite ? "" : 'import "./ditto.css";\n'}${importLines ? importLines + "\n" : ""}// Generated by clone-site. Do not edit by hand.\n${preBlock}export default function Page() {\n return (\n <>\n${bodyJsx}${wireBody}${lottieBody}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -409,6 +418,10 @@ export function generateSiteApp(opts: {
|
||||
outRoutes.push({ routePath: route.routePath, href, dir, nodeCount: route.ir.doc.nodeCount });
|
||||
}
|
||||
if (anyWires) writeText(join(appDir, "src", isVite ? "ditto" : join("app", "ditto"), "DittoWire.tsx"), DITTO_WIRE_TSX);
|
||||
if (anyLottie) writeText(join(appDir, "src", isVite ? "ditto" : join("app", "ditto"), "DittoLottie.tsx"), DITTO_LOTTIE_TSX);
|
||||
// Deferred package.json — inject lottie-web only when a route replays a Lottie animation.
|
||||
const sitePkg = isVite ? (tw ? PACKAGE_JSON_VITE_TW : PACKAGE_JSON_VITE) : (tw ? PACKAGE_JSON_TW : PACKAGE_JSON);
|
||||
writeText(join(appDir, "package.json"), anyLottie ? injectLottieDep(sitePkg) : sitePkg);
|
||||
if (isVite) {
|
||||
writeText(join(appDir, "vite.config.ts"), generateViteConfig(viteEntries));
|
||||
for (const [rel, body] of seoStaticFiles(seoInventory, seoRoutes)) writeText(join(appDir, "public", rel), body);
|
||||
@@ -442,7 +455,7 @@ export function generateSiteApp(opts: {
|
||||
componentCount: extracted.chrome.length + extracted.routes.reduce((sum, route) => sum + route.components.length, 0),
|
||||
svgCount: 0,
|
||||
hasContentModule: false,
|
||||
runtimeUtilities: anyWires ? ["DittoWire"] : [],
|
||||
runtimeUtilities: [...(anyWires ? ["DittoWire"] : []), ...(anyLottie ? ["DittoLottie"] : [])],
|
||||
});
|
||||
|
||||
return { routes: outRoutes, assetsCopied, assetsMissing, components, extracted, seoInventory };
|
||||
|
||||
Reference in New Issue
Block a user