@decocms/start 5.3.0-rc.2 → 5.4.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.
@@ -1,221 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Pull the live decofile from a Deco production site into `.deco/blocks/`.
4
- *
5
- * Closes the snapshot drift problem: without this, each site keeps a manually
6
- * checked-in copy of the CMS state in `.deco/blocks/*.json`. Anything edited
7
- * in the CMS UI after the last manual sync is invisible to the worker until
8
- * someone re-snapshots it.
9
- *
10
- * Endpoint: every Deco site exposes `GET /.decofile` which returns the full
11
- * blocks map as JSON. We fetch that, split it back into one-file-per-block,
12
- * URL-encode the filename so it round-trips through `generate-blocks.ts`'s
13
- * `decodeBlockName`, and atomically replace `.deco/blocks/` so a half-finished
14
- * sync can never leave the site in a broken state.
15
- *
16
- * Usage (from a site root):
17
- * tsx node_modules/@decocms/start/scripts/sync-decofile.ts \
18
- * --site lojabagaggio
19
- *
20
- * Flags:
21
- * --site Production site name (used to build https://www.<site>.com.br when --url is omitted)
22
- * --url Full base URL to fetch from. Overrides --site.
23
- * --out Output directory (default: .deco/blocks)
24
- * --dry-run Compute and print the diff vs the on-disk snapshot, do not write
25
- * --no-clean Do not wipe the output directory first (additive merge)
26
- */
27
- import fs from "node:fs";
28
- import path from "node:path";
29
-
30
- interface ParsedArgs {
31
- site?: string;
32
- url?: string;
33
- out: string;
34
- dryRun: boolean;
35
- clean: boolean;
36
- }
37
-
38
- function parseArgs(): ParsedArgs {
39
- const argv = process.argv.slice(2);
40
- const get = (name: string): string | undefined => {
41
- const i = argv.indexOf(`--${name}`);
42
- if (i === -1 || !argv[i + 1] || argv[i + 1].startsWith("--")) return undefined;
43
- return argv[i + 1];
44
- };
45
- const has = (name: string): boolean => argv.includes(`--${name}`);
46
-
47
- return {
48
- site: get("site"),
49
- url: get("url"),
50
- out: get("out") ?? ".deco/blocks",
51
- dryRun: has("dry-run"),
52
- clean: !has("no-clean"),
53
- };
54
- }
55
-
56
- /**
57
- * The CMS may emit keys that already contain URL-encoded sequences (eg.
58
- * `pages-%C3%9Altimas...`). To keep filenames and on-disk diffs stable, we
59
- * peel any encoding off the key first and then apply a single canonical
60
- * encoding pass when writing to disk.
61
- */
62
- function normalizeKey(rawKey: string): string {
63
- let k = rawKey;
64
- while (k.includes("%")) {
65
- try {
66
- const next = decodeURIComponent(k);
67
- if (next === k) break;
68
- k = next;
69
- } catch {
70
- break;
71
- }
72
- }
73
- return k;
74
- }
75
-
76
- /**
77
- * Encode a block key into a filename that survives `decodeBlockName` in
78
- * `generate-blocks.ts`. Keys must already be normalized via `normalizeKey`.
79
- */
80
- function encodeBlockKeyToFilename(key: string): string {
81
- return encodeURIComponent(key) + ".json";
82
- }
83
-
84
- function normalizeBlocks(raw: Record<string, unknown>): Record<string, unknown> {
85
- const out: Record<string, unknown> = {};
86
- for (const [k, v] of Object.entries(raw)) {
87
- out[normalizeKey(k)] = v;
88
- }
89
- return out;
90
- }
91
-
92
- async function fetchDecofile(baseUrl: string): Promise<Record<string, unknown>> {
93
- const url = baseUrl.replace(/\/$/, "") + "/.decofile";
94
- console.log(`Fetching ${url} ...`);
95
- const res = await fetch(url, {
96
- headers: { "user-agent": "decocms-sync-decofile/1.0" },
97
- });
98
- if (!res.ok) {
99
- throw new Error(`GET ${url} returned ${res.status} ${res.statusText}`);
100
- }
101
- const json = (await res.json()) as Record<string, unknown>;
102
- if (!json || typeof json !== "object") {
103
- throw new Error(`Unexpected response shape from ${url}`);
104
- }
105
- return json;
106
- }
107
-
108
- function readExistingSnapshot(dir: string): Record<string, unknown> {
109
- if (!fs.existsSync(dir)) return {};
110
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
111
- const out: Record<string, unknown> = {};
112
- for (const f of files) {
113
- const key = normalizeKey(f.replace(/\.json$/, ""));
114
- try {
115
- out[key] = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
116
- } catch {
117
- // ignore unparseable
118
- }
119
- }
120
- return out;
121
- }
122
-
123
- interface DiffResult {
124
- added: string[];
125
- removed: string[];
126
- changed: string[];
127
- }
128
-
129
- function diffSnapshots(next: Record<string, unknown>, prev: Record<string, unknown>): DiffResult {
130
- const nextKeys = new Set(Object.keys(next));
131
- const prevKeys = new Set(Object.keys(prev));
132
- const added: string[] = [];
133
- const removed: string[] = [];
134
- const changed: string[] = [];
135
- for (const k of nextKeys) {
136
- if (!prevKeys.has(k)) added.push(k);
137
- else if (JSON.stringify(next[k]) !== JSON.stringify(prev[k])) changed.push(k);
138
- }
139
- for (const k of prevKeys) {
140
- if (!nextKeys.has(k)) removed.push(k);
141
- }
142
- return { added, removed, changed };
143
- }
144
-
145
- function writeAtomically(dir: string, blocks: Record<string, unknown>, clean: boolean): void {
146
- const stagingDir = `${dir}.tmp-${process.pid}`;
147
- if (fs.existsSync(stagingDir)) fs.rmSync(stagingDir, { recursive: true, force: true });
148
- fs.mkdirSync(stagingDir, { recursive: true });
149
-
150
- for (const [key, value] of Object.entries(blocks)) {
151
- const filename = encodeBlockKeyToFilename(key);
152
- fs.writeFileSync(path.join(stagingDir, filename), JSON.stringify(value, null, 2));
153
- }
154
-
155
- if (clean) {
156
- if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
157
- fs.renameSync(stagingDir, dir);
158
- } else {
159
- // additive: copy from staging into target
160
- fs.mkdirSync(dir, { recursive: true });
161
- for (const f of fs.readdirSync(stagingDir)) {
162
- fs.copyFileSync(path.join(stagingDir, f), path.join(dir, f));
163
- }
164
- fs.rmSync(stagingDir, { recursive: true, force: true });
165
- }
166
- }
167
-
168
- async function main(): Promise<void> {
169
- const args = parseArgs();
170
- if (!args.site && !args.url) {
171
- console.error("Usage: tsx sync-decofile.ts --site <name> OR --url <base-url>");
172
- process.exit(2);
173
- }
174
-
175
- const baseUrl = args.url ?? `https://www.${args.site}.com.br`;
176
- const outDir = path.resolve(process.cwd(), args.out);
177
-
178
- const next = normalizeBlocks(await fetchDecofile(baseUrl));
179
- const prev = readExistingSnapshot(outDir);
180
- const diff = diffSnapshots(next, prev);
181
-
182
- console.log("");
183
- console.log(` fetched ${Object.keys(next).length} blocks`);
184
- console.log(` on disk ${Object.keys(prev).length} blocks`);
185
- console.log(` added ${diff.added.length}`);
186
- console.log(` removed ${diff.removed.length}`);
187
- console.log(` changed ${diff.changed.length}`);
188
- console.log("");
189
-
190
- const sample = (xs: string[], n = 8) =>
191
- xs
192
- .slice(0, n)
193
- .map((x) => ` ${x}`)
194
- .join("\n");
195
- if (diff.added.length)
196
- console.log(`Added (showing ${Math.min(8, diff.added.length)}):\n${sample(diff.added)}\n`);
197
- if (diff.removed.length)
198
- console.log(
199
- `Removed (showing ${Math.min(8, diff.removed.length)}):\n${sample(diff.removed)}\n`,
200
- );
201
- if (diff.changed.length)
202
- console.log(
203
- `Changed (showing ${Math.min(8, diff.changed.length)}):\n${sample(diff.changed)}\n`,
204
- );
205
-
206
- if (args.dryRun) {
207
- console.log("--dry-run set, not writing.");
208
- return;
209
- }
210
-
211
- writeAtomically(outDir, next, args.clean);
212
- console.log(
213
- `Wrote ${Object.keys(next).length} blocks to ${path.relative(process.cwd(), outDir)}`,
214
- );
215
- console.log("Next: pnpm run generate:blocks (or your build script)");
216
- }
217
-
218
- main().catch((err) => {
219
- console.error(err);
220
- process.exit(1);
221
- });