solidstep 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
- {
2
- "name": "solidstep",
3
- "version": "0.3.0",
4
- "description": "Next Step SolidJS Framework for building web applications.",
5
- "type": "module",
6
- "author": "HamzaKV <hamzakv333@gmail.com>",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/HamzaKV/solidstep.git"
10
- },
11
- "license": "MIT",
12
- "exports": {
13
- ".": "./index.js",
14
- "./hooks/action-state": "./utils/hooks/action-state.js",
15
- "./utils/cache": "./utils/cache.js",
16
- "./utils/cookies": "./utils/cookies.js",
17
- "./utils/cors": "./utils/cors.js",
18
- "./utils/csp": "./utils/csp.js",
19
- "./utils/csrf": "./utils/csrf.js",
20
- "./utils/error-handler": "./utils/error-handler.js",
21
- "./utils/fetch.client": "./utils/fetch.client.js",
22
- "./utils/fetch.server": "./utils/fetch.server.js",
23
- "./utils/loader": "./utils/loader.js",
24
- "./utils/logger": "./utils/logger.js",
25
- "./utils/meta": "./utils/meta.js",
26
- "./utils/redirect": "./utils/redirect.js",
27
- "./utils/server-only": "./utils/server-only.js"
28
- },
29
- "scripts": {
30
- "clean": "rimraf ./dist",
31
- "copy-files:root": "copyfiles -u 0 README.md package.json generate/**/* LICENSE ./dist",
32
- "build": "pnpm clean && tsc && pnpm copy-files:root",
33
- "test:local": "pnpm build && cd ./dist && pnpm link --global",
34
- "test:local:clean": "pnpm unlink && pnpm clean",
35
- "roll": "pnpm build && cd dist && npm publish"
36
- },
37
- "keywords": [
38
- "solidjs",
39
- "web-development",
40
- "typescript",
41
- "npm",
42
- "solidstep",
43
- "framework"
44
- ],
45
- "dependencies": {
46
- "@vinxi/server-functions": "0.5.1",
47
- "pino": "^10.1.0",
48
- "seroval": "^1.3.2",
49
- "seroval-plugins": "^1.3.2",
50
- "undici": "^7.15.0",
51
- "vite-plugin-solid": "^2.11.7"
52
- },
53
- "devDependencies": {
54
- "copyfiles": "^2.4.1",
55
- "rimraf": "^6.0.1",
56
- "solid-js": "^1.9.7",
57
- "typescript": "^5.8.3",
58
- "vinxi": "^0.5.8"
59
- },
60
- "peerDependencies": {
61
- "solid-js": "^1.9.7",
62
- "vinxi": "^0.5.8"
63
- },
64
- "engines": {
65
- "node": ">=20"
66
- },
67
- "publishConfig": {
68
- "access": "public"
69
- }
70
- }
1
+ {
2
+ "name": "solidstep",
3
+ "version": "0.3.1",
4
+ "description": "Next Step SolidJS Framework for building web applications.",
5
+ "type": "module",
6
+ "author": "HamzaKV <hamzakv333@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/HamzaKV/solidstep.git"
10
+ },
11
+ "license": "MIT",
12
+ "exports": {
13
+ ".": "./index.js",
14
+ "./hooks/action-state": "./utils/hooks/action-state.js",
15
+ "./utils/cache": "./utils/cache.js",
16
+ "./utils/cookies": "./utils/cookies.js",
17
+ "./utils/cors": "./utils/cors.js",
18
+ "./utils/csp": "./utils/csp.js",
19
+ "./utils/csrf": "./utils/csrf.js",
20
+ "./utils/error-handler": "./utils/error-handler.js",
21
+ "./utils/fetch.client": "./utils/fetch.client.js",
22
+ "./utils/fetch.server": "./utils/fetch.server.js",
23
+ "./utils/loader": "./utils/loader.js",
24
+ "./utils/logger": "./utils/logger.js",
25
+ "./utils/meta": "./utils/meta.js",
26
+ "./utils/redirect": "./utils/redirect.js",
27
+ "./utils/server-only": "./utils/server-only.js"
28
+ },
29
+ "scripts": {
30
+ "clean": "rimraf ./dist",
31
+ "copy-files:root": "copyfiles -u 0 README.md package.json generate/**/* LICENSE ./dist",
32
+ "build": "pnpm clean && tsc && pnpm copy-files:root",
33
+ "test:local": "pnpm build && cd ./dist && pnpm link --global",
34
+ "test:local:clean": "pnpm unlink && pnpm clean",
35
+ "roll": "pnpm build && cd dist && npm publish"
36
+ },
37
+ "keywords": [
38
+ "solidjs",
39
+ "web-development",
40
+ "typescript",
41
+ "npm",
42
+ "solidstep",
43
+ "framework"
44
+ ],
45
+ "dependencies": {
46
+ "@vinxi/server-functions": "0.5.1",
47
+ "pino": "^10.1.0",
48
+ "seroval": "^1.3.2",
49
+ "seroval-plugins": "^1.3.2",
50
+ "undici": "^7.15.0",
51
+ "vite-plugin-solid": "^2.11.7"
52
+ },
53
+ "devDependencies": {
54
+ "copyfiles": "^2.4.1",
55
+ "rimraf": "^6.0.1",
56
+ "solid-js": "^1.9.7",
57
+ "typescript": "^5.8.3",
58
+ "vinxi": "^0.5.8"
59
+ },
60
+ "peerDependencies": {
61
+ "solid-js": "^1.9.7",
62
+ "vinxi": "^0.5.8"
63
+ },
64
+ "engines": {
65
+ "node": ">=20"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }
package/server.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AA4kBA,QAAA,MAAM,OAAO,2FA0XX,CAAC;AAEH,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAkfA,QAAA,MAAM,OAAO,2FAmXX,CAAC;AAEH,eAAe,OAAO,CAAC"}
package/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { eventHandler, toWebRequest, setHeader, setResponseStatus } from 'vinxi/http';
1
+ import { eventHandler, toWebRequest, setHeader, setResponseStatus, } from 'vinxi/http';
2
2
  import { getManifest } from 'vinxi/manifest';
