@decocms/tanstack 7.68.0 → 7.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/tanstack",
3
- "version": "7.68.0",
3
+ "version": "7.69.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "lint:unused": "knip"
28
28
  },
29
29
  "dependencies": {
30
- "@decocms/blocks": "7.68.0",
31
- "@decocms/blocks-admin": "7.68.0",
32
- "@decocms/blocks-cli": "7.68.0",
30
+ "@decocms/blocks": "7.69.0",
31
+ "@decocms/blocks-admin": "7.69.0",
32
+ "@decocms/blocks-cli": "7.69.0",
33
33
  "@deco-cx/warp-node": "^0.3.16",
34
34
  "fast-json-patch": "^3.1.0",
35
35
  "ws": "^8.18.0"
@@ -250,6 +250,15 @@ describe("ensureBlocksHydrated when the bundle ships no decofile", () => {
250
250
  await expect(ensureBlocksHydrated(env)).rejects.toThrow(/not found and this bundle ships no/);
251
251
  });
252
252
 
253
+ it("throws when fast-deploy is off at runtime instead of serving {}", async () => {
254
+ withStubbedBundle();
255
+ setBlocks({});
256
+ const { kv } = makeKV();
257
+
258
+ await expect(ensureBlocksHydrated({})).rejects.toThrow(/fast-deploy is off/);
259
+ await expect(ensureBlocksHydrated({ DECO_KV: kv })).rejects.toThrow(/fast-deploy is off/);
260
+ });
261
+
253
262
  it("throws when the snapshot exists but is empty", async () => {
254
263
  // A seed that wrote `{}` would otherwise take the success branch, latch,
255
264
  // and serve the empty edge-cached site this guard exists to prevent.
@@ -138,7 +138,16 @@ function bundleHasNoDecofile(): boolean {
138
138
  * request retries instead of pinning the isolate to an empty decofile.
139
139
  */
140
140
  export function ensureBlocksHydrated(env: Env, _ctx?: ExecutionContextLike): Promise<void> {
141
- if (!isFastDeployEnabled(env)) return Promise.resolve();
141
+ if (!isFastDeployEnabled(env)) {
142
+ // A stubbed bundle with fast-deploy off would serve an empty 200 the edge caches.
143
+ return bundleHasNoDecofile()
144
+ ? Promise.reject(
145
+ new Error(
146
+ `[CMS/KV] this bundle ships no decofile but fast-deploy is off (set ${FAST_DEPLOY_ENV}=1 and bind DECO_KV)`,
147
+ ),
148
+ )
149
+ : Promise.resolve();
150
+ }
142
151
  if (G.__deco!.kvHydrated) return Promise.resolve();
143
152
  if (G.__deco!.kvHydration) return G.__deco!.kvHydration;
144
153
 
@@ -173,27 +173,52 @@ export function generateSchemaArgs(siteName) {
173
173
  return ["--site", siteName];
174
174
  }
175
175
 
176
+ /**
177
+ * Env var a deploy pipeline sets to declare "I seed `decofile:<deployment-id>`
178
+ * into KV BEFORE activating this version". Set by the cfworkers-builder under
179
+ * exactly the condition that guards its seed step, so the stub below and the
180
+ * seed can never drift apart.
181
+ */
182
+ const SEEDED_DEPLOY_ENV = "DECO_SEEDED_DEPLOY";
183
+
184
+ /** Whether this build runs inside a pipeline that seeds KV before activation. */
185
+ function isSeededDeployPipeline() {
186
+ const v = process.env[SEEDED_DEPLOY_ENV];
187
+ return v === "1" || v === "true";
188
+ }
189
+
176
190
  /**
177
191
  * @param {object} [opts]
178
- * @param {boolean} [opts.fastDeploy=false] Stub `blocks.gen` out of the SERVER
179
- * bundle too, so the worker's only copy of the decofile is the one hydrated
180
- * from KV at cold start.
192
+ * @param {boolean | "auto"} [opts.fastDeploy="auto"] Stub `blocks.gen` out of
193
+ * the SERVER bundle too, so the worker's only copy of the decofile is the one
194
+ * hydrated from KV at cold start.
181
195
  *
182
196
  * Without this, a fast-deploy site holds the decofile TWICE: the bundled
183
197
  * `JSON.parse(...)` graph stays permanently reachable through the site's
184
198
  * `import { blocks } from ".deco/blocks.gen"` module binding, and
185
199
  * `ensureBlocksHydrated` adds a second graph via `setBlocks()`. For a 10MB
186
- * decofile that is tens of MB of avoidable isolate memory.
200
+ * decofile that is tens of MB of avoidable isolate memory, on a platform
201
+ * whose per-isolate cap is 128MB and has no GC knob.
202
+ *
203
+ * It is gated because it removes the fallback: with no bundled snapshot, a
204
+ * cold start that cannot read KV has no content to serve, and
205
+ * `ensureBlocksHydrated` fails the request loudly (5xx) rather than serving
206
+ * an empty site that the edge would then cache — see `kvHydration.ts`. That
207
+ * is only safe when the deploy pipeline seeds `decofile:<deployment-id>`
208
+ * BEFORE the new version activates.
187
209
  *
188
- * Opt-in because it removes the fallback: with no bundled snapshot, a cold
189
- * start that cannot read KV has no content to serve. `ensureBlocksHydrated`
190
- * detects that case and fails the request loudly (5xx) instead of serving an
191
- * empty site — see `kvHydration.ts`. Only enable alongside
192
- * `DECO_FAST_DEPLOY=1` + a `DECO_KV` binding + a deploy pipeline that seeds
193
- * `decofile:<deployment-id>` BEFORE the new version activates.
210
+ * `"auto"` (default) stubs only when the pipeline declares it seeds, via
211
+ * `DECO_SEEDED_DEPLOY`. A site building through Cloudflare Workers Builds or
212
+ * a manual `wrangler deploy` never sets it, so it keeps the bundled snapshot
213
+ * and behaves exactly as before — no site-side config, no way to half-enable
214
+ * it. `true` forces the stub, `false` disables it. A stubbed bundle also
215
+ * needs `DECO_FAST_DEPLOY=1` and a `DECO_KV` binding at runtime — without
216
+ * them `ensureBlocksHydrated` fails every request rather than serve `{}`.
194
217
  * @returns {import("vite").PluginOption}
195
218
  */
196
- export function decoVitePlugin({ fastDeploy = false } = {}) {
219
+ export function decoVitePlugin({ fastDeploy = "auto" } = {}) {
220
+ // Resolved once: the pipeline's answer cannot change mid-build.
221
+ const stubBlocks = fastDeploy === "auto" ? isSeededDeployPipeline() : fastDeploy === true;
197
222
  // Set from the `config` hook (which Vite always runs before `load`). The
198
223
  // fastDeploy stub must apply to production builds ONLY: `vite dev` has no KV
199
224
  // to hydrate from, and the dev bootstrap below deliberately relies on reading
@@ -239,7 +264,7 @@ export function decoVitePlugin({ fastDeploy = false } = {}) {
239
264
  // `import { blocks }` binding keeps the bundled graph alive for the
240
265
  // isolate's whole life, even after `setBlocks()` swaps in the KV one).
241
266
  // Build only — see `isBuild` above.
242
- if (fastDeploy && isBuild) {
267
+ if (stubBlocks && isBuild) {
243
268
  return "export const blocks = {};";
244
269
  }
245
270
 
@@ -785,7 +810,7 @@ export function decoVitePlugin({ fastDeploy = false } = {}) {
785
810
  // is not equivalent — draft-preview overrides can make a stubbed bundle
786
811
  // look populated, and a legitimately-empty decofile (new site) would
787
812
  // look stubbed.
788
- __DECO_BLOCKS_STUBBED__: JSON.stringify(fastDeploy && command === "build"),
813
+ __DECO_BLOCKS_STUBBED__: JSON.stringify(stubBlocks && command === "build"),
789
814
  };
790
815
 
791
816
  // Only split chunks for production builds — dev uses unbundled ESM.
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { decoVitePlugin } from "./plugin.js";
3
3
 
4
4
  /**
@@ -68,6 +68,12 @@ describe("decoVitePlugin — fastDeploy server stub", () => {
68
68
  const id = "/repo/.deco/blocks.gen.ts";
69
69
  const STUB = "export const blocks = {};";
70
70
 
71
+ // The default is "auto", which reads DECO_SEEDED_DEPLOY. Pin it off so the
72
+ // "default does not stub" assertions below can't be silently satisfied (or
73
+ // broken) by an ambient env var.
74
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "");
75
+ afterEach(() => vi.stubEnv("DECO_SEEDED_DEPLOY", ""));
76
+
71
77
  /** @param {{fastDeploy?: boolean}} opts @param {"build"|"serve"} command */
72
78
  const make = (opts, command) => {
73
79
  const p = /** @type {any} */ (decoVitePlugin(opts));
@@ -110,3 +116,61 @@ describe("decoVitePlugin — fastDeploy server stub", () => {
110
116
  expect(def.define.__DECO_BLOCKS_STUBBED__).toBe("false");
111
117
  });
112
118
  });
119
+
120
+ /**
121
+ * The `"auto"` default: stub only when the deploy pipeline declares it seeds
122
+ * `decofile:<deployment-id>` into KV BEFORE activating the new version.
123
+ *
124
+ * Stubbing without that guarantee is a hard outage, not a degraded mode — the
125
+ * bundle has no decofile, so `ensureBlocksHydrated` 5xxs on purpose rather than
126
+ * serving an empty site the edge would cache. Cloudflare Workers Builds and a
127
+ * manual `wrangler deploy` do not seed, and they never set the env.
128
+ */
129
+ describe("decoVitePlugin — fastDeploy auto gate", () => {
130
+ const id = "/repo/.deco/blocks.gen.ts";
131
+ const STUB = "export const blocks = {};";
132
+
133
+ afterEach(() => vi.unstubAllEnvs());
134
+
135
+ /** @param {"build"|"serve"} command */
136
+ const makeDefault = (command) => {
137
+ const p = /** @type {any} */ (decoVitePlugin());
138
+ const cfg = p.config({}, { command });
139
+ return { plugin: p, cfg };
140
+ };
141
+
142
+ it("does not stub when the pipeline does not declare a seed step", () => {
143
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "");
144
+ const { plugin, cfg } = makeDefault("build");
145
+ expect(plugin.load(id, { ssr: true })).not.toBe(STUB);
146
+ expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
147
+ });
148
+
149
+ it("stubs on a production build when the pipeline declares it seeds", () => {
150
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
151
+ const { plugin, cfg } = makeDefault("build");
152
+ expect(plugin.load(id, { ssr: true })).toBe(STUB);
153
+ expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("true");
154
+ });
155
+
156
+ it("still never stubs SSR in dev, even inside a seeding pipeline", () => {
157
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
158
+ const { plugin, cfg } = makeDefault("serve");
159
+ expect(plugin.load(id, { ssr: true })).not.toBe(STUB);
160
+ expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
161
+ });
162
+
163
+ it("explicit false wins over a seeding pipeline", () => {
164
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
165
+ const p = /** @type {any} */ (decoVitePlugin({ fastDeploy: false }));
166
+ const cfg = p.config({}, { command: "build" });
167
+ expect(p.load(id, { ssr: true })).not.toBe(STUB);
168
+ expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
169
+ });
170
+
171
+ it("explicit true wins over a non-seeding pipeline", () => {
172
+ vi.stubEnv("DECO_SEEDED_DEPLOY", "");
173
+ const p = /** @type {any} */ (decoVitePlugin({ fastDeploy: true }));
174
+ expect(p.config({}, { command: "build" }).define.__DECO_BLOCKS_STUBBED__).toBe("true");
175
+ });
176
+ });