@decocms/tanstack 7.77.7 → 7.77.9

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/tanstack",
3
- "version": "7.77.7",
3
+ "version": "7.77.9",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "lint:unused": "knip"
28
28
  },
29
29
  "dependencies": {
30
- "@decocms/blocks": "7.77.7",
31
- "@decocms/blocks-admin": "7.77.7",
32
- "@decocms/blocks-cli": "7.77.7",
30
+ "@decocms/blocks": "7.77.9",
31
+ "@decocms/blocks-admin": "7.77.9",
32
+ "@decocms/blocks-cli": "7.77.9",
33
33
  "@deco-cx/warp-node": "^0.3.16",
34
34
  "fast-json-patch": "^3.1.0",
35
35
  "ws": "^8.18.0"
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { decoParseSearch, decoStringifySearch } from "./router";
3
+
4
+ describe("decoStringifySearch ∘ decoParseSearch is a fixed point", () => {
5
+ // TanStack Router redirects (307) on the server when
6
+ // stringifySearch(parseSearch(search)) differs from the request's search —
7
+ // every query string that round-trips unchanged is a request that never
8
+ // redirects.
9
+ it.each([
10
+ "?q=camisa&sort=",
11
+ "?utm_source=",
12
+ "?a=&b=",
13
+ "?q=camisa",
14
+ "?filter.brand=Nike&filter.brand=Adidas",
15
+ // (`%20` is re-encoded as `+` by URLSearchParams — a pre-existing
16
+ // normalization, out of this fix's scope.)
17
+ "?q=a+b&page=2",
18
+ ])("round-trips %s unchanged", (search) => {
19
+ expect(decoStringifySearch(decoParseSearch(search))).toBe(search);
20
+ });
21
+
22
+ it("parses a value-less param to an empty string", () => {
23
+ expect(decoParseSearch("?q=camisa&sort=")).toEqual({ q: "camisa", sort: "" });
24
+ });
25
+
26
+ it("still drops undefined and null (how navigate() removes a param)", () => {
27
+ expect(decoStringifySearch({ q: "camisa", sort: undefined, page: null })).toBe("?q=camisa");
28
+ expect(decoStringifySearch({ q: undefined })).toBe("");
29
+ });
30
+
31
+ it("keeps an explicit empty string as key= and arrays as repeated keys", () => {
32
+ expect(decoStringifySearch({ sort: "", f: ["a", "b"] })).toBe("?sort=&f=a&f=b");
33
+ });
34
+ });
package/src/sdk/router.ts CHANGED
@@ -30,10 +30,22 @@ export const decoParseSearch: SearchParser = (searchStr) => {
30
30
  return result;
31
31
  };
32
32
 
33
+ /**
34
+ * Serializes the router's search object back to a query string.
35
+ *
36
+ * Must be a fixed point of {@link decoParseSearch}: on the server, TanStack
37
+ * Router compares the request's href with `stringifySearch(parseSearch(...))`
38
+ * (`router.beforeLoad`) and answers a 307 to the normalized URL when they
39
+ * differ. A value-less parameter (`?sort=`, `?utm_source=`) parses to `""`,
40
+ * so dropping `""` here turned every such request into a redirect that
41
+ * Fresh — and TanStack's own default serializer, which only drops
42
+ * `undefined` — never issued. `undefined`/`null` still remove a param, which
43
+ * is how `navigate({ search: { q: undefined } })` clears one.
44
+ */
33
45
  export const decoStringifySearch: SearchSerializer = (search) => {
34
46
  const params = new URLSearchParams();
35
47
  for (const [key, value] of Object.entries(search)) {
36
- if (value === undefined || value === null || value === "") continue;
48
+ if (value === undefined || value === null) continue;
37
49
  if (Array.isArray(value)) {
38
50
  for (const v of value) params.append(key, String(v));
39
51
  } else {