Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions packages/blocks-cli/scripts/migrate/fast-deploy-scaffold.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "vitest";
import { parseJsonc } from "../lib/jsonc";
import { generateWranglerConfig } from "./phase-scaffold";
import { checks } from "./phase-verify";
import { generateSetup } from "./templates/setup";
import type { MigrationContext } from "./types";

/**
* Fast Deploy is inert unless THREE things line up: the `DECO_KV` binding,
* `DECO_FAST_DEPLOY=1`, and `setup.ts` handing the KV resolver to
* `@decocms/blocks-admin` (which cannot import `@decocms/tanstack` itself).
*
* Two of three is the worst state: it looks configured, a Studio publish
* reports success, and nothing ever reaches KV. These tests pin the scaffold to
* emit all three and pin the verify check to actually catch a partial config.
*/

const FIXTURE: MigrationContext = {
sourceDir: "",
siteName: "test-site",
platform: "custom",
vtexAccount: null,
gtmId: null,
importMap: {},
discoveredNpmDeps: {},
themeColors: {},
tailwindConfig: {
colors: {},
fontFamily: {},
screens: {},
safelist: [],
safelistPatterns: [],
plugins: [],
reviewItems: [],
animations: {},
keyframes: {},
},
fontFamily: null,
googleFonts: [],
layout: null,
files: [],
sectionMetas: [],
islandClassifications: [],
loaderInventory: [],
patterns: {},
scaffoldedFiles: [],
transformedFiles: [],
deletedFiles: [],
movedFiles: [],
manualReview: [],
dryRun: false,
verbose: false,
} as unknown as MigrationContext;

const fastDeployCheck = checks.find((c) => c.name === "Fast Deploy wired end to end");

/** Write a site tree with the given wrangler + setup contents. */
function makeSite(wrangler: string, setup: string): MigrationContext {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fd-scaffold-"));
fs.writeFileSync(path.join(dir, "wrangler.jsonc"), wrangler);
fs.mkdirSync(path.join(dir, "src"), { recursive: true });
fs.writeFileSync(path.join(dir, "src", "setup.ts"), setup);
return { ...FIXTURE, sourceDir: dir };
}

describe("scaffolded wrangler.jsonc", () => {
// Parse it rather than grep it: the template is hand-written JSONC, and a
// stray comma or comment that breaks parsing silently disables fast-deploy
// everywhere at once (the builder, the control-plane and the verify check
// below all read this file through a JSONC parser).
const cfg = parseJsonc(generateWranglerConfig(FIXTURE)) as {
kv_namespaces: Array<{ binding: string }>;
vars: Record<string, string>;
tail_consumers?: unknown[];
};

it("declares the DECO_KV binding and DECO_FAST_DEPLOY together", () => {
expect(cfg.kv_namespaces.map((n) => n.binding)).toContain("DECO_KV");
expect(cfg.vars.DECO_FAST_DEPLOY).toBe("1");
expect(cfg.vars.DECO_SITE_NAME).toBe("test-site");
});

it("declares a tail consumer, without which exceededMemory is unobservable", () => {
// Cloudflare kills the isolate over the 128MB cap before any in-worker code
// could report it — the tail worker is the only channel that sees it.
expect(cfg.tail_consumers).toBeTruthy();
});
});

describe("scaffolded setup.ts", () => {
it("calls setupTanstackFastDeploy, or the Studio publish write-through no-ops", () => {
const setup = generateSetup(FIXTURE);
expect(setup).toContain("setupTanstackFastDeploy()");
expect(setup).toMatch(/import \{[^}]*setupTanstackFastDeploy[^}]*\} from "@decocms\/tanstack"/);
});
});

