davaux 0.8.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/BASELINE.md +169 -0
- package/CLAUDE.md +518 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ROADMAP.md +198 -0
- package/build.mjs +101 -0
- package/client/control.ts +247 -0
- package/client/hydrate.ts +37 -0
- package/client/index.ts +19 -0
- package/client/jsx-runtime.ts +209 -0
- package/client/resource.ts +122 -0
- package/client/signal.ts +211 -0
- package/client/store.ts +110 -0
- package/client/useHead.ts +63 -0
- package/dist/build/config.d.ts +3 -0
- package/dist/build/config.d.ts.map +1 -0
- package/dist/build/config.js +38 -0
- package/dist/build/config.js.map +7 -0
- package/dist/build/index.d.ts +2 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +13 -0
- package/dist/build/index.js.map +7 -0
- package/dist/build/plugins.d.ts +7 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +85 -0
- package/dist/build/plugins.js.map +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/control.d.ts +49 -0
- package/dist/client/control.d.ts.map +1 -0
- package/dist/client/control.js +154 -0
- package/dist/client/control.js.map +7 -0
- package/dist/client/hydrate.d.ts +7 -0
- package/dist/client/hydrate.d.ts.map +1 -0
- package/dist/client/hydrate.js +23 -0
- package/dist/client/hydrate.js.map +7 -0
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +32 -0
- package/dist/client/index.js.map +7 -0
- package/dist/client/jsx-runtime.d.ts +40 -0
- package/dist/client/jsx-runtime.d.ts.map +1 -0
- package/dist/client/jsx-runtime.js +139 -0
- package/dist/client/jsx-runtime.js.map +7 -0
- package/dist/client/resource.d.ts +31 -0
- package/dist/client/resource.d.ts.map +1 -0
- package/dist/client/resource.js +64 -0
- package/dist/client/resource.js.map +7 -0
- package/dist/client/signal.d.ts +90 -0
- package/dist/client/signal.d.ts.map +1 -0
- package/dist/client/signal.js +115 -0
- package/dist/client/signal.js.map +7 -0
- package/dist/client/store.d.ts +26 -0
- package/dist/client/store.d.ts.map +1 -0
- package/dist/client/store.js +63 -0
- package/dist/client/store.js.map +7 -0
- package/dist/client/useHead.d.ts +28 -0
- package/dist/client/useHead.d.ts.map +1 -0
- package/dist/client/useHead.js +33 -0
- package/dist/client/useHead.js.map +7 -0
- package/dist/config.d.ts +182 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +7 -0
- package/dist/create-multisite.d.ts +2 -0
- package/dist/create-multisite.d.ts.map +1 -0
- package/dist/create-multisite.js +291 -0
- package/dist/create-multisite.js.map +7 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +179 -0
- package/dist/create.js.map +7 -0
- package/dist/dev/blueprints.d.ts +11 -0
- package/dist/dev/blueprints.d.ts.map +1 -0
- package/dist/dev/blueprints.js +65 -0
- package/dist/dev/blueprints.js.map +7 -0
- package/dist/dev/components.d.ts +19 -0
- package/dist/dev/components.d.ts.map +1 -0
- package/dist/dev/components.js +87 -0
- package/dist/dev/components.js.map +7 -0
- package/dist/dev/insert.d.ts +11 -0
- package/dist/dev/insert.d.ts.map +1 -0
- package/dist/dev/insert.js +160 -0
- package/dist/dev/insert.js.map +7 -0
- package/dist/dev/remove.d.ts +53 -0
- package/dist/dev/remove.d.ts.map +1 -0
- package/dist/dev/remove.js +518 -0
- package/dist/dev/remove.js.map +7 -0
- package/dist/dev/watch.d.ts +26 -0
- package/dist/dev/watch.d.ts.map +1 -0
- package/dist/dev/watch.js +2905 -0
- package/dist/dev/watch.js.map +7 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +7 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +191 -0
- package/dist/generate.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/island.d.ts +24 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +15 -0
- package/dist/island.js.map +7 -0
- package/dist/jsx-runtime.d.ts +406 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +90 -0
- package/dist/jsx-runtime.js.map +7 -0
- package/dist/link.d.ts +27 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +29 -0
- package/dist/link.js.map +7 -0
- package/dist/oml/fragment.d.ts +16 -0
- package/dist/oml/fragment.d.ts.map +1 -0
- package/dist/oml/fragment.js +26 -0
- package/dist/oml/fragment.js.map +7 -0
- package/dist/oml/index.d.ts +11 -0
- package/dist/oml/index.d.ts.map +1 -0
- package/dist/oml/index.js +21 -0
- package/dist/oml/index.js.map +7 -0
- package/dist/oml/jsx-runtime.d.ts +34 -0
- package/dist/oml/jsx-runtime.d.ts.map +1 -0
- package/dist/oml/jsx-runtime.js +59 -0
- package/dist/oml/jsx-runtime.js.map +7 -0
- package/dist/oml/jsx.d.ts +14 -0
- package/dist/oml/jsx.d.ts.map +1 -0
- package/dist/oml/jsx.js +96 -0
- package/dist/oml/jsx.js.map +7 -0
- package/dist/oml/page.d.ts +7 -0
- package/dist/oml/page.d.ts.map +1 -0
- package/dist/oml/page.js +6 -0
- package/dist/oml/page.js.map +7 -0
- package/dist/oml/render.d.ts +13 -0
- package/dist/oml/render.d.ts.map +1 -0
- package/dist/oml/render.js +117 -0
- package/dist/oml/render.js.map +7 -0
- package/dist/oml/types.d.ts +79 -0
- package/dist/oml/types.d.ts.map +1 -0
- package/dist/oml/types.js +64 -0
- package/dist/oml/types.js.map +7 -0
- package/dist/router/handler.d.ts +53 -0
- package/dist/router/handler.d.ts.map +1 -0
- package/dist/router/handler.js +342 -0
- package/dist/router/handler.js.map +7 -0
- package/dist/router/matcher.d.ts +21 -0
- package/dist/router/matcher.d.ts.map +1 -0
- package/dist/router/matcher.js +28 -0
- package/dist/router/matcher.js.map +7 -0
- package/dist/router/scanner.d.ts +17 -0
- package/dist/router/scanner.d.ts.map +1 -0
- package/dist/router/scanner.js +197 -0
- package/dist/router/scanner.js.map +7 -0
- package/dist/server/index.d.ts +23 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +29 -0
- package/dist/server/index.js.map +7 -0
- package/dist/signal.d.ts +15 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +29 -0
- package/dist/signal.js.map +7 -0
- package/dist/ssg.d.ts +45 -0
- package/dist/ssg.d.ts.map +1 -0
- package/dist/ssg.js +175 -0
- package/dist/ssg.js.map +7 -0
- package/dist/test/actions.test.d.ts +2 -0
- package/dist/test/actions.test.d.ts.map +1 -0
- package/dist/test/body-limits.test.d.ts +2 -0
- package/dist/test/body-limits.test.d.ts.map +1 -0
- package/dist/test/errors.test.d.ts +2 -0
- package/dist/test/errors.test.d.ts.map +1 -0
- package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
- package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_error.d.ts +3 -0
- package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_global.d.ts +3 -0
- package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/action.page.d.ts +6 -0
- package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
- package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/state.page.d.ts +3 -0
- package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
- package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
- package/dist/test/helpers.d.ts +37 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/layouts.test.d.ts +2 -0
- package/dist/test/layouts.test.d.ts.map +1 -0
- package/dist/test/middleware.test.d.ts +2 -0
- package/dist/test/middleware.test.d.ts.map +1 -0
- package/dist/test/multipart.test.d.ts +2 -0
- package/dist/test/multipart.test.d.ts.map +1 -0
- package/dist/test/oml-routing.test.d.ts +2 -0
- package/dist/test/oml-routing.test.d.ts.map +1 -0
- package/dist/test/oml.test.d.ts +2 -0
- package/dist/test/oml.test.d.ts.map +1 -0
- package/dist/test/redirects.test.d.ts +2 -0
- package/dist/test/redirects.test.d.ts.map +1 -0
- package/dist/test/routing.test.d.ts +2 -0
- package/dist/test/routing.test.d.ts.map +1 -0
- package/dist/test/ssg.test.d.ts +2 -0
- package/dist/test/ssg.test.d.ts.map +1 -0
- package/dist/test/web-response.test.d.ts +2 -0
- package/dist/test/web-response.test.d.ts.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +292 -0
- package/dist/types.js.map +7 -0
- package/package.json +103 -0
- package/pka.config.json +32 -0
- package/src/build/config.ts +42 -0
- package/src/build/index.ts +6 -0
- package/src/build/plugins.ts +118 -0
- package/src/cli.ts +502 -0
- package/src/config.ts +197 -0
- package/src/create-multisite.ts +310 -0
- package/src/create.ts +194 -0
- package/src/dev/blueprints.ts +75 -0
- package/src/dev/components.ts +108 -0
- package/src/dev/insert.ts +221 -0
- package/src/dev/remove.ts +677 -0
- package/src/dev/watch.ts +3098 -0
- package/src/env.d.ts +5 -0
- package/src/errors.ts +64 -0
- package/src/generate.ts +228 -0
- package/src/index.ts +67 -0
- package/src/island.ts +47 -0
- package/src/jsx-runtime.d.ts +408 -0
- package/src/jsx-runtime.d.ts.map +1 -0
- package/src/jsx-runtime.ts +536 -0
- package/src/link.ts +49 -0
- package/src/oml/fragment.ts +54 -0
- package/src/oml/index.ts +21 -0
- package/src/oml/jsx-runtime.ts +121 -0
- package/src/oml/jsx.ts +151 -0
- package/src/oml/page.ts +13 -0
- package/src/oml/render.ts +181 -0
- package/src/oml/types.ts +159 -0
- package/src/router/handler.ts +515 -0
- package/src/router/matcher.ts +52 -0
- package/src/router/scanner.ts +272 -0
- package/src/server/index.ts +49 -0
- package/src/signal.ts +39 -0
- package/src/ssg.ts +253 -0
- package/src/test/actions.test.ts +40 -0
- package/src/test/body-limits.test.ts +83 -0
- package/src/test/errors.test.ts +53 -0
- package/src/test/fixtures/routes/[id].page.ts +3 -0
- package/src/test/fixtures/routes/_error.ts +6 -0
- package/src/test/fixtures/routes/_global.ts +8 -0
- package/src/test/fixtures/routes/_layout-template.ts +7 -0
- package/src/test/fixtures/routes/_layout.ts +7 -0
- package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
- package/src/test/fixtures/routes/_middleware.ts +8 -0
- package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
- package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
- package/src/test/fixtures/routes/about.page.ts +6 -0
- package/src/test/fixtures/routes/action.page.ts +11 -0
- package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
- package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
- package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
- package/src/test/fixtures/routes/api/upload.post.ts +14 -0
- package/src/test/fixtures/routes/api/users.get.ts +3 -0
- package/src/test/fixtures/routes/api/xml.get.ts +5 -0
- package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
- package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
- package/src/test/fixtures/routes/index.page.ts +3 -0
- package/src/test/fixtures/routes/oml.page.ts +7 -0
- package/src/test/fixtures/routes/redirect.page.ts +3 -0
- package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
- package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
- package/src/test/fixtures/routes/state.page.ts +4 -0
- package/src/test/fixtures/routes/throw.page.ts +5 -0
- package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
- package/src/test/helpers.ts +132 -0
- package/src/test/layouts.test.ts +76 -0
- package/src/test/middleware.test.ts +69 -0
- package/src/test/multipart.test.ts +91 -0
- package/src/test/oml-routing.test.ts +59 -0
- package/src/test/oml.test.ts +429 -0
- package/src/test/redirects.test.ts +32 -0
- package/src/test/routing.test.ts +118 -0
- package/src/test/ssg.test.ts +273 -0
- package/src/test/web-response.test.ts +33 -0
- package/src/types.ts +670 -0
- package/tsconfig.client.json +17 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import type { OmlCacheConfig } from '../config.js'
|
|
4
|
+
import { renderToHtml, renderToHtmlDev } from '../oml/render.js'
|
|
5
|
+
import type { OmlNode } from '../oml/types.js'
|
|
6
|
+
import type {
|
|
7
|
+
ActionFn,
|
|
8
|
+
ErrorPageHandler,
|
|
9
|
+
ErrorPageProps,
|
|
10
|
+
LayoutFile,
|
|
11
|
+
LayoutProps,
|
|
12
|
+
MiddlewareFile,
|
|
13
|
+
MiddlewareFn,
|
|
14
|
+
RequestContext,
|
|
15
|
+
RouteFile,
|
|
16
|
+
ScanResult,
|
|
17
|
+
} from '../types.js'
|
|
18
|
+
import {
|
|
19
|
+
makeContext,
|
|
20
|
+
type PayloadTooLargeError,
|
|
21
|
+
type RedirectError,
|
|
22
|
+
type ValidationError,
|
|
23
|
+
} from '../types.js'
|
|
24
|
+
import { createMatcher, type MatchFn } from './matcher.js'
|
|
25
|
+
|
|
26
|
+
const METHOD_TYPES: Record<string, string[]> = {
|
|
27
|
+
GET: ['page', 'get'],
|
|
28
|
+
POST: ['post', 'page'], // 'page' to support defineAction exports
|
|
29
|
+
PUT: ['put'],
|
|
30
|
+
PATCH: ['patch'],
|
|
31
|
+
DELETE: ['delete'],
|
|
32
|
+
HEAD: ['head', 'page', 'get'],
|
|
33
|
+
OPTIONS: ['options'],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A route file paired with its compiled URL matcher, ready for dispatch. */
|
|
37
|
+
export interface CompiledRoute {
|
|
38
|
+
route: RouteFile
|
|
39
|
+
match: MatchFn
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The compiled representation of a Davaux application, ready to dispatch requests. */
|
|
43
|
+
export interface CompiledApp {
|
|
44
|
+
routes: CompiledRoute[]
|
|
45
|
+
layouts: LayoutFile[]
|
|
46
|
+
middlewares: MiddlewareFile[]
|
|
47
|
+
/** Compiled path to `src/middleware.ts` — runs on every request before route matching. */
|
|
48
|
+
appMiddlewarePath?: string
|
|
49
|
+
isDev: boolean
|
|
50
|
+
/** Whether the visual editor and inspector overlay are active. Requires `editor.enabled: true` in config. */
|
|
51
|
+
editorEnabled: boolean
|
|
52
|
+
clientScripts: string[]
|
|
53
|
+
clientStylesheets: string[]
|
|
54
|
+
errorPage?: string
|
|
55
|
+
basePath: string
|
|
56
|
+
/**
|
|
57
|
+
* In-process OML cache. Keyed by filePath::pathname+search. Only populated in production
|
|
58
|
+
* for routes matched by `omlCacheConfig`. Stores the OmlNode and pre-rendered HTML string
|
|
59
|
+
* so cache hits require zero renderToHtml traversal.
|
|
60
|
+
*/
|
|
61
|
+
omlCache: Map<string, { node: OmlNode; html: string }>
|
|
62
|
+
/** In dev mode, stores the last rendered OmlNode per URL for the inspector. Not populated in production. */
|
|
63
|
+
devOmlStore?: Map<string, OmlNode>
|
|
64
|
+
/**
|
|
65
|
+
* OML production cache configuration from `davaux.config.ts` `oml.cache`.
|
|
66
|
+
* When absent, no routes are cached in production.
|
|
67
|
+
*/
|
|
68
|
+
omlCacheConfig?: OmlCacheConfig
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compile a scan result into a `CompiledApp` ready to dispatch requests.
|
|
73
|
+
* Typically called once at startup (or on each rebuild in dev mode).
|
|
74
|
+
*/
|
|
75
|
+
export function buildApp(
|
|
76
|
+
result: ScanResult,
|
|
77
|
+
isDev = false,
|
|
78
|
+
clientScripts: string[] = [],
|
|
79
|
+
clientStylesheets: string[] = [],
|
|
80
|
+
appMiddlewarePath?: string,
|
|
81
|
+
basePath = '',
|
|
82
|
+
editorEnabled = false,
|
|
83
|
+
omlCacheConfig?: OmlCacheConfig,
|
|
84
|
+
): CompiledApp {
|
|
85
|
+
return {
|
|
86
|
+
routes: result.routes.map((route) => ({
|
|
87
|
+
route,
|
|
88
|
+
match: createMatcher(route.urlPattern),
|
|
89
|
+
})),
|
|
90
|
+
layouts: result.layouts,
|
|
91
|
+
middlewares: result.middlewares,
|
|
92
|
+
appMiddlewarePath,
|
|
93
|
+
isDev,
|
|
94
|
+
editorEnabled,
|
|
95
|
+
clientScripts,
|
|
96
|
+
clientStylesheets,
|
|
97
|
+
errorPage: result.errorPage,
|
|
98
|
+
basePath,
|
|
99
|
+
omlCache: new Map(),
|
|
100
|
+
devOmlStore: isDev && editorEnabled ? new Map() : undefined,
|
|
101
|
+
omlCacheConfig,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── OML cache helpers ────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function matchesCachePattern(pathname: string, pattern: string): boolean {
|
|
108
|
+
const regexStr = pattern
|
|
109
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars (not * or :)
|
|
110
|
+
.replace(/:[^/]+/g, '[^/]+') // :param → any non-slash segment
|
|
111
|
+
.replace(/\*/g, '.*') // * → anything
|
|
112
|
+
return new RegExp(`^${regexStr}$`).test(pathname)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isOmlCacheable(pathname: string, config: OmlCacheConfig | undefined): boolean {
|
|
116
|
+
if (!config) return false
|
|
117
|
+
if ('include' in config) return config.include.some((p) => matchesCachePattern(pathname, p))
|
|
118
|
+
return !config.exclude.some((p) => matchesCachePattern(pathname, p))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isOmlNode(value: unknown): value is OmlNode {
|
|
122
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
|
|
123
|
+
const t = (value as Record<string, unknown>).type
|
|
124
|
+
return t === 'element' || t === '#component' || t === '#fragment' || t === '#text' || t === '#raw'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function importModule(filePath: string, isDev: boolean): Promise<Record<string, unknown>> {
|
|
128
|
+
const url = isDev ? `${filePath}?t=${Date.now()}` : filePath
|
|
129
|
+
try {
|
|
130
|
+
return await (import(url) as Promise<Record<string, unknown>>)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
133
|
+
throw new Error(`[davaux] Failed to load module: ${filePath}\n ${detail}`, { cause: err })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Find layouts that apply to a given route file, ordered innermost → outermost.
|
|
138
|
+
function findLayouts(routeFilePath: string, layouts: LayoutFile[]): LayoutFile[] {
|
|
139
|
+
const routeDir = dirname(routeFilePath)
|
|
140
|
+
return layouts
|
|
141
|
+
.filter((l) => routeDir === l.dirPath || routeDir.startsWith(`${l.dirPath}/`))
|
|
142
|
+
.sort((a, b) => b.dirPath.length - a.dirPath.length) // deepest first
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find middlewares that apply to a given route file, ordered outermost → innermost.
|
|
146
|
+
function findMiddlewares(routeFilePath: string, middlewares: MiddlewareFile[]): MiddlewareFile[] {
|
|
147
|
+
const routeDir = dirname(routeFilePath)
|
|
148
|
+
return middlewares
|
|
149
|
+
.filter((m) => routeDir === m.dirPath || routeDir.startsWith(`${m.dirPath}/`))
|
|
150
|
+
.sort((a, b) => a.dirPath.length - b.dirPath.length) // shallowest first
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runWithMiddleware(
|
|
154
|
+
middlewares: MiddlewareFile[],
|
|
155
|
+
ctx: RequestContext,
|
|
156
|
+
isDev: boolean,
|
|
157
|
+
handler: () => Promise<void>,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const step = async (i: number): Promise<void> => {
|
|
160
|
+
if (i < middlewares.length) {
|
|
161
|
+
const mod = await importModule(middlewares[i].filePath, isDev)
|
|
162
|
+
const fn = mod.default as MiddlewareFn
|
|
163
|
+
await fn(ctx, () => step(i + 1))
|
|
164
|
+
} else {
|
|
165
|
+
await handler()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await step(0)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sendPage(html: string, res: ServerResponse): void {
|
|
172
|
+
if (!res.headersSent) {
|
|
173
|
+
const trimmed = html.trimStart()
|
|
174
|
+
const full =
|
|
175
|
+
trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
|
|
176
|
+
res.writeHead(res.statusCode, {
|
|
177
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
178
|
+
})
|
|
179
|
+
res.end(full)
|
|
180
|
+
} else if (!res.writableEnded) {
|
|
181
|
+
res.end(html)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function sendPageWithDeferred(
|
|
186
|
+
html: string,
|
|
187
|
+
ctx: import('../types.js').RequestContext,
|
|
188
|
+
res: ServerResponse,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
if (ctx._deferredSlots.size === 0) {
|
|
191
|
+
sendPage(html, res)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (res.headersSent) return
|
|
195
|
+
const trimmed = html.trimStart()
|
|
196
|
+
const shell =
|
|
197
|
+
trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
|
|
198
|
+
res.writeHead(res.statusCode, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
199
|
+
res.write(shell)
|
|
200
|
+
for (const [name, contentPromise] of ctx._deferredSlots) {
|
|
201
|
+
const content = await contentPromise
|
|
202
|
+
const slotHtml = isOmlNode(content) ? renderToHtml(content) : (content as string)
|
|
203
|
+
res.write(`<template for="${name}">${slotHtml}</template>`)
|
|
204
|
+
}
|
|
205
|
+
res.end()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function renderPage(
|
|
209
|
+
route: RouteFile,
|
|
210
|
+
ctx: RequestContext,
|
|
211
|
+
layouts: LayoutFile[],
|
|
212
|
+
isDev: boolean,
|
|
213
|
+
omlCache: Map<string, { node: OmlNode; html: string }>,
|
|
214
|
+
opts?: {
|
|
215
|
+
preloadedMod?: Record<string, unknown>
|
|
216
|
+
devOmlStore?: Map<string, OmlNode>
|
|
217
|
+
omlCacheConfig?: OmlCacheConfig
|
|
218
|
+
},
|
|
219
|
+
): Promise<string> {
|
|
220
|
+
const { preloadedMod, devOmlStore, omlCacheConfig } = opts ?? {}
|
|
221
|
+
const mod = preloadedMod ?? (await importModule(route.filePath, isDev))
|
|
222
|
+
const handler = mod.default as (
|
|
223
|
+
ctx: RequestContext,
|
|
224
|
+
) => string | OmlNode | Promise<string | OmlNode>
|
|
225
|
+
|
|
226
|
+
const cacheKey =
|
|
227
|
+
!isDev && isOmlCacheable(ctx.url.pathname, omlCacheConfig)
|
|
228
|
+
? `${route.filePath}::${ctx.url.pathname}${ctx.url.search}`
|
|
229
|
+
: null
|
|
230
|
+
const cached = cacheKey ? omlCache.get(cacheKey) : undefined
|
|
231
|
+
|
|
232
|
+
let html: string
|
|
233
|
+
if (cached !== undefined) {
|
|
234
|
+
html = cached.html
|
|
235
|
+
} else {
|
|
236
|
+
const result = await handler(ctx)
|
|
237
|
+
if (isOmlNode(result)) {
|
|
238
|
+
html = isDev && devOmlStore ? renderToHtmlDev(result) : renderToHtml(result)
|
|
239
|
+
if (cacheKey) omlCache.set(cacheKey, { node: result, html })
|
|
240
|
+
if (devOmlStore) {
|
|
241
|
+
const sp = new URLSearchParams(ctx.url.search)
|
|
242
|
+
sp.delete('_editor')
|
|
243
|
+
const search = sp.toString()
|
|
244
|
+
devOmlStore.set(ctx.url.pathname + (search ? `?${search}` : ''), result)
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
html = result as string
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Wrap in applicable layouts from innermost to outermost
|
|
252
|
+
const applicable = findLayouts(route.filePath, layouts)
|
|
253
|
+
for (const layout of applicable) {
|
|
254
|
+
const lmod = await importModule(layout.filePath, isDev)
|
|
255
|
+
const lhandler = lmod.default as (
|
|
256
|
+
props: LayoutProps,
|
|
257
|
+
) => string | OmlNode | Promise<string | OmlNode>
|
|
258
|
+
// Pass children as Promise<string> so the JSX runtime renders it without escaping
|
|
259
|
+
const lresult = await lhandler({
|
|
260
|
+
children: Object.assign(Promise.resolve(html), { toString: () => html }),
|
|
261
|
+
ctx,
|
|
262
|
+
})
|
|
263
|
+
html = isOmlNode(lresult) ? renderToHtml(lresult) : (lresult as string)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return html
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function initCtx(
|
|
270
|
+
req: IncomingMessage,
|
|
271
|
+
res: ServerResponse,
|
|
272
|
+
params: Record<string, string>,
|
|
273
|
+
url: URL,
|
|
274
|
+
app: CompiledApp,
|
|
275
|
+
): RequestContext {
|
|
276
|
+
const ctx = makeContext(req, res, params, url, app.basePath)
|
|
277
|
+
ctx.head.stylesheets.push(...app.clientStylesheets)
|
|
278
|
+
ctx.head.scripts.push(...app.clientScripts)
|
|
279
|
+
if (app.isDev) {
|
|
280
|
+
ctx.head.scripts.push('/_davaux/livereload.js')
|
|
281
|
+
if (app.editorEnabled && !url.searchParams.has('_editor')) {
|
|
282
|
+
ctx.head.scripts.push('/_davaux/inspector.js')
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return ctx
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function sendErrorPage(
|
|
289
|
+
status: number,
|
|
290
|
+
message: string,
|
|
291
|
+
req: IncomingMessage,
|
|
292
|
+
res: ServerResponse,
|
|
293
|
+
url: URL,
|
|
294
|
+
app: CompiledApp,
|
|
295
|
+
existingCtx?: RequestContext,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
if (res.headersSent) return
|
|
298
|
+
if (!app.errorPage) {
|
|
299
|
+
res.writeHead(status, { 'Content-Type': 'text/plain' })
|
|
300
|
+
res.end(message)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const ctx = existingCtx ?? initCtx(req, res, {}, url, app)
|
|
305
|
+
const mod = await importModule(app.errorPage, app.isDev)
|
|
306
|
+
const handler = mod.default as ErrorPageHandler
|
|
307
|
+
const rawErrorResult = await handler({ status, message, ctx } satisfies ErrorPageProps)
|
|
308
|
+
let html = isOmlNode(rawErrorResult) ? renderToHtml(rawErrorResult) : (rawErrorResult as string)
|
|
309
|
+
|
|
310
|
+
// Apply layouts so error pages inherit site chrome
|
|
311
|
+
const applicable = findLayouts(app.errorPage, app.layouts)
|
|
312
|
+
for (const layout of applicable) {
|
|
313
|
+
const lmod = await importModule(layout.filePath, app.isDev)
|
|
314
|
+
const lhandler = lmod.default as (
|
|
315
|
+
props: LayoutProps,
|
|
316
|
+
) => string | OmlNode | Promise<string | OmlNode>
|
|
317
|
+
const lresult = await lhandler({
|
|
318
|
+
children: Object.assign(Promise.resolve(html), { toString: () => html }),
|
|
319
|
+
ctx,
|
|
320
|
+
})
|
|
321
|
+
html = isOmlNode(lresult) ? renderToHtml(lresult) : (lresult as string)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const trimmed = html.trimStart()
|
|
325
|
+
const full =
|
|
326
|
+
trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
|
|
327
|
+
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
328
|
+
res.end(full)
|
|
329
|
+
} catch {
|
|
330
|
+
res.writeHead(status, { 'Content-Type': 'text/plain' })
|
|
331
|
+
res.end(message)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Dispatch a single HTTP request through the compiled app.
|
|
337
|
+
* Runs app middleware → route matching → scoped middleware → route handler.
|
|
338
|
+
* Handles redirects, validation errors, payload-too-large, and 404s internally.
|
|
339
|
+
*/
|
|
340
|
+
export async function dispatch(
|
|
341
|
+
req: IncomingMessage,
|
|
342
|
+
res: ServerResponse,
|
|
343
|
+
app: CompiledApp,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
const method = (req.method ?? 'GET').toUpperCase()
|
|
346
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
|
|
347
|
+
const allowed = METHOD_TYPES[method] ?? []
|
|
348
|
+
|
|
349
|
+
// ctx is created once and shared between global middleware and route handlers.
|
|
350
|
+
// params starts empty and is populated in-place after a route match.
|
|
351
|
+
const ctx = initCtx(req, res, {}, url, app)
|
|
352
|
+
|
|
353
|
+
// Inner handler: route matching → scoped middleware → route handler → 404.
|
|
354
|
+
// Never throws — all errors are caught and turned into responses here.
|
|
355
|
+
const handleRoutes = async (): Promise<void> => {
|
|
356
|
+
for (const { route, match } of app.routes) {
|
|
357
|
+
if (!allowed.includes(route.type)) continue
|
|
358
|
+
|
|
359
|
+
const result = match(url.pathname)
|
|
360
|
+
if (!result) continue
|
|
361
|
+
|
|
362
|
+
// Populate params into the shared ctx now that we have a match
|
|
363
|
+
Object.assign(ctx.params, result.params)
|
|
364
|
+
|
|
365
|
+
const applicable = findMiddlewares(route.filePath, app.middlewares)
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await runWithMiddleware(applicable, ctx, app.isDev, async () => {
|
|
369
|
+
if (route.type === 'page') {
|
|
370
|
+
if (method === 'POST') {
|
|
371
|
+
const mod = await importModule(route.filePath, app.isDev)
|
|
372
|
+
const actionFn = mod.action as ActionFn | undefined
|
|
373
|
+
if (typeof actionFn !== 'function') {
|
|
374
|
+
if (!res.headersSent) {
|
|
375
|
+
res.writeHead(405, {
|
|
376
|
+
Allow: 'GET, HEAD',
|
|
377
|
+
'Content-Type': 'text/plain',
|
|
378
|
+
})
|
|
379
|
+
res.end('Method Not Allowed')
|
|
380
|
+
}
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
const result = await actionFn(ctx)
|
|
384
|
+
if (result !== undefined) ctx.state.actionResult = result
|
|
385
|
+
await sendPageWithDeferred(
|
|
386
|
+
await renderPage(route, ctx, app.layouts, app.isDev, app.omlCache, {
|
|
387
|
+
preloadedMod: mod,
|
|
388
|
+
devOmlStore: app.devOmlStore,
|
|
389
|
+
omlCacheConfig: app.omlCacheConfig,
|
|
390
|
+
}),
|
|
391
|
+
ctx,
|
|
392
|
+
res,
|
|
393
|
+
)
|
|
394
|
+
} else {
|
|
395
|
+
await sendPageWithDeferred(
|
|
396
|
+
await renderPage(route, ctx, app.layouts, app.isDev, app.omlCache, {
|
|
397
|
+
devOmlStore: app.devOmlStore,
|
|
398
|
+
omlCacheConfig: app.omlCacheConfig,
|
|
399
|
+
}),
|
|
400
|
+
ctx,
|
|
401
|
+
res,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// API route
|
|
406
|
+
const mod = await importModule(route.filePath, app.isDev)
|
|
407
|
+
const handler = mod.default as (ctx: RequestContext) => unknown
|
|
408
|
+
const value = await handler(ctx)
|
|
409
|
+
if (!res.headersSent && value !== undefined) {
|
|
410
|
+
if (value instanceof Response) {
|
|
411
|
+
await writeWebResponse(value, res)
|
|
412
|
+
} else {
|
|
413
|
+
res.writeHead(res.statusCode, {
|
|
414
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
415
|
+
})
|
|
416
|
+
res.end(JSON.stringify(value))
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (isRedirect(err)) throw err
|
|
423
|
+
if (isValidation(err)) throw err
|
|
424
|
+
if (isPayloadTooLarge(err)) throw err
|
|
425
|
+
console.error(`[davaux] Error in ${route.filePath}:`, err)
|
|
426
|
+
await sendErrorPage(
|
|
427
|
+
500,
|
|
428
|
+
err instanceof Error ? err.message : 'Internal Server Error',
|
|
429
|
+
req,
|
|
430
|
+
res,
|
|
431
|
+
url,
|
|
432
|
+
app,
|
|
433
|
+
ctx,
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
return // route matched — stop iterating
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await sendErrorPage(404, 'Not Found', req, res, url, app, ctx)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// App middleware (src/middleware.ts) → routes.
|
|
443
|
+
// Errors thrown anywhere in the chain are caught here.
|
|
444
|
+
try {
|
|
445
|
+
if (app.appMiddlewarePath) {
|
|
446
|
+
const mod = await importModule(app.appMiddlewarePath, app.isDev)
|
|
447
|
+
const fn = mod.default as MiddlewareFn
|
|
448
|
+
await fn(ctx, handleRoutes)
|
|
449
|
+
} else {
|
|
450
|
+
await handleRoutes()
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (isRedirect(err)) {
|
|
454
|
+
if (!res.headersSent) {
|
|
455
|
+
res.writeHead(err.status, { Location: err.url })
|
|
456
|
+
res.end()
|
|
457
|
+
}
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
if (isValidation(err)) {
|
|
461
|
+
const body: Record<string, unknown> = { error: err.message }
|
|
462
|
+
const cause = err.cause
|
|
463
|
+
if (
|
|
464
|
+
cause != null &&
|
|
465
|
+
typeof cause === 'object' &&
|
|
466
|
+
Array.isArray((cause as { issues?: unknown }).issues)
|
|
467
|
+
) {
|
|
468
|
+
body.issues = (cause as { issues: unknown }).issues
|
|
469
|
+
}
|
|
470
|
+
if (!res.headersSent) {
|
|
471
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
472
|
+
res.end(JSON.stringify(body))
|
|
473
|
+
}
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
if (isPayloadTooLarge(err)) {
|
|
477
|
+
if (!res.headersSent) {
|
|
478
|
+
res.writeHead(413, { 'Content-Type': 'text/plain' })
|
|
479
|
+
res.end(err.message)
|
|
480
|
+
}
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
console.error('[davaux] Error in global middleware:', err)
|
|
484
|
+
await sendErrorPage(
|
|
485
|
+
500,
|
|
486
|
+
err instanceof Error ? err.message : 'Internal Server Error',
|
|
487
|
+
req,
|
|
488
|
+
res,
|
|
489
|
+
url,
|
|
490
|
+
app,
|
|
491
|
+
ctx,
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function writeWebResponse(webRes: Response, res: ServerResponse): Promise<void> {
|
|
497
|
+
const headers: Record<string, string> = {}
|
|
498
|
+
webRes.headers.forEach((value, key) => {
|
|
499
|
+
headers[key] = value
|
|
500
|
+
})
|
|
501
|
+
res.writeHead(webRes.status, headers)
|
|
502
|
+
res.end(Buffer.from(await webRes.arrayBuffer()))
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isRedirect(err: unknown): err is RedirectError {
|
|
506
|
+
return (err as RedirectError)?.isRedirect === true
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isValidation(err: unknown): err is ValidationError {
|
|
510
|
+
return (err as ValidationError)?.isValidation === true
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isPayloadTooLarge(err: unknown): err is PayloadTooLargeError {
|
|
514
|
+
return (err as PayloadTooLargeError)?.isPayloadTooLarge === true
|
|
515
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Result of a successful URL pattern match, containing the extracted named params. */
|
|
2
|
+
export interface MatchResult {
|
|
3
|
+
params: Record<string, string>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** A compiled URL matcher. Returns a `MatchResult` on match, `null` on miss. */
|
|
7
|
+
export type MatchFn = (pathname: string) => MatchResult | null
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compile a URL pattern string into a fast matcher function.
|
|
11
|
+
*
|
|
12
|
+
* Pattern segments:
|
|
13
|
+
* - `:param` — matches a single path segment and captures it by name.
|
|
14
|
+
* - `*param` — catch-all; matches the rest of the path including slashes.
|
|
15
|
+
* - anything else — matched literally.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const match = createMatcher('/blog/:slug')
|
|
19
|
+
* match('/blog/hello') // → { params: { slug: 'hello' } }
|
|
20
|
+
* match('/about') // → null
|
|
21
|
+
*/
|
|
22
|
+
export function createMatcher(urlPattern: string): MatchFn {
|
|
23
|
+
const paramNames: string[] = []
|
|
24
|
+
|
|
25
|
+
// Build regex from pattern segments
|
|
26
|
+
const regexParts = urlPattern.split('/').map((segment) => {
|
|
27
|
+
if (segment.startsWith('*')) {
|
|
28
|
+
paramNames.push(segment.slice(1))
|
|
29
|
+
return '(.+)' // catch-all: matches multiple path segments including slashes
|
|
30
|
+
}
|
|
31
|
+
if (segment.startsWith(':')) {
|
|
32
|
+
paramNames.push(segment.slice(1))
|
|
33
|
+
return '([^/]+)'
|
|
34
|
+
}
|
|
35
|
+
// Escape regex special chars in static segments
|
|
36
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const pattern = new RegExp(`^${regexParts.join('\\/')}$`)
|
|
40
|
+
|
|
41
|
+
return (pathname: string): MatchResult | null => {
|
|
42
|
+
const match = pathname.match(pattern)
|
|
43
|
+
if (!match) return null
|
|
44
|
+
|
|
45
|
+
const params: Record<string, string> = {}
|
|
46
|
+
paramNames.forEach((name, i) => {
|
|
47
|
+
params[name] = decodeURIComponent(match[i + 1])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return { params }
|
|
51
|
+
}
|
|
52
|
+
}
|