@decocms/tanstack 8.0.0 → 8.1.0-next.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 +12 -5
- package/src/__conformance__/observability.test.ts +221 -0
- package/src/hooks/CdnSegmentMarker.tsx +38 -0
- package/src/hooks/DecoPageRenderer.tsx +91 -29
- package/src/hooks/DecoRootLayout.tsx +139 -58
- package/src/hooks/DraftPreviewIndicator.test.tsx +60 -0
- package/src/hooks/DraftPreviewIndicator.tsx +64 -0
- package/src/hooks/NavigationProgress.test.tsx +58 -0
- package/src/hooks/NavigationProgress.tsx +45 -3
- package/src/hooks/PreviewProviders.test.tsx +42 -0
- package/src/hooks/PreviewProviders.tsx +21 -2
- package/src/hooks/deferredObserverTiming.test.tsx +91 -0
- package/src/hooks/index.ts +1 -0
- package/src/index.ts +30 -13
- package/src/routes/cmsRoute.test.ts +168 -1
- package/src/routes/cmsRoute.ts +148 -39
- package/src/routes/pageUrl.test.ts +6 -6
- package/src/routes/pageUrl.ts +5 -4
- package/src/routes/withSiteGlobals.test.ts +37 -0
- package/src/routes/withSiteGlobals.ts +45 -24
- package/src/sdk/backgroundTasks.test.ts +68 -0
- package/src/sdk/backgroundTasks.ts +38 -0
- package/src/sdk/cdnSegment.test.ts +67 -0
- package/src/sdk/cdnSegment.ts +71 -0
- package/src/sdk/deferredSectionLoader.test.ts +1 -1
- package/src/sdk/draft.test.ts +341 -0
- package/src/sdk/draft.ts +277 -0
- package/src/sdk/draftShared.ts +16 -0
- package/src/sdk/kvHydration.test.ts +156 -0
- package/src/sdk/kvHydration.ts +82 -12
- package/src/sdk/kvRedirects.test.ts +132 -0
- package/src/sdk/kvRedirects.ts +125 -0
- package/src/sdk/router.ts +70 -61
- package/src/sdk/serverFnFetch.ts +59 -0
- package/src/sdk/speculationRules.test.ts +82 -0
- package/src/sdk/speculationRules.ts +136 -0
- package/src/sdk/startEntry.ts +31 -0
- package/src/sdk/workerEntry.cacheStorage.test.ts +147 -0
- package/src/sdk/workerEntry.test.ts +554 -16
- package/src/sdk/workerEntry.trackingCache.test.ts +328 -0
- package/src/sdk/workerEntry.ts +915 -68
- package/src/v8/__conformance__/delivery.test.ts +100 -0
- package/src/v8/binding.ts +70 -0
- package/src/v8/kvLoader.bundle.test.ts +42 -0
- package/src/v8/kvLoader.test.ts +74 -0
- package/src/v8/kvLoader.ts +43 -0
- package/src/vite/plugin.js +180 -8
- package/src/vite/plugin.test.js +126 -1
- package/src/vite/plugin.test.ts +125 -1
package/src/vite/plugin.test.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { decoVitePlugin } from "./plugin.js";
|
|
5
|
+
import { decoVitePlugin, generateSchemaArgs } from "./plugin.js";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* The Vite plugin's `resolveId` / `load` hooks are pure functions over their
|
|
@@ -53,6 +56,17 @@ describe("decoVitePlugin client stubs (regression guard)", () => {
|
|
|
53
56
|
});
|
|
54
57
|
});
|
|
55
58
|
|
|
59
|
+
describe("generateSchemaArgs (regression: meta.gen.json must be composed)", () => {
|
|
60
|
+
// Both generate-schema.ts invocations in configureServer (watch regen + cold
|
|
61
|
+
// start) derive their args from this helper. generate-schema always composes
|
|
62
|
+
// before writing, so the manifest carries every framework website/* block
|
|
63
|
+
// (matchers/Page/Resolvable) that Studio — which reads meta.gen.json as-is —
|
|
64
|
+
// needs, e.g. the variant date matcher. See a production storefront regression.
|
|
65
|
+
it("targets the given site", () => {
|
|
66
|
+
expect(generateSchemaArgs("my-site")).toEqual(["--site", "my-site"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
describe("decoVitePlugin __DECO_BUILD_HASH__ injection", () => {
|
|
57
71
|
let originalEnv;
|
|
58
72
|
|
|
@@ -111,3 +125,114 @@ describe("decoVitePlugin __DECO_BUILD_HASH__ injection", () => {
|
|
|
111
125
|
}
|
|
112
126
|
});
|
|
113
127
|
});
|
|
128
|
+
|
|
129
|
+
describe("decoVitePlugin meta.gen.json on SSR (regression: vite:json double-parse)", () => {
|
|
130
|
+
// Vite's json plugin transforms every `.json` id — even when another plugin
|
|
131
|
+
// already loaded it as JS — so the virtual id must not end in `.json`.
|
|
132
|
+
let dir;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
dir = mkdtempSync(path.join(tmpdir(), "deco-meta-"));
|
|
136
|
+
mkdirSync(path.join(dir, ".deco"));
|
|
137
|
+
mkdirSync(path.join(dir, "src"));
|
|
138
|
+
writeFileSync(path.join(dir, ".deco", "meta.gen.json"), '{"major":1,"schema":{}}');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("resolves to a virtual id vite:json does not transform, and loads it as JSON.parse", () => {
|
|
146
|
+
const p = getPlugin();
|
|
147
|
+
const importer = path.join(dir, "src", "setup.ts");
|
|
148
|
+
const id = p.resolveId.call({}, "../.deco/meta.gen.json", importer, { ssr: true });
|
|
149
|
+
expect(id).toBeTypeOf("string");
|
|
150
|
+
expect(id.endsWith(".json")).toBe(false);
|
|
151
|
+
|
|
152
|
+
const watched = [];
|
|
153
|
+
const code = p.load.call({ addWatchFile: (f) => watched.push(f) }, id, { ssr: true });
|
|
154
|
+
expect(code).toBe(`export default JSON.parse(${JSON.stringify('{"major":1,"schema":{}}')});`);
|
|
155
|
+
expect(watched).toEqual([path.join(dir, ".deco", "meta.gen.json")]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("gives each meta.gen.json its own module id (no shared state)", () => {
|
|
159
|
+
const p = getPlugin();
|
|
160
|
+
const other = mkdtempSync(path.join(tmpdir(), "deco-meta-other-"));
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(path.join(other, "meta.gen.json"), '{"major":2}');
|
|
163
|
+
const a = p.resolveId.call({}, "../.deco/meta.gen.json", path.join(dir, "src", "setup.ts"), {
|
|
164
|
+
ssr: true,
|
|
165
|
+
});
|
|
166
|
+
const b = p.resolveId.call({}, path.join(other, "meta.gen.json"), path.join(dir, "x.ts"), {
|
|
167
|
+
ssr: true,
|
|
168
|
+
});
|
|
169
|
+
expect(a).not.toBe(b);
|
|
170
|
+
// Loading in reverse order still returns each file's own content.
|
|
171
|
+
expect(p.load.call({}, b, { ssr: true })).toContain('\\"major\\":2');
|
|
172
|
+
expect(p.load.call({}, a, { ssr: true })).toContain('\\"major\\":1');
|
|
173
|
+
} finally {
|
|
174
|
+
rmSync(other, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("wraps aliased imports after the rest of the pipeline resolves them", async () => {
|
|
179
|
+
const p = getPlugin();
|
|
180
|
+
const file = path.join(dir, ".deco", "meta.gen.json");
|
|
181
|
+
const seen = [];
|
|
182
|
+
const ctx = {
|
|
183
|
+
resolve: async (spec, imp, opts) => {
|
|
184
|
+
seen.push([spec, opts.skipSelf]);
|
|
185
|
+
return { id: file };
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const id = await p.resolveId.call(
|
|
189
|
+
ctx,
|
|
190
|
+
"~/.deco/meta.gen.json",
|
|
191
|
+
path.join(dir, "src", "setup.ts"),
|
|
192
|
+
{
|
|
193
|
+
ssr: true,
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
expect(id.endsWith(".json")).toBe(false);
|
|
197
|
+
expect(seen).toEqual([["~/.deco/meta.gen.json", true]]);
|
|
198
|
+
expect(p.load.call({}, id, { ssr: true })).toContain("JSON.parse(");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("leaves an aliased import alone when it doesn't resolve to an existing file", async () => {
|
|
202
|
+
const p = getPlugin();
|
|
203
|
+
const ctx = { resolve: async () => null };
|
|
204
|
+
const id = await p.resolveId.call(ctx, "~/missing/meta.gen.json", path.join(dir, "x.ts"), {
|
|
205
|
+
ssr: true,
|
|
206
|
+
});
|
|
207
|
+
expect(id).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("falls through when a resolver in the chain throws for the alias", async () => {
|
|
211
|
+
const p = getPlugin();
|
|
212
|
+
const ctx = {
|
|
213
|
+
resolve: async () => {
|
|
214
|
+
throw new Error("exports map error");
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const id = await p.resolveId.call(ctx, "~/x/meta.gen.json", path.join(dir, "x.ts"), {
|
|
218
|
+
ssr: true,
|
|
219
|
+
});
|
|
220
|
+
expect(id).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("falls through to Vite when meta.gen.json does not exist yet", () => {
|
|
224
|
+
const p = getPlugin();
|
|
225
|
+
const importer = path.join(dir, "src", "setup.ts");
|
|
226
|
+
expect(
|
|
227
|
+
p.resolveId.call({}, "../missing/meta.gen.json", importer, { ssr: true }),
|
|
228
|
+
).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("still stubs meta.gen.json on the client", () => {
|
|
232
|
+
const p = getPlugin();
|
|
233
|
+
const importer = path.join(dir, "src", "setup.ts");
|
|
234
|
+
expect(p.resolveId.call({}, "../.deco/meta.gen.json", importer, { ssr: false })).toBe(
|
|
235
|
+
"\0stub:meta-gen",
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
package/src/vite/plugin.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { decoVitePlugin } from "./plugin.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -50,3 +50,127 @@ describe("decoVitePlugin — loaders.gen client stub", () => {
|
|
|
50
50
|
expect(plugin.load("/repo/src/actions/foo.ts", { ssr: false })).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `fastDeploy: true` must stub blocks.gen out of the SERVER bundle — but only
|
|
56
|
+
* for production builds.
|
|
57
|
+
*
|
|
58
|
+
* Without the stub, a fast-deploy site holds the decofile twice: the bundled
|
|
59
|
+
* JSON.parse(...) graph stays reachable via the site's `import { blocks }`
|
|
60
|
+
* binding for the isolate's whole life, and ensureBlocksHydrated adds a second
|
|
61
|
+
* graph. Measured at 13.6 MB + 13.4 MB for a 9.2 MB decofile.
|
|
62
|
+
*
|
|
63
|
+
* In `vite dev` there is no KV to hydrate from, and the dev bootstrap relies on
|
|
64
|
+
* reading the `.json` sibling the stub bypasses — so stubbing there would blank
|
|
65
|
+
* the local site.
|
|
66
|
+
*/
|
|
67
|
+
describe("decoVitePlugin — fastDeploy server stub", () => {
|
|
68
|
+
const id = "/repo/.deco/blocks.gen.ts";
|
|
69
|
+
const STUB = "export const blocks = {};";
|
|
70
|
+
|
|
71
|
+
// The default is "auto", which reads DECO_SEEDED_DEPLOY. Pin it off so the
|
|
72
|
+
// "default does not stub" assertions below can't be silently satisfied (or
|
|
73
|
+
// broken) by an ambient env var.
|
|
74
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "");
|
|
75
|
+
afterEach(() => vi.stubEnv("DECO_SEEDED_DEPLOY", ""));
|
|
76
|
+
|
|
77
|
+
/** @param {{fastDeploy?: boolean}} opts @param {"build"|"serve"} command */
|
|
78
|
+
const make = (opts, command) => {
|
|
79
|
+
const p = /** @type {any} */ (decoVitePlugin(opts));
|
|
80
|
+
p.config({}, { command }); // Vite always runs config() before load()
|
|
81
|
+
return p;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
it("stubs blocks.gen on SSR for a production build", () => {
|
|
85
|
+
expect(make({ fastDeploy: true }, "build").load(id, { ssr: true })).toBe(STUB);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does NOT stub on SSR in dev, where there is no KV to hydrate from", () => {
|
|
89
|
+
expect(make({ fastDeploy: true }, "serve").load(id, { ssr: true })).not.toBe(STUB);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("keeps stubbing the client bundle regardless", () => {
|
|
93
|
+
expect(make({ fastDeploy: true }, "build").load(id, { ssr: false })).toBe(STUB);
|
|
94
|
+
expect(make({ fastDeploy: true }, "serve").load(id, { ssr: false })).toBe(STUB);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does NOT stub blocks.gen on SSR by default (bundled fallback preserved)", () => {
|
|
98
|
+
// No .json sibling on disk for this synthetic path, so the hook falls
|
|
99
|
+
// through to Vite (undefined). The point is it never returns the stub.
|
|
100
|
+
expect(make({}, "build").load(id, { ssr: true })).not.toBe(STUB);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("leaves unrelated modules alone", () => {
|
|
104
|
+
expect(make({ fastDeploy: true }, "build").load("/repo/src/x.tsx", { ssr: true })).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("defines __DECO_BLOCKS_STUBBED__ so the runtime knows whether it has a fallback", () => {
|
|
108
|
+
const build = /** @type {any} */ (decoVitePlugin({ fastDeploy: true })).config({}, { command: "build" });
|
|
109
|
+
expect(build.define.__DECO_BLOCKS_STUBBED__).toBe("true");
|
|
110
|
+
|
|
111
|
+
// Dev and the default must both report "no, you still have a decofile",
|
|
112
|
+
// otherwise ensureBlocksHydrated would start failing requests.
|
|
113
|
+
const dev = /** @type {any} */ (decoVitePlugin({ fastDeploy: true })).config({}, { command: "serve" });
|
|
114
|
+
expect(dev.define.__DECO_BLOCKS_STUBBED__).toBe("false");
|
|
115
|
+
const def = /** @type {any} */ (decoVitePlugin()).config({}, { command: "build" });
|
|
116
|
+
expect(def.define.__DECO_BLOCKS_STUBBED__).toBe("false");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The `"auto"` default: stub only when the deploy pipeline declares it seeds
|
|
122
|
+
* `decofile:<deployment-id>` into KV BEFORE activating the new version.
|
|
123
|
+
*
|
|
124
|
+
* Stubbing without that guarantee is a hard outage, not a degraded mode — the
|
|
125
|
+
* bundle has no decofile, so `ensureBlocksHydrated` 5xxs on purpose rather than
|
|
126
|
+
* serving an empty site the edge would cache. Cloudflare Workers Builds and a
|
|
127
|
+
* manual `wrangler deploy` do not seed, and they never set the env.
|
|
128
|
+
*/
|
|
129
|
+
describe("decoVitePlugin — fastDeploy auto gate", () => {
|
|
130
|
+
const id = "/repo/.deco/blocks.gen.ts";
|
|
131
|
+
const STUB = "export const blocks = {};";
|
|
132
|
+
|
|
133
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
134
|
+
|
|
135
|
+
/** @param {"build"|"serve"} command */
|
|
136
|
+
const makeDefault = (command) => {
|
|
137
|
+
const p = /** @type {any} */ (decoVitePlugin());
|
|
138
|
+
const cfg = p.config({}, { command });
|
|
139
|
+
return { plugin: p, cfg };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
it("does not stub when the pipeline does not declare a seed step", () => {
|
|
143
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "");
|
|
144
|
+
const { plugin, cfg } = makeDefault("build");
|
|
145
|
+
expect(plugin.load(id, { ssr: true })).not.toBe(STUB);
|
|
146
|
+
expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("stubs on a production build when the pipeline declares it seeds", () => {
|
|
150
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
|
|
151
|
+
const { plugin, cfg } = makeDefault("build");
|
|
152
|
+
expect(plugin.load(id, { ssr: true })).toBe(STUB);
|
|
153
|
+
expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("true");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("still never stubs SSR in dev, even inside a seeding pipeline", () => {
|
|
157
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
|
|
158
|
+
const { plugin, cfg } = makeDefault("serve");
|
|
159
|
+
expect(plugin.load(id, { ssr: true })).not.toBe(STUB);
|
|
160
|
+
expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("explicit false wins over a seeding pipeline", () => {
|
|
164
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "1");
|
|
165
|
+
const p = /** @type {any} */ (decoVitePlugin({ fastDeploy: false }));
|
|
166
|
+
const cfg = p.config({}, { command: "build" });
|
|
167
|
+
expect(p.load(id, { ssr: true })).not.toBe(STUB);
|
|
168
|
+
expect(cfg.define.__DECO_BLOCKS_STUBBED__).toBe("false");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("explicit true wins over a non-seeding pipeline", () => {
|
|
172
|
+
vi.stubEnv("DECO_SEEDED_DEPLOY", "");
|
|
173
|
+
const p = /** @type {any} */ (decoVitePlugin({ fastDeploy: true }));
|
|
174
|
+
expect(p.config({}, { command: "build" }).define.__DECO_BLOCKS_STUBBED__).toBe("true");
|
|
175
|
+
});
|
|
176
|
+
});
|