import * as fs from "node:fs"; import * as path from "node:path"; // `lib-utils` is imported lazily — see end of phase-cleanup. Eager // generation of all 11 shims left every site with dead code that had // to be cleaned up by hand. import { extractTheme } from "./analyzers/theme-extractor"; import { generateAppCss } from "./templates/app-css"; import { generateCacheConfig } from "./templates/cache-config"; import { generateCiFiles } from "./templates/ci-yml"; import { generateCommerceInit } from "./templates/commerce-init"; import { generateCommerceLoaders } from "./templates/commerce-loaders"; import { generateSyncBlocksBotYml } from "./templates/sync-blocks-bot-yml"; import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules"; import { generateHooks } from "./templates/hooks"; import { generateKnipConfig } from "./templates/knip-config"; import { generateLockfileCheckYml } from "./templates/lockfile-check-yml"; import { generateMainPushGuardYml } from "./templates/main-push-guard-yml"; import { CANONICAL_BUN_VERSION, CANONICAL_NODE_VERSION, generatePackageJson, } from "./templates/package-json"; import { generateParityYml } from "./templates/parity-yml"; import { generatePerfFiles } from "./templates/perf-yml"; import { generatePlaywrightFiles } from "./templates/playwright-yml"; import { generateReactDoctorYml } from "./templates/react-doctor-yml"; import { generateRoutes } from "./templates/routes"; import { generateSdkFiles } from "./templates/sdk-gen"; import { generateSectionLoaders } from "./templates/section-loaders"; import { generateServerEntry } from "./templates/server-entry"; import { generateSetup } from "./templates/setup"; import { generateTsconfig } from "./templates/tsconfig"; import { generateTypeFiles } from "./templates/types-gen"; import { generateUiComponents } from "./templates/ui-components"; import { generateViteConfig } from "./templates/vite-config"; import type { MigrationContext } from "./types"; import { log, logPhase } from "./types"; function writeFile(ctx: MigrationContext, relPath: string, content: string) { const fullPath = path.join(ctx.sourceDir, relPath); if (ctx.dryRun) { log(ctx, `[DRY] Would create: ${relPath}`); ctx.scaffoldedFiles.push(relPath); return; } const dir = path.dirname(fullPath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(fullPath, content, "utf-8"); log(ctx, `Created: ${relPath}`); ctx.scaffoldedFiles.push(relPath); } function writeMultiFile(ctx: MigrationContext, files: Record) { for (const [filePath, content] of Object.entries(files)) { writeFile(ctx, filePath, content); } } export function scaffold(ctx: MigrationContext): void { logPhase("Scaffold"); // Root config files. // wrangler.jsonc is scaffolded with a minimal dev config. For deploy, // Cloudflare Workers Builds ignores the `name` field (D6.3 — the // repo<->worker connection is wired once in the CF dashboard). Committing // this file is required for `@cloudflare/vite-plugin` to mount the worker // in local dev; without it every route returns a silent 404. writeFile(ctx, "package.json", generatePackageJson(ctx)); writeFile(ctx, "tsconfig.json", generateTsconfig()); writeFile(ctx, "vite.config.ts", generateViteConfig(ctx)); writeFile(ctx, "knip.config.ts", generateKnipConfig()); writeFile(ctx, ".gitignore", generateGitignore()); writeFile(ctx, "wrangler.jsonc", generateWranglerConfig(ctx)); // Deploy / preview pipelines are owned by Cloudflare Workers Builds // configured per-worker in the CF dashboard (D6.3). The migration // does NOT scaffold deploy/preview/sync-secrets workflows in the site // repo; the operator wires the repo<->worker connection in the CF // dashboard once after the first push. writeFile( ctx, ".prettierrc", JSON.stringify( { semi: true, singleQuote: false, trailingComma: "all" as const, printWidth: 100, tabWidth: 2, }, null, 2, ) + "\n", ); // PR-time lockfile guardrail. Lives in the site repo (per-site, not // a centralised reusable workflow) because per D6.3 we are not // scaffolding caller stubs into storefronts. Bun version is pinned // in lockstep with `package.json` via CANONICAL_BUN_VERSION. writeFile( ctx, ".github/workflows/lockfile-check.yml", generateLockfileCheckYml(CANONICAL_BUN_VERSION), ); // Advisory per-PR performance workflow: detects which sections changed, // maps them to CMS page paths, runs Lighthouse against the CF preview URL // (PR vs main), and posts a comparison comment. Gate = CLS + TBT only. writeMultiFile(ctx, generatePerfFiles(ctx.siteName)); // Per-PR quality pipeline (ci.yml) + its no-suppressions gate. BLOCKS on // generate + build; the migration-cleanliness gates (no-suppressions, // typecheck, format, knip) ship advisory so day-one CI is green. Node pinned // in lockstep with the perf/playwright workflows. writeMultiFile(ctx, generateCiFiles(CANONICAL_NODE_VERSION, CANONICAL_BUN_VERSION)); // Branch-protection surrogate: fails visibly if a commit reaches main // without a PR (never blocks the push itself). writeFile(ctx, ".github/workflows/main-push-guard.yml", generateMainPushGuardYml()); // Functional E2E harness (chromium + webkit) + self-contained config/smoke. writeMultiFile(ctx, generatePlaywrightFiles(CANONICAL_BUN_VERSION)); // Advisory React lint (react-doctor): comments on PRs, never fails. writeFile(ctx, ".github/workflows/react-doctor.yml", generateReactDoctorYml()); // Advisory parity validation: compares each PR preview against the original // live storefront (@decocms/parity). Inert until PARITY_PROD_URL is set. writeFile(ctx, ".github/workflows/parity.yml", generateParityYml(ctx.siteName)); // Daily content pull from the still-live storefront (`/.decofile` -> // `.deco/blocks` -> PR). Replaces the legacy cross-repo-PAT push sync; inert // until the operator sets the repo variable SYNC_BLOCKS_ORIGIN. writeFile( ctx, ".github/workflows/sync-blocks-bot.yml", generateSyncBlocksBotYml(CANONICAL_BUN_VERSION), ); // Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts) writeMultiFile(ctx, generateServerEntry(ctx)); // Route files writeMultiFile(ctx, generateRoutes(ctx)); // Secrets — extract env vars referenced by source AppContext // Must be generated BEFORE commerce-loaders and section-loaders since // those templates check for the secrets file to wire `...secrets` spreads. writeFile(ctx, "src/utils/secrets.ts", generateSecrets(ctx)); // Apps writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx)); // account.json is copied from source (if exists) or generated as fallback if (!ctx.files.some((f) => f.path === "account.json" && f.action !== "delete")) { const accountName = ctx.vtexAccount || ctx.siteName; writeFile(ctx, "src/account.json", JSON.stringify(accountName)); } // Setup infrastructure writeFile(ctx, "src/setup.ts", generateSetup(ctx)); writeFile(ctx, "src/cache-config.ts", generateCacheConfig(ctx)); writeFile(ctx, "src/setup/commerce-loaders.ts", generateCommerceLoaders(ctx)); // Server-only registration of COMMERCE_LOADERS + invoke — imported by // worker-entry.ts, not setup.ts, so the loader/action graph stays out of the // client bundle. writeFile(ctx, "src/setup/commerce-init.ts", generateCommerceInit(ctx)); writeFile(ctx, "src/setup/section-loaders.ts", generateSectionLoaders(ctx)); // Theme extraction + Styles const theme = extractTheme(ctx); writeFile(ctx, "src/styles/app.css", generateAppCss(ctx, theme)); // Type definitions writeMultiFile(ctx, generateTypeFiles(ctx)); // UI components (Image, Picture, Video) writeMultiFile(ctx, generateUiComponents(ctx)); // Platform hooks (useCart, useUser, useWishlist) writeMultiFile(ctx, generateHooks(ctx)); // SDK shims + generated utilities writeFile(ctx, "src/sdk/signal.ts", generateSignalShim()); writeFile(ctx, "src/sdk/clx.ts", generateClxShim()); writeFile(ctx, "src/sdk/debounce.ts", generateDebounceShim()); writeFile(ctx, "src/sdk/logger.ts", generateLoggerStub()); writeMultiFile(ctx, generateSdkFiles(ctx)); // VTEX utility wrappers (signature-compatible stubs) are no longer // generated eagerly here. They're written lazily at end of phase-cleanup, // after all import rewrites have run, so that we only emit shims that // some file actually imports. See `writeImportedLibShims` in phase-cleanup. // Replace Context-based useDevice with SSR-safe useSyncExternalStore version. // @decocms/tanstack shell-renders sections in a separate React root without // Device.Provider, so the old createContext pattern throws during SSR. writeFile(ctx, "src/contexts/device.tsx", generateDeviceContext()); // Location matcher — server-side geolocation matching if (hasLocationMatcher(ctx)) { writeFile(ctx, "src/matchers/location.ts", generateLocationMatcher()); } // SiteTheme component (replaces apps/website/components/Theme.tsx) const usesSiteTheme = ctx.files.some((f) => { if (f.action === "delete") return false; try { const content = fs.readFileSync(f.absPath, "utf-8"); return content.includes("SiteTheme"); } catch { return false; } }); if (usesSiteTheme) { writeFile(ctx, "src/components/ui/Theme.tsx", generateSiteThemeComponent()); } // Migration tooling policy pointer rule (D1–D5 + priorities). // The canonical rule lives in decocms/blocks; this is a tiny // pointer that loads on every Cursor session in the migrated site // so agents working on the site know where the policy is and what // it means here. See MIGRATION_TOOLING_PLAN.md (Wave 12-H). writeFile( ctx, ".cursor/rules/migration-tooling-policy.mdc", generateMigrationPolicyPointerRule(ctx.siteName), ); // Create public/ directory if (!ctx.dryRun) { fs.mkdirSync(path.join(ctx.sourceDir, "public"), { recursive: true }); } console.log(` Scaffolded ${ctx.scaffoldedFiles.length} files`); } function generateSiteThemeComponent(): string { return `export interface Font { family: string; styleSheet?: string; } export interface Props { colorScheme?: "light" | "dark" | "any"; fonts?: Font[]; variables?: Array<{ name: string; value: string }>; } export default function SiteTheme({ variables, fonts, colorScheme }: Props) { const cssVars = variables?.length ? \`:root { \${variables.map((v) => \`\${v.name}: \${v.value};\`).join(" ")} }\` : ""; const colorSchemeCss = colorScheme && colorScheme !== "any" ? \`:root { color-scheme: \${colorScheme}; }\` : ""; const css = [cssVars, colorSchemeCss].filter(Boolean).join("\\n"); return ( <> {fonts?.map((font) => font.styleSheet ? ( ) : null )} {css &&