@decocms/blocks 7.16.3 → 7.16.4
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,7 +1,7 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { loadDecofileDirectory } from "./loadDecofileDirectory";
|
|
6
6
|
|
|
7
7
|
describe("loadDecofileDirectory", () => {
|
|
@@ -49,4 +49,61 @@ describe("loadDecofileDirectory", () => {
|
|
|
49
49
|
writeFileSync(join(dir, "broken.json"), "{not valid json");
|
|
50
50
|
await expect(loadDecofileDirectory(dir)).rejects.toThrow(/broken\.json/);
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it("URL-decodes the filename stem once to produce the block key", async () => {
|
|
54
|
+
// Classic deco stores blocks as encodeURIComponent(<block id>).json and
|
|
55
|
+
// derives the id back with one decodeURIComponent (parseBlockId). Content
|
|
56
|
+
// references saved blocks by the DECODED id ("Cores dos preços"), so the
|
|
57
|
+
// map must be keyed by it or every such reference dangles.
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(dir, "Cores%20dos%20pre%C3%A7os.json"),
|
|
60
|
+
JSON.stringify({ __resolveType: "site/loaders/priceColors.ts" }),
|
|
61
|
+
);
|
|
62
|
+
// Double-encoded page snapshot (deco.cx corpus shape): decodes ONCE.
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(dir, "pages-Acess%25C3%25B3rios-421596.json"),
|
|
65
|
+
JSON.stringify({ __resolveType: "website/pages/Page.tsx" }),
|
|
66
|
+
);
|
|
67
|
+
// Encoded slash becomes a real slash in the id — ids are plain map keys.
|
|
68
|
+
writeFileSync(join(dir, "collections%2Fblog%2Fauthors.json"), JSON.stringify({ authors: [] }));
|
|
69
|
+
|
|
70
|
+
const blocks = await loadDecofileDirectory(dir);
|
|
71
|
+
|
|
72
|
+
expect(blocks["Cores dos preços"]).toEqual({
|
|
73
|
+
__resolveType: "site/loaders/priceColors.ts",
|
|
74
|
+
});
|
|
75
|
+
expect(blocks["pages-Acess%C3%B3rios-421596"]).toEqual({
|
|
76
|
+
__resolveType: "website/pages/Page.tsx",
|
|
77
|
+
});
|
|
78
|
+
expect(blocks["collections/blog/authors"]).toEqual({ authors: [] });
|
|
79
|
+
// The raw stems must NOT appear as keys.
|
|
80
|
+
expect(blocks["Cores%20dos%20pre%C3%A7os"]).toBeUndefined();
|
|
81
|
+
expect(blocks["pages-Acess%25C3%25B3rios-421596"]).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps a stem verbatim when it is not valid percent-encoding", async () => {
|
|
85
|
+
// A lone % (e.g. a page literally named "50% off") throws in
|
|
86
|
+
// decodeURIComponent — the stem is already the id, keep it.
|
|
87
|
+
writeFileSync(join(dir, "pages-50% off-7.json"), JSON.stringify({ x: 1 }));
|
|
88
|
+
|
|
89
|
+
const blocks = await loadDecofileDirectory(dir);
|
|
90
|
+
expect(blocks["pages-50% off-7"]).toEqual({ x: 1 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("resolves decoded-key collisions deterministically (last sorted filename wins) and warns", async () => {
|
|
94
|
+
writeFileSync(join(dir, "A B.json"), JSON.stringify({ raw: true }));
|
|
95
|
+
writeFileSync(join(dir, "A%20B.json"), JSON.stringify({ encoded: true }));
|
|
96
|
+
|
|
97
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
98
|
+
try {
|
|
99
|
+
const blocks = await loadDecofileDirectory(dir);
|
|
100
|
+
// Code-unit sort: " " (0x20) < "%" (0x25), so "A B.json" comes first
|
|
101
|
+
// and "A%20B.json" is processed last — the encoded file wins.
|
|
102
|
+
expect(blocks["A B"]).toEqual({ encoded: true });
|
|
103
|
+
expect(Object.keys(blocks)).toHaveLength(1);
|
|
104
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("A%20B.json"));
|
|
105
|
+
} finally {
|
|
106
|
+
warn.mockRestore();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
52
109
|
});
|
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Derive a block id (decofile map key) from a `.deco/blocks` filename.
|
|
6
|
+
*
|
|
7
|
+
* Classic deco stores each block as `encodeURIComponent(<block id>).json`
|
|
8
|
+
* and derives the id back with exactly one decodeURIComponent — this is
|
|
9
|
+
* upstream `parseBlockId` (deco-cx/deco, engine/decofile/fsFolder). Block
|
|
10
|
+
* CONTENT references saved blocks by the DECODED id (`"__resolveType":
|
|
11
|
+
* "Cores dos preços"`), and the Studio editor derives the write-back
|
|
12
|
+
* filename as `encodeURIComponent(<key>)`, so keying the map by the raw
|
|
13
|
+
* stem breaks both: references dangle at resolve time and editor saves
|
|
14
|
+
* land in a freshly-created double-encoded file.
|
|
15
|
+
*
|
|
16
|
+
* A stem that is not valid percent-encoding (e.g. a page literally named
|
|
17
|
+
* "50% off") throws in decodeURIComponent — such a stem IS the id already
|
|
18
|
+
* (classic deco would have stored it encoded), so it is kept verbatim,
|
|
19
|
+
* matching upstream behavior for pre-encoding-era files.
|
|
20
|
+
*/
|
|
21
|
+
export function parseBlockId(filename: string): string {
|
|
22
|
+
const stem = filename.endsWith(".json") ? filename.slice(0, -".json".length) : filename;
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(stem);
|
|
25
|
+
} catch {
|
|
26
|
+
return stem;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
/**
|
|
5
31
|
* Loads a directory of legacy per-block JSON snapshot files (the format
|
|
6
32
|
* produced by the pre-v2 Deco admin, e.g. `.deco/blocks/<name>.json`) into
|
|
7
33
|
* a single blocks map suitable for setBlocks(). Each file becomes one
|
|
8
|
-
* entry keyed by
|
|
34
|
+
* entry keyed by `parseBlockId(<filename>)` — the filename stem URL-decoded
|
|
35
|
+
* once, matching the classic deco runtime's key convention.
|
|
36
|
+
*
|
|
37
|
+
* Beyond that single decode the key is not renamed or normalized: whatever
|
|
38
|
+
* prefix convention a block's filename already carries (e.g. real page
|
|
39
|
+
* block snapshots are named `pages-<slug>-<id>.json`) is preserved, because
|
|
40
|
+
* getAllPages() in loader.ts filters blocks by `key.startsWith("pages-")`
|
|
41
|
+
* before findPageByPath() matches on each page's own `.path` field. So the
|
|
42
|
+
* key format IS load-bearing for page blocks — decoding never touches the
|
|
43
|
+
* `pages-` prefix itself, only the percent-escapes after it.
|
|
9
44
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* verbatim, because getAllPages() in loader.ts filters blocks by
|
|
14
|
-
* `key.startsWith("pages-")` before findPageByPath() matches on each page's
|
|
15
|
-
* own `.path` field. So the key format IS load-bearing for page blocks —
|
|
16
|
-
* it must keep whatever prefix its source filename had — but this loader
|
|
17
|
-
* never needs to invent or choose that prefix itself, since it simply
|
|
18
|
-
* passes the on-disk filename through.
|
|
45
|
+
* Two filenames can decode to the same id (`A B.json` + `A%20B.json`).
|
|
46
|
+
* Files are processed in sorted filename order and the last one wins, so
|
|
47
|
+
* the outcome is deterministic; a console.warn names the shadowed file.
|
|
19
48
|
*
|
|
20
49
|
* Replaces the abandoned @decocms/start/node tier's loadAllDecofileBlocks,
|
|
21
50
|
* which no longer exists on any reachable blocks version — ported
|
|
@@ -28,21 +57,33 @@ export async function loadDecofileDirectory(dir: string): Promise<Record<string,
|
|
|
28
57
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
29
58
|
const blocks: Record<string, unknown> = {};
|
|
30
59
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
const files = entries
|
|
61
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
62
|
+
.map((entry) => entry.name)
|
|
63
|
+
.sort();
|
|
64
|
+
|
|
65
|
+
const parsed = await Promise.all(
|
|
66
|
+
files.map(async (name) => {
|
|
67
|
+
const content = await fs.readFile(join(dir, name), "utf8");
|
|
68
|
+
try {
|
|
69
|
+
return { name, value: JSON.parse(content) as unknown };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`loadDecofileDirectory: failed to parse ${name}: ${(error as Error).message}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
45
76
|
);
|
|
46
77
|
|
|
78
|
+
for (const { name, value } of parsed) {
|
|
79
|
+
const key = parseBlockId(name);
|
|
80
|
+
if (key in blocks) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`loadDecofileDirectory: block id ${JSON.stringify(key)} maps to multiple files; ${JSON.stringify(name)} wins`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
blocks[key] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
return blocks;
|
|
48
89
|
}
|