3
3
  import { generateHydrationScript, renderToString } from 'solid-js/web';
4
4
  import fileRoutes, {} from 'vinxi/routes';
@@ -8,52 +8,31 @@ import { handleServerFunction } from './utils/server-action.server';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { createNode, insertRoute, matchRoute, } from './utils/path-router';
12
+ // Module cache for dynamically imported modules
13
+ const moduleCache = new Map();
14
+ const getCachedModule = async (importFn) => {
15
+ const key = importFn.src;
16
+ if (moduleCache.has(key)) {
17
+ return moduleCache.get(key);
18
+ }
19
+ const module = await importFn.import();
20
+ moduleCache.set(key, module);
21
+ return module;
22
+ };
11
23
  const isPageFile = (file) => file.endsWith('page.tsx') ||
12
24
  file.endsWith('page.jsx') ||
13
25
  file.endsWith('page.ts') ||
14
26
  file.endsWith('page.js');
15
27
  const isRouteFile = (file) => file.endsWith('route.ts') || file.endsWith('route.js');
16
- const parseSegment = (part) => {
17
- if (part.startsWith('[[') && part.endsWith(']]')) {
18
- return `:^*${part.slice(5, -2)}`;
19
- }
20
- return part.startsWith('[')
21
- ? `:${part.slice(1, -1).replace(/\.\.\./, '*')}`
22
- : part;
23
- };
24
- const createMatcher = (routePath) => {
25
- const keys = [];
26
- const segments = routePath.split('/').map((part) => {
27
- if (part.startsWith(':')) {
28
- if (part.startsWith(':^*')) {
29
- // Catch-all inclusive
30
- keys.push(part.slice(3));
31
- return '?(.*)?';
32
- }
33
- if (part.startsWith(':*')) {
34
- // Catch-all
35
- keys.push(part.slice(2));
36
- return '.*';
37
- }
38
- // Standard param
39
- keys.push(part.slice(1));
40
- return '([^/]+)';
41
- }
42
- return part;
43
- });
44
- return {
45
- regex: new RegExp(`^${segments.join('/')}$`),
46
- keys,
47
- };
48
- };
49
28
  const getNormalizedPath = (path, clean) => {
50
- const segments = path.split('/').slice(2).map(parseSegment);
29
+ const segments = path.split('/').slice(2);
51
30
  if (clean)
52
31
  return `/${segments.filter((s) => !s.startsWith('(')).join('/')}`;
53
32
  return `/${segments.join('/')}`;
54
33
  };
