@decocms/blocks-cli 7.14.0 → 7.15.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/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lint:unused": "knip"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@decocms/blocks": "7.
|
|
33
|
+
"@decocms/blocks": "7.15.0",
|
|
34
34
|
"ts-morph": "^27.0.0",
|
|
35
35
|
"tsx": "^4.22.5"
|
|
36
36
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the generated `.deco/loaders.gen.ts` shape: loaders route through
|
|
3
|
+
* `createLoaderEntry` (so their cache/cacheKey exports drive dedup) while
|
|
4
|
+
* actions stay plain pass-throughs (never cached/deduped).
|
|
5
|
+
*
|
|
6
|
+
* The script is spawned as a subprocess (`npx tsx generate-loaders.ts`) exactly
|
|
7
|
+
* how sites invoke it.
|
|
8
|
+
*/
|
|
9
|
+
import * as cp from "node:child_process";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
const SCRIPT = path.resolve(__dirname, "generate-loaders.ts");
|
|
16
|
+
|
|
17
|
+
function run(cwd: string): { stdout: string; stderr: string; code: number } {
|
|
18
|
+
const r = cp.spawnSync("npx", ["tsx", SCRIPT], { encoding: "utf8", cwd });
|
|
19
|
+
return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? -1 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("generate-loaders — loader vs action emit", () => {
|
|
23
|
+
let dir: string;
|
|
24
|
+
let out: string;
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-loaders-"));
|
|
28
|
+
const write = (rel: string, content: string) => {
|
|
29
|
+
const abs = path.join(dir, rel);
|
|
30
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
31
|
+
fs.writeFileSync(abs, content);
|
|
32
|
+
};
|
|
33
|
+
write("src/loaders/related.ts", "export default async () => [];\n");
|
|
34
|
+
write("src/actions/addToCart.ts", "export default async () => ({});\n");
|
|
35
|
+
|
|
36
|
+
const r = run(dir);
|
|
37
|
+
expect(r.code).toBe(0);
|
|
38
|
+
out = fs.readFileSync(path.join(dir, ".deco/loaders.gen.ts"), "utf8");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("imports createLoaderEntry when any loader is present", () => {
|
|
46
|
+
expect(out).toContain(
|
|
47
|
+
'import { createLoaderEntry } from "@decocms/blocks/sdk/cachedLoader";',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("wraps loaders (both alias keys) with createLoaderEntry under the non-.ts name", () => {
|
|
52
|
+
expect(out).toContain(
|
|
53
|
+
'"site/loaders/related": createLoaderEntry("site/loaders/related", () => import(',
|
|
54
|
+
);
|
|
55
|
+
expect(out).toContain(
|
|
56
|
+
'"site/loaders/related.ts": createLoaderEntry("site/loaders/related", () => import(',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps actions as plain pass-throughs — never routed through createLoaderEntry", () => {
|
|
61
|
+
expect(out).toContain('"site/actions/addToCart": async (props: any, request?: Request) => {');
|
|
62
|
+
expect(out).not.toContain('createLoaderEntry("site/actions/addToCart"');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -158,6 +158,12 @@ function isReferenced(key: string): boolean {
|
|
|
158
158
|
interface LoaderEntry {
|
|
159
159
|
key: string;
|
|
160
160
|
importPath: string;
|
|
161
|
+
/**
|
|
162
|
+
* Loaders route through `createLoaderEntry` so their `cache`/`cacheKey`
|
|
163
|
+
* exports drive single-flight dedup. Actions MUST NOT be cached/deduped
|
|
164
|
+
* (they mutate) — they stay plain pass-throughs.
|
|
165
|
+
*/
|
|
166
|
+
kind: "loader" | "action";
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
const entries: LoaderEntry[] = [];
|
|
@@ -175,6 +181,7 @@ for (const filePath of walkDir(loadersDir)) {
|
|
|
175
181
|
entries.push({
|
|
176
182
|
key,
|
|
177
183
|
importPath: relativeImportPath(outFile, filePath),
|
|
184
|
+
kind: "loader",
|
|
178
185
|
});
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -190,33 +197,56 @@ for (const filePath of walkDir(actionsDir)) {
|
|
|
190
197
|
entries.push({
|
|
191
198
|
key,
|
|
192
199
|
importPath: relativeImportPath(outFile, filePath),
|
|
200
|
+
kind: "action",
|
|
193
201
|
});
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
197
205
|
|
|
206
|
+
const hasLoaderEntries = entries.some((e) => e.kind === "loader");
|
|
207
|
+
|
|
198
208
|
const lines: string[] = [
|
|
199
209
|
"// Auto-generated by @decocms/blocks-cli/scripts/generate-loaders.ts",
|
|
200
210
|
"// Do not edit manually. Run `npm run generate:loaders` to update.",
|
|
201
211
|
"//",
|
|
202
|
-
"//
|
|
203
|
-
"//
|
|
212
|
+
"// Loader entries route through createLoaderEntry so their cache/cacheKey",
|
|
213
|
+
"// exports drive single-flight dedup (concurrent identical calls in one",
|
|
214
|
+
"// render collapse to one upstream call). Actions stay plain pass-throughs",
|
|
215
|
+
"// (never cached/deduped). Custom-wired entries should be excluded via",
|
|
216
|
+
"// --exclude and added manually in setup.ts.",
|
|
204
217
|
"",
|
|
205
|
-
"export const siteLoaders: Record<string, (props: any, request?: Request) => Promise<any>> = {",
|
|
206
218
|
];
|
|
219
|
+
if (hasLoaderEntries) {
|
|
220
|
+
lines.push('import { createLoaderEntry } from "@decocms/blocks/sdk/cachedLoader";');
|
|
221
|
+
lines.push("");
|
|
222
|
+
}
|
|
223
|
+
lines.push(
|
|
224
|
+
"export const siteLoaders: Record<string, (props: any, request?: Request) => Promise<any>> = {",
|
|
225
|
+
);
|
|
207
226
|
|
|
208
227
|
// Cast the dynamic-import default to `any` so legacy 3-arg
|
|
209
228
|
// `(props, req, ctx)` Fresh/Deno loaders still type-check. Any ctx-dependent
|
|
210
229
|
// path in the loader body throws at runtime and must be refactored.
|
|
211
230
|
for (const entry of entries) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
231
|
+
if (entry.kind === "loader") {
|
|
232
|
+
// Both alias keys share the same dedup namespace (the non-.ts name) so a
|
|
233
|
+
// render that references either collapses onto one in-flight call.
|
|
234
|
+
lines.push(
|
|
235
|
+
` "${entry.key}": createLoaderEntry("${entry.key}", () => import("${entry.importPath}")),`,
|
|
236
|
+
);
|
|
237
|
+
lines.push(
|
|
238
|
+
` "${entry.key}.ts": createLoaderEntry("${entry.key}", () => import("${entry.importPath}")),`,
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
lines.push(` "${entry.key}": async (props: any, request?: Request) => {`);
|
|
242
|
+
lines.push(` const mod = await import("${entry.importPath}");`);
|
|
243
|
+
lines.push(" return (mod.default as any)(props, request);");
|
|
244
|
+
lines.push(" },");
|
|
245
|
+
lines.push(` "${entry.key}.ts": async (props: any, request?: Request) => {`);
|
|
246
|
+
lines.push(` const mod = await import("${entry.importPath}");`);
|
|
247
|
+
lines.push(" return (mod.default as any)(props, request);");
|
|
248
|
+
lines.push(" },");
|
|
249
|
+
}
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
lines.push("};");
|
|
@@ -4,59 +4,19 @@ import type { TransformResult } from "../types";
|
|
|
4
4
|
* Removes dead code patterns from the old Deco stack that don't work
|
|
5
5
|
* in TanStack Start:
|
|
6
6
|
*
|
|
7
|
-
* - `export const cache = "stale-while-revalidate"` (old cache system)
|
|
8
|
-
* - `export const cacheKey = ...` (old cache key generation)
|
|
9
7
|
* - `crypto.subtle.digestSync(...)` (Deno-only sync API)
|
|
8
|
+
* - non-active platform branches (see `stripNonPlatformCode`)
|
|
10
9
|
*
|
|
10
|
+
* NOTE: `export const cache` / `export const cacheKey` are KEPT — they are no
|
|
11
|
+
* longer dead. `@decocms/blocks` wires them via `createLoaderEntry` so
|
|
12
|
+
* `cache: "stale-while-revalidate"` drives single-flight dedup keyed by
|
|
13
|
+
* `cacheKey(props, req)` (the #339 N+1 fix). Only flagged in the notes so the
|
|
14
|
+
* author verifies the `(props, req)` signature.
|
|
11
15
|
* NOTE: `export const loader` is kept — it's a server-side function the CMS calls.
|
|
12
16
|
* NOTE: invoke.* calls are NOT migrated — they are RPC calls to the server
|
|
13
17
|
* where the CMS config (API keys, etc.) is available. The runtime.ts invoke
|
|
14
18
|
* proxy handles routing them to /deco/invoke/*.
|
|
15
19
|
*/
|
|
16
|
-
/**
|
|
17
|
-
* Remove an `export const <name> = ...` block using brace-counting
|
|
18
|
-
* so nested `{}` (for loops, if/else) don't cause premature truncation.
|
|
19
|
-
*/
|
|
20
|
-
function removeExportConstBlock(src: string, name: string): string {
|
|
21
|
-
const pattern = new RegExp(`^export\\s+const\\s+${name}\\s*=`, "m");
|
|
22
|
-
const match = pattern.exec(src);
|
|
23
|
-
if (!match) return src;
|
|
24
|
-
|
|
25
|
-
// Find the arrow `=>` first, then the opening `{` of the body.
|
|
26
|
-
// This avoids matching destructuring braces in parameters like
|
|
27
|
-
// `export const loader = ({ groups }: Props) => { ... }`
|
|
28
|
-
let pos = match.index + match[0].length;
|
|
29
|
-
// Look for `=>`
|
|
30
|
-
const arrowIdx = src.indexOf("=>", pos);
|
|
31
|
-
if (arrowIdx === -1) {
|
|
32
|
-
// No arrow function — try simple brace from current position
|
|
33
|
-
while (pos < src.length && src[pos] !== "{") pos++;
|
|
34
|
-
} else {
|
|
35
|
-
// Start searching for `{` after the arrow
|
|
36
|
-
pos = arrowIdx + 2;
|
|
37
|
-
while (pos < src.length && src[pos] !== "{") pos++;
|
|
38
|
-
}
|
|
39
|
-
if (pos >= src.length) return src; // no brace body, skip
|
|
40
|
-
|
|
41
|
-
// Count braces to find the matching closing brace
|
|
42
|
-
let depth = 0;
|
|
43
|
-
const start = match.index;
|
|
44
|
-
for (; pos < src.length; pos++) {
|
|
45
|
-
if (src[pos] === "{") depth++;
|
|
46
|
-
else if (src[pos] === "}") {
|
|
47
|
-
depth--;
|
|
48
|
-
if (depth === 0) {
|
|
49
|
-
// Skip optional semicolon and trailing newline
|
|
50
|
-
let end = pos + 1;
|
|
51
|
-
if (end < src.length && src[end] === ";") end++;
|
|
52
|
-
if (end < src.length && src[end] === "\n") end++;
|
|
53
|
-
return src.slice(0, start) + src.slice(end);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return src; // unbalanced braces, don't touch
|
|
58
|
-
}
|
|
59
|
-
|
|
60
20
|
const ALL_PLATFORMS = ["vtex", "shopify", "linx", "vnda", "wake", "nuvemshop"];
|
|
61
21
|
|
|
62
22
|
/**
|
|
@@ -169,36 +129,22 @@ export function transformDeadCode(content: string, platform?: string): Transform
|
|
|
169
129
|
let changed = false;
|
|
170
130
|
let result = content;
|
|
171
131
|
|
|
172
|
-
//
|
|
132
|
+
// KEEP `export const cache` / `export const cacheKey`. These are NO LONGER
|
|
133
|
+
// dead: `@decocms/blocks` wires them via `createLoaderEntry` (generated in
|
|
134
|
+
// `.deco/loaders.gen.ts`) so `cache: "stale-while-revalidate"` opts the loader
|
|
135
|
+
// into single-flight dedup keyed by `cacheKey(props, req)`. Stripping them
|
|
136
|
+
// would silently reintroduce the #339 N+1 (same loader run once per section).
|
|
137
|
+
// Flag them so the author verifies the `(props, req)` signature — old
|
|
138
|
+
// `deco-cx/apps` `cacheKey(props, req, ctx)` loses its 3rd `ctx` arg here.
|
|
173
139
|
if (/^export\s+const\s+cache\s*=/m.test(result)) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/^export\s+const\s+cache\s*=\s*["'][^"']*["'];?\s*\n?/gm,
|
|
177
|
-
"",
|
|
178
|
-
);
|
|
179
|
-
// Object form: export const cache = { maxAge: 60 * 10 };
|
|
180
|
-
result = result.replace(
|
|
181
|
-
/^export\s+const\s+cache\s*=\s*\{[^}]*\};?\s*\n?/gm,
|
|
182
|
-
"",
|
|
140
|
+
notes.push(
|
|
141
|
+
"KEPT `export const cache` — now drives single-flight dedup via createLoaderEntry (run `generate` to wire it).",
|
|
183
142
|
);
|
|
184
|
-
// Multiline object form (use brace-counting)
|
|
185
|
-
if (/^export\s+const\s+cache\s*=/m.test(result)) {
|
|
186
|
-
result = removeExportConstBlock(result, "cache");
|
|
187
|
-
}
|
|
188
|
-
changed = true;
|
|
189
|
-
notes.push("Removed dead `export const cache` (old caching system)");
|
|
190
143
|
}
|
|
191
|
-
|
|
192
|
-
// Remove old cacheKey export (can be multiline with brace-counting)
|
|
193
144
|
if (/^export\s+const\s+cacheKey\s*=/m.test(result)) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
result = result.replace(
|
|
197
|
-
/^export\s+const\s+cacheKey\s*=[^;]*;\s*\n?/gm,
|
|
198
|
-
"",
|
|
145
|
+
notes.push(
|
|
146
|
+
"MANUAL: `export const cacheKey` is kept and now active — verify its signature is `(props, req)`; the old `ctx` 3rd arg is no longer passed.",
|
|
199
147
|
);
|
|
200
|
-
changed = true;
|
|
201
|
-
notes.push("Removed dead `export const cacheKey` (old caching system)");
|
|
202
148
|
}
|
|
203
149
|
|
|
204
150
|
// NOTE: `export const loader` is kept — these are server-side functions
|