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 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
+ ![Render Slot Image](./image-small.png)
2
+
3
+ # route-sprout 🌱 (typed API route builder DSL)
4
+
5
+ [![npm version](https://img.shields.io/npm/v/route-sprout?color=blue)](https://www.npmjs.com/package/route-sprout)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/route-sprout)](https://bundlephobia.com/package/route-sprout)
7
+ [![license](https://img.shields.io/npm/l/route-sprout)](./LICENSE)
8
+ [![Types](https://img.shields.io/badge/TypeScript-ready-blue?logo=typescript)](https://www.typescriptlang.org/)
9
+ [![GitHub stars](https://img.shields.io/github/stars/PiotrSiatkowski/route-sprout?style=social)](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
@@ -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
@@ -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
+ }