@mindees/router 0.1.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/dist/search.js ADDED
@@ -0,0 +1,112 @@
1
+ import { RouterError } from "./errors.js";
2
+ //#region src/search.ts
3
+ /**
4
+ * Search (query) params — parsing, serializing, and **typed, validated** access.
5
+ *
6
+ * Search params are first-class typed state in Quantum. A route declares a
7
+ * {@link StandardSchemaV1} schema; reads are validated and fully typed via
8
+ * {@link StandardSchemaV1.InferOutput}. This is the capability Expo Router and
9
+ * React Router lack (they return raw, untyped strings). See ADR-0003.
10
+ *
11
+ * Conventions:
12
+ * - repeated keys (`?tag=a&tag=b`) parse to a `string[]`; single keys to a `string`;
13
+ * - coercion (string → number/boolean/date) is delegated to the schema
14
+ * (e.g. `z.coerce.number()`), so this module never guesses types.
15
+ *
16
+ * @module
17
+ */
18
+ /**
19
+ * Parse a query string into a record. Accepts an optional leading `?`. Repeated
20
+ * keys collapse into an array, preserving order.
21
+ *
22
+ * @example
23
+ * parseQuery('?page=2&tag=a&tag=b') // { page: '2', tag: ['a', 'b'] }
24
+ */
25
+ function parseQuery(search) {
26
+ const out = Object.create(null);
27
+ const query = search.startsWith("?") ? search.slice(1) : search;
28
+ if (query.length === 0) return out;
29
+ for (const pair of query.split("&")) {
30
+ if (pair.length === 0) continue;
31
+ const eq = pair.indexOf("=");
32
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
33
+ const rawValue = eq === -1 ? "" : pair.slice(eq + 1);
34
+ const key = safeDecode(rawKey);
35
+ const value = safeDecode(rawValue);
36
+ const existing = out[key];
37
+ if (existing === void 0) out[key] = value;
38
+ else if (Array.isArray(existing)) existing.push(value);
39
+ else out[key] = [existing, value];
40
+ }
41
+ return out;
42
+ }
43
+ /**
44
+ * Serialize a record into a query string (no leading `?`). `null`/`undefined`
45
+ * values are skipped; arrays emit one `key=value` pair each. Keys are sorted for
46
+ * deterministic, cache-friendly output.
47
+ *
48
+ * @example
49
+ * stringifyQuery({ page: 2, tag: ['a', 'b'] }) // 'page=2&tag=a&tag=b'
50
+ */
51
+ function stringifyQuery(query) {
52
+ const parts = [];
53
+ for (const key of Object.keys(query).sort()) {
54
+ const value = query[key];
55
+ if (value === null || value === void 0) continue;
56
+ const encKey = encodeURIComponent(key);
57
+ if (Array.isArray(value)) for (const item of value) parts.push(`${encKey}=${encodeURIComponent(String(item))}`);
58
+ else parts.push(`${encKey}=${encodeURIComponent(String(value))}`);
59
+ }
60
+ return parts.join("&");
61
+ }
62
+ /**
63
+ * Validate `input` against a Standard Schema, **without throwing** on invalid
64
+ * input — returns a discriminated result. Throws {@link RouterError}
65
+ * (`ASYNC_SCHEMA`) only for the programming error of passing an async schema,
66
+ * since navigation-time parsing must be synchronous.
67
+ */
68
+ function safeValidateSearch(schema, input) {
69
+ const result = schema["~standard"].validate(input);
70
+ if (result instanceof Promise) throw new RouterError("ASYNC_SCHEMA", "Asynchronous schemas are not supported for synchronous search-param validation.");
71
+ if (result.issues) return {
72
+ ok: false,
73
+ issues: result.issues
74
+ };
75
+ return {
76
+ ok: true,
77
+ value: result.value
78
+ };
79
+ }
80
+ /**
81
+ * Validate `input` against a Standard Schema, returning the typed output or
82
+ * throwing {@link RouterError} (`VALIDATE_SEARCH` with the issues, or
83
+ * `ASYNC_SCHEMA`).
84
+ *
85
+ * @example
86
+ * const schema = z.object({ page: z.coerce.number() }) // Zod, Valibot, ArkType…
87
+ * validateSearch(schema, { page: '2' }) // { page: 2 }
88
+ */
89
+ function validateSearch(schema, input) {
90
+ const result = safeValidateSearch(schema, input);
91
+ if (!result.ok) throw new RouterError("VALIDATE_SEARCH", formatIssues(result.issues), result.issues);
92
+ return result.value;
93
+ }
94
+ /** Render Standard Schema issues into a single readable message. */
95
+ function formatIssues(issues) {
96
+ return `Search validation failed: ${issues.map((issue) => {
97
+ const path = issue.path?.map((seg) => typeof seg === "object" ? seg.key : seg).join(".");
98
+ return path ? `${path}: ${issue.message}` : issue.message;
99
+ }).join("; ")}`;
100
+ }
101
+ /** Decode a URI component, falling back to the raw value on malformed input. */
102
+ function safeDecode(value) {
103
+ try {
104
+ return decodeURIComponent(value.replace(/\+/g, " "));
105
+ } catch {
106
+ return value;
107
+ }
108
+ }
109
+ //#endregion
110
+ export { parseQuery, safeValidateSearch, stringifyQuery, validateSearch };
111
+
112
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Search (query) params — parsing, serializing, and **typed, validated** access.\n *\n * Search params are first-class typed state in Quantum. A route declares a\n * {@link StandardSchemaV1} schema; reads are validated and fully typed via\n * {@link StandardSchemaV1.InferOutput}. This is the capability Expo Router and\n * React Router lack (they return raw, untyped strings). See ADR-0003.\n *\n * Conventions:\n * - repeated keys (`?tag=a&tag=b`) parse to a `string[]`; single keys to a `string`;\n * - coercion (string → number/boolean/date) is delegated to the schema\n * (e.g. `z.coerce.number()`), so this module never guesses types.\n *\n * @module\n */\n\nimport { RouterError } from './errors'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n/** A value accepted when serializing a query string. */\nexport type QueryValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | ReadonlyArray<string | number | boolean>\n\n/**\n * Parse a query string into a record. Accepts an optional leading `?`. Repeated\n * keys collapse into an array, preserving order.\n *\n * @example\n * parseQuery('?page=2&tag=a&tag=b') // { page: '2', tag: ['a', 'b'] }\n */\nexport function parseQuery(search: string): Record<string, string | string[]> {\n // Null-prototype accumulator: a normal `{}` inherits Object.prototype, so a key\n // like `constructor`/`toString`/`__proto__` would resolve to a builtin on the\n // `out[key]` existence probe (leaking a function into a bogus array) or hit the\n // `__proto__` setter (mutating the result's prototype). With no prototype, every\n // key — including those — is an ordinary absent-then-own data property.\n const out: Record<string, string | string[]> = Object.create(null)\n const query = search.startsWith('?') ? search.slice(1) : search\n if (query.length === 0) return out\n\n for (const pair of query.split('&')) {\n if (pair.length === 0) continue\n const eq = pair.indexOf('=')\n const rawKey = eq === -1 ? pair : pair.slice(0, eq)\n const rawValue = eq === -1 ? '' : pair.slice(eq + 1)\n const key = safeDecode(rawKey)\n const value = safeDecode(rawValue)\n\n const existing = out[key]\n if (existing === undefined) {\n out[key] = value\n } else if (Array.isArray(existing)) {\n existing.push(value)\n } else {\n out[key] = [existing, value]\n }\n }\n return out\n}\n\n/**\n * Serialize a record into a query string (no leading `?`). `null`/`undefined`\n * values are skipped; arrays emit one `key=value` pair each. Keys are sorted for\n * deterministic, cache-friendly output.\n *\n * @example\n * stringifyQuery({ page: 2, tag: ['a', 'b'] }) // 'page=2&tag=a&tag=b'\n */\nexport function stringifyQuery(query: Record<string, QueryValue>): string {\n const parts: string[] = []\n for (const key of Object.keys(query).sort()) {\n const value = query[key]\n if (value === null || value === undefined) continue\n const encKey = encodeURIComponent(key)\n if (Array.isArray(value)) {\n for (const item of value) parts.push(`${encKey}=${encodeURIComponent(String(item))}`)\n } else {\n parts.push(`${encKey}=${encodeURIComponent(String(value))}`)\n }\n }\n return parts.join('&')\n}\n\n/** The result of a non-throwing validation. */\nexport type ValidationResult<T> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\n/**\n * Validate `input` against a Standard Schema, **without throwing** on invalid\n * input — returns a discriminated result. Throws {@link RouterError}\n * (`ASYNC_SCHEMA`) only for the programming error of passing an async schema,\n * since navigation-time parsing must be synchronous.\n */\nexport function safeValidateSearch<S extends StandardSchemaV1>(\n schema: S,\n input: unknown,\n): ValidationResult<StandardSchemaV1.InferOutput<S>> {\n const result = schema['~standard'].validate(input)\n if (result instanceof Promise) {\n throw new RouterError(\n 'ASYNC_SCHEMA',\n 'Asynchronous schemas are not supported for synchronous search-param validation.',\n )\n }\n // Discriminate on `issues` (truthiness): a schema may legitimately yield a\n // success value of `undefined`, so `value` is not a reliable discriminant.\n if (result.issues) {\n return { ok: false, issues: result.issues }\n }\n return { ok: true, value: result.value }\n}\n\n/**\n * Validate `input` against a Standard Schema, returning the typed output or\n * throwing {@link RouterError} (`VALIDATE_SEARCH` with the issues, or\n * `ASYNC_SCHEMA`).\n *\n * @example\n * const schema = z.object({ page: z.coerce.number() }) // Zod, Valibot, ArkType…\n * validateSearch(schema, { page: '2' }) // { page: 2 }\n */\nexport function validateSearch<S extends StandardSchemaV1>(\n schema: S,\n input: unknown,\n): StandardSchemaV1.InferOutput<S> {\n const result = safeValidateSearch(schema, input)\n if (!result.ok) {\n throw new RouterError('VALIDATE_SEARCH', formatIssues(result.issues), result.issues)\n }\n return result.value\n}\n\n/** Render Standard Schema issues into a single readable message. */\nfunction formatIssues(issues: ReadonlyArray<StandardSchemaV1.Issue>): string {\n const lines = issues.map((issue) => {\n const path = issue.path?.map((seg) => (typeof seg === 'object' ? seg.key : seg)).join('.')\n return path ? `${path}: ${issue.message}` : issue.message\n })\n return `Search validation failed: ${lines.join('; ')}`\n}\n\n/** Decode a URI component, falling back to the raw value on malformed input. */\nfunction safeDecode(value: string): string {\n try {\n return decodeURIComponent(value.replace(/\\+/g, ' '))\n } catch {\n return value\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,WAAW,QAAmD;CAM5E,MAAM,MAAyC,OAAO,OAAO,IAAI;CACjE,MAAM,QAAQ,OAAO,WAAW,GAAG,IAAI,OAAO,MAAM,CAAC,IAAI;CACzD,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,KAAK,MAAM,QAAQ,MAAM,MAAM,GAAG,GAAG;EACnC,IAAI,KAAK,WAAW,GAAG;EACvB,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC3B,MAAM,SAAS,OAAO,KAAK,OAAO,KAAK,MAAM,GAAG,EAAE;EAClD,MAAM,WAAW,OAAO,KAAK,KAAK,KAAK,MAAM,KAAK,CAAC;EACnD,MAAM,MAAM,WAAW,MAAM;EAC7B,MAAM,QAAQ,WAAW,QAAQ;EAEjC,MAAM,WAAW,IAAI;EACrB,IAAI,aAAa,KAAA,GACf,IAAI,OAAO;OACN,IAAI,MAAM,QAAQ,QAAQ,GAC/B,SAAS,KAAK,KAAK;OAEnB,IAAI,OAAO,CAAC,UAAU,KAAK;CAE/B;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,eAAe,OAA2C;CACxE,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE,KAAK,GAAG;EAC3C,MAAM,QAAQ,MAAM;EACpB,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW;EAC3C,MAAM,SAAS,mBAAmB,GAAG;EACrC,IAAI,MAAM,QAAQ,KAAK,GACrB,KAAK,MAAM,QAAQ,OAAO,MAAM,KAAK,GAAG,OAAO,GAAG,mBAAmB,OAAO,IAAI,CAAC,GAAG;OAEpF,MAAM,KAAK,GAAG,OAAO,GAAG,mBAAmB,OAAO,KAAK,CAAC,GAAG;CAE/D;CACA,OAAO,MAAM,KAAK,GAAG;AACvB;;;;;;;AAaA,SAAgB,mBACd,QACA,OACmD;CACnD,MAAM,SAAS,OAAO,aAAa,SAAS,KAAK;CACjD,IAAI,kBAAkB,SACpB,MAAM,IAAI,YACR,gBACA,iFACF;CAIF,IAAI,OAAO,QACT,OAAO;EAAE,IAAI;EAAO,QAAQ,OAAO;CAAO;CAE5C,OAAO;EAAE,IAAI;EAAM,OAAO,OAAO;CAAM;AACzC;;;;;;;;;;AAWA,SAAgB,eACd,QACA,OACiC;CACjC,MAAM,SAAS,mBAAmB,QAAQ,KAAK;CAC/C,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,YAAY,mBAAmB,aAAa,OAAO,MAAM,GAAG,OAAO,MAAM;CAErF,OAAO,OAAO;AAChB;;AAGA,SAAS,aAAa,QAAuD;CAK3E,OAAO,6BAJO,OAAO,KAAK,UAAU;EAClC,MAAM,OAAO,MAAM,MAAM,KAAK,QAAS,OAAO,QAAQ,WAAW,IAAI,MAAM,GAAI,EAAE,KAAK,GAAG;EACzF,OAAO,OAAO,GAAG,KAAK,IAAI,MAAM,YAAY,MAAM;CACpD,CACwC,EAAE,KAAK,IAAI;AACrD;;AAGA,SAAS,WAAW,OAAuB;CACzC,IAAI;EACF,OAAO,mBAAmB,MAAM,QAAQ,OAAO,GAAG,CAAC;CACrD,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,90 @@
1
+ //#region src/standard-schema.d.ts
2
+ /**
3
+ * Standard Schema — the validator-agnostic interface (types only, vendored).
4
+ *
5
+ * Quantum validates search/route params through {@link StandardSchemaV1}, the
6
+ * common interface co-designed by the authors of Zod, Valibot, and ArkType. Any
7
+ * compliant validator (Zod ≥ 3.24 / all of 4.x, Valibot ≥ 1, ArkType ≥ 2, and
8
+ * 20+ others) exposes a `~standard` property and is accepted **directly** — no
9
+ * per-library adapters, no lock-in.
10
+ *
11
+ * These ~60 lines are **vendored on purpose** (the spec FAQ explicitly blesses
12
+ * copy/paste; the project guarantees no breaking change without a major version
13
+ * bump). Vendoring means `@mindees/router` takes **zero runtime dependency** to
14
+ * support every validator — the "batteries included, dependencies excluded"
15
+ * doctrine.
16
+ *
17
+ * This is the spec's **v1 validation interface** (`StandardSchemaV1`). `validate`
18
+ * is typed single-argument — the subset Quantum needs; a spec 1.1.0 validator's
19
+ * optional second `options` argument remains assignable (fewer-params rule), so
20
+ * Zod 4 / Valibot 1 / ArkType 2 schemas are accepted directly.
21
+ *
22
+ * Portions adapted from `@standard-schema/spec`, MIT License,
23
+ * Copyright (c) 2024 Colin McDonnell. Permission is hereby granted, free of
24
+ * charge, to any person obtaining a copy of this software and associated
25
+ * documentation files, to deal in the Software without restriction. The above
26
+ * copyright notice and this permission notice shall be included in all copies or
27
+ * substantial portions of the Software.
28
+ *
29
+ * @see https://standardschema.dev
30
+ * @see https://github.com/standard-schema/standard-schema
31
+ * @module
32
+ */
33
+ /** A schema that conforms to the Standard Schema specification. */
34
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
35
+ /** The Standard Schema properties. */
36
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
37
+ }
38
+ declare namespace StandardSchemaV1 {
39
+ /** The Standard Schema properties interface. */
40
+ interface Props<Input = unknown, Output = Input> {
41
+ /** The version number of the standard. */
42
+ readonly version: 1;
43
+ /** The vendor name of the schema library. */
44
+ readonly vendor: string;
45
+ /** Validates unknown input values. May be synchronous or asynchronous. */
46
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
47
+ /** Inferred types associated with the schema (present only at the type level). */
48
+ readonly types?: Types<Input, Output> | undefined;
49
+ }
50
+ /** The result interface of the validate function. */
51
+ type Result<Output> = SuccessResult<Output> | FailureResult;
52
+ /** The result interface if validation succeeds. */
53
+ interface SuccessResult<Output> {
54
+ /** The typed output value. */
55
+ readonly value: Output;
56
+ /** The non-existent issues. */
57
+ readonly issues?: undefined;
58
+ }
59
+ /** The result interface if validation fails. */
60
+ interface FailureResult {
61
+ /** The issues of failed validation. */
62
+ readonly issues: ReadonlyArray<Issue>;
63
+ }
64
+ /** The issue interface of the failure output. */
65
+ interface Issue {
66
+ /** The error message of the issue. */
67
+ readonly message: string;
68
+ /** The path of the issue, if any. */
69
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
70
+ }
71
+ /** The path segment interface of the issue. */
72
+ interface PathSegment {
73
+ /** The key representing a path segment. */
74
+ readonly key: PropertyKey;
75
+ }
76
+ /** The Standard Schema types interface. */
77
+ interface Types<Input = unknown, Output = Input> {
78
+ /** The input type of the schema. */
79
+ readonly input: Input;
80
+ /** The output type of the schema. */
81
+ readonly output: Output;
82
+ }
83
+ /** Infers the input type of a Standard Schema. */
84
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
85
+ /** Infers the output type of a Standard Schema. */
86
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
87
+ }
88
+ //#endregion
89
+ export { StandardSchemaV1 };
90
+ //# sourceMappingURL=standard-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-schema.d.ts","names":[],"sources":["../src/standard-schema.ts"],"mappings":";;AAiCA;;;;;;;;;;;;;;;;;;AAE4D;AAG5D;;;;;;;;;;;;UALiB,gBAAA,2BAA2C,KAAA;EAmB7B;EAAA,SAjBpB,WAAA,EAAa,gBAAA,CAAiB,KAAA,CAAM,KAAA,EAAO,MAAA;AAAA;AAAA,kBAGrC,gBAAA;EA2BI;EAAA,UAzBF,KAAA,2BAAgC,KAAA;IAiCH;IAAA,SA/BnC,OAAA;IAqCK;IAAA,SAnCL,MAAA;IAyCO;IAAA,SAvCP,QAAA,GAAW,KAAA,cAAmB,MAAA,CAAO,MAAA,IAAU,OAAA,CAAQ,MAAA,CAAO,MAAA;IA6CnC;IAAA,SA3C3B,KAAA,GAAQ,KAAA,CAAM,KAAA,EAAO,MAAA;EAAA;EAgDO;EAAA,KA5C3B,MAAA,WAAiB,aAAA,CAAc,MAAA,IAAU,aAAA;EA4CM;EAAA,UAzC1C,aAAA;IAyCqD;IAAA,SAvC3D,KAAA,EAAO,MAAA;IAjBK;IAAA,SAmBZ,MAAA;EAAA;EAjBA;EAAA,UAqBM,aAAA;IAjBN;IAAA,SAmBA,MAAA,EAAQ,aAAA,CAAc,KAAA;EAAA;EAnBe;EAAA,UAuB/B,KAAA;IAvBiD;IAAA,SAyBvD,OAAA;IAvBA;IAAA,SAyBA,IAAA,GAAO,aAAA,CAAc,WAAA,GAAc,WAAA;EAAA;EAzBd;EAAA,UA6Bf,WAAA;IAzBE;IAAA,SA2BR,GAAA,EAAK,WAAA;EAAA;EA3BqC;EAAA,UA+BpC,KAAA,2BAAgC,KAAA;IA5BlB;IAAA,SA8BpB,KAAA,EAAO,KAAA;IA5BA;IAAA,SA8BP,MAAA,EAAQ,MAAA;EAAA;EAtBR;EAAA,KA0BC,UAAA,gBAA0B,gBAAA,IAAoB,WAAA,CACxD,MAAA;EA3B+B;EAAA,KA+BrB,WAAA,gBAA2B,gBAAA,IAAoB,WAAA,CACzD,MAAA;AAAA"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mindees/router",
3
+ "version": "0.1.0",
4
+ "description": "Quantum — the typed, signals-native router for MindeesNative: codegen-free typed path params, Standard Schema validated search params, and selector-isolated reactive route state.",
5
+ "keywords": [
6
+ "router",
7
+ "typed-router",
8
+ "type-safe-routing",
9
+ "typescript",
10
+ "signals",
11
+ "standard-schema",
12
+ "search-params",
13
+ "cross-platform",
14
+ "react-native-alternative",
15
+ "expo-router-alternative"
16
+ ],
17
+ "license": "MIT OR Apache-2.0",
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/mindees/mindees.git",
35
+ "directory": "packages/router"
36
+ },
37
+ "dependencies": {
38
+ "@mindees/core": "0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "happy-dom": "20.9.0",
42
+ "valibot": "1.4.1",
43
+ "zod": "4.4.3",
44
+ "@mindees/renderer": "0.1.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "typecheck": "tsc --noEmit"
49
+ }
50
+ }