@decocms/tanstack 7.20.5 → 7.20.7

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.20.5",
3
+ "version": "7.20.7",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -24,9 +24,9 @@
24
24
  "lint:unused": "knip"
25
25
  },
26
26
  "dependencies": {
27
- "@decocms/blocks": "7.20.5",
28
- "@decocms/blocks-admin": "7.20.5",
29
- "@decocms/blocks-cli": "7.20.5",
27
+ "@decocms/blocks": "7.20.7",
28
+ "@decocms/blocks-admin": "7.20.7",
29
+ "@decocms/blocks-cli": "7.20.7",
30
30
  "@deco-cx/warp-node": "^0.3.16",
31
31
  "fast-json-patch": "^3.1.0",
32
32
  "ws": "^8.18.0"
@@ -19,6 +19,15 @@
19
19
  * it on the client cuts a typically-large module out of the browser bundle.
20
20
  * Match is done by substring on the import id, so any path style works.
21
21
  *
22
+ * loaders.gen.ts handling:
23
+ * The site invoke registry (`export const siteLoaders`) registers every site
24
+ * loader/action behind a dynamic `import()`. It is reachable from the client
25
+ * entry (router -> setup -> commerce-loaders -> loaders.gen), so without a stub
26
+ * Vite emits each loader/action module as a public client chunk — leaking the
27
+ * module source (including any hardcoded credential) into the browser assets.
28
+ * Invoke runs server-side only, so the client never needs it. Stubbed to an
29
+ * empty object on the client; SSR keeps the real module.
30
+ *
22
31
  * manualChunks:
23
32
  * `@decocms/tanstack` and `@decocms/apps` are intentionally NOT split
24
33
  * into their own chunks. They have circular re-exports that produce a
@@ -193,6 +202,22 @@ export function decoVitePlugin() {
193
202
  // backward-compatible sites that haven't regenerated).
194
203
  }
195
204
 
205
+ // loaders.gen.ts — the site's invoke registry (`export const siteLoaders`).
206
+ // It registers every site loader/action behind a dynamic `import()`, so if
207
+ // it stays in the CLIENT module graph (it's reachable via
208
+ // router -> setup -> commerce-loaders -> loaders.gen) Vite emits each
209
+ // loader/action module as a public client chunk. That leaks the module's
210
+ // source — including any hardcoded credential in it — into the browser
211
+ // assets. The invoke handler runs server-side only (`/deco/invoke` ->
212
+ // handleInvoke reads the registry via getRegisteredLoaders()), and client
213
+ // components reach loaders/actions exclusively through the HTTP `invoke`
214
+ // proxy (which imports no modules). So the client never needs `siteLoaders`.
215
+ // Stub it to an empty object on the client, mirroring blocks.gen.ts above;
216
+ // SSR keeps the real module.
217
+ if (id.endsWith("loaders.gen.ts") && !options?.ssr) {
218
+ return "export const siteLoaders = {};";
219
+ }
220
+
196
221
  // Virtual module stubs.
197
222
  return STUB_SOURCE[id];
198
223
  },
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { decoVitePlugin } from "./plugin.js";
3
+
4
+ /**
5
+ * Permanent regression test for the site-action/loader bundle leak.
6
+ *
7
+ * `.deco/loaders.gen.ts` registers every site loader/action behind a dynamic
8
+ * `import()` and is reachable from the CLIENT entry (router -> setup ->
9
+ * commerce-loaders -> loaders.gen). Without the client stub, Vite emits each
10
+ * loader/action module as a public client chunk, leaking the module source
11
+ * (including any hardcoded credential in it) into the browser assets.
12
+ *
13
+ * The load() hook must replace loaders.gen.ts with an empty `siteLoaders` on
14
+ * the client so those dynamic imports vanish from the client graph, while SSR
15
+ * keeps the real module (invoke runs server-side only). If this ever regresses,
16
+ * a token hardcoded in a site action becomes publicly downloadable again.
17
+ */
18
+ describe("decoVitePlugin — loaders.gen client stub", () => {
19
+ // The plugin's load hook is a plain function that uses no `this`.
20
+ const plugin = decoVitePlugin() as {
21
+ load: (id: string, options?: { ssr?: boolean }) => string | undefined;
22
+ };
23
+ const id = "/repo/.deco/loaders.gen.ts";
24
+
25
+ it("stubs loaders.gen.ts to an empty siteLoaders in the CLIENT build", () => {
26
+ const out = plugin.load(id, { ssr: false });
27
+ expect(out).toBe("export const siteLoaders = {};");
28
+ // The stub must carry no dynamic import — that's the whole point.
29
+ expect(out).not.toMatch(/import\s*\(/);
30
+ });
31
+
32
+ it("leaves loaders.gen.ts untouched in the SSR build (real registry)", () => {
33
+ expect(plugin.load(id, { ssr: true })).toBeUndefined();
34
+ });
35
+
36
+ it("defaults to the client (stubbed) behavior when ssr is unset", () => {
37
+ // Rollup/Vite may call load() without an options.ssr flag; the guard is
38
+ // `!options?.ssr`, so an absent flag must be treated as client.
39
+ expect(plugin.load(id)).toBe("export const siteLoaders = {};");
40
+ expect(plugin.load(id, {})).toBe("export const siteLoaders = {};");
41
+ });
42
+
43
+ it("still stubs blocks.gen.ts on the client (existing behavior intact)", () => {
44
+ const out = plugin.load("/repo/.deco/blocks.gen.ts", { ssr: false });
45
+ expect(out).toBe("export const blocks = {};");
46
+ });
47
+
48
+ it("does not touch unrelated modules", () => {
49
+ expect(plugin.load("/repo/src/sections/Hero.tsx", { ssr: false })).toBeUndefined();
50
+ expect(plugin.load("/repo/src/actions/foo.ts", { ssr: false })).toBeUndefined();
51
+ });
52
+ });