solidstep 0.3.0 → 0.3.2

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.2",
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,2FAuXX,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,8 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOpt
395
342
  loaderData: loaderData,
396
343
  };
397
344
  };
398
- let routeManifest = {};
345
+ let routeManifest = null;
346
+ let clientManifest = null;
399
347
  const hydrationScript = ({ nonce, }) => {
400
348
  const script = generateHydrationScript();
401
349
  return nonce
@@ -424,40 +372,21 @@ const handler = eventHandler(async (event) => {
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
- 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
- }
378
+ if (!clientManifest) {
379
+ clientManifest = getManifest('client');
458
380
  }
381
+ const cspNonce = event.locals?.cspNonce;
382
+ const urlObj = new URL(req.url);
383
+ const pathnamePart = urlObj.pathname;
384
+ const searchParams = Object.fromEntries(urlObj.searchParams);
385
+ const match = matchRoute(routeManifest, pathnamePart);
386
+ const matched = match?.handler;
387
+ const params = match?.params || {};
459
388
  if (matched && matched.type === 'route') {
460
- const routeModule = await matched.handler.import();
389
+ const routeModule = await getCachedModule(matched.handler);
461
390
  const reqMethod = req.method?.toUpperCase();
462
391
  if (reqMethod) {
463
392
  const handler = routeModule[reqMethod];
@@ -514,10 +443,14 @@ const handler = eventHandler(async (event) => {
514
443
  try {
515
444
  if (!matched) {
516
445
  try {
517
- const notFoundPage = routeManifest['/'];
446
+ const match = matchRoute(routeManifest, '/');
447
+ const notFoundEntry = match.handler;
448
+ if (!notFoundEntry) {
449
+ throw new Error('No not-found page configured');
450
+ }
518
451
  const { rendered, documentMeta, documentAssets, loaderData, } = await render({
519
452
  toRender: 'not-found',
520
- entry: notFoundPage,
453
+ entry: notFoundEntry,
521
454
  routeParams: {},
522
455
  searchParams: {},
523
456
  req: req,
@@ -547,8 +480,10 @@ const handler = eventHandler(async (event) => {
547
480
  }
548
481
  }
549
482
  else {
550
- const { options } = matched.mainPage.options
551
- ? await matched.mainPage.options.import()
483
+ const { options } = matched
484
+ .mainPage.options
485
+ ? await getCachedModule(matched.mainPage
486
+ .options)
552
487
  : { options: {} };
553
488
  if (options?.responseHeaders) {
554
489
  const headers = options.responseHeaders;
@@ -651,7 +586,8 @@ const handler = eventHandler(async (event) => {
651
586
  console.error(e1);
652
587
  }
653
588
  try {
654
- const errorPage = matched.errorPage;
589
+ const errorPage = matched
590
+ .errorPage;
655
591
  if (!errorPage) {
656
592
  throw e1;
657
593
  }
@@ -663,7 +599,7 @@ const handler = eventHandler(async (event) => {
663
599
  req: req,
664
600
  pageOptions: {},
665
601
  cspNonce,
666
- error: e1
602
+ error: e1,
667
603
  });
668
604
  assets.push(...documentAssets);
669
605
  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
+ };