55
34
  const createRouteManifest = async () => {
56
- const entries = {};
35
+ const rootNode = createNode();
57
36
  const allRoutes = [];
58
37
  const layoutsMap = new Map();
59
38
  const loadingPagesMap = new Map();
@@ -102,7 +81,6 @@ const createRouteManifest = async () => {
102
81
  const groupName = group.path
103
82
  .split('/')
104
83
  .filter((s) => !s.startsWith('('))
105
- .map(parseSegment)
106
84
  .at(-1);
107
85
  if (!groupName)
108
86
  continue;
@@ -134,11 +112,8 @@ const createRouteManifest = async () => {
134
112
  });
135
113
  }
136
114
  }
137
- const { regex: matcherRegex, keys } = createMatcher(routePath);
138
- entries[routePath] = {
115
+ const entry = {
139
116
  type: 'page',
140
- regex: matcherRegex,
141
- paramKeys: keys,
142
117
  mainPage: {
143
118
  manifestPath: fileRoute.path,
144
119
  page: fileRoute.$component,
@@ -170,47 +145,19 @@ const createRouteManifest = async () => {
170
145
  layouts: layouts,
171
146
  groups: groups,
172
147
  };
148
+ insertRoute(rootNode, routePath, entry);
173
149
  }
174
150
  else if (src && isRouteFile(src)) {
175
- const { regex: matcherRegex, keys } = createMatcher(routePath);
176
- entries[routePath] = {
151
+ const entry = {
177
152
  type: 'route',
178
- regex: matcherRegex,
179
- paramKeys: keys,
153
+ routePath,
180
154
  handler: fileRoute.$handler,
181
155
  manifestPath: fileRoute.path,
182
156
  };
157
+ insertRoute(rootNode, routePath, entry);
183
158
  }
184
159
  }
185
- return entries;
186
- };
187
- const extractRouteParams = (route, url) => {
188
- const routeSegments = route.split('/').filter(s => !(s.startsWith('('))).filter(Boolean);
189
- const urlSegments = url.split('/').filter(Boolean);
190
- const params = {};
191
- let matched = true;
192
- for (let i = 0; i < routeSegments.length; i++) {
193
- const routeSeg = routeSegments[i];
194
- const urlSeg = urlSegments[i];
195
- const isDynamic = routeSeg.startsWith('[') && routeSeg.endsWith(']');
196
- if (isDynamic) {
197
- if (routeSeg.includes('...')) {
198
- // Catch-all parameter
199
- const isCatchAll = routeSeg.startsWith('[[') && routeSeg.endsWith(']]');
200
- const paramName = routeSeg.slice(isCatchAll ? 5 : 4, isCatchAll ? -2 : -1);
201
- params[paramName] = urlSegments.slice(i);
202
- break; // No more segments to match
203
- }
204
- const paramName = routeSeg.slice(1, -1);
205
- params[paramName] = urlSeg;
206
- }
207
- else if (routeSeg !== urlSeg) {
208
- matched = false;
209
- break;
210
- }
211
- }
212
- if (matched)
213
- return { route, params };
160
+ return rootNode;
214
161
  };
215
162
  const template = `
216
163
  <!DOCTYPE html>
@@ -244,7 +191,7 @@ const generateHtmlHead = (meta) => {
244
191
  .join('\n');
245
192
  return head;
246
193
  };
247
- const render = async ({ toRender, entry, routeParams, searchParams, req, pageOptions, cspNonce, error }) => {
194
+ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOptions, cspNonce, error, }) => {
248
195
  const url = new URL(req.url);
249
196
  const path = url.pathname;
250
197
  const cachedEntry = getCache(path);
@@ -264,12 +211,12 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
264
211
  const moduleSrc = `${layout.layout.src}&pick=$css`;
265
212
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
266
213
  assets.push(...moduleAssets);
267
- const { default: layoutModule } = await layout.layout.import();
214
+ const { default: layoutModule } = await getCachedModule(layout.layout);
268
215
  const { loader: layoutLoader } = layout.loader
269
- ? await layout.loader.import()
216
+ ? await getCachedModule(layout.loader)
270
217
  : { loader: null };
271
218
  const { generateMeta: generateMetaPage } = layout.generateMeta
272
- ? await layout.generateMeta.import()
219
+ ? await getCachedModule(layout.generateMeta)
273
220
  : { generateMeta: null };
274
221
  let data = {};
275
222
  if (generateMetaPage) {
@@ -299,9 +246,9 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
299
246
  const moduleSrc = `${group.page.src}&pick=$css`;
300
247
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
301
248
  assets.push(...moduleAssets);
302
- const { default: groupPage } = await group.page.import();
249
+ const { default: groupPage } = await getCachedModule(group.page);
303
250
  const { loader: groupLoader } = group.loader
304
- ? await group.loader.import()
251
+ ? await getCachedModule(group.loader)
305
252
  : { loader: null };
306
253
  let data = {};
307
254
  if (groupLoader) {
@@ -339,12 +286,12 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
339
286
  const moduleSrc = `${pageToRender.page.src}&pick=$css`;
340
287
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
341
288
  assets.push(...moduleAssets);
342
- const { default: page } = await pageToRender.page.import();
289
+ const { default: page } = await getCachedModule(pageToRender.page);
343
290
  const { loader: pageLoader } = pageToRender.loader
344
- ? await pageToRender.loader.import()
291
+ ? await getCachedModule(pageToRender.loader)
345
292
  : { loader: null };
346
293
  const { generateMeta } = pageToRender.generateMeta
347
- ? await pageToRender.generateMeta.import()
294
+ ? await getCachedModule(pageToRender.generateMeta)
348
295
  : { generateMeta: null };
349
296
  let data = {};
350
297
  if (pageLoader) {
@@ -395,7 +342,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
395
342
  loaderData: loaderData,
396
343
  };
397
344
  };
398
- let routeManifest = {};
345
+ let routeManifest = null;
399
346
  const hydrationScript = ({ nonce, }) => {
400
347
  const script = generateHydrationScript();
401
348
  return nonce
@@ -418,46 +365,25 @@ const onStart = async () => {
418
365
  }
419
366
  };
420
367
  onStart();
368
+ const clientManifest = getManifest('client');
421
369
  const handler = eventHandler(async (event) => {
422
370
  const req = toWebRequest(event);
423
371
  try {
424
372
  if (req.url?.includes('_server')) {
425
373
  return handleServerFunction(event);
426
374
  }
427
- const clientManifest = getManifest('client');
428
- if (!routeManifest || Object.keys(routeManifest).length === 0) {
375
+ if (!routeManifest) {
429
376
  routeManifest = await createRouteManifest();
430
377
  }
431
378
  const cspNonce = event.locals?.cspNonce;
432
- const url = new URL(req.url).pathname;
433
- // extract route params and search params
434
- let params = {};
435
- const searchParams = {};
436
- const [pathnamePart, searchParamPart] = url.split('?');
437
- if (searchParamPart) {
438
- for (const param of searchParamPart.split('&')) {
439
- const [key, value] = param.split('=');
440
- searchParams[key] = decodeURIComponent(value || '');
441
- }
442
- }
443
- let matched;
444
- for (const entry of Object.values(routeManifest)) {
445
- const match = entry.regex.exec(pathnamePart);
446
- if (match) {
447
- matched = entry;
448
- if (entry.paramKeys) {
449
- const routePath = matched && matched.type === 'route'
450
- ? matched.manifestPath.split('/').slice(2).join('/')
451
- : matched && matched.type === 'page'
452
- ? matched.mainPage.manifestPath.split('/').slice(2).join('/')
453
- : '/';
454
- params = extractRouteParams(routePath, pathnamePart)?.params || {};
455
- }
456
- break;
457
- }
458
- }
379
+ const urlObj = new URL(req.url);
380
+ const pathnamePart = urlObj.pathname;
381
+ const searchParams = Object.fromEntries(urlObj.searchParams);
382
+ const match = matchRoute(routeManifest, pathnamePart);
383
+ const matched = match?.handler;
384
+ const params = match?.params || {};
459
385
  if (matched && matched.type === 'route') {
460
- const routeModule = await matched.handler.import();
386
+ const routeModule = await getCachedModule(matched.handler);
461
387
  const reqMethod = req.method?.toUpperCase();
462
388
  if (reqMethod) {
463
389
  const handler = routeModule[reqMethod];
@@ -514,10 +440,14 @@ const handler = eventHandler(async (event) => {
514
440
  try {
515
441
  if (!matched) {
516
442
  try {
517
- const notFoundPage = routeManifest['/'];
443
+ const match = matchRoute(routeManifest, '/');
444
+ const notFoundEntry = match.handler;
445
+ if (!notFoundEntry) {
446
+ throw new Error('No not-found page configured');
447
+ }
518
448
  const { rendered, documentMeta, documentAssets, loaderData, } = await render({
519
449
  toRender: 'not-found',
520
- entry: notFoundPage,
450
+ entry: notFoundEntry,
521
451
  routeParams: {},
522
452
  searchParams: {},
523
453
  req: req,
@@ -547,8 +477,10 @@ const handler = eventHandler(async (event) => {
547
477
  }
548
478
  }
549
479
  else {
550
- const { options } = matched.mainPage.options
551
- ? await matched.mainPage.options.import()
480
+ const { options } = matched
481
+ .mainPage.options
482
+ ? await getCachedModule(matched.mainPage
483
+ .options)
552
484
  : { options: {} };
553
485
  if (options?.responseHeaders) {
554
486
  const headers = options.responseHeaders;
@@ -651,7 +583,8 @@ const handler = eventHandler(async (event) => {
651
583
  console.error(e1);
652
584
  }
653
585
  try {
654
- const errorPage = matched.errorPage;
586
+ const errorPage = matched
587
+ .errorPage;
655
588
  if (!errorPage) {
656
589
  throw e1;
657
590
  }
@@ -663,7 +596,7 @@ const handler = eventHandler(async (event) => {
663
596
  req: req,
664
597
  pageOptions: {},
665
598
  cspNonce,
666
- error: e1
599
+ error: e1,
667
600
  });
668
601
  assets.push(...documentAssets);
669
602
  clientHydrationScript = `
package/utils/cache.js CHANGED
@@ -39,7 +39,7 @@ export const getCache = (key) => {
39
39
  const entry = cacheMap.get(key);
40
40
  if (!entry || !entry.expiresAt)
41
41
  return null;
42
- if (entry.expiresAt && entry.expiresAt < Date.now()) {
42
+ if (entry.expiresAt && entry.expiresAt < performance.now()) {
43
43
  cacheMap.delete(key);
44
44
  if (entry.prev)
45
45
  entry.prev.next = entry.next;
@@ -58,14 +58,14 @@ export const setCache = (key, value, ttlMs) => {
58
58
  if (cacheMap.has(key)) {
59
59
  const node = cacheMap.get(key);
60
60
  node.value = value;
61
- node.expiresAt = ttlMs ? Date.now() + ttlMs : null;
61
+ node.expiresAt = ttlMs ? performance.now() + ttlMs : null;
62
62
  moveToFront(node);
63
63
  return;
64
64
  }
65
65
  const newNode = {
66
66
  key,
67
67
  value,
68
- expiresAt: ttlMs ? Date.now() + ttlMs : null
68
+ expiresAt: ttlMs ? performance.now() + ttlMs : null
69
69
  };
70
70
  newNode.next = head;
71
71
  if (head)
@@ -0,0 +1,70 @@
1
+ export type Import = {
2
+ src: string;
3
+ import: any;
4
+ };
5
+ export type RoutePageHandler = {
6
+ type: 'page';
7
+ mainPage: {
8
+ manifestPath: string;
9
+ page: Import;
10
+ loader?: Import;
11
+ generateMeta?: Import;
12
+ options?: Import;
13
+ };
14
+ loadingPage?: {
15
+ manifestPath: string;
16
+ page: Import;
17
+ generateMeta?: Import;
18
+ };
19
+ errorPage?: {
20
+ manifestPath: string;
21
+ page: Import;
22
+ generateMeta?: Import;
23
+ };
24
+ notFoundPage?: {
25
+ manifestPath: string;
26
+ page: Import;
27
+ generateMeta?: Import;
28
+ };
29
+ layouts: {
30
+ manifestPath: string;
31
+ layout: Import;
32
+ loader?: Import;
33
+ generateMeta?: Import;
34
+ }[];
35
+ groups?: {
36
+ [key: string]: {
37
+ manifestPath: string;
38
+ page: Import;
39
+ loader?: Import;
40
+ };
41
+ };
42
+ };
43
+ export type RouteHandler = {
44
+ type: 'route';
45
+ handler: Import;
46
+ manifestPath: string;
47
+ } | RoutePageHandler;
48
+ type Params = Record<string, string | string[]>;
49
+ export type RouteNode = {
50
+ staticChildren: Map<string, RouteNode>;
51
+ paramChild?: {
52
+ name: string;
53
+ node: RouteNode;
54
+ };
55
+ catchAllChild?: {
56
+ name: string;
57
+ optional: boolean;
58
+ node: RouteNode;
59
+ };
60
+ handler?: RouteHandler;
61
+ };
62
+ export declare const createNode: () => RouteNode;
63
+ export declare const insertRoute: (root: RouteNode, path: string, handler: RouteHandler) => void;
64
+ type MatchResult = {
65
+ handler: RouteHandler;
66
+ params: Params;
67
+ } | null;
68
+ export declare const matchRoute: (root: RouteNode, path: string) => MatchResult;
69
+ export {};
70
+ //# sourceMappingURL=path-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-router.d.ts","sourceRoot":"","sources":["../../utils/path-router.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,MAAM,GAAG;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,WAAW,CAAC,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,SAAS,CAAC,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,YAAY,CAAC,EAAE;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,OAAO,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;KACzB,EAAE,CAAC;IACJ,MAAM,CAAC,EAAE;QACL,CAAC,GAAG,EAAE,MAAM,GAAG;YACX,YAAY,EAAE,MAAM,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,CAAC,EAAE,MAAM,CAAC;SACnB,CAAC;KACL,CAAC;CACL,CAAC;AAEF,MAAM,MAAM,YAAY,GAClB;IACE,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACxB,GACC,gBAAgB,CAAC;AAEvB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;AAEhD,MAAM,MAAM,SAAS,GAAG;IACpB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAEvC,UAAU,CAAC,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,SAAS,CAAC;KACnB,CAAA;IAED,aAAa,CAAC,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,OAAO,CAAC;QAClB,IAAI,EAAE,SAAS,CAAC;KACnB,CAAA;IAED,OAAO,CAAC,EAAE,YAAY,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,SAE5B,CAAC;AAyCH,eAAO,MAAM,WAAW,GACpB,MAAM,SAAS,EACf,MAAM,MAAM,EACZ,SAAS,YAAY,SAyCxB,CAAC;AAEF,KAAK,WAAW,GAAG;IACf,OAAO,EAAE,YAAY,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;CACjB,GAAG,IAAI,CAAA;AAER,eAAO,MAAM,UAAU,GACnB,MAAM,SAAS,EACf,MAAM,MAAM,KACb,WAmDF,CAAA"}
@@ -0,0 +1,97 @@
1
+ export const createNode = () => ({
2
+ staticChildren: new Map()
3
+ });
4
+ const parseSegment = (segment) => {
5
+ // [[...slug]]
6
+ if (segment.startsWith('[[...') && segment.endsWith(']]')) {
7
+ return { type: 'catchAll', name: segment.slice(5, -2), optional: true };
8
+ }
9
+ // [...slug]
10
+ if (segment.startsWith('[...') && segment.endsWith(']')) {
11
+ return { type: 'catchAll', name: segment.slice(4, -1), optional: false };
12
+ }
13
+ // [id]
14
+ if (segment.startsWith('[') && segment.endsWith(']')) {
15
+ return { type: 'param', name: segment.slice(1, -1) };
16
+ }
17
+ return { type: 'static', value: segment };
18
+ };
19
+ export const insertRoute = (root, path, handler) => {
20
+ const segments = path.split('/').filter(Boolean);
21
+ let node = root;
22
+ for (const segment of segments) {
23
+ const parsed = parseSegment(segment);
24
+ if (parsed.type === 'static' && parsed.value) {
25
+ if (!node.staticChildren.has(parsed.value)) {
26
+ node.staticChildren.set(parsed.value, createNode());
27
+ }
28
+ node = node.staticChildren.get(parsed.value);
29
+ continue;
30
+ }
31
+ if (parsed.type === 'param') {
32
+ if (!node.paramChild) {
33
+ node.paramChild = {
34
+ name: parsed.name,
35
+ node: createNode()
36
+ };
37
+ }
38
+ node = node.paramChild.node;
39
+ continue;
40
+ }
41
+ if (parsed.type === 'catchAll') {
42
+ if (!node.catchAllChild) {
43
+ node.catchAllChild = {
44
+ name: parsed.name,
45
+ optional: parsed.optional,
46
+ node: createNode()
47
+ };
48
+ }
49
+ node = node.catchAllChild.node;
50
+ break; // catch-all always consumes the rest
51
+ }
52
+ }
53
+ node.handler = handler;
54
+ };
55
+ export const matchRoute = (root, path) => {
56
+ const segments = path.split('/').filter(Boolean);
57
+ const params = {};
58
+ const walk = (node, index) => {
59
+ // End of path
60
+ if (index === segments.length) {
61
+ if (node.handler)
62
+ return node.handler;
63
+ // Optional catch-all can match empty
64
+ if (node.catchAllChild?.optional) {
65
+ params[node.catchAllChild.name] = [];
66
+ return node.catchAllChild.node.handler ?? null;
67
+ }
68
+ return null;
69
+ }
70
+ const segment = segments[index];
71
+ // 1. Static
72
+ const staticChild = node.staticChildren.get(segment);
73
+ if (staticChild) {
74
+ const res = walk(staticChild, index + 1);
75
+ if (res)
76
+ return res;
77
+ }
78
+ // 2. Param
79
+ if (node.paramChild) {
80
+ params[node.paramChild.name] = segment;
81
+ const res = walk(node.paramChild.node, index + 1);
82
+ if (res)
83
+ return res;
84
+ delete params[node.paramChild.name];
85
+ }
86
+ // 3. Catch-all
87
+ if (node.catchAllChild) {
88
+ params[node.catchAllChild.name] = segments.slice(index);
89
+ return node.catchAllChild.node.handler ?? null;
90
+ }
91
+ return null;
92
+ };
93
+ const handler = walk(root, 0);
94
+ if (!handler)
95
+ return null;
96
+ return { handler, params };
97
+ };