describe('verify check "Fast Deploy wired end to end"', () => {
const FULL_WRANGLER = `{
// comment + trailing comma: real wrangler.jsonc, not JSON
"kv_namespaces": [{ "binding": "DECO_KV", "id": "" }],
"vars": { "DECO_FAST_DEPLOY": "1" },
}`;
const FULL_SETUP = "setupTanstackFastDeploy();\n";

it("passes on a fully wired site", () => {
expect(fastDeployCheck?.fn(makeSite(FULL_WRANGLER, FULL_SETUP))).toBe(true);
});

it("fails when the DECO_KV binding is missing", () => {
const w = '{ "kv_namespaces": [], "vars": { "DECO_FAST_DEPLOY": "1" } }';
expect(fastDeployCheck?.fn(makeSite(w, FULL_SETUP))).toBe(false);
});

it("fails when DECO_FAST_DEPLOY is not set", () => {
const w = '{ "kv_namespaces": [{ "binding": "DECO_KV", "id": "" }] }';
expect(fastDeployCheck?.fn(makeSite(w, FULL_SETUP))).toBe(false);
});

it("fails when setup.ts never hands over the KV resolver", () => {
expect(fastDeployCheck?.fn(makeSite(FULL_WRANGLER, "// nothing\n"))).toBe(false);
});

it("is an error, not a warning — a silent half-config is the failure mode", () => {
expect(fastDeployCheck?.severity).toBe("error");
});
});
56 changes: 41 additions & 15 deletions packages/blocks-cli/scripts/migrate/phase-scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,21 +277,47 @@ export { type Font as SiteThemeFont };
`;
}

function generateWranglerConfig(ctx: MigrationContext): string {
return (
JSON.stringify(
{
name: ctx.siteName,
main: "src/worker-entry.ts",
compatibility_date: "2025-05-01",
compatibility_flags: ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
cache: { enabled: true },
kv_namespaces: [{ binding: "SITES_KV", id: "dev-sites-kv" }],
},
null,
2,
) + "\n"
);
export function generateWranglerConfig(ctx: MigrationContext): string {
// Emitted as real JSONC (not JSON.stringify) so the comments survive — every
// field below is load-bearing and the next person to edit this file needs to
// know why. The ids are placeholders: the control-plane owns them (it
// provisions the site's OWN namespaces and overrides these at build time),
// so a wrong id here can never reach production.
return `{
"name": "${ctx.siteName}",
"main": "src/worker-entry.ts",
"compatibility_date": "2025-05-01",
// no_handle_cross_request_promise_resolution: the framework's SWR/dedup
// caches hold in-flight promises across requests; without this flag the
// worker hangs.
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
// Workers Cache — tiered cache IN FRONT of the worker. On a hit the worker
// never runs. Per-response cacheability is still driven by Cache-Control.
"cache": { "enabled": true },
// Surfaces the build sha as service.version on every span and log line.
"version_metadata": { "binding": "CF_VERSION_METADATA" },
// Tail worker. The ONLY channel that can see exceededMemory / exceededCpu:
// Cloudflare kills the isolate before any in-worker code could report them.
// Without this a memory incident is invisible — see docs/runbooks/.
"tail_consumers": [{ "service": "deco-otel-tail" }],
"kv_namespaces": [
// Fast Deploy content store. Holds decofile:<deployment-id> + the revision
// index, so a CMS publish goes live without a code deploy. The id is
// written by the control-plane at site creation and re-forced from
// CF_KV_NAMESPACE_ID on every build (per-site KV isolation).
{ "binding": "DECO_KV", "id": "" },
// A/B testing assignments.
{ "binding": "SITES_KV", "id": "" }
],
"vars": {
"DECO_SITE_NAME": "${ctx.siteName}",
"DECO_ENV_NAME": "production",
// Fast Deploy opt-in. Requires BOTH this flag AND the DECO_KV binding
// above; inert if either is missing. Set to "0" to disable.
"DECO_FAST_DEPLOY": "1"
}
}
`;
}

function generateGitignore(): string {
Expand Down
40 changes: 40 additions & 0 deletions packages/blocks-cli/scripts/migrate/phase-verify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { parseJsonc } from "../lib/jsonc";
import type { MigrationContext } from "./types";
import { logPhase } from "./types";

Expand Down Expand Up @@ -84,6 +85,45 @@ export const checks: Check[] = [
return true;
},
},
{
// Fast Deploy needs THREE things and is inert unless all three are present:
// the DECO_KV binding, DECO_FAST_DEPLOY=1, and setup.ts handing the KV
// resolver to blocks-admin. Two of three is the worst state — it looks
// configured, the Studio publish reports success, and nothing reaches KV.
name: "Fast Deploy wired end to end",
severity: "error",
fn: (ctx) => {
const missing: string[] = [];
const wranglerPath = path.join(ctx.sourceDir, "wrangler.jsonc");
try {
const cfg = parseJsonc(fs.readFileSync(wranglerPath, "utf-8")) as {
kv_namespaces?: Array<{ binding?: string }>;
vars?: Record<string, unknown>;
};
const flag = cfg.vars?.DECO_FAST_DEPLOY;
if (!(cfg.kv_namespaces ?? []).some((n) => n?.binding === "DECO_KV")) {
missing.push("wrangler.jsonc kv_namespaces is missing the DECO_KV binding");
}
if (flag !== "1" && flag !== "true") {
missing.push('wrangler.jsonc vars.DECO_FAST_DEPLOY is not "1"');
}
} catch (e) {
missing.push(`wrangler.jsonc could not be parsed: ${(e as Error).message}`);
}

const setupPath = path.join(ctx.sourceDir, "src", "setup.ts");
const setup = fs.existsSync(setupPath) ? fs.readFileSync(setupPath, "utf-8") : "";
if (!setup.includes("setupTanstackFastDeploy()")) {
missing.push("src/setup.ts does not call setupTanstackFastDeploy()");
}

if (missing.length > 0) {
for (const m of missing) console.log(` ${m}`);
return false;
}
return true;
},
},
{
name: "Old artifacts removed",
severity: "error",
Expand Down
9 changes: 8 additions & 1 deletion packages/blocks-cli/scripts/migrate/templates/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps-vtex";` : ""}${h
import { registerLocationMatcher } from "./matchers/location";` : ""}
import { blocks as generatedBlocks } from "../.deco/blocks.gen";
import { sectionMeta, syncComponents, loadingFallbacks, renderJsons } from "../.deco/sections.gen";
import { PreviewProviders } from "@decocms/tanstack";
import { PreviewProviders, setupTanstackFastDeploy } from "@decocms/tanstack";
// @ts-ignore Vite ?url import
import appCss from "./styles/app.css?url";

Expand All @@ -131,6 +131,13 @@ createSiteSetup({
return null;
},
});

// -- Fast Deploy --
// Hands the KV binding resolver to @decocms/blocks-admin, which cannot import
// @decocms/tanstack itself (wrong direction in the package graph). Without this
// call a Studio publish silently no-ops instead of writing through to KV.
// Inert unless the worker has DECO_FAST_DEPLOY=1 + a DECO_KV binding.
setupTanstackFastDeploy();
${isVtex ? `
// -- VTEX wiring --
setVtexFetch(createInstrumentedFetch("vtex"));
Expand Down