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,83 @@
|
|
|
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('ctx.send()', () => {
|
|
6
|
+
test('responds with custom content-type and body', async () => {
|
|
7
|
+
const app = makeApp({ routes: [route('api/xml.get.ts', '/api/xml', 'get')] })
|
|
8
|
+
const { status, body, headers } = await request(app, { url: '/api/xml' })
|
|
9
|
+
assert.equal(status, 200)
|
|
10
|
+
assert.equal(body, '<root><item>hello</item></root>')
|
|
11
|
+
assert.equal(headers['content-type'], 'application/xml; charset=utf-8')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('ctx.send() with custom status code', async () => {
|
|
15
|
+
const app = makeApp({ routes: [route('api/xml.get.ts', '/api/xml', 'get')] })
|
|
16
|
+
const { status } = await request(app, { url: '/api/xml' })
|
|
17
|
+
assert.equal(status, 200)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('ctx.formAll()', () => {
|
|
22
|
+
const app = makeApp({ routes: [route('api/form-all.post.ts', '/form-all', 'post')] })
|
|
23
|
+
|
|
24
|
+
test('single-value fields are wrapped in arrays', async () => {
|
|
25
|
+
const { status, body } = await request(app, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
url: '/form-all',
|
|
28
|
+
body: 'title=Hello&author=Alice',
|
|
29
|
+
})
|
|
30
|
+
assert.equal(status, 200)
|
|
31
|
+
const data = JSON.parse(body)
|
|
32
|
+
assert.deepEqual(data.title, ['Hello'])
|
|
33
|
+
assert.deepEqual(data.author, ['Alice'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('multi-value fields collect all values', async () => {
|
|
37
|
+
const { status, body } = await request(app, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
url: '/form-all',
|
|
40
|
+
body: 'tag=tech&tag=news&tag=coding',
|
|
41
|
+
})
|
|
42
|
+
assert.equal(status, 200)
|
|
43
|
+
const data = JSON.parse(body)
|
|
44
|
+
assert.deepEqual(data.tag, ['tech', 'news', 'coding'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('mixed single and multi-value fields', async () => {
|
|
48
|
+
const { status, body } = await request(app, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
url: '/form-all',
|
|
51
|
+
body: 'title=Post&tag=a&tag=b',
|
|
52
|
+
})
|
|
53
|
+
assert.equal(status, 200)
|
|
54
|
+
const data = JSON.parse(body)
|
|
55
|
+
assert.deepEqual(data.title, ['Post'])
|
|
56
|
+
assert.deepEqual(data.tag, ['a', 'b'])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('body size limits', () => {
|
|
61
|
+
test('form body within limit succeeds', async () => {
|
|
62
|
+
const app = makeApp({ routes: [route('api/form-limited.post.ts', '/upload', 'post')] })
|
|
63
|
+
const { status, body } = await request(app, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
url: '/upload',
|
|
66
|
+
body: 'name=Alice',
|
|
67
|
+
})
|
|
68
|
+
assert.equal(status, 200)
|
|
69
|
+
const data = JSON.parse(body) as { ok: boolean; keys: number }
|
|
70
|
+
assert.equal(data.ok, true)
|
|
71
|
+
assert.equal(data.keys, 1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('form body exceeding limit returns 413', async () => {
|
|
75
|
+
const app = makeApp({ routes: [route('api/form-limited.post.ts', '/upload', 'post')] })
|
|
76
|
+
const { status } = await request(app, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
url: '/upload',
|
|
79
|
+
body: `name=${'x'.repeat(100)}`, // 105 bytes > 50 byte limit
|
|
80
|
+
})
|
|
81
|
+
assert.equal(status, 413)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { fix, layout, makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
const errorPage = fix('_error.ts')
|
|
6
|
+
|
|
7
|
+
describe('error pages', () => {
|
|
8
|
+
test('unmatched route returns 404 plain text when no error page is configured', async () => {
|
|
9
|
+
const app = makeApp({ routes: [] })
|
|
10
|
+
const { status, body } = await request(app, { url: '/missing' })
|
|
11
|
+
assert.equal(status, 404)
|
|
12
|
+
assert.ok(body.includes('Not Found'))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('unmatched route renders custom error page with status 404', async () => {
|
|
16
|
+
const app = makeApp({ routes: [], errorPage })
|
|
17
|
+
const { status, body } = await request(app, { url: '/missing' })
|
|
18
|
+
assert.equal(status, 404)
|
|
19
|
+
const result = JSON.parse(body) as Record<string, unknown>
|
|
20
|
+
assert.equal(result.status, 404)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('handler that throws renders custom error page with status 500', async () => {
|
|
24
|
+
const app = makeApp({
|
|
25
|
+
routes: [route('throw.page.ts', '/')],
|
|
26
|
+
errorPage,
|
|
27
|
+
})
|
|
28
|
+
const { status, body } = await request(app, { url: '/' })
|
|
29
|
+
assert.equal(status, 500)
|
|
30
|
+
const result = JSON.parse(body) as Record<string, unknown>
|
|
31
|
+
assert.equal(result.status, 500)
|
|
32
|
+
assert.ok(typeof result.message === 'string')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('error page inherits layout when one is configured', async () => {
|
|
36
|
+
const app = makeApp({
|
|
37
|
+
routes: [],
|
|
38
|
+
layouts: [layout('_layout.ts')],
|
|
39
|
+
errorPage,
|
|
40
|
+
})
|
|
41
|
+
const { status, body } = await request(app, { url: '/missing' })
|
|
42
|
+
assert.equal(status, 404)
|
|
43
|
+
// The layout wraps the error page output
|
|
44
|
+
assert.ok(body.includes('<html>'))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('error page receives ctx.state populated by app middleware', async () => {
|
|
48
|
+
const app = makeApp({ routes: [], errorPage }, fix('_global.ts'))
|
|
49
|
+
const { body } = await request(app, { url: '/missing' })
|
|
50
|
+
const result = JSON.parse(body) as Record<string, unknown>
|
|
51
|
+
assert.deepEqual(result.order, ['global'])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineMiddleware } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
// Pushes 'global' onto ctx.state.order so middleware ordering can be verified.
|
|
4
|
+
export default defineMiddleware(async (ctx, next) => {
|
|
5
|
+
if (!ctx.state.order) ctx.state.order = []
|
|
6
|
+
;(ctx.state.order as string[]).push('global')
|
|
7
|
+
await next()
|
|
8
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineLayout } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
// Template literal layout using ${children} directly — no await needed.
|
|
4
|
+
export default defineLayout(({ children, ctx }) => {
|
|
5
|
+
const title = ctx.head.title ?? 'Test'
|
|
6
|
+
return `<html><head><title>${title}</title></head><body>${children}</body></html>`
|
|
7
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineLayout } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
export default defineLayout(async ({ children, ctx }) => {
|
|
4
|
+
const title = ctx.head.title ?? 'Test'
|
|
5
|
+
const content = await children
|
|
6
|
+
return `<html><head><title>${title}</title></head><body>${content}</body></html>`
|
|
7
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineLayout } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
export default defineLayout(async ({ children, ctx }) => {
|
|
4
|
+
const content = await children
|
|
5
|
+
const scripts = ctx.head.scripts.map((s) => `<script src="${s}"></script>`).join('')
|
|
6
|
+
const styles = ctx.head.stylesheets.map((s) => `<link rel="stylesheet" href="${s}">`).join('')
|
|
7
|
+
return `<html><head>${styles}${scripts}</head><body>${content}</body></html>`
|
|
8
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineMiddleware } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
// Pushes 'scoped' onto ctx.state.order so middleware ordering can be verified.
|
|
4
|
+
export default defineMiddleware(async (ctx, next) => {
|
|
5
|
+
if (!ctx.state.order) ctx.state.order = []
|
|
6
|
+
;(ctx.state.order as string[]).push('scoped')
|
|
7
|
+
await next()
|
|
8
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineAction, definePage } from '../../../index.js'
|
|
2
|
+
|
|
3
|
+
export const action = defineAction(async (ctx) => {
|
|
4
|
+
const data = await ctx.form()
|
|
5
|
+
return { submitted: data.name ?? '(none)' }
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export default definePage((ctx) => {
|
|
9
|
+
const result = ctx.state.actionResult as { submitted: string } | undefined
|
|
10
|
+
return result ? `submitted:${result.submitted}` : 'form'
|
|
11
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineHandler } from '../../../../index.js'
|
|
2
|
+
|
|
3
|
+
export default defineHandler(async (ctx) => {
|
|
4
|
+
const shape = ctx.query.get('shape') ?? 'json'
|
|
5
|
+
|
|
6
|
+
if (shape === 'text') {
|
|
7
|
+
return new Response('hello plain', { status: 200, headers: { 'Content-Type': 'text/plain' } })
|
|
8
|
+
}
|
|
9
|
+
if (shape === 'created') {
|
|
10
|
+
return Response.json({ created: true }, { status: 201 })
|
|
11
|
+
}
|
|
12
|
+
if (shape === 'nocontent') {
|
|
13
|
+
return new Response(null, { status: 204 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Response.json({ ok: true })
|
|
17
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineHandler } from '../../../../index.js'
|
|
2
|
+
|
|
3
|
+
export default defineHandler(async (ctx) => {
|
|
4
|
+
const result = await ctx.multipart()
|
|
5
|
+
return {
|
|
6
|
+
fields: result.fields,
|
|
7
|
+
files: Object.fromEntries(
|
|
8
|
+
Object.entries(result.files).map(([k, f]) => [
|
|
9
|
+
k,
|
|
10
|
+
{ filename: f.filename, mimetype: f.mimetype, size: f.data.length },
|
|
11
|
+
]),
|
|
12
|
+
),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineMiddleware } from '../../../../index.js'
|
|
2
|
+
|
|
3
|
+
// Returns 401 when no ?token query param is present.
|
|
4
|
+
export default defineMiddleware(async (ctx, next) => {
|
|
5
|
+
if (!ctx.query.get('token')) {
|
|
6
|
+
ctx.res.writeHead(401, { 'Content-Type': 'text/plain' })
|
|
7
|
+
ctx.res.end('Unauthorized')
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
await next()
|
|
11
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx } from '../../../oml/jsx-runtime.js'
|
|
2
|
+
import { definePage } from '../../../types.js'
|
|
3
|
+
|
|
4
|
+
export default definePage(async (ctx) => {
|
|
5
|
+
const name = ctx.query.get('name') ?? 'World'
|
|
6
|
+
return jsx('div', { class: 'oml-page', children: `Hello, ${name}!` })
|
|
7
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { definePage, defineStaticPaths, type ExtractParams } from '../../../../index.js'
|
|
2
|
+
|
|
3
|
+
export const getStaticPaths = defineStaticPaths(() => [
|
|
4
|
+
{ params: { slug: 'hello' } },
|
|
5
|
+
{ params: { slug: 'world' } },
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export default definePage<ExtractParams<'ssg/[slug]'>>((ctx) => `<h1>${ctx.params.slug}</h1>`)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import type { OmlCacheConfig } from '../config.js'
|
|
5
|
+
import type { CompiledApp } from '../router/handler.js'
|
|
6
|
+
import { buildApp, dispatch } from '../router/handler.js'
|
|
7
|
+
import type { LayoutFile, MiddlewareFile, RouteFile, RouteType, ScanResult } from '../types.js'
|
|
8
|
+
|
|
9
|
+
const FIXTURES = fileURLToPath(new URL('./fixtures/routes', import.meta.url))
|
|
10
|
+
|
|
11
|
+
/** Resolve a path relative to the fixtures/routes directory. */
|
|
12
|
+
export function fix(...parts: string[]): string {
|
|
13
|
+
return join(FIXTURES, ...parts)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Build a RouteFile pointing to a fixture, deriving params from the URL pattern. */
|
|
17
|
+
export function route(file: string, urlPattern: string, type: RouteType = 'page'): RouteFile {
|
|
18
|
+
const params = [
|
|
19
|
+
...[...urlPattern.matchAll(/:(\w+)/g)].map((m) => m[1]),
|
|
20
|
+
...[...urlPattern.matchAll(/\*(\w+)/g)].map((m) => m[1]),
|
|
21
|
+
]
|
|
22
|
+
return { filePath: fix(file), urlPattern, type, params }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Build a LayoutFile pointing to a fixture. */
|
|
26
|
+
export function layout(file: string): LayoutFile {
|
|
27
|
+
const filePath = fix(file)
|
|
28
|
+
return { filePath, dirPath: dirname(filePath) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build a MiddlewareFile pointing to a fixture. */
|
|
32
|
+
export function middleware(file: string): MiddlewareFile {
|
|
33
|
+
const filePath = fix(file)
|
|
34
|
+
return { filePath, dirPath: dirname(filePath) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Construct a minimal ScanResult, defaulting all collections to empty. */
|
|
38
|
+
export function makeScan(opts: Partial<ScanResult> = {}): ScanResult {
|
|
39
|
+
return {
|
|
40
|
+
routes: [],
|
|
41
|
+
layouts: [],
|
|
42
|
+
middlewares: [],
|
|
43
|
+
errorPage: undefined,
|
|
44
|
+
...opts,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build a CompiledApp from a partial ScanResult and an optional app middleware path. */
|
|
49
|
+
export function makeApp(
|
|
50
|
+
scan: Partial<ScanResult> = {},
|
|
51
|
+
appMiddlewarePath?: string,
|
|
52
|
+
omlCacheConfig?: OmlCacheConfig,
|
|
53
|
+
): CompiledApp {
|
|
54
|
+
return buildApp(makeScan(scan), false, [], [], appMiddlewarePath, '', false, omlCacheConfig)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Mock request ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function mockReq(options: {
|
|
60
|
+
method?: string
|
|
61
|
+
url: string
|
|
62
|
+
headers?: Record<string, string>
|
|
63
|
+
body?: string
|
|
64
|
+
}): IncomingMessage {
|
|
65
|
+
const { method = 'GET', url, headers = {}, body = '' } = options
|
|
66
|
+
return {
|
|
67
|
+
method,
|
|
68
|
+
url,
|
|
69
|
+
headers: { host: 'localhost', ...headers },
|
|
70
|
+
on(event: string, listener: (...args: unknown[]) => void) {
|
|
71
|
+
if (event === 'data' && body) setImmediate(() => listener(Buffer.from(body)))
|
|
72
|
+
if (event === 'end') setImmediate(() => listener())
|
|
73
|
+
return this
|
|
74
|
+
},
|
|
75
|
+
removeListener() {
|
|
76
|
+
return this
|
|
77
|
+
},
|
|
78
|
+
destroy() {
|
|
79
|
+
return this
|
|
80
|
+
},
|
|
81
|
+
} as unknown as IncomingMessage
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Mock response ────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function mockRes() {
|
|
87
|
+
let status = 200
|
|
88
|
+
let body = ''
|
|
89
|
+
let sent = false
|
|
90
|
+
let ended = false
|
|
91
|
+
const hdrs: Record<string, string | string[]> = {}
|
|
92
|
+
|
|
93
|
+
const res = {
|
|
94
|
+
get statusCode() {
|
|
95
|
+
return status
|
|
96
|
+
},
|
|
97
|
+
set statusCode(v: number) {
|
|
98
|
+
status = v
|
|
99
|
+
},
|
|
100
|
+
get headersSent() {
|
|
101
|
+
return sent
|
|
102
|
+
},
|
|
103
|
+
get writableEnded() {
|
|
104
|
+
return ended
|
|
105
|
+
},
|
|
106
|
+
writeHead(code: number, h?: Record<string, string>) {
|
|
107
|
+
status = code
|
|
108
|
+
sent = true
|
|
109
|
+
if (h) for (const [k, v] of Object.entries(h)) hdrs[k.toLowerCase()] = v
|
|
110
|
+
},
|
|
111
|
+
end(data?: string | Buffer) {
|
|
112
|
+
if (data != null) body = typeof data === 'string' ? data : data.toString('utf-8')
|
|
113
|
+
ended = true
|
|
114
|
+
},
|
|
115
|
+
getHeader: (name: string) => hdrs[name.toLowerCase()],
|
|
116
|
+
setHeader(name: string, value: string | string[]) {
|
|
117
|
+
hdrs[name.toLowerCase()] = value
|
|
118
|
+
},
|
|
119
|
+
} as unknown as ServerResponse
|
|
120
|
+
|
|
121
|
+
return { res, result: () => ({ status, body, headers: hdrs }) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Convenience dispatch helper ──────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** Run a request through an app and return the captured response. */
|
|
127
|
+
export async function request(app: CompiledApp, options: Parameters<typeof mockReq>[0]) {
|
|
128
|
+
const req = mockReq(options)
|
|
129
|
+
const { res, result } = mockRes()
|
|
130
|
+
await dispatch(req, res, app)
|
|
131
|
+
return result()
|
|
132
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { layout, makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
describe('layouts', () => {
|
|
6
|
+
test('layout wraps page content with outer HTML shell', async () => {
|
|
7
|
+
const app = makeApp({
|
|
8
|
+
routes: [route('index.page.ts', '/')],
|
|
9
|
+
layouts: [layout('_layout.ts')],
|
|
10
|
+
})
|
|
11
|
+
const { status, body } = await request(app, { url: '/' })
|
|
12
|
+
assert.equal(status, 200)
|
|
13
|
+
assert.ok(body.includes('<html>'))
|
|
14
|
+
assert.ok(body.includes('</html>'))
|
|
15
|
+
assert.ok(body.includes('Home'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('page content appears inside layout body', async () => {
|
|
19
|
+
const app = makeApp({
|
|
20
|
+
routes: [route('about.page.ts', '/about')],
|
|
21
|
+
layouts: [layout('_layout.ts')],
|
|
22
|
+
})
|
|
23
|
+
const { body } = await request(app, { url: '/about' })
|
|
24
|
+
// The layout fixture wraps with <body>...</body>
|
|
25
|
+
assert.ok(body.includes('<body>'))
|
|
26
|
+
assert.ok(body.includes('<h1>About</h1>'))
|
|
27
|
+
assert.ok(body.includes('</body>'))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ctx.head properties set by page are readable in layout', async () => {
|
|
31
|
+
const app = makeApp({
|
|
32
|
+
routes: [route('about.page.ts', '/about')],
|
|
33
|
+
layouts: [layout('_layout.ts')],
|
|
34
|
+
})
|
|
35
|
+
const { body } = await request(app, { url: '/about' })
|
|
36
|
+
// The layout fixture renders ctx.head.title in <title>
|
|
37
|
+
assert.ok(body.includes('<title>About</title>'))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('layout does not apply to routes outside its directory', async () => {
|
|
41
|
+
// layout('_layout.ts') covers the root fixtures dir
|
|
42
|
+
// If we only register it for /auth, it should not wrap the root route
|
|
43
|
+
// We test the inverse: root layout covers all descendant routes
|
|
44
|
+
const app = makeApp({
|
|
45
|
+
routes: [route('auth/protected.page.ts', '/auth/protected')],
|
|
46
|
+
layouts: [layout('_layout.ts')],
|
|
47
|
+
})
|
|
48
|
+
const { body } = await request(app, { url: '/auth/protected?token=x' })
|
|
49
|
+
// The root layout covers /auth/protected because it's a descendant
|
|
50
|
+
assert.ok(body.includes('<html>'))
|
|
51
|
+
assert.ok(body.includes('Protected'))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('template literal layout renders children without await', async () => {
|
|
55
|
+
const app = makeApp({
|
|
56
|
+
routes: [route('about.page.ts', '/about')],
|
|
57
|
+
layouts: [layout('_layout-template.ts')],
|
|
58
|
+
})
|
|
59
|
+
const { status, body } = await request(app, { url: '/about' })
|
|
60
|
+
assert.equal(status, 200)
|
|
61
|
+
// Children must be injected as raw HTML, not "[object Promise]"
|
|
62
|
+
assert.ok(body.includes('<h1>About</h1>'), 'page content rendered')
|
|
63
|
+
assert.ok(!body.includes('[object'), 'no Promise leak into output')
|
|
64
|
+
assert.ok(body.includes('<title>About</title>'), 'ctx.head.title readable')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('response includes DOCTYPE when layout provides full HTML', async () => {
|
|
68
|
+
const app = makeApp({
|
|
69
|
+
routes: [route('index.page.ts', '/')],
|
|
70
|
+
layouts: [layout('_layout.ts')],
|
|
71
|
+
})
|
|
72
|
+
const { body } = await request(app, { url: '/' })
|
|
73
|
+
// sendPage prepends DOCTYPE if not present; the layout starts with <html>
|
|
74
|
+
assert.ok(body.startsWith('<!DOCTYPE html>') || body.includes('<html>'))
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { fix, makeApp, middleware, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
describe('middleware', () => {
|
|
6
|
+
test('scoped middleware runs before the route handler', async () => {
|
|
7
|
+
const app = makeApp({
|
|
8
|
+
routes: [route('state.page.ts', '/')],
|
|
9
|
+
middlewares: [middleware('_middleware.ts')],
|
|
10
|
+
})
|
|
11
|
+
const { body } = await request(app, { url: '/' })
|
|
12
|
+
const state = JSON.parse(body) as Record<string, unknown>
|
|
13
|
+
assert.deepEqual(state.order, ['scoped'])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('scoped middleware does not run for routes in other directories', async () => {
|
|
17
|
+
// auth/_middleware.ts only covers auth/*, not root routes
|
|
18
|
+
const app = makeApp({
|
|
19
|
+
routes: [route('state.page.ts', '/'), route('auth/protected.page.ts', '/auth/protected')],
|
|
20
|
+
middlewares: [middleware('auth/_middleware.ts')],
|
|
21
|
+
})
|
|
22
|
+
// Root route should not be intercepted by the auth middleware
|
|
23
|
+
const { status } = await request(app, { url: '/' })
|
|
24
|
+
assert.equal(status, 200)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('scoped middleware gates access to routes it covers', async () => {
|
|
28
|
+
const app = makeApp({
|
|
29
|
+
routes: [route('auth/protected.page.ts', '/auth/protected')],
|
|
30
|
+
middlewares: [middleware('auth/_middleware.ts')],
|
|
31
|
+
})
|
|
32
|
+
// Without token → 401
|
|
33
|
+
const denied = await request(app, { url: '/auth/protected' })
|
|
34
|
+
assert.equal(denied.status, 401)
|
|
35
|
+
|
|
36
|
+
// With token → 200
|
|
37
|
+
const allowed = await request(app, { url: '/auth/protected?token=secret' })
|
|
38
|
+
assert.equal(allowed.status, 200)
|
|
39
|
+
assert.ok(allowed.body.includes('Protected'))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('app middleware runs on matched routes', async () => {
|
|
43
|
+
const app = makeApp({ routes: [route('state.page.ts', '/')] }, fix('_global.ts'))
|
|
44
|
+
const { body } = await request(app, { url: '/' })
|
|
45
|
+
const state = JSON.parse(body) as Record<string, unknown>
|
|
46
|
+
assert.deepEqual(state.order, ['global'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('app middleware runs on 404 responses', async () => {
|
|
50
|
+
const app = makeApp({ routes: [], errorPage: fix('_error.ts') }, fix('_global.ts'))
|
|
51
|
+
const { body } = await request(app, { url: '/missing' })
|
|
52
|
+
const result = JSON.parse(body) as Record<string, unknown>
|
|
53
|
+
assert.equal(result.status, 404)
|
|
54
|
+
assert.deepEqual(result.order, ['global'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('app middleware runs before scoped middleware', async () => {
|
|
58
|
+
const app = makeApp(
|
|
59
|
+
{
|
|
60
|
+
routes: [route('state.page.ts', '/')],
|
|
61
|
+
middlewares: [middleware('_middleware.ts')],
|
|
62
|
+
},
|
|
63
|
+
fix('_global.ts'),
|
|
64
|
+
)
|
|
65
|
+
const { body } = await request(app, { url: '/' })
|
|
66
|
+
const state = JSON.parse(body) as Record<string, unknown>
|
|
67
|
+
assert.deepEqual(state.order, ['global', 'scoped'])
|
|
68
|
+
})
|
|
69
|
+
})
|