@decocms/start 6.4.2 → 6.4.4
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 +1 -1
- package/src/cms/loader.test.ts +20 -0
- package/src/cms/loader.ts +26 -15
- package/src/vite/plugin.js +22 -0
package/package.json
CHANGED
package/src/cms/loader.test.ts
CHANGED
|
@@ -139,6 +139,26 @@ describe("findPageByPath specificity", () => {
|
|
|
139
139
|
expect(match?.blockKey).toBe("pages-bf");
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("prefers the home page over an optional-group splat catch-all", () => {
|
|
143
|
+
// Regression: /{granado/}?* matches "/" and was out-ranking the home
|
|
144
|
+
// because the `{granado` segment counted as a param. The home block
|
|
145
|
+
// is a literal-only `/` path and must always win.
|
|
146
|
+
setBlocks({
|
|
147
|
+
"pages-home": {
|
|
148
|
+
name: "Home",
|
|
149
|
+
path: "/",
|
|
150
|
+
sections: [],
|
|
151
|
+
},
|
|
152
|
+
"pages-pdp-plp": {
|
|
153
|
+
name: "PDP & PLP",
|
|
154
|
+
path: "/{granado/}?*",
|
|
155
|
+
sections: [],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const match = findPageByPath("/");
|
|
159
|
+
expect(match?.blockKey).toBe("pages-home");
|
|
160
|
+
});
|
|
161
|
+
|
|
142
162
|
it("falls back to the splat page for unknown URLs", () => {
|
|
143
163
|
const match = findPageByPath("/perfumaria");
|
|
144
164
|
expect(match?.blockKey).toBe("pages-pdp-plp");
|
package/src/cms/loader.ts
CHANGED
|
@@ -128,28 +128,39 @@ export function withBlocksOverride<T>(override: Record<string, unknown>, fn: ()
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// Higher key wins. Compared lexicographically:
|
|
131
|
-
// [literalSegments, paramSegments
|
|
132
|
-
// So `/foo/bar` > `/foo/:x` > `/foo/*` > `/*`, and `/my-account/*` > `/*`.
|
|
131
|
+
// [hasNoWildcard, literalSegments, paramSegments]
|
|
133
132
|
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
133
|
+
// `hasNoWildcard` is the top key so a literal-only path always beats any
|
|
134
|
+
// pattern that contains `*` or `{group}?` — including the empty-parts case
|
|
135
|
+
// `/` (literals=0) vs the catch-all `/{prefix/}?*` (literals=0, params=1).
|
|
136
|
+
// Without this, the URLPattern fix (#213/#214) inadvertently lets a
|
|
137
|
+
// `/{group/}?*` catch-all out-rank an exact `/` home page because the
|
|
138
|
+
// `{group` segment counted as a param. See deco-sites/granadobr-tanstack
|
|
139
|
+
// where `/` was being routed to the granado PDP/PLP block's NotFound
|
|
140
|
+
// fallback.
|
|
141
|
+
//
|
|
142
|
+
// Order produced:
|
|
143
|
+
// /foo/bar (no wildcard, literals=2) > /foo/:x (no wildcard, lit=1, param=1)
|
|
144
|
+
// /foo (no wildcard) > /{granado/}?* (has wildcard) > /*
|
|
138
145
|
function pathSpecificityKey(path: string): [number, number, number] {
|
|
139
146
|
const parts = path.split("/").filter(Boolean);
|
|
140
147
|
let literals = 0;
|
|
141
148
|
let params = 0;
|
|
142
|
-
let
|
|
149
|
+
let hasWildcard = false;
|
|
143
150
|
for (const part of parts) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
else
|
|
151
|
+
// A wildcard is any `*`, optional group `{...}?`, or any segment
|
|
152
|
+
// bearing `?` — these all make the pattern match strictly more URLs
|
|
153
|
+
// than a plain literal/`:param`/`:slug([\w-]+)` segment, so they
|
|
154
|
+
// are demoted to "least specific" together regardless of count.
|
|
155
|
+
if (part.includes("*") || /[{}?]/.test(part)) {
|
|
156
|
+
hasWildcard = true;
|
|
157
|
+
} else if (part.startsWith(":") || part.startsWith("$")) {
|
|
158
|
+
params++;
|
|
159
|
+
} else {
|
|
160
|
+
literals++;
|
|
161
|
+
}
|
|
151
162
|
}
|
|
152
|
-
return [
|
|
163
|
+
return [hasWildcard ? 0 : 1, literals, params];
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
export function getAllPages(): Array<{ key: string; page: DecoPage }> {
|
package/src/vite/plugin.js
CHANGED
|
@@ -471,6 +471,28 @@ export function decoVitePlugin() {
|
|
|
471
471
|
env.optimizeDeps.esbuildOptions.jsx = "automatic";
|
|
472
472
|
env.optimizeDeps.esbuildOptions.jsxImportSource = "react";
|
|
473
473
|
}
|
|
474
|
+
|
|
475
|
+
// Force @decocms/start through the SSR transform pipeline so TanStack
|
|
476
|
+
// Start's compiler can register the framework's createServerFn handlers
|
|
477
|
+
// (loadDeferredSection, etc.) in the per-environment serverFnsById
|
|
478
|
+
// manifest. Without this, Vite pre-bundles @decocms/start via
|
|
479
|
+
// optimizeDeps before plugins run, the handler never enters the
|
|
480
|
+
// manifest, and every POST /_serverFn/* call from the browser returns
|
|
481
|
+
// HTTP 500 ("Invalid server function ID"). See #197.
|
|
482
|
+
if (name === "ssr") {
|
|
483
|
+
env.resolve = env.resolve || {};
|
|
484
|
+
const existing = env.resolve.noExternal;
|
|
485
|
+
const additions = ["@decocms/start"];
|
|
486
|
+
if (existing === true) {
|
|
487
|
+
// Already noExternal everything — nothing to add.
|
|
488
|
+
} else if (Array.isArray(existing)) {
|
|
489
|
+
env.resolve.noExternal = [...new Set([...existing, ...additions])];
|
|
490
|
+
} else if (existing) {
|
|
491
|
+
env.resolve.noExternal = [existing, ...additions];
|
|
492
|
+
} else {
|
|
493
|
+
env.resolve.noExternal = additions;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
474
496
|
},
|
|
475
497
|
|
|
476
498
|
generateBundle(_, bundle) {
|