route-sprout 1.0.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/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/api.cjs +2 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +96 -0
- package/dist/api.d.ts +96 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Piotr Siatkowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# route-sprout 🌱 (typed API route builder DSL)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/route-sprout)
|
|
6
|
+
[](https://bundlephobia.com/package/route-sprout)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://github.com/PiotrSiatkowski/route-sprout)
|
|
10
|
+
|
|
11
|
+
A tiny, cute DSL that grows **type-safe, composable URL builders** from a declarative route tree.
|
|
12
|
+
|
|
13
|
+
- ✅ Strong TypeScript inference from your route definition
|
|
14
|
+
- ✅ Nested resources with `slot('id')` parameters
|
|
15
|
+
- ✅ Optional path gates with `wrap('admin', predicate, …)`
|
|
16
|
+
- ✅ Ad‑hoc conditional segments anywhere with `.when(cond, segments) and join(segments)`
|
|
17
|
+
- ✅ Search params supported (`string` or `URLSearchParams`)
|
|
18
|
+
|
|
19
|
+
> Think of it as a little route bonsai: you shape the tree once, then pluck URLs from any branch.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i route-sprout
|
|
27
|
+
# or
|
|
28
|
+
pnpm add route-sprout
|
|
29
|
+
# or
|
|
30
|
+
yarn add route-sprout
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { root, path, slot, keep } from "route-sprout";
|
|
37
|
+
|
|
38
|
+
const Api = root([
|
|
39
|
+
path("invoices", [
|
|
40
|
+
keep(),
|
|
41
|
+
slot("id", [
|
|
42
|
+
path("price"),
|
|
43
|
+
path("customers")
|
|
44
|
+
]),
|
|
45
|
+
path("statistics"),
|
|
46
|
+
]),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
Api.invoices(); // "/invoices"
|
|
50
|
+
Api.invoices("page=1"); // "/invoices?page=1"
|
|
51
|
+
Api.invoices.id("abc")("a=1"); // "/invoices/abc?a=1"
|
|
52
|
+
Api.invoices.id("abc").customers(); // "/invoices/abc/customers"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why this exists
|
|
58
|
+
|
|
59
|
+
When you have lots of endpoints, you usually end up with:
|
|
60
|
+
|
|
61
|
+
- string concatenation sprinkled everywhere
|
|
62
|
+
- duplicated base paths
|
|
63
|
+
- typos that compile fine and fail at runtime
|
|
64
|
+
- route refactors that turn into treasure hunts
|
|
65
|
+
|
|
66
|
+
This DSL gives you:
|
|
67
|
+
|
|
68
|
+
- a single, declarative source of truth
|
|
69
|
+
- fluent, discoverable usage (`Api.invoices.id("x").customers()`)
|
|
70
|
+
- TypeScript autocomplete and type checking from **usage**, not comments
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Concepts
|
|
75
|
+
|
|
76
|
+
### `path(name, children?)`
|
|
77
|
+
|
|
78
|
+
A **static** path segment.
|
|
79
|
+
|
|
80
|
+
- `path("invoices")` → `/invoices`
|
|
81
|
+
- Nested paths compose: `path("a", [path("b")])` → `/a/b`
|
|
82
|
+
|
|
83
|
+
Leaf paths (no children) are callable and return a URL:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
path("health"); // Api.health() -> "/health"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `keep()`
|
|
90
|
+
|
|
91
|
+
Marks a path (or slot/wrap subtree) as **callable** at that position.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
path("orders", [keep(), path("export")]);
|
|
95
|
+
// Api.orders() -> "/orders"
|
|
96
|
+
// Api.orders.export() -> "/orders/export"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `slot(name, children?)`
|
|
100
|
+
|
|
101
|
+
A **parameterized** segment, typically used for IDs.
|
|
102
|
+
|
|
103
|
+
The `name` is only the **property key**. It is **not** inserted into the path.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
path("invoices", [slot("id")]);
|
|
107
|
+
// Api.invoices.id("abc")() -> "/invoices/abc"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
With children:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
path("invoices", [
|
|
114
|
+
slot("id", [
|
|
115
|
+
path("price"),
|
|
116
|
+
path("customers")
|
|
117
|
+
]),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// Api.invoices.id("abc").customers() -> "/invoices/abc/customers"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `wrap(name, predicate, children?)`
|
|
124
|
+
|
|
125
|
+
A conditional segment *defined in the tree*.
|
|
126
|
+
|
|
127
|
+
If `predicate(arg)` is `true`, `name` becomes a real path segment.
|
|
128
|
+
If `false`, it’s a pass-through (does not change the path).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
type User = { isAdmin: boolean } | null;
|
|
132
|
+
|
|
133
|
+
path("core", [
|
|
134
|
+
wrap("admin", (u: User) => !!u?.isAdmin, [
|
|
135
|
+
path("invoices", [keep()]),
|
|
136
|
+
]),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
Api.core.admin({ isAdmin: true }).invoices(); // "/core/admin/invoices"
|
|
140
|
+
Api.core.admin({ isAdmin: false }).invoices(); // "/core/invoices"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> `wrap` is ideal for *well-known*, reusable gates: `admin`, `v2`, `tenant`, etc.
|
|
144
|
+
|
|
145
|
+
### `.when(cond, segment | segment[])`
|
|
146
|
+
|
|
147
|
+
Ad‑hoc conditional segment insertion at **runtime**, anywhere in the chain.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
Api.core.when(isAdmin, "admin").invoices();
|
|
151
|
+
Api.core.when(true, ["tenant", tenantId]).invoices();
|
|
152
|
+
Api.invoices.id("abc").when(flags.preview, "preview").activities();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `cond = false` → no-op
|
|
156
|
+
- `segment` can be a single segment or an array of segments
|
|
157
|
+
- empty segments are ignored (your `url()` filters them out)
|
|
158
|
+
|
|
159
|
+
> `.when()` is ideal when you don’t want to bake a wrapper into the route tree.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Search params
|
|
164
|
+
|
|
165
|
+
All callable endpoints accept an optional `search` parameter:
|
|
166
|
+
|
|
167
|
+
- `string` (already encoded)
|
|
168
|
+
- `URLSearchParams` (will be coerced to string via template interpolation)
|
|
169
|
+
- `object` (will be coerced into URLSearchParams)
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
Api.invoices("page=2&size=25");
|
|
173
|
+
|
|
174
|
+
const sp = new URLSearchParams({ page: "2", size: "25" });
|
|
175
|
+
Api.invoices(sp);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Full example
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { root, path, slot, keep, wrap } from "route-sprout";
|
|
184
|
+
|
|
185
|
+
type PortalUser = { isAdmin?: boolean } | null;
|
|
186
|
+
|
|
187
|
+
export const Api = root([
|
|
188
|
+
path("core", [
|
|
189
|
+
wrap("admin", (u: PortalUser) => !!u?.isAdmin, [
|
|
190
|
+
path("invoices", [
|
|
191
|
+
keep()
|
|
192
|
+
]),
|
|
193
|
+
path("customers", [
|
|
194
|
+
slot("id"),
|
|
195
|
+
keep()
|
|
196
|
+
]),
|
|
197
|
+
]),
|
|
198
|
+
]),
|
|
199
|
+
path("invoices", [
|
|
200
|
+
keep(),
|
|
201
|
+
slot("id", [
|
|
202
|
+
path("price"),
|
|
203
|
+
path("customers")
|
|
204
|
+
]),
|
|
205
|
+
path("statistics"),
|
|
206
|
+
]),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
// usage
|
|
210
|
+
Api.invoices(); // "/invoices"
|
|
211
|
+
Api.invoices.id("123").customers(); // "/invoices/123/customers"
|
|
212
|
+
|
|
213
|
+
// runtime insert
|
|
214
|
+
Api.core.when(true, "v2").invoices(); // "/core/v2/invoices"
|
|
215
|
+
Api.core.admin({ isAdmin: true }).when(true, "v2").invoices(); // "/core/admin/v2/invoices"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Autocomplete-friendly patterns
|
|
219
|
+
Because everything is computed from the definition tree, your editor can autocomplete:
|
|
220
|
+
|
|
221
|
+
- paths (`Api.invoices`, `Api.orders.export`)
|
|
222
|
+
- slots (`Api.invoices.id(…)`)
|
|
223
|
+
- nested children (`…id("x").customers()`)
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## API reference
|
|
228
|
+
|
|
229
|
+
### Exports
|
|
230
|
+
|
|
231
|
+
- `root(defs)`
|
|
232
|
+
- `path(name, rest?)`
|
|
233
|
+
- `slot(name, rest?)`
|
|
234
|
+
- `wrap(name, when, rest?)`
|
|
235
|
+
- `keep()`
|
|
236
|
+
|
|
237
|
+
### Types
|
|
238
|
+
|
|
239
|
+
- `Segment = string | number`
|
|
240
|
+
- `SParams = string | URLSearchParams`
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Dialects
|
|
245
|
+
|
|
246
|
+
If you like your DSLs with different “flavors”, route-sprout ships **dialects** as subpath exports.
|
|
247
|
+
Each dialect is the same engine, just different helper names.
|
|
248
|
+
|
|
249
|
+
Import a dialect like this:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { grow, tree, seed, leaf, nest } from "route-sprout/dialect-tree";
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Available dialects
|
|
256
|
+
|
|
257
|
+
#### `route-sprout/dialect-path` (default)
|
|
258
|
+
- **root / path / slot / keep / wrap**
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { root, path, slot, keep, wrap } from "route-sprout/dialect-path";
|
|
262
|
+
|
|
263
|
+
const Api = root([
|
|
264
|
+
path("invoices", [keep(), slot("id")]),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
Api.invoices.id("123")(); // "/invoices/123"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### `route-sprout/dialect-step`
|
|
271
|
+
- **make / step / item / self / gate**
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { make, step, item, self, gate } from "route-sprout/dialect-step";
|
|
275
|
+
|
|
276
|
+
const Api = make([
|
|
277
|
+
step("orders", [self(), item("id"), step("export")]),
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
Api.orders.export(); // "/orders/export"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### `route-sprout/dialect-tree`
|
|
284
|
+
- **grow / tree / seed / twig / nest**
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { grow, tree, seed, twig, nest } from "route-sprout/dialect-tree";
|
|
288
|
+
|
|
289
|
+
const Api = grow([
|
|
290
|
+
tree("core", [
|
|
291
|
+
nest("admin", (u: { isAdmin?: boolean } | null) => !!u?.isAdmin, [
|
|
292
|
+
tree("jobs", [twig()]),
|
|
293
|
+
]),
|
|
294
|
+
]),
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
Api.core.admin({ isAdmin: true }).jobs(); // "/core/admin/jobs"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### `route-sprout/dialect-node`
|
|
301
|
+
- **link / node / bind / base / mask**
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { link, node, bind, base, mask } from "route-sprout/dialect-graph";
|
|
305
|
+
|
|
306
|
+
const Api = link([
|
|
307
|
+
node("tasks", [base(), bind("id", [node("logs")])]),
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
Api.tasks.id("x").logs(); // "/tasks/x/logs"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Mix-and-match?
|
|
314
|
+
|
|
315
|
+
Dialects are meant to be **all-in** per codebase/file. Technically you can mix imports, but future-you will sigh loudly.
|
|
316
|
+
|
|
317
|
+
## Gotchas & design notes
|
|
318
|
+
|
|
319
|
+
- `slot("id")` uses `"id"` **only as a property name**, not a URL segment.
|
|
320
|
+
- ✅ `/invoices/123`
|
|
321
|
+
- ❌ `/invoices/id/123`
|
|
322
|
+
- `.when()` rebuilds a subtree and returns a new object/function.
|
|
323
|
+
- It does **not** mutate the original branch.
|
|
324
|
+
- Empty segments are ignored in the final URL (because `url()` does `filter()`).
|
|
325
|
+
- If you want stricter behavior (throw on empty segment), enforce it in your own `.when` wrapper.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Testing
|
|
330
|
+
|
|
331
|
+
This library is friendly to unit tests because the output is just strings.
|
|
332
|
+
|
|
333
|
+
Example (Vitest):
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { expect, test } from "vitest";
|
|
337
|
+
import { root, path, slot, keep } from "route-sprout";
|
|
338
|
+
|
|
339
|
+
test("builds routes", () => {
|
|
340
|
+
const Api = root([path("invoices", [keep(), slot("id")])] as const);
|
|
341
|
+
expect(Api.invoices()).toBe("/invoices");
|
|
342
|
+
expect(Api.invoices.id("x")()).toBe("/invoices/x");
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## License
|
|
349
|
+
|
|
350
|
+
MIT
|
package/dist/api.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var S=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var g=(n,e)=>{for(var o in e)S(n,o,{get:e[o],enumerable:!0})},k=(n,e,o,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of h(e))!y.call(n,a)&&a!==o&&S(n,a,{get:()=>e[a],enumerable:!(s=u(e,a))||s.enumerable});return n};var D=n=>k(S({},"__esModule",{value:!0}),n);var A={};g(A,{keep:()=>P,path:()=>d,root:()=>x,slot:()=>R,wrap:()=>w});module.exports=D(A);var P=()=>({kind:"keep"}),d=(n,e)=>({kind:"path",name:n,rest:e??[]}),R=(n,e)=>({kind:"slot",name:n,rest:e??[]}),w=(n,e,o)=>({kind:"wrap",name:n,when:e,rest:o??[]}),l=(n,e)=>`${n.filter(Boolean).join("/")}${e?`?${e}`:""}`.replace("//","/");function x(n){return m([],d("/",n))}function m(n,e){let o=t=>t.rest.some(r=>r.kind==="keep"),s=e.kind==="slot"||e.kind==="wrap"?n:e.name?[...n,e.name]:n,a=o(e)?t=>l(s,t):{};for(let t of e.rest)t.kind==="slot"?t.rest.length===0?a[t.name]=r=>i=>l([...s,r],i):a[t.name]=r=>{let i=m([...s,r],t);return Object.assign(o(t)?c=>l([...s,r],c):{},i)}:t.kind==="path"?t.rest.length===0?a[t.name]=r=>l([...s,t.name],r):a[t.name]=m(s,t):t.kind==="wrap"&&(a[t.name]=r=>{let c=t.when(r)?[...s,t.name]:s,p=m(c,t);return Object.assign(o(t)?f=>l(c,f):{},p)});return N(a,s,e.rest)}function N(n,e,o){return n.when=(s,a)=>m(s?[...e,...Array.isArray(a)?a:[a]]:e,d("",o)),n.join=s=>m([...e,...Array.isArray(s)?s:[s]],d("",o)),n}0&&(module.exports={keep,path,root,slot,wrap});
|
|
2
|
+
//# sourceMappingURL=api.cjs.map
|
package/dist/api.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api.ts"],"sourcesContent":["import { Keep, Path, Wrap, Slot, PathDef, SlotDef, Segment, SParams, RoutesFromDefs } from './dsl'\n\n// ---------- DSL helpers (typed) ----------\nexport const keep = (): Keep => ({ kind: 'keep' })\n\nexport const path = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n>(\n\tname: Name,\n\trest?: Rest\n): Path<Name, Rest> => ({ kind: 'path', name, rest: (rest ?? []) as Rest })\n\nexport const slot = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n>(\n\tname: Name,\n\trest?: Rest\n): Slot<Name, Rest> => ({ kind: 'slot', name, rest: (rest ?? []) as Rest })\n\nexport const wrap = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n\tArgs = unknown,\n>(\n\tname: Name,\n\twhen: (args: Args) => boolean,\n\trest?: Rest\n): Wrap<Name, Rest, Args> => ({ kind: 'wrap', name, when, rest: (rest ?? []) as Rest })\n\n// ---------- Runtime implementation ----------\nconst url = (path: Segment[], search?: SParams) =>\n\t`${path.filter(Boolean).join('/')}${search ? `?${search}` : ''}`.replace('//', '/')\n\n// ---------- Typed root signature ----------\nexport function root<const Defs extends readonly PathDef[]>(\n\tdefs: Defs\n): RoutesFromDefs<Defs> {\n\treturn buildPath([], path('/', defs)) as unknown as RoutesFromDefs<Defs>\n}\n\nfunction buildPath(prefix: Segment[], def: SlotDef) {\n\tconst hasKeep = (pathDef: SlotDef) => pathDef.rest.some((c) => c.kind === 'keep')\n\tconst allPath =\n\t\tdef.kind === 'slot' || def.kind === 'wrap'\n\t\t\t? prefix\n\t\t\t: def.name\n\t\t\t\t? [...prefix, def.name]\n\t\t\t\t: prefix\n\n\t// If there is a keep(), the path itself is callable and acts as \"keep\"\n\tconst target: any = hasKeep(def) ? (search?: SParams) => url(allPath, search) : {}\n\n\tfor (const child of def.rest) {\n\t\tif (child.kind === 'slot') {\n\t\t\tif (child.rest.length === 0) {\n\t\t\t\ttarget[child.name] = (param: Segment) => (search?: SParams) =>\n\t\t\t\t\turl([...allPath, param], search)\n\t\t\t} else {\n\t\t\t\ttarget[child.name] = (param: Segment) => {\n\t\t\t\t\t// Build subtree for nested parts under :id\n\t\t\t\t\t// Synthetic path with empty name so we don't add extra segment.\n\t\t\t\t\tconst subTree = buildPath([...allPath, param], child)\n\n\t\t\t\t\t// Attach children (info, activities, etc.) to that function\n\t\t\t\t\treturn Object.assign(\n\t\t\t\t\t\thasKeep(child)\n\t\t\t\t\t\t\t? (search?: SParams) => url([...allPath, param], search)\n\t\t\t\t\t\t\t: {},\n\t\t\t\t\t\tsubTree\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (child.kind === 'path') {\n\t\t\tif (child.rest.length === 0) {\n\t\t\t\ttarget[child.name] = (search?: SParams) => url([...allPath, child.name], search)\n\t\t\t} else {\n\t\t\t\ttarget[child.name] = buildPath(allPath, child)\n\t\t\t}\n\t\t} else if (child.kind === 'wrap') {\n\t\t\ttarget[child.name] = (arg: unknown) => {\n\t\t\t\tconst enabled = child.when(arg)\n\t\t\t\tconst wrapped = enabled ? [...allPath, child.name] : allPath\n\t\t\t\tconst subTree = buildPath(wrapped, child as any)\n\n\t\t\t\treturn Object.assign(\n\t\t\t\t\t// if wrap has keep(), it becomes callable at that point\n\t\t\t\t\thasKeep(child as any) ? (search?: SParams) => url(wrapped, search) : {},\n\t\t\t\t\tsubTree\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn attachWhenAndJoin(target, allPath, def.rest)\n}\n\nfunction attachWhenAndJoin(target: any, basePath: Segment[], rest: readonly PathDef[]) {\n\ttarget.when = (cond: boolean, seg: Segment | readonly Segment[]) => {\n\t\t// Rebuild \"same subtree\" at a new prefix:\n\t\t// Use a synthetic path '' so we don't append an extra segment name.\n\t\treturn buildPath(\n\t\t\tcond ? [...basePath, ...(Array.isArray(seg) ? seg : [seg])] : basePath,\n\t\t\tpath('', rest)\n\t\t)\n\t}\n\ttarget.join = (seg: Segment | readonly Segment[]) => {\n\t\treturn buildPath([...basePath, ...(Array.isArray(seg) ? seg : [seg])], path('', rest))\n\t}\n\n\treturn target\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,UAAAE,EAAA,SAAAC,EAAA,SAAAC,EAAA,SAAAC,EAAA,SAAAC,IAAA,eAAAC,EAAAP,GAGO,IAAME,EAAO,KAAa,CAAE,KAAM,MAAO,GAEnCC,EAAO,CAInBK,EACAC,KACuB,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAOC,GAAQ,CAAC,CAAW,GAE5DJ,EAAO,CAInBG,EACAC,KACuB,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAOC,GAAQ,CAAC,CAAW,GAE5DH,EAAO,CAKnBE,EACAE,EACAD,KAC6B,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAAE,EAAM,KAAOD,GAAQ,CAAC,CAAW,GAG/EE,EAAM,CAACR,EAAiBS,IAC7B,GAAGT,EAAK,OAAO,OAAO,EAAE,KAAK,GAAG,CAAC,GAAGS,EAAS,IAAIA,CAAM,GAAK,EAAE,GAAG,QAAQ,KAAM,GAAG,EAG5E,SAASR,EACfS,EACuB,CACvB,OAAOC,EAAU,CAAC,EAAGX,EAAK,IAAKU,CAAI,CAAC,CACrC,CAEA,SAASC,EAAUC,EAAmBC,EAAc,CACnD,IAAMC,EAAWC,GAAqBA,EAAQ,KAAK,KAAMC,GAAMA,EAAE,OAAS,MAAM,EAC1EC,EACLJ,EAAI,OAAS,QAAUA,EAAI,OAAS,OACjCD,EACAC,EAAI,KACH,CAAC,GAAGD,EAAQC,EAAI,IAAI,EACpBD,EAGCM,EAAcJ,EAAQD,CAAG,EAAKJ,GAAqBD,EAAIS,EAASR,CAAM,EAAI,CAAC,EAEjF,QAAWU,KAASN,EAAI,KACnBM,EAAM,OAAS,OACdA,EAAM,KAAK,SAAW,EACzBD,EAAOC,EAAM,IAAI,EAAKC,GAAoBX,GACzCD,EAAI,CAAC,GAAGS,EAASG,CAAK,EAAGX,CAAM,EAEhCS,EAAOC,EAAM,IAAI,EAAKC,GAAmB,CAGxC,IAAMC,EAAUV,EAAU,CAAC,GAAGM,EAASG,CAAK,EAAGD,CAAK,EAGpD,OAAO,OAAO,OACbL,EAAQK,CAAK,EACTV,GAAqBD,EAAI,CAAC,GAAGS,EAASG,CAAK,EAAGX,CAAM,EACrD,CAAC,EACJY,CACD,CACD,EAESF,EAAM,OAAS,OACrBA,EAAM,KAAK,SAAW,EACzBD,EAAOC,EAAM,IAAI,EAAKV,GAAqBD,EAAI,CAAC,GAAGS,EAASE,EAAM,IAAI,EAAGV,CAAM,EAE/ES,EAAOC,EAAM,IAAI,EAAIR,EAAUM,EAASE,CAAK,EAEpCA,EAAM,OAAS,SACzBD,EAAOC,EAAM,IAAI,EAAKG,GAAiB,CAEtC,IAAMC,EADUJ,EAAM,KAAKG,CAAG,EACJ,CAAC,GAAGL,EAASE,EAAM,IAAI,EAAIF,EAC/CI,EAAUV,EAAUY,EAASJ,CAAY,EAE/C,OAAO,OAAO,OAEbL,EAAQK,CAAY,EAAKV,GAAqBD,EAAIe,EAASd,CAAM,EAAI,CAAC,EACtEY,CACD,CACD,GAIF,OAAOG,EAAkBN,EAAQD,EAASJ,EAAI,IAAI,CACnD,CAEA,SAASW,EAAkBN,EAAaO,EAAqBnB,EAA0B,CACtF,OAAAY,EAAO,KAAO,CAACQ,EAAeC,IAGtBhB,EACNe,EAAO,CAAC,GAAGD,EAAU,GAAI,MAAM,QAAQE,CAAG,EAAIA,EAAM,CAACA,CAAG,CAAE,EAAIF,EAC9DzB,EAAK,GAAIM,CAAI,CACd,EAEDY,EAAO,KAAQS,GACPhB,EAAU,CAAC,GAAGc,EAAU,GAAI,MAAM,QAAQE,CAAG,EAAIA,EAAM,CAACA,CAAG,CAAE,EAAG3B,EAAK,GAAIM,CAAI,CAAC,EAG/EY,CACR","names":["api_exports","__export","keep","path","root","slot","wrap","__toCommonJS","name","rest","when","url","search","defs","buildPath","prefix","def","hasKeep","pathDef","c","allPath","target","child","param","subTree","arg","wrapped","attachWhenAndJoin","basePath","cond","seg"]}
|
package/dist/api.d.cts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ---------- Shared public types ----------
|
|
2
|
+
type Segment = string | number
|
|
3
|
+
type SParams = string | URLSearchParams
|
|
4
|
+
|
|
5
|
+
// ---------- DSL definition types ----------
|
|
6
|
+
type Keep = { kind: 'keep' }
|
|
7
|
+
|
|
8
|
+
type Path<
|
|
9
|
+
Name extends string = string,
|
|
10
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
11
|
+
> = { kind: 'path'; name: Name; rest: Rest }
|
|
12
|
+
|
|
13
|
+
type Slot<
|
|
14
|
+
Name extends string = string,
|
|
15
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
16
|
+
> = { kind: 'slot'; name: Name; rest: Rest }
|
|
17
|
+
|
|
18
|
+
type Wrap<
|
|
19
|
+
Name extends string = string,
|
|
20
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
21
|
+
Args = unknown,
|
|
22
|
+
> = { kind: 'wrap'; name: Name; rest: Rest; when: (args: Args) => boolean }
|
|
23
|
+
|
|
24
|
+
type SlotDef =
|
|
25
|
+
| Path<string, readonly PathDef[]>
|
|
26
|
+
| Slot<string, readonly PathDef[]>
|
|
27
|
+
| Wrap<string, readonly PathDef[], any>
|
|
28
|
+
type PathDef = SlotDef | Keep
|
|
29
|
+
|
|
30
|
+
// ---------- Type-level route builder ----------
|
|
31
|
+
interface Whenable {
|
|
32
|
+
when(cond: boolean, seg: Segment | readonly Segment[]): this
|
|
33
|
+
join(seg: Segment | readonly Segment[]): this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type HasKeep<Rest extends readonly PathDef[]> =
|
|
37
|
+
Extract<Rest[number], Keep> extends never ? false : true
|
|
38
|
+
|
|
39
|
+
type NonKeepChildren<Rest extends readonly PathDef[]> = Exclude<Rest[number], Keep>
|
|
40
|
+
|
|
41
|
+
type PropsFromChildren<Rest extends readonly PathDef[]> = {
|
|
42
|
+
[C in NonKeepChildren<Rest> as C extends { name: infer N extends string }
|
|
43
|
+
? N
|
|
44
|
+
: never]: C extends Path<any, any>
|
|
45
|
+
? RouteFromPath<C>
|
|
46
|
+
: C extends Slot<any, any>
|
|
47
|
+
? RouteFromSlot<C>
|
|
48
|
+
: C extends Wrap<any, any, any>
|
|
49
|
+
? RouteFromWrap<C>
|
|
50
|
+
: never
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type WithWhen<T> = T & Whenable
|
|
54
|
+
|
|
55
|
+
// Example: apply it to the outputs
|
|
56
|
+
type RouteFromPath<N extends Path<any, any>> = WithWhen<
|
|
57
|
+
N['rest'] extends readonly []
|
|
58
|
+
? (search?: SParams) => string
|
|
59
|
+
: HasKeep<N['rest']> extends true
|
|
60
|
+
? ((search?: SParams) => string) & PropsFromChildren<N['rest']>
|
|
61
|
+
: PropsFromChildren<N['rest']>
|
|
62
|
+
>
|
|
63
|
+
|
|
64
|
+
type SlotResult<Rest extends readonly PathDef[]> = WithWhen<
|
|
65
|
+
Rest extends readonly []
|
|
66
|
+
? (search?: SParams) => string
|
|
67
|
+
: HasKeep<Rest> extends true
|
|
68
|
+
? ((search?: SParams) => string) & PropsFromChildren<Rest>
|
|
69
|
+
: PropsFromChildren<Rest>
|
|
70
|
+
>
|
|
71
|
+
|
|
72
|
+
type RouteFromSlot<I extends Slot<any, any>> = (param: Segment) => SlotResult<I['rest']>
|
|
73
|
+
|
|
74
|
+
type WrapArg<W extends Wrap<any, any, any>> = Parameters<W['when']>[0]
|
|
75
|
+
|
|
76
|
+
type WrapResult<Rest extends readonly PathDef[]> = WithWhen<
|
|
77
|
+
HasKeep<Rest> extends true
|
|
78
|
+
? ((search?: SParams) => string) & PropsFromChildren<Rest>
|
|
79
|
+
: PropsFromChildren<Rest>
|
|
80
|
+
>
|
|
81
|
+
|
|
82
|
+
type RouteFromWrap<W extends Wrap<any, any, any>> = (arg: WrapArg<W>) => WrapResult<W['rest']>
|
|
83
|
+
|
|
84
|
+
type RoutesFromDefs<Defs extends readonly PathDef[]> = WithWhen<
|
|
85
|
+
HasKeep<Defs> extends true
|
|
86
|
+
? ((search?: SParams) => string) & PropsFromChildren<Defs>
|
|
87
|
+
: PropsFromChildren<Defs>
|
|
88
|
+
>
|
|
89
|
+
|
|
90
|
+
declare const keep: () => Keep;
|
|
91
|
+
declare const path: <const Name extends string, const Rest extends readonly PathDef[] = readonly []>(name: Name, rest?: Rest) => Path<Name, Rest>;
|
|
92
|
+
declare const slot: <const Name extends string, const Rest extends readonly PathDef[] = readonly []>(name: Name, rest?: Rest) => Slot<Name, Rest>;
|
|
93
|
+
declare const wrap: <const Name extends string, const Rest extends readonly PathDef[] = readonly [], Args = unknown>(name: Name, when: (args: Args) => boolean, rest?: Rest) => Wrap<Name, Rest, Args>;
|
|
94
|
+
declare function root<const Defs extends readonly PathDef[]>(defs: Defs): RoutesFromDefs<Defs>;
|
|
95
|
+
|
|
96
|
+
export { keep, path, root, slot, wrap };
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ---------- Shared public types ----------
|
|
2
|
+
type Segment = string | number
|
|
3
|
+
type SParams = string | URLSearchParams
|
|
4
|
+
|
|
5
|
+
// ---------- DSL definition types ----------
|
|
6
|
+
type Keep = { kind: 'keep' }
|
|
7
|
+
|
|
8
|
+
type Path<
|
|
9
|
+
Name extends string = string,
|
|
10
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
11
|
+
> = { kind: 'path'; name: Name; rest: Rest }
|
|
12
|
+
|
|
13
|
+
type Slot<
|
|
14
|
+
Name extends string = string,
|
|
15
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
16
|
+
> = { kind: 'slot'; name: Name; rest: Rest }
|
|
17
|
+
|
|
18
|
+
type Wrap<
|
|
19
|
+
Name extends string = string,
|
|
20
|
+
Rest extends readonly PathDef[] = readonly PathDef[],
|
|
21
|
+
Args = unknown,
|
|
22
|
+
> = { kind: 'wrap'; name: Name; rest: Rest; when: (args: Args) => boolean }
|
|
23
|
+
|
|
24
|
+
type SlotDef =
|
|
25
|
+
| Path<string, readonly PathDef[]>
|
|
26
|
+
| Slot<string, readonly PathDef[]>
|
|
27
|
+
| Wrap<string, readonly PathDef[], any>
|
|
28
|
+
type PathDef = SlotDef | Keep
|
|
29
|
+
|
|
30
|
+
// ---------- Type-level route builder ----------
|
|
31
|
+
interface Whenable {
|
|
32
|
+
when(cond: boolean, seg: Segment | readonly Segment[]): this
|
|
33
|
+
join(seg: Segment | readonly Segment[]): this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type HasKeep<Rest extends readonly PathDef[]> =
|
|
37
|
+
Extract<Rest[number], Keep> extends never ? false : true
|
|
38
|
+
|
|
39
|
+
type NonKeepChildren<Rest extends readonly PathDef[]> = Exclude<Rest[number], Keep>
|
|
40
|
+
|
|
41
|
+
type PropsFromChildren<Rest extends readonly PathDef[]> = {
|
|
42
|
+
[C in NonKeepChildren<Rest> as C extends { name: infer N extends string }
|
|
43
|
+
? N
|
|
44
|
+
: never]: C extends Path<any, any>
|
|
45
|
+
? RouteFromPath<C>
|
|
46
|
+
: C extends Slot<any, any>
|
|
47
|
+
? RouteFromSlot<C>
|
|
48
|
+
: C extends Wrap<any, any, any>
|
|
49
|
+
? RouteFromWrap<C>
|
|
50
|
+
: never
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type WithWhen<T> = T & Whenable
|
|
54
|
+
|
|
55
|
+
// Example: apply it to the outputs
|
|
56
|
+
type RouteFromPath<N extends Path<any, any>> = WithWhen<
|
|
57
|
+
N['rest'] extends readonly []
|
|
58
|
+
? (search?: SParams) => string
|
|
59
|
+
: HasKeep<N['rest']> extends true
|
|
60
|
+
? ((search?: SParams) => string) & PropsFromChildren<N['rest']>
|
|
61
|
+
: PropsFromChildren<N['rest']>
|
|
62
|
+
>
|
|
63
|
+
|
|
64
|
+
type SlotResult<Rest extends readonly PathDef[]> = WithWhen<
|
|
65
|
+
Rest extends readonly []
|
|
66
|
+
? (search?: SParams) => string
|
|
67
|
+
: HasKeep<Rest> extends true
|
|
68
|
+
? ((search?: SParams) => string) & PropsFromChildren<Rest>
|
|
69
|
+
: PropsFromChildren<Rest>
|
|
70
|
+
>
|
|
71
|
+
|
|
72
|
+
type RouteFromSlot<I extends Slot<any, any>> = (param: Segment) => SlotResult<I['rest']>
|
|
73
|
+
|
|
74
|
+
type WrapArg<W extends Wrap<any, any, any>> = Parameters<W['when']>[0]
|
|
75
|
+
|
|
76
|
+
type WrapResult<Rest extends readonly PathDef[]> = WithWhen<
|
|
77
|
+
HasKeep<Rest> extends true
|
|
78
|
+
? ((search?: SParams) => string) & PropsFromChildren<Rest>
|
|
79
|
+
: PropsFromChildren<Rest>
|
|
80
|
+
>
|
|
81
|
+
|
|
82
|
+
type RouteFromWrap<W extends Wrap<any, any, any>> = (arg: WrapArg<W>) => WrapResult<W['rest']>
|
|
83
|
+
|
|
84
|
+
type RoutesFromDefs<Defs extends readonly PathDef[]> = WithWhen<
|
|
85
|
+
HasKeep<Defs> extends true
|
|
86
|
+
? ((search?: SParams) => string) & PropsFromChildren<Defs>
|
|
87
|
+
: PropsFromChildren<Defs>
|
|
88
|
+
>
|
|
89
|
+
|
|
90
|
+
declare const keep: () => Keep;
|
|
91
|
+
declare const path: <const Name extends string, const Rest extends readonly PathDef[] = readonly []>(name: Name, rest?: Rest) => Path<Name, Rest>;
|
|
92
|
+
declare const slot: <const Name extends string, const Rest extends readonly PathDef[] = readonly []>(name: Name, rest?: Rest) => Slot<Name, Rest>;
|
|
93
|
+
declare const wrap: <const Name extends string, const Rest extends readonly PathDef[] = readonly [], Args = unknown>(name: Name, when: (args: Args) => boolean, rest?: Rest) => Wrap<Name, Rest, Args>;
|
|
94
|
+
declare function root<const Defs extends readonly PathDef[]>(defs: Defs): RoutesFromDefs<Defs>;
|
|
95
|
+
|
|
96
|
+
export { keep, path, root, slot, wrap };
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var u=()=>({kind:"keep"}),d=(t,n)=>({kind:"path",name:t,rest:n??[]}),h=(t,n)=>({kind:"slot",name:t,rest:n??[]}),y=(t,n,r)=>({kind:"wrap",name:t,when:n,rest:r??[]}),l=(t,n)=>`${t.filter(Boolean).join("/")}${n?`?${n}`:""}`.replace("//","/");function g(t){return m([],d("/",t))}function m(t,n){let r=e=>e.rest.some(o=>o.kind==="keep"),s=n.kind==="slot"||n.kind==="wrap"?t:n.name?[...t,n.name]:t,a=r(n)?e=>l(s,e):{};for(let e of n.rest)e.kind==="slot"?e.rest.length===0?a[e.name]=o=>i=>l([...s,o],i):a[e.name]=o=>{let i=m([...s,o],e);return Object.assign(r(e)?c=>l([...s,o],c):{},i)}:e.kind==="path"?e.rest.length===0?a[e.name]=o=>l([...s,e.name],o):a[e.name]=m(s,e):e.kind==="wrap"&&(a[e.name]=o=>{let c=e.when(o)?[...s,e.name]:s,S=m(c,e);return Object.assign(r(e)?p=>l(c,p):{},S)});return f(a,s,n.rest)}function f(t,n,r){return t.when=(s,a)=>m(s?[...n,...Array.isArray(a)?a:[a]]:n,d("",r)),t.join=s=>m([...n,...Array.isArray(s)?s:[s]],d("",r)),t}export{u as keep,d as path,g as root,h as slot,y as wrap};
|
|
2
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api.ts"],"sourcesContent":["import { Keep, Path, Wrap, Slot, PathDef, SlotDef, Segment, SParams, RoutesFromDefs } from './dsl'\n\n// ---------- DSL helpers (typed) ----------\nexport const keep = (): Keep => ({ kind: 'keep' })\n\nexport const path = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n>(\n\tname: Name,\n\trest?: Rest\n): Path<Name, Rest> => ({ kind: 'path', name, rest: (rest ?? []) as Rest })\n\nexport const slot = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n>(\n\tname: Name,\n\trest?: Rest\n): Slot<Name, Rest> => ({ kind: 'slot', name, rest: (rest ?? []) as Rest })\n\nexport const wrap = <\n\tconst Name extends string,\n\tconst Rest extends readonly PathDef[] = readonly [],\n\tArgs = unknown,\n>(\n\tname: Name,\n\twhen: (args: Args) => boolean,\n\trest?: Rest\n): Wrap<Name, Rest, Args> => ({ kind: 'wrap', name, when, rest: (rest ?? []) as Rest })\n\n// ---------- Runtime implementation ----------\nconst url = (path: Segment[], search?: SParams) =>\n\t`${path.filter(Boolean).join('/')}${search ? `?${search}` : ''}`.replace('//', '/')\n\n// ---------- Typed root signature ----------\nexport function root<const Defs extends readonly PathDef[]>(\n\tdefs: Defs\n): RoutesFromDefs<Defs> {\n\treturn buildPath([], path('/', defs)) as unknown as RoutesFromDefs<Defs>\n}\n\nfunction buildPath(prefix: Segment[], def: SlotDef) {\n\tconst hasKeep = (pathDef: SlotDef) => pathDef.rest.some((c) => c.kind === 'keep')\n\tconst allPath =\n\t\tdef.kind === 'slot' || def.kind === 'wrap'\n\t\t\t? prefix\n\t\t\t: def.name\n\t\t\t\t? [...prefix, def.name]\n\t\t\t\t: prefix\n\n\t// If there is a keep(), the path itself is callable and acts as \"keep\"\n\tconst target: any = hasKeep(def) ? (search?: SParams) => url(allPath, search) : {}\n\n\tfor (const child of def.rest) {\n\t\tif (child.kind === 'slot') {\n\t\t\tif (child.rest.length === 0) {\n\t\t\t\ttarget[child.name] = (param: Segment) => (search?: SParams) =>\n\t\t\t\t\turl([...allPath, param], search)\n\t\t\t} else {\n\t\t\t\ttarget[child.name] = (param: Segment) => {\n\t\t\t\t\t// Build subtree for nested parts under :id\n\t\t\t\t\t// Synthetic path with empty name so we don't add extra segment.\n\t\t\t\t\tconst subTree = buildPath([...allPath, param], child)\n\n\t\t\t\t\t// Attach children (info, activities, etc.) to that function\n\t\t\t\t\treturn Object.assign(\n\t\t\t\t\t\thasKeep(child)\n\t\t\t\t\t\t\t? (search?: SParams) => url([...allPath, param], search)\n\t\t\t\t\t\t\t: {},\n\t\t\t\t\t\tsubTree\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (child.kind === 'path') {\n\t\t\tif (child.rest.length === 0) {\n\t\t\t\ttarget[child.name] = (search?: SParams) => url([...allPath, child.name], search)\n\t\t\t} else {\n\t\t\t\ttarget[child.name] = buildPath(allPath, child)\n\t\t\t}\n\t\t} else if (child.kind === 'wrap') {\n\t\t\ttarget[child.name] = (arg: unknown) => {\n\t\t\t\tconst enabled = child.when(arg)\n\t\t\t\tconst wrapped = enabled ? [...allPath, child.name] : allPath\n\t\t\t\tconst subTree = buildPath(wrapped, child as any)\n\n\t\t\t\treturn Object.assign(\n\t\t\t\t\t// if wrap has keep(), it becomes callable at that point\n\t\t\t\t\thasKeep(child as any) ? (search?: SParams) => url(wrapped, search) : {},\n\t\t\t\t\tsubTree\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn attachWhenAndJoin(target, allPath, def.rest)\n}\n\nfunction attachWhenAndJoin(target: any, basePath: Segment[], rest: readonly PathDef[]) {\n\ttarget.when = (cond: boolean, seg: Segment | readonly Segment[]) => {\n\t\t// Rebuild \"same subtree\" at a new prefix:\n\t\t// Use a synthetic path '' so we don't append an extra segment name.\n\t\treturn buildPath(\n\t\t\tcond ? [...basePath, ...(Array.isArray(seg) ? seg : [seg])] : basePath,\n\t\t\tpath('', rest)\n\t\t)\n\t}\n\ttarget.join = (seg: Segment | readonly Segment[]) => {\n\t\treturn buildPath([...basePath, ...(Array.isArray(seg) ? seg : [seg])], path('', rest))\n\t}\n\n\treturn target\n}\n"],"mappings":"AAGO,IAAMA,EAAO,KAAa,CAAE,KAAM,MAAO,GAEnCC,EAAO,CAInBC,EACAC,KACuB,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAOC,GAAQ,CAAC,CAAW,GAE5DC,EAAO,CAInBF,EACAC,KACuB,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAOC,GAAQ,CAAC,CAAW,GAE5DE,EAAO,CAKnBH,EACAI,EACAH,KAC6B,CAAE,KAAM,OAAQ,KAAAD,EAAM,KAAAI,EAAM,KAAOH,GAAQ,CAAC,CAAW,GAG/EI,EAAM,CAACN,EAAiBO,IAC7B,GAAGP,EAAK,OAAO,OAAO,EAAE,KAAK,GAAG,CAAC,GAAGO,EAAS,IAAIA,CAAM,GAAK,EAAE,GAAG,QAAQ,KAAM,GAAG,EAG5E,SAASC,EACfC,EACuB,CACvB,OAAOC,EAAU,CAAC,EAAGV,EAAK,IAAKS,CAAI,CAAC,CACrC,CAEA,SAASC,EAAUC,EAAmBC,EAAc,CACnD,IAAMC,EAAWC,GAAqBA,EAAQ,KAAK,KAAMC,GAAMA,EAAE,OAAS,MAAM,EAC1EC,EACLJ,EAAI,OAAS,QAAUA,EAAI,OAAS,OACjCD,EACAC,EAAI,KACH,CAAC,GAAGD,EAAQC,EAAI,IAAI,EACpBD,EAGCM,EAAcJ,EAAQD,CAAG,EAAKL,GAAqBD,EAAIU,EAAST,CAAM,EAAI,CAAC,EAEjF,QAAWW,KAASN,EAAI,KACnBM,EAAM,OAAS,OACdA,EAAM,KAAK,SAAW,EACzBD,EAAOC,EAAM,IAAI,EAAKC,GAAoBZ,GACzCD,EAAI,CAAC,GAAGU,EAASG,CAAK,EAAGZ,CAAM,EAEhCU,EAAOC,EAAM,IAAI,EAAKC,GAAmB,CAGxC,IAAMC,EAAUV,EAAU,CAAC,GAAGM,EAASG,CAAK,EAAGD,CAAK,EAGpD,OAAO,OAAO,OACbL,EAAQK,CAAK,EACTX,GAAqBD,EAAI,CAAC,GAAGU,EAASG,CAAK,EAAGZ,CAAM,EACrD,CAAC,EACJa,CACD,CACD,EAESF,EAAM,OAAS,OACrBA,EAAM,KAAK,SAAW,EACzBD,EAAOC,EAAM,IAAI,EAAKX,GAAqBD,EAAI,CAAC,GAAGU,EAASE,EAAM,IAAI,EAAGX,CAAM,EAE/EU,EAAOC,EAAM,IAAI,EAAIR,EAAUM,EAASE,CAAK,EAEpCA,EAAM,OAAS,SACzBD,EAAOC,EAAM,IAAI,EAAKG,GAAiB,CAEtC,IAAMC,EADUJ,EAAM,KAAKG,CAAG,EACJ,CAAC,GAAGL,EAASE,EAAM,IAAI,EAAIF,EAC/CI,EAAUV,EAAUY,EAASJ,CAAY,EAE/C,OAAO,OAAO,OAEbL,EAAQK,CAAY,EAAKX,GAAqBD,EAAIgB,EAASf,CAAM,EAAI,CAAC,EACtEa,CACD,CACD,GAIF,OAAOG,EAAkBN,EAAQD,EAASJ,EAAI,IAAI,CACnD,CAEA,SAASW,EAAkBN,EAAaO,EAAqBtB,EAA0B,CACtF,OAAAe,EAAO,KAAO,CAACQ,EAAeC,IAGtBhB,EACNe,EAAO,CAAC,GAAGD,EAAU,GAAI,MAAM,QAAQE,CAAG,EAAIA,EAAM,CAACA,CAAG,CAAE,EAAIF,EAC9DxB,EAAK,GAAIE,CAAI,CACd,EAEDe,EAAO,KAAQS,GACPhB,EAAU,CAAC,GAAGc,EAAU,GAAI,MAAM,QAAQE,CAAG,EAAIA,EAAM,CAACA,CAAG,CAAE,EAAG1B,EAAK,GAAIE,CAAI,CAAC,EAG/Ee,CACR","names":["keep","path","name","rest","slot","wrap","when","url","search","root","defs","buildPath","prefix","def","hasKeep","pathDef","c","allPath","target","child","param","subTree","arg","wrapped","attachWhenAndJoin","basePath","cond","seg"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "route-sprout",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny, cute DSL that grows type-safe, composable URL builders from a declarative route tree.",
|
|
5
|
+
"author": "Piotr Siatkowski <p.siatkowski@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"routing",
|
|
9
|
+
"tiny",
|
|
10
|
+
"api",
|
|
11
|
+
"tree",
|
|
12
|
+
"DSL",
|
|
13
|
+
"typed",
|
|
14
|
+
"no-boilerplate",
|
|
15
|
+
"flexibility"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git://github.com/PiotrSiatkowski/route-sprout.git",
|
|
20
|
+
"web": "https://github.com/PiotrSiatkowski/route-sprout"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/PiotrSiatkowski/route-sprout/issues"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/**/*"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/dsl.d.ts",
|
|
36
|
+
"import": "./dist/api.js",
|
|
37
|
+
"require": "./dist/api.cjs",
|
|
38
|
+
"default": "./dist/api.js"
|
|
39
|
+
},
|
|
40
|
+
"./dialect-path": {
|
|
41
|
+
"types": "./dist/dsl.d.ts",
|
|
42
|
+
"import": "./dist/dialect-path.js",
|
|
43
|
+
"require": "./dist/dialect-path.cjs",
|
|
44
|
+
"default": "./dist/dialect-path.js"
|
|
45
|
+
},
|
|
46
|
+
"./dialect-node": {
|
|
47
|
+
"types": "./dist/dsl.d.ts",
|
|
48
|
+
"import": "./dist/dialect-node.js",
|
|
49
|
+
"require": "./dist/dialect-node.cjs",
|
|
50
|
+
"default": "./dist/dialect-node.js"
|
|
51
|
+
},
|
|
52
|
+
"./dialect-step": {
|
|
53
|
+
"types": "./dist/dsl.d.ts",
|
|
54
|
+
"import": "./dist/dialect-step.js",
|
|
55
|
+
"require": "./dist/dialect-step.cjs",
|
|
56
|
+
"default": "./dist/dialect-step.js"
|
|
57
|
+
},
|
|
58
|
+
"./dialect-tree": {
|
|
59
|
+
"types": "./dist/dsl.d.ts",
|
|
60
|
+
"import": "./dist/dialect-tree.js",
|
|
61
|
+
"require": "./dist/dialect-tree.cjs",
|
|
62
|
+
"default": "./dist/dialect-tree.js"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "vitest && tsup src/api.ts --format esm,cjs --dts --clean --sourcemap --minify",
|
|
67
|
+
"publish": "npm publish --access public",
|
|
68
|
+
"test": "vitest"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"prettier": "^3.5.3",
|
|
72
|
+
"tsup": "^8.5.0",
|
|
73
|
+
"typescript": "5.8.3",
|
|
74
|
+
"vitest": "^4.0.16"
|
|
75
|
+
},
|
|
76
|
+
"private": false
|
|
77
|
+
}
|