@decocms/tanstack 7.21.0 → 7.22.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 +4 -4
- package/src/routes/cmsRoute.test.ts +63 -0
- package/src/routes/cmsRoute.ts +64 -27
- package/src/vite/plugin-scaffold-gen.test.js +106 -0
- package/src/vite/plugin.js +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/tanstack",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.22.0",
|
|
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.
|
|
28
|
-
"@decocms/blocks-admin": "7.
|
|
29
|
-
"@decocms/blocks-cli": "7.
|
|
27
|
+
"@decocms/blocks": "7.22.0",
|
|
28
|
+
"@decocms/blocks-admin": "7.22.0",
|
|
29
|
+
"@decocms/blocks-cli": "7.22.0",
|
|
30
30
|
"@deco-cx/warp-node": "^0.3.16",
|
|
31
31
|
"fast-json-patch": "^3.1.0",
|
|
32
32
|
"ws": "^8.18.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { registerCacheableSections, registerSectionLoader } from "@decocms/blocks/cms";
|
|
2
|
+
import type { ResolvedSection } from "@decocms/blocks/cms";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { runSectionLoadersWithSeo } from "./cmsRoute";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression guard for #355: body sections and the SEO section used to be
|
|
8
|
+
* resolved sequentially (`runSectionLoaders` for the body, then a separate
|
|
9
|
+
* `runSingleSectionLoader` call for the SEO section inside `buildPageSeo`).
|
|
10
|
+
* On a PDP, both commonly resolve the SAME product via the SAME commerce
|
|
11
|
+
* loader — sequential calls always missed the fetch-cache layer's in-flight
|
|
12
|
+
* dedup (the body call had already completed by the time the SEO call
|
|
13
|
+
* started). Batching them into one `runSectionLoaders` call (this file's
|
|
14
|
+
* `runSectionLoadersWithSeo`) lets identical concurrent requests share one
|
|
15
|
+
* origin round-trip.
|
|
16
|
+
*
|
|
17
|
+
* This test proves the batching by registering a *cacheable* section loader
|
|
18
|
+
* (the same SWR + in-flight-dedup mechanism real commerce loaders use) and
|
|
19
|
+
* asserting it's invoked only once when the body section and SEO section
|
|
20
|
+
* share the same component + props.
|
|
21
|
+
*/
|
|
22
|
+
describe("runSectionLoadersWithSeo (#355)", () => {
|
|
23
|
+
it("dedupes a same-component/same-props SEO section against a body section via the cacheable-section in-flight cache", async () => {
|
|
24
|
+
const component = `test/sections/DedupCheck-${Date.now()}.tsx`;
|
|
25
|
+
let calls = 0;
|
|
26
|
+
|
|
27
|
+
registerCacheableSections({ [component]: { maxAge: 60_000 } });
|
|
28
|
+
registerSectionLoader(component, async (props) => {
|
|
29
|
+
calls++;
|
|
30
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
31
|
+
return { ...props, loaded: true };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const bodySection: ResolvedSection = { component, props: { sku: "123" }, key: component, index: 0 };
|
|
35
|
+
const seoSection: ResolvedSection = { component, props: { sku: "123" }, key: `${component}-seo`, index: -1 };
|
|
36
|
+
|
|
37
|
+
const request = new Request("https://store.com/produto/123");
|
|
38
|
+
const { enrichedSections, enrichedSeoSection } = await runSectionLoadersWithSeo(
|
|
39
|
+
[bodySection],
|
|
40
|
+
seoSection,
|
|
41
|
+
request,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(calls).toBe(1);
|
|
45
|
+
expect((enrichedSections[0].props as any).loaded).toBe(true);
|
|
46
|
+
expect((enrichedSeoSection?.props as any).loaded).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("runs plain runSectionLoaders when there is no SEO section", async () => {
|
|
50
|
+
const component = `test/sections/NoSeo-${Date.now()}.tsx`;
|
|
51
|
+
registerSectionLoader(component, async (props) => ({ ...props, loaded: true }));
|
|
52
|
+
|
|
53
|
+
const bodySection: ResolvedSection = { component, props: {}, key: component, index: 0 };
|
|
54
|
+
const { enrichedSections, enrichedSeoSection } = await runSectionLoadersWithSeo(
|
|
55
|
+
[bodySection],
|
|
56
|
+
null,
|
|
57
|
+
new Request("https://store.com/"),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect((enrichedSections[0].props as any).loaded).toBe(true);
|
|
61
|
+
expect(enrichedSeoSection).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -124,6 +124,36 @@ function persistFlags(matcherCtx: MatcherContext): StoredFlag[] {
|
|
|
124
124
|
return recorded;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Run the body sections' loaders and the SEO section's loader in the SAME
|
|
129
|
+
* batch (`Promise.all` via `runSectionLoaders`), instead of sequentially
|
|
130
|
+
* (body first, then `buildPageSeo` separately awaiting the SEO section).
|
|
131
|
+
*
|
|
132
|
+
* On a PDP, the SEO section (e.g. SeoPDP) commonly resolves the same
|
|
133
|
+
* product as a body section via the same commerce loader. Running them
|
|
134
|
+
* sequentially meant the SEO fetch always missed any in-flight request
|
|
135
|
+
* dedup at the fetch-cache layer (apps-vtex's fetchWithCache) — the body
|
|
136
|
+
* fetch had already completed and left the in-flight map by the time the
|
|
137
|
+
* SEO fetch started, so the origin got hit twice per navigation (#355).
|
|
138
|
+
* Batching them gives concurrent identical requests a chance to share one
|
|
139
|
+
* origin round-trip.
|
|
140
|
+
*/
|
|
141
|
+
/** @internal exported for tests */
|
|
142
|
+
export async function runSectionLoadersWithSeo(
|
|
143
|
+
resolvedSections: ResolvedSection[],
|
|
144
|
+
seoSection: ResolvedSection | null | undefined,
|
|
145
|
+
request: Request,
|
|
146
|
+
): Promise<{ enrichedSections: ResolvedSection[]; enrichedSeoSection: ResolvedSection | null | undefined }> {
|
|
147
|
+
if (!seoSection) {
|
|
148
|
+
return { enrichedSections: await runSectionLoaders(resolvedSections, request), enrichedSeoSection: seoSection };
|
|
149
|
+
}
|
|
150
|
+
const enrichedAll = await runSectionLoaders([...resolvedSections, seoSection], request);
|
|
151
|
+
return {
|
|
152
|
+
enrichedSections: enrichedAll.slice(0, -1),
|
|
153
|
+
enrichedSeoSection: enrichedAll[enrichedAll.length - 1],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
127
157
|
async function loadCmsPageInternal(fullPath: string) {
|
|
128
158
|
const [basePath] = fullPath.split("?");
|
|
129
159
|
// On client-side navigation getRequestUrl() is the /_serverFn/... endpoint,
|
|
@@ -150,13 +180,14 @@ async function loadCmsPageInternal(fullPath: string) {
|
|
|
150
180
|
headers: originRequest.headers,
|
|
151
181
|
});
|
|
152
182
|
|
|
153
|
-
// Resolve page sections
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
// Resolve page sections (+ the SEO section, batched together — see #355)
|
|
184
|
+
// and site globals in parallel. Globals are merged into the same
|
|
185
|
+
// `resolvedSections` array so the path through this server function is
|
|
186
|
+
// identical for SSR (F5) and SPA (<Link>) navigations — see #233 for the
|
|
187
|
+
// previous SPA-breakage when `withSiteGlobals` ran client-side and saw an
|
|
188
|
+
// empty client-bundled `blocks.gen.ts`.
|
|
189
|
+
const [{ enrichedSections, enrichedSeoSection }, globals] = await Promise.all([
|
|
190
|
+
runSectionLoadersWithSeo(page.resolvedSections, page.seoSection, request),
|
|
160
191
|
resolveSiteGlobals(),
|
|
161
192
|
]);
|
|
162
193
|
|
|
@@ -178,7 +209,7 @@ async function loadCmsPageInternal(fullPath: string) {
|
|
|
178
209
|
const device = detectDevice(ua);
|
|
179
210
|
|
|
180
211
|
// Build SEO: merge page-level seo block (primary) with section-contributed SEO (secondary)
|
|
181
|
-
const seo =
|
|
212
|
+
const seo = buildPageSeo(page.seoSection, enrichedSeoSection, mergedSections);
|
|
182
213
|
|
|
183
214
|
// Destructure seoSection out — it's an internal artifact, not serialized to client
|
|
184
215
|
const { seoSection: _seo, ...pageData } = page;
|
|
@@ -241,8 +272,8 @@ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async (
|
|
|
241
272
|
const page = await resolveDecoPage("/", matcherCtx);
|
|
242
273
|
if (!page) return null;
|
|
243
274
|
const flags = persistFlags(matcherCtx);
|
|
244
|
-
const [enrichedSections, globals] = await Promise.all([
|
|
245
|
-
|
|
275
|
+
const [{ enrichedSections, enrichedSeoSection }, globals] = await Promise.all([
|
|
276
|
+
runSectionLoadersWithSeo(page.resolvedSections, page.seoSection, request),
|
|
246
277
|
resolveSiteGlobals(),
|
|
247
278
|
]);
|
|
248
279
|
|
|
@@ -255,7 +286,7 @@ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async (
|
|
|
255
286
|
await preloadSectionComponents(eagerKeys);
|
|
256
287
|
|
|
257
288
|
const device = detectDevice(ua);
|
|
258
|
-
const seo =
|
|
289
|
+
const seo = buildPageSeo(page.seoSection, enrichedSeoSection, mergedSections);
|
|
259
290
|
|
|
260
291
|
const { seoSection: _seo, ...pageData } = page;
|
|
261
292
|
|
|
@@ -432,15 +463,26 @@ type CmsPageLoaderData = {
|
|
|
432
463
|
// ---------------------------------------------------------------------------
|
|
433
464
|
|
|
434
465
|
/**
|
|
435
|
-
* Process the resolved SEO section from page.seo,
|
|
436
|
-
*
|
|
437
|
-
*
|
|
466
|
+
* Process the resolved SEO section from page.seo, apply title/description
|
|
467
|
+
* templates, and merge with section-contributed SEO.
|
|
468
|
+
*
|
|
469
|
+
* `seoSection` is the raw (pre-loader) resolved section — its `props` are
|
|
470
|
+
* used for the CMS-configured `titleTemplate`/`descriptionTemplate` fields,
|
|
471
|
+
* which a loader transforming `{jsonLD} → {title, description}` may not
|
|
472
|
+
* pass through unchanged. `enrichedSeoSection` must already have gone
|
|
473
|
+
* through that registered section loader (e.g., SEOPDP transforms
|
|
474
|
+
* {jsonLD: ProductDetailsPage} → {title, description, ...}) — batched
|
|
475
|
+
* together with the body sections in `runSectionLoadersWithSeo` so a
|
|
476
|
+
* commerce loader shared by both (the same product fetched by both the PDP
|
|
477
|
+
* body section and the SeoPDP section) gets a chance to dedupe at the
|
|
478
|
+
* fetch-cache layer instead of resolving the page twice (#355). This
|
|
479
|
+
* function itself no longer runs any loader.
|
|
438
480
|
*/
|
|
439
|
-
|
|
481
|
+
function buildPageSeo(
|
|
440
482
|
seoSection: ResolvedSection | null | undefined,
|
|
483
|
+
enrichedSeoSection: ResolvedSection | null | undefined,
|
|
441
484
|
enrichedSections: ResolvedSection[],
|
|
442
|
-
|
|
443
|
-
): Promise<PageSeo> {
|
|
485
|
+
): PageSeo {
|
|
444
486
|
// Secondary source: SEO sections embedded in the sections array
|
|
445
487
|
const sectionSeo = extractSeoFromSections(enrichedSections);
|
|
446
488
|
|
|
@@ -472,16 +514,11 @@ async function buildPageSeo(
|
|
|
472
514
|
return merged;
|
|
473
515
|
}
|
|
474
516
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
try
|
|
479
|
-
|
|
480
|
-
if (enriched) enrichedProps = enriched.props;
|
|
481
|
-
} catch {
|
|
482
|
-
// Section loader failed — use the raw resolved props
|
|
483
|
-
}
|
|
484
|
-
|
|
517
|
+
// enrichedSeoSection should always be present once seoSection is, but
|
|
518
|
+
// fall back to the raw section's props (pre-loader) if the loader batch
|
|
519
|
+
// somehow dropped it — same "use the raw resolved props" resilience the
|
|
520
|
+
// old inline try/catch had around the loader call.
|
|
521
|
+
const enrichedProps = (enrichedSeoSection ?? seoSection).props;
|
|
485
522
|
const pageSeo = extractSeoFromProps(enrichedProps);
|
|
486
523
|
|
|
487
524
|
// Replicate original SeoV2 loader logic: `_title ?? appTitle`
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
const execCalls = [];
|
|
8
|
+
const execFileSyncCalls = [];
|
|
9
|
+
|
|
10
|
+
vi.mock("node:child_process", () => {
|
|
11
|
+
const exec = (cmd, _opts, cb) => {
|
|
12
|
+
execCalls.push(cmd);
|
|
13
|
+
cb(null, "", "");
|
|
14
|
+
};
|
|
15
|
+
const execFileSync = (cmd, args) => {
|
|
16
|
+
execFileSyncCalls.push([cmd, args]);
|
|
17
|
+
return "";
|
|
18
|
+
};
|
|
19
|
+
return { exec, execFileSync, default: { exec, execFileSync } };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Regression guard for #371: `vite dev` never re-ran the sections/loaders/
|
|
24
|
+
* invoke generators, only `npm run build` did (via the full `generate`
|
|
25
|
+
* orchestrator) — so editing a section/loader's TS interface left the
|
|
26
|
+
* `.gen` files silently stale until a manual `generate` run. This test
|
|
27
|
+
* exercises `configureServer`'s scaffold-gen hook against a real
|
|
28
|
+
* (empty) tmp dir + a real EventEmitter as `server.watcher`, with
|
|
29
|
+
* `node:child_process` mocked so no real subprocess is spawned.
|
|
30
|
+
*/
|
|
31
|
+
describe("decoVitePlugin — sections/loaders/invoke dev regeneration (#371)", () => {
|
|
32
|
+
let tmp;
|
|
33
|
+
let cwdSpy;
|
|
34
|
+
let plugin;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
vi.resetModules();
|
|
38
|
+
execCalls.length = 0;
|
|
39
|
+
execFileSyncCalls.length = 0;
|
|
40
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-scaffold-gen-"));
|
|
41
|
+
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(tmp);
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
({ decoVitePlugin: plugin } = await import("./plugin.js"));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.useRealTimers();
|
|
48
|
+
cwdSpy.mockRestore();
|
|
49
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function makeServer() {
|
|
54
|
+
return {
|
|
55
|
+
watcher: new EventEmitter(),
|
|
56
|
+
config: { define: {} },
|
|
57
|
+
environments: undefined,
|
|
58
|
+
httpServer: { once: () => {} },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it("runs the sections/loaders/invoke generator on dev startup (debounced)", async () => {
|
|
63
|
+
const p = Array.isArray(plugin()) ? plugin()[0] : plugin();
|
|
64
|
+
const server = makeServer();
|
|
65
|
+
p.configureServer(server);
|
|
66
|
+
|
|
67
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
68
|
+
|
|
69
|
+
const scaffoldCalls = execCalls.filter((c) => c.includes("--only sections,loaders,invoke"));
|
|
70
|
+
expect(scaffoldCalls).toHaveLength(1);
|
|
71
|
+
expect(scaffoldCalls[0]).toContain("generate.ts");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("debounces multiple src/ file-watcher events into a single regen call", async () => {
|
|
75
|
+
const p = Array.isArray(plugin()) ? plugin()[0] : plugin();
|
|
76
|
+
const server = makeServer();
|
|
77
|
+
p.configureServer(server);
|
|
78
|
+
|
|
79
|
+
// Consume the cold-start call first.
|
|
80
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
81
|
+
execCalls.length = 0;
|
|
82
|
+
|
|
83
|
+
const srcFile = path.join(tmp, "src", "sections", "Foo.tsx");
|
|
84
|
+
server.watcher.emit("change", srcFile);
|
|
85
|
+
server.watcher.emit("change", srcFile);
|
|
86
|
+
server.watcher.emit("add", srcFile);
|
|
87
|
+
|
|
88
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
89
|
+
|
|
90
|
+
const scaffoldCalls = execCalls.filter((c) => c.includes("--only sections,loaders,invoke"));
|
|
91
|
+
expect(scaffoldCalls).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not trigger for a file outside src/", async () => {
|
|
95
|
+
const p = Array.isArray(plugin()) ? plugin()[0] : plugin();
|
|
96
|
+
const server = makeServer();
|
|
97
|
+
p.configureServer(server);
|
|
98
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
99
|
+
execCalls.length = 0;
|
|
100
|
+
|
|
101
|
+
server.watcher.emit("change", path.join(tmp, "README.md"));
|
|
102
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
103
|
+
|
|
104
|
+
expect(execCalls.filter((c) => c.includes("--only sections,loaders,invoke"))).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/vite/plugin.js
CHANGED
|
@@ -552,6 +552,71 @@ export function decoVitePlugin() {
|
|
|
552
552
|
if (isSchemaSource(file)) scheduleSchemaGen();
|
|
553
553
|
});
|
|
554
554
|
|
|
555
|
+
// --- sections.gen.ts / loaders.gen.ts / invoke.gen.ts regeneration (#371) ---
|
|
556
|
+
// Unlike meta.gen.json (regenerated above via generate-schema.ts
|
|
557
|
+
// directly), these three were never re-run in dev — only `npm run build`
|
|
558
|
+
// (which chains the full `generate` orchestrator) touched them. Editing
|
|
559
|
+
// a section/loader's TS interface (new prop, new @title) updated the TS
|
|
560
|
+
// source but left the `.gen` files stale until a manual `generate` run:
|
|
561
|
+
// typecheck passed and the runtime worked, but the studio/admin form
|
|
562
|
+
// silently kept showing the old schema.
|
|
563
|
+
//
|
|
564
|
+
// Reuses the SAME `generate.ts` orchestrator sites already run for
|
|
565
|
+
// `build`, restricted to --only sections,loaders,invoke so the schema
|
|
566
|
+
// regen above isn't duplicated. The orchestrator's own two-tier digest
|
|
567
|
+
// cache (.deco/generate.digests.json + .deco/.cache/stat-memo.json)
|
|
568
|
+
// makes a steady-state run (nothing changed) a cheap no-op — a cache
|
|
569
|
+
// miss only pays the cost for the generator(s) whose actual inputs
|
|
570
|
+
// changed. Best-effort: a failure here only warns (matches the schema
|
|
571
|
+
// regen's own error handling above) — dev keeps running off whatever
|
|
572
|
+
// was last generated, same as before this feature existed.
|
|
573
|
+
let scaffoldTimer = null;
|
|
574
|
+
let scaffoldInFlight = false;
|
|
575
|
+
let scaffoldQueued = false;
|
|
576
|
+
const runScaffoldGen = () => {
|
|
577
|
+
if (scaffoldInFlight) {
|
|
578
|
+
scaffoldQueued = true;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
scaffoldInFlight = true;
|
|
582
|
+
const start = Date.now();
|
|
583
|
+
const scriptPath = path.resolve(cwd, "node_modules/@decocms/blocks-cli/scripts/generate.ts");
|
|
584
|
+
const cmd = `npx tsx ${JSON.stringify(scriptPath)} --only sections,loaders,invoke --site ${schemaSiteName}`;
|
|
585
|
+
exec(cmd, { cwd }, (err) => {
|
|
586
|
+
scaffoldInFlight = false;
|
|
587
|
+
if (err) {
|
|
588
|
+
console.warn("[deco] sections/loaders/invoke generation failed:", err.message);
|
|
589
|
+
} else {
|
|
590
|
+
console.log(`[deco] sections/loaders/invoke checked (${Date.now() - start}ms)`);
|
|
591
|
+
}
|
|
592
|
+
if (scaffoldQueued) {
|
|
593
|
+
scaffoldQueued = false;
|
|
594
|
+
scheduleScaffoldGen();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
const scheduleScaffoldGen = () => {
|
|
599
|
+
if (scaffoldTimer) clearTimeout(scaffoldTimer);
|
|
600
|
+
scaffoldTimer = setTimeout(() => {
|
|
601
|
+
scaffoldTimer = null;
|
|
602
|
+
runScaffoldGen();
|
|
603
|
+
}, 500);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Cold-start: run once on dev startup. The digest cache makes this a
|
|
607
|
+
// fast no-op when nothing changed since the last `generate` run (e.g.
|
|
608
|
+
// right after a `build`) — fire-and-forget, doesn't block the first request.
|
|
609
|
+
scheduleScaffoldGen();
|
|
610
|
+
|
|
611
|
+
// Reuse the same src/**/*.{ts,tsx} watch predicate as the schema regen
|
|
612
|
+
// above — sections/loaders/actions all live under the same tree.
|
|
613
|
+
server.watcher.on("change", (file) => {
|
|
614
|
+
if (isSchemaSource(file)) scheduleScaffoldGen();
|
|
615
|
+
});
|
|
616
|
+
server.watcher.on("add", (file) => {
|
|
617
|
+
if (isSchemaSource(file)) scheduleScaffoldGen();
|
|
618
|
+
});
|
|
619
|
+
|
|
555
620
|
// Tunnel + daemon: connect local dev to admin.deco.cx
|
|
556
621
|
// Activated only when both DECO_SITE_NAME and DECO_ENV_NAME are set.
|
|
557
622
|
// Omitting DECO_ENV_NAME runs Vite fully local (no tunnel registration),
|