@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "6.4.2",
3
+ "version": "6.4.4",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -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, hasNoSplat]
132
- // So `/foo/bar` > `/foo/:x` > `/foo/*` > `/*`, and `/my-account/*` > `/*`.
131
+ // [hasNoWildcard, literalSegments, paramSegments]
133
132
  //
134
- // URLPattern syntax (`{group}?`, `:slug([\w-]+)`, trailing `*`) is supported:
135
- // any segment containing `{`, `}`, or `?` counts as a param, and a segment
136
- // containing `*` flips the splat bit. This keeps optional-group patterns
137
- // (e.g. `/{granado/}?*`) from out-ranking real literal pages.
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 hasSplat = false;
149
+ let hasWildcard = false;
143
150
  for (const part of parts) {
144
- if (part.includes("*")) hasSplat = true;
145
- else if (
146
- part.startsWith(":") ||
147
- part.startsWith("$") ||
148
- /[{}?]/.test(part)
149
- ) params++;
150
- else literals++;
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 [literals, params, hasSplat ? 0 : 1];
163
+ return [hasWildcard ? 0 : 1, literals, params];
153
164
  }
154
165
 
155
166
  export function getAllPages(): Array<{ key: string; page: DecoPage }> {
@@ -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) {