@decocms/blocks-cli 7.66.7 → 7.67.1

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/blocks-cli",
3
- "version": "7.66.7",
3
+ "version": "7.67.1",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "lint:unused": "knip"
34
34
  },
35
35
  "dependencies": {
36
- "@decocms/blocks": "7.66.7",
36
+ "@decocms/blocks": "7.67.1",
37
37
  "ts-morph": "^27.0.0",
38
38
  "tsx": "^4.22.5"
39
39
  },
@@ -10,6 +10,7 @@ import {
10
10
  snapshotKey,
11
11
  } from "@decocms/blocks/cms";
12
12
  import { createKvRestClient, type KvRestClient, kvConfigFromEnv } from "./lib/cf-kv-rest";
13
+ import { readDecofileFromDir } from "./lib/read-decofile";
13
14
  import { kvNamespaceIdFromToml, kvNamespaceIdFromWrangler } from "./lib/wrangler-config";
14
15
  import {
15
16
  buildSnapshot,
@@ -341,3 +342,71 @@ describe("sync-helpers", () => {
341
342
  expect(paths).not.toContain(undefined);
342
343
  });
343
344
  });
345
+
346
+ describe("readDecofileFromDir — CSV redirects", () => {
347
+ /** A site tree: `.deco/blocks/*.json` + `public/*.csv`. Returns the blocks dir. */
348
+ function makeSite(blocks: Record<string, unknown>, csv: Record<string, string>): string {
349
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "decofile-csv-"));
350
+ const blocksDir = path.join(root, ".deco", "blocks");
351
+ fs.mkdirSync(blocksDir, { recursive: true });
352
+ fs.mkdirSync(path.join(root, "public"), { recursive: true });
353
+ for (const [name, value] of Object.entries(blocks)) {
354
+ fs.writeFileSync(path.join(blocksDir, name), JSON.stringify(value));
355
+ }
356
+ for (const [name, value] of Object.entries(csv)) {
357
+ fs.writeFileSync(path.join(root, "public", name), value);
358
+ }
359
+ return blocksDir;
360
+ }
361
+
362
+ it("materializes a CSV loader nested under site.routes[] into a top-level block", () => {
363
+ // The shape that broke production: the CSV loader is nested inside
364
+ // `site.json -> routes[]`, where `loadRedirects` (top-level only) can't see
365
+ // it, and the CSV itself was only ever read by the unported Fresh loader.
366
+ const blocksDir = makeSite(
367
+ {
368
+ "site.json": {
369
+ __resolveType: "site/apps/site.ts",
370
+ routes: [{ __resolveType: "website/loaders/redirectsFromCsv.ts", from: "static/r.csv" }],
371
+ },
372
+ },
373
+ { "r.csv": "from,to,type\n/old,/new,permanent\n/tmp,/other\n" },
374
+ );
375
+
376
+ const { blocks } = readDecofileFromDir(blocksDir, { silent: true });
377
+
378
+ expect(blocks["__csv_redirects__r.csv"]).toEqual({
379
+ __resolveType: "website/loaders/redirects.ts",
380
+ redirects: [
381
+ { from: "/old", to: "/new", type: "permanent" },
382
+ { from: "/tmp", to: "/other", type: "temporary" },
383
+ ],
384
+ });
385
+ // The real blocks are untouched.
386
+ expect(blocks.site).toBeDefined();
387
+ });
388
+
389
+ it("lets a real block win over the synthetic one on a key collision", () => {
390
+ const blocksDir = makeSite(
391
+ {
392
+ "site.json": {
393
+ routes: [{ __resolveType: "website/loaders/redirectsFromCsv.ts", from: "r.csv" }],
394
+ },
395
+ // Decodes to exactly the synthetic key the CSV materializer produces.
396
+ "__csv_redirects__r.csv.json": { curated: true },
397
+ },
398
+ { "r.csv": "/old,/new\n" },
399
+ );
400
+
401
+ const { blocks } = readDecofileFromDir(blocksDir, { silent: true });
402
+
403
+ // `{ ...csv, ...blocks }` — the curated block wins, never the synthetic one.
404
+ expect(blocks["__csv_redirects__r.csv"]).toEqual({ curated: true });
405
+ });
406
+
407
+ it("is a no-op when no CSV loader is referenced", () => {
408
+ const blocksDir = makeSite({ "site.json": { __resolveType: "site/apps/site.ts" } }, {});
409
+ const { blocks } = readDecofileFromDir(blocksDir, { silent: true });
410
+ expect(Object.keys(blocks)).toEqual(["site"]);
411
+ });
412
+ });
@@ -6,7 +6,7 @@
6
6
  * the fast-deploy CI scripts (`migrate-blocks-to-kv.ts`, `sync-blocks-to-kv.ts`)
7
7
  * produce a decofile byte-identical to the bundled `blocks.gen` snapshot.
8
8
  *
9
- * Reuses `scripts/lib/blocks-dedupe.ts`.
9
+ * Reuses `scripts/lib/blocks-dedupe.ts` and `scripts/lib/csv-redirects.ts`.
10
10
  */
11
11
 
12
12
  import * as fs from "node:fs";
@@ -18,6 +18,7 @@ import {
18
18
  decodeBlockNameWithPasses,
19
19
  mergeCandidates,
20
20
  } from "./blocks-dedupe";
21
+ import { buildCsvRedirectBlocks } from "./csv-redirects";
21
22
 
22
23
  export interface ReadDecofileResult {
23
24
  /** Decoded block key → parsed block JSON. */
@@ -66,5 +67,15 @@ export function readDecofileFromDir(blocksDir: string, opts: { silent?: boolean
66
67
  for (const [key, candidate] of Object.entries(winners)) {
67
68
  blocks[key] = candidate.parsed;
68
69
  }
69
- return { blocks, collisions };
70
+
71
+ // Synthetic `__csv_redirects__*` blocks, exactly as `generate-blocks` builds
72
+ // them. Without this the KV snapshot is NOT byte-identical to `blocks.gen`:
73
+ // a site whose redirects come from a CSV would lose every one of them the
74
+ // moment the bundled snapshot stops being the fallback (the fastDeploy stub
75
+ // makes KV the only source), turning migration 301s into silent 404s.
76
+ // Same precedence as the generator — a curated CMS redirect wins over CSV.
77
+ const csvBlocks = buildCsvRedirectBlocks(blocks, { blocksDir, silent: opts.silent });
78
+ const merged = Object.keys(csvBlocks).length ? { ...csvBlocks, ...blocks } : blocks;
79
+
80
+ return { blocks: merged, collisions };
70
81
  }