@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 +4 -4
- package/src/vite/plugin.js +25 -0
- package/src/vite/plugin.test.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/tanstack",
|
|
3
|
-
"version": "7.20.
|
|
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.
|
|
28
|
-
"@decocms/blocks-admin": "7.20.
|
|
29
|
-
"@decocms/blocks-cli": "7.20.
|
|
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"
|
package/src/vite/plugin.js
CHANGED
|
@@ -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
|
+
});
|