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,272 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readdir } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
4
|
+
import type {
|
|
5
|
+
IslandFile,
|
|
6
|
+
LayoutFile,
|
|
7
|
+
MiddlewareFile,
|
|
8
|
+
RouteFile,
|
|
9
|
+
RouteType,
|
|
10
|
+
ScanResult,
|
|
11
|
+
} from '../types.js'
|
|
12
|
+
|
|
13
|
+
// Maps file suffix → HTTP route type. Longer suffixes first (more specific).
|
|
14
|
+
const SUFFIX_MAP: [suffix: string, type: RouteType][] = [
|
|
15
|
+
['.page.tsx', 'page'],
|
|
16
|
+
['.page.ts', 'page'],
|
|
17
|
+
['.page.jsx', 'page'],
|
|
18
|
+
['.page.js', 'page'],
|
|
19
|
+
['.get.tsx', 'get'],
|
|
20
|
+
['.get.ts', 'get'],
|
|
21
|
+
['.get.jsx', 'get'],
|
|
22
|
+
['.get.js', 'get'],
|
|
23
|
+
['.post.tsx', 'post'],
|
|
24
|
+
['.post.ts', 'post'],
|
|
25
|
+
['.post.jsx', 'post'],
|
|
26
|
+
['.post.js', 'post'],
|
|
27
|
+
['.put.tsx', 'put'],
|
|
28
|
+
['.put.ts', 'put'],
|
|
29
|
+
['.put.jsx', 'put'],
|
|
30
|
+
['.put.js', 'put'],
|
|
31
|
+
['.patch.tsx', 'patch'],
|
|
32
|
+
['.patch.ts', 'patch'],
|
|
33
|
+
['.patch.jsx', 'patch'],
|
|
34
|
+
['.patch.js', 'patch'],
|
|
35
|
+
['.delete.tsx', 'delete'],
|
|
36
|
+
['.delete.ts', 'delete'],
|
|
37
|
+
['.delete.jsx', 'delete'],
|
|
38
|
+
['.delete.js', 'delete'],
|
|
39
|
+
['.head.ts', 'head'],
|
|
40
|
+
['.head.js', 'head'],
|
|
41
|
+
['.options.ts', 'options'],
|
|
42
|
+
['.options.js', 'options'],
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// Files beginning with _ are framework-reserved (layouts, middleware, etc.)
|
|
46
|
+
const LAYOUT_NAMES = new Set(['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'])
|
|
47
|
+
|
|
48
|
+
const ERROR_PAGE_NAMES = new Set(['_error.tsx', '_error.ts', '_error.jsx', '_error.js'])
|
|
49
|
+
|
|
50
|
+
const MIDDLEWARE_NAMES = new Set([
|
|
51
|
+
'_middleware.tsx',
|
|
52
|
+
'_middleware.ts',
|
|
53
|
+
'_middleware.jsx',
|
|
54
|
+
'_middleware.js',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
function resolveRouteSuffix(
|
|
58
|
+
filename: string,
|
|
59
|
+
suffixMap: [string, RouteType][],
|
|
60
|
+
): [stripped: string, type: RouteType] | null {
|
|
61
|
+
for (const [suffix, type] of suffixMap) {
|
|
62
|
+
if (filename.endsWith(suffix)) return [filename.slice(0, -suffix.length), type]
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function segmentToParam(segment: string): {
|
|
68
|
+
pattern: string
|
|
69
|
+
param: string | null
|
|
70
|
+
} {
|
|
71
|
+
const catchAll = segment.match(/^\[\.\.\.([^\]]+)\]$/)
|
|
72
|
+
if (catchAll) return { pattern: `*${catchAll[1]}`, param: catchAll[1] }
|
|
73
|
+
const m = segment.match(/^\[([^\]]+)\]$/)
|
|
74
|
+
if (m) return { pattern: `:${m[1]}`, param: m[1] }
|
|
75
|
+
return { pattern: segment, param: null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fileToUrlPattern(rel: string): {
|
|
79
|
+
urlPattern: string
|
|
80
|
+
params: string[]
|
|
81
|
+
} {
|
|
82
|
+
const params: string[] = []
|
|
83
|
+
const segments = rel.split('/').map((seg) => {
|
|
84
|
+
const { pattern, param } = segmentToParam(seg)
|
|
85
|
+
if (param) params.push(param)
|
|
86
|
+
return pattern
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (segments.at(-1) === 'index') segments.pop()
|
|
90
|
+
|
|
91
|
+
const raw = `/${segments.join('/')}`
|
|
92
|
+
return { urlPattern: raw.replace(/\/+$/, '') || '/', params }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function walk(dir: string, files: string[] = []): Promise<string[]> {
|
|
96
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const full = join(dir, entry.name)
|
|
99
|
+
entry.isDirectory() ? await walk(full, files) : files.push(full)
|
|
100
|
+
}
|
|
101
|
+
return files
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Recursively scan `islandsDir` for client island component files.
|
|
106
|
+
* Files beginning with `_` are ignored. Returns an empty array when the
|
|
107
|
+
* directory does not exist.
|
|
108
|
+
*/
|
|
109
|
+
export async function scanIslands(islandsDir: string): Promise<IslandFile[]> {
|
|
110
|
+
if (!existsSync(islandsDir)) return []
|
|
111
|
+
|
|
112
|
+
const islands: IslandFile[] = []
|
|
113
|
+
|
|
114
|
+
async function walkIslands(dir: string): Promise<void> {
|
|
115
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const full = join(dir, entry.name)
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
await walkIslands(full)
|
|
120
|
+
} else if (/\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.startsWith('_')) {
|
|
121
|
+
const id = entry.name.replace(/\.(tsx?|jsx?)$/, '')
|
|
122
|
+
islands.push({ filePath: full, id })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await walkIslands(islandsDir)
|
|
128
|
+
return islands
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Which HTTP methods does each route type respond to?
|
|
132
|
+
// Mirrors the METHOD_TYPES map in handler.ts, inverted per type.
|
|
133
|
+
const TYPE_METHODS: Record<RouteType, string[]> = {
|
|
134
|
+
page: ['GET', 'HEAD', 'POST'], // POST only when an `action` export is present, but we warn conservatively
|
|
135
|
+
get: ['GET', 'HEAD'], // HEAD checks 'get' in its allowed list
|
|
136
|
+
post: ['POST'],
|
|
137
|
+
put: ['PUT'],
|
|
138
|
+
patch: ['PATCH'],
|
|
139
|
+
delete: ['DELETE'],
|
|
140
|
+
head: ['HEAD'],
|
|
141
|
+
options: ['OPTIONS'],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function checkConflicts(routes: RouteFile[], routesDir: string): void {
|
|
145
|
+
// Group routes by URL pattern — conflicts only possible within the same pattern.
|
|
146
|
+
const byPattern = new Map<string, RouteFile[]>()
|
|
147
|
+
for (const route of routes) {
|
|
148
|
+
const group = byPattern.get(route.urlPattern) ?? []
|
|
149
|
+
group.push(route)
|
|
150
|
+
byPattern.set(route.urlPattern, group)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const [pattern, group] of byPattern) {
|
|
154
|
+
if (group.length < 2) continue
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < group.length; i++) {
|
|
157
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
158
|
+
const a = group[i]
|
|
159
|
+
const b = group[j]
|
|
160
|
+
const overlap = TYPE_METHODS[a.type].filter((m) => TYPE_METHODS[b.type].includes(m))
|
|
161
|
+
if (overlap.length === 0) continue
|
|
162
|
+
|
|
163
|
+
const aRel = relative(routesDir, a.filePath)
|
|
164
|
+
const bRel = relative(routesDir, b.filePath)
|
|
165
|
+
|
|
166
|
+
if (a.type === b.type) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[davaux] warning: duplicate ${a.type} route at ${pattern}\n` +
|
|
169
|
+
` ${aRel}\n` +
|
|
170
|
+
` ${bRel}\n` +
|
|
171
|
+
` Only the first will be used.`,
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
// Explicit method routes sort before page routes, so they always win.
|
|
175
|
+
const winner = a.type !== 'page' ? aRel : bRel
|
|
176
|
+
console.warn(
|
|
177
|
+
`[davaux] warning: ${overlap.join(' + ')} ${pattern} matched by both ${aRel} (${a.type}) and ${bRel} (${b.type}) — ${winner} takes priority`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Recursively scan `routesDir` for route, layout, middleware, and error-page files.
|
|
187
|
+
* Routes are sorted so static segments beat dynamic segments, and explicit
|
|
188
|
+
* HTTP-method routes beat page routes at the same URL pattern.
|
|
189
|
+
*
|
|
190
|
+
* @param extraSuffixes - Additional `[suffix, RouteType]` pairs contributed by
|
|
191
|
+
* plugins (e.g. `[['.page.md', 'page']]` from `@davaux/markdown`).
|
|
192
|
+
*/
|
|
193
|
+
export async function scanRoutes(
|
|
194
|
+
routesDir: string,
|
|
195
|
+
extraSuffixes: [string, RouteType][] = [],
|
|
196
|
+
): Promise<ScanResult> {
|
|
197
|
+
const suffixMap: [string, RouteType][] = [...SUFFIX_MAP, ...extraSuffixes]
|
|
198
|
+
const allFiles = await walk(routesDir)
|
|
199
|
+
const routes: RouteFile[] = []
|
|
200
|
+
const layouts: LayoutFile[] = []
|
|
201
|
+
const middlewares: MiddlewareFile[] = []
|
|
202
|
+
let errorPage: string | undefined
|
|
203
|
+
|
|
204
|
+
for (const filePath of allFiles) {
|
|
205
|
+
const rel = relative(routesDir, filePath)
|
|
206
|
+
const name = basename(rel)
|
|
207
|
+
|
|
208
|
+
// Framework-reserved file: layout
|
|
209
|
+
if (LAYOUT_NAMES.has(name)) {
|
|
210
|
+
layouts.push({ filePath, dirPath: dirname(filePath) })
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Framework-reserved file: error page (first one wins)
|
|
215
|
+
if (ERROR_PAGE_NAMES.has(name)) {
|
|
216
|
+
errorPage ??= filePath
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Framework-reserved file: scoped middleware
|
|
221
|
+
if (MIDDLEWARE_NAMES.has(name)) {
|
|
222
|
+
middlewares.push({ filePath, dirPath: dirname(filePath) })
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Skip other underscore-prefixed files silently
|
|
227
|
+
if (name.startsWith('_')) continue
|
|
228
|
+
|
|
229
|
+
const resolved = resolveRouteSuffix(rel, suffixMap)
|
|
230
|
+
if (!resolved) {
|
|
231
|
+
// Warn about .tsx files sitting directly in the routes root — they look like
|
|
232
|
+
// components that belong in src/components/ rather than src/routes/.
|
|
233
|
+
if (name.endsWith('.tsx') && dirname(filePath) === routesDir) {
|
|
234
|
+
console.warn(
|
|
235
|
+
`[davaux] warning: ${rel} is not a route file and will be ignored.\n` +
|
|
236
|
+
` If this is a shared component, move it to src/components/ or co-locate it\n` +
|
|
237
|
+
` in a subdirectory alongside the routes that use it.`,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const [stripped, type] = resolved
|
|
244
|
+
const { urlPattern, params } = fileToUrlPattern(stripped)
|
|
245
|
+
routes.push({ filePath, urlPattern, type, params })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
checkConflicts(routes, routesDir)
|
|
249
|
+
|
|
250
|
+
// Explicit method routes (post, get, put, …) beat page routes at the same URL.
|
|
251
|
+
// This matters for POST: both `login.post.ts` and `login.page.tsx` (with defineAction)
|
|
252
|
+
// resolve to `/login`, but the explicit file should always win.
|
|
253
|
+
const TYPE_PRIORITY: Record<string, number> = { page: 1 } // all explicit methods default to 0
|
|
254
|
+
|
|
255
|
+
// Static segments before dynamic before catch-all; shorter paths before longer; explicit methods before pages.
|
|
256
|
+
const dynamicWeight = (pattern: string) => {
|
|
257
|
+
const segs = pattern.split('/')
|
|
258
|
+
if (segs.some((s) => s.startsWith('*'))) return 1000
|
|
259
|
+
return segs.filter((s) => s.startsWith(':')).length
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
routes.sort((a, b) => {
|
|
263
|
+
const ad = dynamicWeight(a.urlPattern)
|
|
264
|
+
const bd = dynamicWeight(b.urlPattern)
|
|
265
|
+
if (ad !== bd) return ad - bd
|
|
266
|
+
if (a.urlPattern.length !== b.urlPattern.length)
|
|
267
|
+
return a.urlPattern.length - b.urlPattern.length
|
|
268
|
+
return (TYPE_PRIORITY[a.type] ?? 0) - (TYPE_PRIORITY[b.type] ?? 0)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return { routes, layouts, middlewares, errorPage }
|
|
272
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
2
|
+
import type { CompiledApp } from '../router/handler.js'
|
|
3
|
+
import { dispatch } from '../router/handler.js'
|
|
4
|
+
|
|
5
|
+
/** Options for `startServer()`. */
|
|
6
|
+
export interface ServerOptions {
|
|
7
|
+
/** TCP port to listen on. Default: `3000` */
|
|
8
|
+
port?: number
|
|
9
|
+
/** Hostname or IP address to bind to. Default: `'localhost'` */
|
|
10
|
+
hostname?: string
|
|
11
|
+
/**
|
|
12
|
+
* Called before the Davaux dispatcher on every request.
|
|
13
|
+
* Return `true` to signal that the request was fully handled (the dispatcher is skipped).
|
|
14
|
+
*/
|
|
15
|
+
onRequest?: (req: IncomingMessage, res: ServerResponse) => boolean | Promise<boolean>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Start the HTTP server and begin accepting requests.
|
|
20
|
+
*
|
|
21
|
+
* @param app - A compiled app from `buildApp()`, or a factory function that returns one
|
|
22
|
+
* (useful in development when the app is rebuilt between requests).
|
|
23
|
+
* @returns The underlying `http.Server` instance.
|
|
24
|
+
*/
|
|
25
|
+
export function startServer(app: CompiledApp | (() => CompiledApp), options: ServerOptions = {}) {
|
|
26
|
+
const { port = 3000, hostname = 'localhost', onRequest } = options
|
|
27
|
+
const getApp = typeof app === 'function' ? app : () => app
|
|
28
|
+
|
|
29
|
+
const server = createServer(async (req, res) => {
|
|
30
|
+
if (onRequest) {
|
|
31
|
+
const handled = await onRequest(req, res)
|
|
32
|
+
if (handled) return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dispatch(req, res, getApp()).catch((err) => {
|
|
36
|
+
console.error('[davaux] Unhandled dispatch error:', err)
|
|
37
|
+
if (!res.headersSent) {
|
|
38
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
39
|
+
res.end('Internal Server Error')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
server.listen(port, hostname, () => {
|
|
45
|
+
console.log(`\n davaux http://${hostname}:${port}\n`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return server
|
|
49
|
+
}
|
package/src/signal.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Server-side stubs — synchronous values, no reactive graph.
|
|
2
|
+
// The reactive implementations in davaux/client are used inside islands.
|
|
3
|
+
|
|
4
|
+
/** A reactive signal: a `[getter, setter]` tuple. On the server this is backed by a plain variable. */
|
|
5
|
+
export type Signal<T> = [get: () => T, set: (value: T) => void]
|
|
6
|
+
|
|
7
|
+
/** A read-only derived value: a zero-argument getter that returns the current result. */
|
|
8
|
+
export type ReadonlySignal<T> = () => T
|
|
9
|
+
|
|
10
|
+
/** Server-side stub — returns a getter/setter pair backed by a plain variable. */
|
|
11
|
+
export function createSignal<T>(value: T): Signal<T> {
|
|
12
|
+
return [
|
|
13
|
+
() => value,
|
|
14
|
+
(v: T) => {
|
|
15
|
+
value = v
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Server-side stub — runs `fn` once synchronously, no reactive tracking. */
|
|
21
|
+
export function createEffect(fn: () => void): void {
|
|
22
|
+
fn()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Server-side stub — evaluates `fn` immediately and returns a static getter. */
|
|
26
|
+
export function createMemo<T>(fn: () => T): ReadonlySignal<T> {
|
|
27
|
+
const result = fn()
|
|
28
|
+
return () => result
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Server-side stub — calls `fn` directly with no tracking context. */
|
|
32
|
+
export function untrack<T>(fn: () => T): T {
|
|
33
|
+
return fn()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Server-side stub — calls `fn` directly with no batching. */
|
|
37
|
+
export function batch<T>(fn: () => T): T {
|
|
38
|
+
return fn()
|
|
39
|
+
}
|
package/src/ssg.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { cp, mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
+
import { dirname, join, relative } from 'node:path'
|
|
5
|
+
import { frameworkWarn } from './errors.js'
|
|
6
|
+
import { buildApp, dispatch } from './router/handler.js'
|
|
7
|
+
import type { GetStaticPathsFn, ScanResult } from './types.js'
|
|
8
|
+
|
|
9
|
+
/** Options for `generateStatic()`. All directory paths should be absolute. */
|
|
10
|
+
export interface SsgOptions {
|
|
11
|
+
/** Output directory for generated HTML files. */
|
|
12
|
+
outDir: string
|
|
13
|
+
/** Build output directory — used to locate `_davaux` assets to copy across. */
|
|
14
|
+
distDir: string
|
|
15
|
+
/** Public directory to copy into `outDir` after generation. */
|
|
16
|
+
publicDir: string
|
|
17
|
+
/** Scanned route manifest from `scanRoutes()`. */
|
|
18
|
+
scan: ScanResult
|
|
19
|
+
/** Client JS bundle URLs injected into every generated page. */
|
|
20
|
+
clientScripts: string[]
|
|
21
|
+
/** Client CSS URLs injected into every generated page. */
|
|
22
|
+
clientStylesheets: string[]
|
|
23
|
+
/** Compiled path to `src/middleware.ts` — applied to every SSG render request. */
|
|
24
|
+
appMiddlewarePath?: string
|
|
25
|
+
/**
|
|
26
|
+
* Controls the URL-to-filename mapping.
|
|
27
|
+
* - `'always'` — `/about` → `about/index.html` (default)
|
|
28
|
+
* - `'never'` — `/about` → `about.html`
|
|
29
|
+
*/
|
|
30
|
+
trailingSlash?: 'always' | 'never'
|
|
31
|
+
/** Base path for subdirectory deployments (e.g. `'/docs'`). */
|
|
32
|
+
basePath?: string
|
|
33
|
+
/** Generate a `404.html` from `_error.tsx`. Default: `true` when `_error.tsx` is present. */
|
|
34
|
+
notFound?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Generate a `sitemap.xml`. Pass `false` to disable, or `{ baseUrl }` to enable.
|
|
37
|
+
* `baseUrl` is prepended to every URL (e.g. `'https://example.com'`).
|
|
38
|
+
*/
|
|
39
|
+
sitemap?: false | { baseUrl: string }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeMockReq(pathname: string): IncomingMessage {
|
|
43
|
+
return {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
url: pathname,
|
|
46
|
+
headers: { host: 'localhost', cookie: '' },
|
|
47
|
+
on(event: string, listener: (...args: unknown[]) => void) {
|
|
48
|
+
// Immediately signal end of body so ctx.json()/ctx.form() resolve empty
|
|
49
|
+
if (event === 'end') setImmediate(() => listener())
|
|
50
|
+
return this
|
|
51
|
+
},
|
|
52
|
+
removeListener() {
|
|
53
|
+
return this
|
|
54
|
+
},
|
|
55
|
+
} as unknown as IncomingMessage
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeMockRes(): [ServerResponse, () => { status: number; html: string }] {
|
|
59
|
+
let status = 200
|
|
60
|
+
let body = ''
|
|
61
|
+
let sent = false
|
|
62
|
+
let ended = false
|
|
63
|
+
const hdrs: Record<string, string | string[]> = {}
|
|
64
|
+
|
|
65
|
+
const res = {
|
|
66
|
+
get statusCode() {
|
|
67
|
+
return status
|
|
68
|
+
},
|
|
69
|
+
set statusCode(v: number) {
|
|
70
|
+
status = v
|
|
71
|
+
},
|
|
72
|
+
get headersSent() {
|
|
73
|
+
return sent
|
|
74
|
+
},
|
|
75
|
+
get writableEnded() {
|
|
76
|
+
return ended
|
|
77
|
+
},
|
|
78
|
+
writeHead(code: number) {
|
|
79
|
+
status = code
|
|
80
|
+
sent = true
|
|
81
|
+
},
|
|
82
|
+
end(data?: string | Buffer) {
|
|
83
|
+
if (data != null) body = typeof data === 'string' ? data : data.toString('utf-8')
|
|
84
|
+
ended = true
|
|
85
|
+
},
|
|
86
|
+
getHeader: (name: string) => hdrs[name.toLowerCase()],
|
|
87
|
+
setHeader(name: string, value: string | string[]) {
|
|
88
|
+
hdrs[name.toLowerCase()] = value
|
|
89
|
+
},
|
|
90
|
+
} as unknown as ServerResponse
|
|
91
|
+
|
|
92
|
+
return [res, () => ({ status, html: body })]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function urlToFile(
|
|
96
|
+
outDir: string,
|
|
97
|
+
url: string,
|
|
98
|
+
trailingSlash: 'always' | 'never' = 'always',
|
|
99
|
+
): string {
|
|
100
|
+
const trimmed = url.replace(/^\//, '').replace(/\/$/, '')
|
|
101
|
+
if (!trimmed) return join(outDir, 'index.html')
|
|
102
|
+
if (trailingSlash === 'never') return join(outDir, `${trimmed}.html`)
|
|
103
|
+
return join(outDir, trimmed, 'index.html')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toSitemapUrl(
|
|
107
|
+
baseUrl: string,
|
|
108
|
+
basePath: string,
|
|
109
|
+
urlPath: string,
|
|
110
|
+
trailingSlash: 'always' | 'never',
|
|
111
|
+
): string {
|
|
112
|
+
const base = baseUrl.replace(/\/$/, '')
|
|
113
|
+
const bp = basePath.replace(/\/$/, '')
|
|
114
|
+
const trimmed = urlPath.replace(/\/$/, '')
|
|
115
|
+
const path = !trimmed ? '/' : trailingSlash === 'never' ? trimmed : `${trimmed}/`
|
|
116
|
+
return `${base}${bp}${path}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pre-render all static pages in the route manifest and write them to `outDir`.
|
|
121
|
+
* Pages with `export const prerender = false` are skipped. Dynamic routes must
|
|
122
|
+
* export `getStaticPaths` to supply the parameter sets to render.
|
|
123
|
+
*
|
|
124
|
+
* Also generates `404.html` (from `_error.tsx`) and `sitemap.xml` when
|
|
125
|
+
* configured, and copies `_davaux` assets and `public/` into `outDir`.
|
|
126
|
+
*/
|
|
127
|
+
export async function generateStatic(opts: SsgOptions): Promise<void> {
|
|
128
|
+
const {
|
|
129
|
+
outDir,
|
|
130
|
+
distDir,
|
|
131
|
+
publicDir,
|
|
132
|
+
scan,
|
|
133
|
+
appMiddlewarePath,
|
|
134
|
+
trailingSlash = 'always',
|
|
135
|
+
basePath = '',
|
|
136
|
+
notFound,
|
|
137
|
+
sitemap,
|
|
138
|
+
} = opts
|
|
139
|
+
|
|
140
|
+
// Prepend basePath to injected asset URLs so the rendered HTML references the correct paths
|
|
141
|
+
const clientScripts = opts.clientScripts.map((s) => `${basePath}${s}`)
|
|
142
|
+
const clientStylesheets = opts.clientStylesheets.map((s) => `${basePath}${s}`)
|
|
143
|
+
|
|
144
|
+
const app = buildApp(scan, false, clientScripts, clientStylesheets, appMiddlewarePath, basePath)
|
|
145
|
+
|
|
146
|
+
// Collect all URL paths to render
|
|
147
|
+
const toRender: string[] = []
|
|
148
|
+
|
|
149
|
+
for (const route of scan.routes) {
|
|
150
|
+
if (route.type !== 'page') continue
|
|
151
|
+
|
|
152
|
+
const mod = (await import(route.filePath)) as Record<string, unknown>
|
|
153
|
+
if (mod.prerender === false) continue
|
|
154
|
+
|
|
155
|
+
if (route.params.length === 0) {
|
|
156
|
+
toRender.push(route.urlPattern)
|
|
157
|
+
} else {
|
|
158
|
+
const fn = mod.getStaticPaths as GetStaticPathsFn | undefined
|
|
159
|
+
if (typeof fn !== 'function') {
|
|
160
|
+
frameworkWarn(
|
|
161
|
+
`[ssg] skipping ${route.urlPattern} — dynamic route has no getStaticPaths export`,
|
|
162
|
+
`Add a getStaticPaths export to ${relative(process.cwd(), route.filePath)}:\n\n` +
|
|
163
|
+
` export const getStaticPaths = async () => [\n` +
|
|
164
|
+
` { params: { /* param: value */ } },\n` +
|
|
165
|
+
` ]`,
|
|
166
|
+
)
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
const paths = await fn()
|
|
170
|
+
for (const sp of paths) {
|
|
171
|
+
let url = route.urlPattern
|
|
172
|
+
for (const [k, v] of Object.entries(sp.params)) url = url.replace(`:${k}`, String(v))
|
|
173
|
+
toRender.push(url)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await mkdir(outDir, { recursive: true })
|
|
179
|
+
|
|
180
|
+
let count = 0
|
|
181
|
+
const renderedUrls: string[] = []
|
|
182
|
+
|
|
183
|
+
for (const url of toRender) {
|
|
184
|
+
try {
|
|
185
|
+
const req = makeMockReq(url)
|
|
186
|
+
const [res, result] = makeMockRes()
|
|
187
|
+
await dispatch(req, res, app)
|
|
188
|
+
const { status, html } = result()
|
|
189
|
+
|
|
190
|
+
if (status >= 300) {
|
|
191
|
+
frameworkWarn(`[ssg] ${url} → HTTP ${status} — skipping`)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const file = urlToFile(outDir, url, trailingSlash)
|
|
196
|
+
await mkdir(dirname(file), { recursive: true })
|
|
197
|
+
const full = html.trimStart().startsWith('<!') ? html : `<!DOCTYPE html>${html}`
|
|
198
|
+
await writeFile(file, full, 'utf-8')
|
|
199
|
+
renderedUrls.push(url)
|
|
200
|
+
count++
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
203
|
+
frameworkWarn(`[ssg] ${url} failed to render — skipping\n ${detail}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Generate 404.html for static hosts (Netlify, GitHub Pages, etc.)
|
|
208
|
+
const shouldGenerateNotFound = notFound !== false && scan.errorPage != null
|
|
209
|
+
if (shouldGenerateNotFound) {
|
|
210
|
+
try {
|
|
211
|
+
const req = makeMockReq('/__davaux_404__')
|
|
212
|
+
const [res, result] = makeMockRes()
|
|
213
|
+
await dispatch(req, res, app)
|
|
214
|
+
const { html } = result()
|
|
215
|
+
const full = html.trimStart().startsWith('<!') ? html : `<!DOCTYPE html>${html}`
|
|
216
|
+
await writeFile(join(outDir, '404.html'), full, 'utf-8')
|
|
217
|
+
console.log('[davaux] Generated 404.html')
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
220
|
+
frameworkWarn(`[ssg] Failed to render 404.html — skipping\n ${detail}`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Generate sitemap.xml
|
|
225
|
+
if (sitemap !== false && sitemap != null) {
|
|
226
|
+
const entries = renderedUrls
|
|
227
|
+
.map(
|
|
228
|
+
(url) =>
|
|
229
|
+
` <url><loc>${toSitemapUrl(sitemap.baseUrl, basePath, url, trailingSlash)}</loc></url>`,
|
|
230
|
+
)
|
|
231
|
+
.join('\n')
|
|
232
|
+
const xml =
|
|
233
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
234
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` +
|
|
235
|
+
`${entries}\n` +
|
|
236
|
+
`</urlset>\n`
|
|
237
|
+
await writeFile(join(outDir, 'sitemap.xml'), xml, 'utf-8')
|
|
238
|
+
console.log(`[davaux] Generated sitemap.xml (${renderedUrls.length} URL(s))`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Copy _davaux assets (islands, client bundle, CSS)
|
|
242
|
+
const assetsDir = join(distDir, '_davaux')
|
|
243
|
+
if (existsSync(assetsDir)) {
|
|
244
|
+
await cp(assetsDir, join(outDir, '_davaux'), { recursive: true })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Copy public/ directory if present
|
|
248
|
+
if (existsSync(publicDir)) {
|
|
249
|
+
await cp(publicDir, outDir, { recursive: true })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(`[davaux] Generated ${count} static page(s) → ${relative(process.cwd(), outDir)}/`)
|
|
253
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
describe('form actions', () => {
|
|
6
|
+
test('GET renders the page normally', async () => {
|
|
7
|
+
const app = makeApp({ routes: [route('action.page.ts', '/')] })
|
|
8
|
+
const { status, body } = await request(app, { url: '/' })
|
|
9
|
+
assert.equal(status, 200)
|
|
10
|
+
assert.ok(body.includes('form'))
|
|
11
|
+
assert.ok(!body.includes('submitted:'))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('POST with action rerenders page with actionResult', async () => {
|
|
15
|
+
const app = makeApp({ routes: [route('action.page.ts', '/')] })
|
|
16
|
+
const { status, body } = await request(app, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
url: '/',
|
|
19
|
+
body: 'name=Alice',
|
|
20
|
+
})
|
|
21
|
+
assert.equal(status, 200)
|
|
22
|
+
assert.ok(body.includes('submitted:Alice'))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('POST to page without action returns 405', async () => {
|
|
26
|
+
const app = makeApp({ routes: [route('index.page.ts', '/')] })
|
|
27
|
+
const { status } = await request(app, { method: 'POST', url: '/' })
|
|
28
|
+
assert.equal(status, 405)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('action can parse multiple form fields', async () => {
|
|
32
|
+
const app = makeApp({ routes: [route('action.page.ts', '/')] })
|
|
33
|
+
const { body } = await request(app, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
url: '/',
|
|
36
|
+
body: 'name=Bob&extra=ignored',
|
|
37
|
+
})
|
|
38
|
+
assert.ok(body.includes('submitted:Bob'))
|
|
39
|
+
})
|
|
40
|
+
})
|