@potentiajs/core 0.1.0-preview.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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/bin/potentia.js +14 -0
- package/examples/file-routing-basic/README.md +156 -0
- package/examples/file-routing-basic/app.js +16 -0
- package/examples/file-routing-basic/generate.js +21 -0
- package/examples/file-routing-basic/routes/health.js +3 -0
- package/examples/file-routing-basic/routes/index.js +3 -0
- package/examples/file-routing-basic/routes/users/[id].js +6 -0
- package/examples/file-routing-basic/routes/users/_routes.js +15 -0
- package/examples/form-rendering-basic/README.md +43 -0
- package/examples/form-rendering-basic/index.js +120 -0
- package/examples/full-flow-basic/README.md +141 -0
- package/examples/full-flow-basic/app.js +16 -0
- package/examples/full-flow-basic/form.js +205 -0
- package/examples/full-flow-basic/generate.js +21 -0
- package/examples/full-flow-basic/routes/index.js +5 -0
- package/examples/full-flow-basic/routes/users/[id].js +5 -0
- package/examples/full-flow-basic/routes/users/_routes.js +8 -0
- package/examples/full-flow-basic/routes/users/index.js +5 -0
- package/examples/full-flow-basic/routes/users/new.js +5 -0
- package/package.json +90 -0
- package/src/cli.js +407 -0
- package/src/dev/file-routing/diagnostics.js +24 -0
- package/src/dev/file-routing/generator.js +126 -0
- package/src/dev/file-routing/index.js +5 -0
- package/src/dev/file-routing/path-mapping.js +125 -0
- package/src/dev/file-routing/scanner.js +154 -0
- package/src/dev/file-routing/writer.js +100 -0
- package/src/file-routing.d.ts +36 -0
- package/src/file-routing.js +1 -0
- package/src/forms.d.ts +9 -0
- package/src/forms.js +334 -0
- package/src/index.d.ts +185 -0
- package/src/index.js +15 -0
- package/src/index.mjs +25 -0
- package/src/kernel/action-projection.js +35 -0
- package/src/kernel/action.js +172 -0
- package/src/kernel/app.js +144 -0
- package/src/kernel/context.js +37 -0
- package/src/kernel/contract-projection.js +129 -0
- package/src/kernel/contract.js +135 -0
- package/src/kernel/diagnostics.js +147 -0
- package/src/kernel/effect.js +119 -0
- package/src/kernel/error.js +93 -0
- package/src/kernel/form-projection.js +251 -0
- package/src/kernel/form-state.js +173 -0
- package/src/kernel/plugin.js +46 -0
- package/src/kernel/response.js +187 -0
- package/src/kernel/result.js +45 -0
- package/src/kernel/route-collection.js +148 -0
- package/src/kernel/route-id.js +11 -0
- package/src/kernel/route-manifest.js +129 -0
- package/src/kernel/route-projection.js +139 -0
- package/src/kernel/route.js +156 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { generateFileRoutes } from '@potentiajs/core/file-routing';
|
|
2
|
+
|
|
3
|
+
export async function generate() {
|
|
4
|
+
const result = await generateFileRoutes({
|
|
5
|
+
rootDir: new URL('./routes', import.meta.url).pathname,
|
|
6
|
+
outputFile: new URL('./.potentia/routes.generated.js', import.meta.url).pathname
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (!result.ok) {
|
|
10
|
+
const details = result.errors.map((error) => `${error.code}: ${error.message}`).join('\n');
|
|
11
|
+
throw new Error(`File route generation failed:\n${details}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (import.meta.main) {
|
|
18
|
+
const result = await generate();
|
|
19
|
+
console.log(`generated ${result.routes} route files and ${result.scopes} route scopes`);
|
|
20
|
+
console.log(result.outputFile);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRoutes } from '@potentiajs/core';
|
|
2
|
+
|
|
3
|
+
export default createRoutes({
|
|
4
|
+
hooks: {
|
|
5
|
+
afterResponse: [(ctx, response) => {
|
|
6
|
+
response.headers.set('x-file-routing-scope', 'users');
|
|
7
|
+
response.headers.set('x-file-routing-path', ctx.path);
|
|
8
|
+
return response;
|
|
9
|
+
}]
|
|
10
|
+
},
|
|
11
|
+
meta: {
|
|
12
|
+
example: 'file-routing-basic',
|
|
13
|
+
scope: 'users'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Form Rendering Basic Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates the experimental optional server-side HTML renderer.
|
|
4
|
+
|
|
5
|
+
It combines:
|
|
6
|
+
|
|
7
|
+
- `action(...)`
|
|
8
|
+
- `projectForm(...)`
|
|
9
|
+
- `createFormState(...)`
|
|
10
|
+
- `renderForm(...)` from `@potentiajs/core/forms`
|
|
11
|
+
|
|
12
|
+
The renderer returns a plain HTML string. It is escaped by default and does not add a frontend runtime, JSX, hydration, client SDK, OpenAPI generator, session/flash helper, or multipart/file handling.
|
|
13
|
+
|
|
14
|
+
## What it shows
|
|
15
|
+
|
|
16
|
+
- projecting an action input contract into form metadata
|
|
17
|
+
- rendering projected fields as plain HTML
|
|
18
|
+
- rendering explicit textarea, hidden, and select fields
|
|
19
|
+
- preserving safe state values
|
|
20
|
+
- omitting sensitive password values
|
|
21
|
+
- rendering field errors
|
|
22
|
+
- using `data-potentia-*` styling/testing hooks
|
|
23
|
+
- adding baseline accessibility attributes such as `aria-describedby` and `aria-invalid`
|
|
24
|
+
- escaping output by default
|
|
25
|
+
|
|
26
|
+
Textarea and hidden inputs require explicit projection metadata (`field.input === 'textarea'` or `field.input === 'hidden'`, with object `field.input.type` tolerated). Options render as `<select>` from string options or `{ value, label }` objects. The renderer does not add classNames, raw HTML, JSX, a frontend runtime, or hydration.
|
|
27
|
+
|
|
28
|
+
## Run locally
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun examples/form-rendering-basic/index.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Package import
|
|
35
|
+
|
|
36
|
+
When installed from the package, import the renderer from the forms subpath:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { action, createFormState, projectForm } from '@potentiajs/core';
|
|
40
|
+
import { renderForm } from '@potentiajs/core/forms';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`renderForm` is intentionally not exported from the package root.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { optional, sigil } from '@weipertda/sigiljs';
|
|
2
|
+
|
|
3
|
+
import { action, createFormState, projectForm } from '../../src/index.js';
|
|
4
|
+
import { renderForm } from '../../src/forms.js';
|
|
5
|
+
|
|
6
|
+
const CreateUserInput = sigil({
|
|
7
|
+
name: String,
|
|
8
|
+
email: String,
|
|
9
|
+
password: optional(String)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const createUserAction = action('users.create.rendered-form', () => ({ ok: true }), {
|
|
13
|
+
input: CreateUserInput,
|
|
14
|
+
meta: { description: 'Create a user from a rendered form' }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const baseProjection = projectForm(createUserAction);
|
|
18
|
+
|
|
19
|
+
export const formProjection = {
|
|
20
|
+
...baseProjection,
|
|
21
|
+
fields: baseProjection.fields.concat([
|
|
22
|
+
{
|
|
23
|
+
kind: 'field',
|
|
24
|
+
name: 'bio',
|
|
25
|
+
path: ['bio'],
|
|
26
|
+
field: 'bio',
|
|
27
|
+
label: 'Bio',
|
|
28
|
+
help: 'Plain text only.',
|
|
29
|
+
placeholder: 'Tell us about this user',
|
|
30
|
+
input: 'textarea',
|
|
31
|
+
required: false,
|
|
32
|
+
multiple: false,
|
|
33
|
+
sensitive: false,
|
|
34
|
+
options: null,
|
|
35
|
+
defaultValue: null,
|
|
36
|
+
contract: { kind: 'example', expected: 'string' },
|
|
37
|
+
meta: null
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: 'field',
|
|
41
|
+
name: 'role',
|
|
42
|
+
path: ['role'],
|
|
43
|
+
field: 'role',
|
|
44
|
+
label: 'Role',
|
|
45
|
+
help: null,
|
|
46
|
+
placeholder: null,
|
|
47
|
+
input: { type: 'text' },
|
|
48
|
+
required: true,
|
|
49
|
+
multiple: false,
|
|
50
|
+
sensitive: false,
|
|
51
|
+
options: ['user', { value: 'admin', label: 'Admin' }],
|
|
52
|
+
defaultValue: 'user',
|
|
53
|
+
contract: { kind: 'example', expected: 'string' },
|
|
54
|
+
meta: null
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
kind: 'field',
|
|
58
|
+
name: 'csrf',
|
|
59
|
+
path: ['csrf'],
|
|
60
|
+
field: 'csrf',
|
|
61
|
+
label: 'CSRF token',
|
|
62
|
+
help: null,
|
|
63
|
+
placeholder: null,
|
|
64
|
+
input: 'hidden',
|
|
65
|
+
required: true,
|
|
66
|
+
multiple: false,
|
|
67
|
+
sensitive: false,
|
|
68
|
+
options: null,
|
|
69
|
+
defaultValue: 'example-token',
|
|
70
|
+
contract: { kind: 'example', expected: 'string' },
|
|
71
|
+
meta: null
|
|
72
|
+
}
|
|
73
|
+
])
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const failedState = createFormState({
|
|
77
|
+
ok: false,
|
|
78
|
+
values: {
|
|
79
|
+
name: 'Ada Lovelace',
|
|
80
|
+
email: 'not-an-email',
|
|
81
|
+
password: 'secret-value-is-not-rendered',
|
|
82
|
+
bio: '<curious engineer>',
|
|
83
|
+
role: 'admin',
|
|
84
|
+
csrf: 'posted-token'
|
|
85
|
+
},
|
|
86
|
+
issues: [
|
|
87
|
+
{
|
|
88
|
+
code: 'invalid_email',
|
|
89
|
+
message: 'Email must be valid',
|
|
90
|
+
path: ['email'],
|
|
91
|
+
field: 'email',
|
|
92
|
+
boundary: 'input',
|
|
93
|
+
source: 'framework',
|
|
94
|
+
expected: 'email',
|
|
95
|
+
received: 'string',
|
|
96
|
+
meta: null
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: 'too_short',
|
|
100
|
+
message: 'Bio is too short',
|
|
101
|
+
path: ['bio'],
|
|
102
|
+
field: 'bio',
|
|
103
|
+
boundary: 'input',
|
|
104
|
+
source: 'framework',
|
|
105
|
+
expected: 'string',
|
|
106
|
+
received: 'string',
|
|
107
|
+
meta: null
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const html = renderForm(formProjection, {
|
|
113
|
+
action: '/users',
|
|
114
|
+
state: failedState,
|
|
115
|
+
submitLabel: 'Create user'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (import.meta.main) {
|
|
119
|
+
console.log(html);
|
|
120
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Full Flow Basic Example
|
|
2
|
+
|
|
3
|
+
This example shows Potentia as a compact contract-native app foundation.
|
|
4
|
+
|
|
5
|
+
It demonstrates:
|
|
6
|
+
|
|
7
|
+
```txt
|
|
8
|
+
file routes
|
|
9
|
+
→ generated routes
|
|
10
|
+
→ app
|
|
11
|
+
→ action/form contract metadata
|
|
12
|
+
→ URL-encoded POST
|
|
13
|
+
→ contract validation
|
|
14
|
+
→ form projection
|
|
15
|
+
→ server-rendered HTML
|
|
16
|
+
→ form state after failure
|
|
17
|
+
→ explicit redirect after success
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No JSX, frontend runtime, browser hydration, client SDK, OpenAPI generator, database, auth, session/flash helper, or multipart/file upload helper is used.
|
|
21
|
+
|
|
22
|
+
## Route tree
|
|
23
|
+
|
|
24
|
+
```txt
|
|
25
|
+
examples/full-flow-basic/
|
|
26
|
+
generate.js
|
|
27
|
+
app.js
|
|
28
|
+
form.js
|
|
29
|
+
routes/
|
|
30
|
+
index.js
|
|
31
|
+
users/
|
|
32
|
+
index.js
|
|
33
|
+
new.js
|
|
34
|
+
[id].js
|
|
35
|
+
_routes.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Generated routes:
|
|
39
|
+
|
|
40
|
+
```txt
|
|
41
|
+
GET /
|
|
42
|
+
GET /users/new
|
|
43
|
+
POST /users
|
|
44
|
+
GET /users/:id
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Generate routes
|
|
48
|
+
|
|
49
|
+
From the repository root:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bun examples/full-flow-basic/generate.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The generated module is written to:
|
|
56
|
+
|
|
57
|
+
```txt
|
|
58
|
+
examples/full-flow-basic/.potentia/routes.generated.js
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The generated output is inspectable JavaScript and is not scanned from the filesystem at request time. It should not be committed for this example.
|
|
62
|
+
|
|
63
|
+
## Check generated routes
|
|
64
|
+
|
|
65
|
+
The CLI can verify the generated file is current:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bun bin/potentia.js routes check \
|
|
69
|
+
--cwd examples/full-flow-basic \
|
|
70
|
+
--root routes \
|
|
71
|
+
--out .potentia/routes.generated.js \
|
|
72
|
+
--json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Run the app locally
|
|
76
|
+
|
|
77
|
+
After generation:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bun examples/full-flow-basic/app.js
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then open:
|
|
84
|
+
|
|
85
|
+
```txt
|
|
86
|
+
http://localhost:3000/users/new
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Form flow
|
|
90
|
+
|
|
91
|
+
### GET form
|
|
92
|
+
|
|
93
|
+
```txt
|
|
94
|
+
GET /users/new
|
|
95
|
+
→ projectForm(createUserAction)
|
|
96
|
+
→ renderForm(...)
|
|
97
|
+
→ HTML response
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### POST validation failure
|
|
101
|
+
|
|
102
|
+
```txt
|
|
103
|
+
POST /users
|
|
104
|
+
content-type: application/x-www-form-urlencoded
|
|
105
|
+
|
|
106
|
+
name=&email=bad&password=secret
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The example validates the parsed input with the SigilJS contract plus small example-level checks. It returns status `400`, renders field errors, preserves safe values, and omits the password value.
|
|
110
|
+
|
|
111
|
+
Rendered forms include `data-potentia-*` styling/testing hooks and baseline accessibility attributes such as `aria-describedby` and `aria-invalid` when help text or errors are present.
|
|
112
|
+
|
|
113
|
+
### POST domain failure
|
|
114
|
+
|
|
115
|
+
```txt
|
|
116
|
+
POST /users
|
|
117
|
+
name=Ada&email=taken@example.com&password=secret
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The example returns status `409` and renders a root form error from `createFormState(...)`.
|
|
121
|
+
|
|
122
|
+
### POST success
|
|
123
|
+
|
|
124
|
+
```txt
|
|
125
|
+
POST /users
|
|
126
|
+
name=Ada&email=ada@example.com&password=secret
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The example redirects explicitly:
|
|
130
|
+
|
|
131
|
+
```txt
|
|
132
|
+
303 Location: /users/ada
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Ergonomic note
|
|
136
|
+
|
|
137
|
+
Current action route execution validates `action.input` before the action handler runs. That is good for API-style failures, but same-route HTML form re-rendering after input-contract failure needs explicit handling today.
|
|
138
|
+
|
|
139
|
+
This example keeps the framework unchanged: it uses `action(...)` + `projectForm(...)` for contract-native form metadata and performs the POST validation/render path explicitly in the route handler.
|
|
140
|
+
|
|
141
|
+
A future design gate could improve action HTML failure rendering ergonomics.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createApp } from '@potentiajs/core';
|
|
2
|
+
|
|
3
|
+
import routes from './.potentia/routes.generated.js';
|
|
4
|
+
|
|
5
|
+
export const app = createApp({
|
|
6
|
+
routes: [routes]
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (import.meta.main) {
|
|
10
|
+
Bun.serve({
|
|
11
|
+
port: 3000,
|
|
12
|
+
fetch: app.fetch
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
console.log('Potentia full-flow example running at http://localhost:3000');
|
|
16
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { sigil } from '@weipertda/sigiljs';
|
|
2
|
+
|
|
3
|
+
import { action, createFormState, projectForm, redirect } from '@potentiajs/core';
|
|
4
|
+
import { renderForm } from '@potentiajs/core/forms';
|
|
5
|
+
|
|
6
|
+
export const takenEmails = new Set(['taken@example.com']);
|
|
7
|
+
|
|
8
|
+
export const CreateUserInput = sigil({
|
|
9
|
+
name: String,
|
|
10
|
+
email: String,
|
|
11
|
+
password: String
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const createUserAction = action('users.create.full-flow', () => null, {
|
|
15
|
+
input: CreateUserInput,
|
|
16
|
+
meta: { description: 'Create a user account' }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const createUserForm = projectForm(createUserAction);
|
|
20
|
+
|
|
21
|
+
export function renderCreateUserPage(options = {}) {
|
|
22
|
+
const state = options.state || null;
|
|
23
|
+
const status = options.status || 200;
|
|
24
|
+
const formHtml = renderForm(createUserForm, {
|
|
25
|
+
action: '/users',
|
|
26
|
+
state: state,
|
|
27
|
+
submitLabel: 'Create account',
|
|
28
|
+
idPrefix: 'create-user'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return htmlResponse(`<!doctype html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8">
|
|
35
|
+
<title>Create user · Potentia full flow</title>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<main>
|
|
39
|
+
<h1>Create a user account</h1>
|
|
40
|
+
<p>This page is rendered on the server from projected action metadata.</p>
|
|
41
|
+
${indent(formHtml, 4)}
|
|
42
|
+
</main>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`, status);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function handleCreateUser(request) {
|
|
48
|
+
const input = await parseUrlEncoded(request);
|
|
49
|
+
const validation = validateCreateUserInput(input);
|
|
50
|
+
|
|
51
|
+
if (!validation.ok) {
|
|
52
|
+
return renderCreateUserPage({
|
|
53
|
+
status: 400,
|
|
54
|
+
state: createFormState({
|
|
55
|
+
ok: false,
|
|
56
|
+
values: input,
|
|
57
|
+
issues: validation.issues,
|
|
58
|
+
error: {
|
|
59
|
+
code: 'POTENTIA_EXAMPLE_VALIDATION_FAILED',
|
|
60
|
+
message: 'Please fix the highlighted fields.'
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (takenEmails.has(input.email)) {
|
|
67
|
+
return renderCreateUserPage({
|
|
68
|
+
status: 409,
|
|
69
|
+
state: createFormState({
|
|
70
|
+
ok: false,
|
|
71
|
+
values: input,
|
|
72
|
+
error: {
|
|
73
|
+
code: 'USER_EMAIL_TAKEN',
|
|
74
|
+
message: 'Email is already in use.'
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return redirect(`/users/${slugify(input.name)}`, 303);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderUserPage(id) {
|
|
84
|
+
return htmlResponse(`<!doctype html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="utf-8">
|
|
88
|
+
<title>User ${escapeHtml(id)} · Potentia full flow</title>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<main>
|
|
92
|
+
<h1>User ${escapeHtml(id)}</h1>
|
|
93
|
+
<p>The account flow completed with an explicit 303 redirect.</p>
|
|
94
|
+
<p><a href="/users/new">Create another user</a></p>
|
|
95
|
+
</main>
|
|
96
|
+
</body>
|
|
97
|
+
</html>`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderHomePage() {
|
|
101
|
+
return htmlResponse(`<!doctype html>
|
|
102
|
+
<html lang="en">
|
|
103
|
+
<head>
|
|
104
|
+
<meta charset="utf-8">
|
|
105
|
+
<title>Potentia full flow</title>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<main>
|
|
109
|
+
<h1>Potentia full-flow example</h1>
|
|
110
|
+
<p>File routes, actions, contracts, projected forms, rendered HTML, form state, and redirects.</p>
|
|
111
|
+
<p><a href="/users/new">Create a user account</a></p>
|
|
112
|
+
</main>
|
|
113
|
+
</body>
|
|
114
|
+
</html>`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateCreateUserInput(input) {
|
|
118
|
+
const parsed = CreateUserInput.safeParse(input);
|
|
119
|
+
const issues = [];
|
|
120
|
+
|
|
121
|
+
if (!parsed.success && parsed.error) {
|
|
122
|
+
issues.push(toIssue(parsed.error));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!input.name) issues.push(issue('missing_required', 'Name is required.', ['name']));
|
|
126
|
+
if (!input.email) {
|
|
127
|
+
issues.push(issue('missing_required', 'Email is required.', ['email']));
|
|
128
|
+
} else if (!input.email.includes('@')) {
|
|
129
|
+
issues.push(issue('invalid_email', 'Email must include @.', ['email']));
|
|
130
|
+
}
|
|
131
|
+
if (!input.password) issues.push(issue('missing_required', 'Password is required.', ['password']));
|
|
132
|
+
|
|
133
|
+
return { ok: issues.length === 0, issues: dedupeIssues(issues) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toIssue(error) {
|
|
137
|
+
const path = Array.isArray(error.path) ? error.path : [];
|
|
138
|
+
return issue(error.code || 'invalid_value', error.message || 'Invalid value.', path);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function issue(code, message, path) {
|
|
142
|
+
return {
|
|
143
|
+
code: code,
|
|
144
|
+
message: message,
|
|
145
|
+
path: path,
|
|
146
|
+
field: path.length > 0 ? path.join('.') : null,
|
|
147
|
+
boundary: 'input',
|
|
148
|
+
source: 'framework',
|
|
149
|
+
expected: 'string',
|
|
150
|
+
received: 'string',
|
|
151
|
+
meta: null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function dedupeIssues(issues) {
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const result = [];
|
|
158
|
+
for (const item of issues) {
|
|
159
|
+
const key = `${item.field}:${item.code}:${item.message}`;
|
|
160
|
+
if (seen.has(key)) continue;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
result.push(item);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function parseUrlEncoded(request) {
|
|
168
|
+
const text = await request.text();
|
|
169
|
+
const params = new URLSearchParams(text);
|
|
170
|
+
return {
|
|
171
|
+
name: params.get('name') || '',
|
|
172
|
+
email: params.get('email') || '',
|
|
173
|
+
password: params.get('password') || ''
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function htmlResponse(html, status = 200) {
|
|
178
|
+
return new Response(html, {
|
|
179
|
+
status: status,
|
|
180
|
+
headers: { 'content-type': 'text/html; charset=utf-8' }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function slugify(value) {
|
|
185
|
+
const slug = String(value || '')
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
189
|
+
.replace(/^-+|-+$/g, '');
|
|
190
|
+
return slug || 'user';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function escapeHtml(value) {
|
|
194
|
+
return String(value ?? '')
|
|
195
|
+
.replace(/&/g, '&')
|
|
196
|
+
.replace(/</g, '<')
|
|
197
|
+
.replace(/>/g, '>')
|
|
198
|
+
.replace(/"/g, '"')
|
|
199
|
+
.replace(/'/g, ''');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function indent(value, spaces) {
|
|
203
|
+
const padding = ' '.repeat(spaces);
|
|
204
|
+
return String(value).split('\n').map((line) => `${padding}${line}`).join('\n');
|
|
205
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { generateFileRoutes } from '@potentiajs/core/file-routing';
|
|
2
|
+
|
|
3
|
+
export async function generate() {
|
|
4
|
+
const result = await generateFileRoutes({
|
|
5
|
+
rootDir: new URL('./routes', import.meta.url).pathname,
|
|
6
|
+
outputFile: new URL('./.potentia/routes.generated.js', import.meta.url).pathname
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (!result.ok) {
|
|
10
|
+
const details = result.errors.map((error) => `${error.code}: ${error.message}`).join('\n');
|
|
11
|
+
throw new Error(`Full-flow route generation failed:\n${details}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (import.meta.main) {
|
|
18
|
+
const result = await generate();
|
|
19
|
+
console.log(`generated ${result.routes} route files and ${result.scopes} route scopes`);
|
|
20
|
+
console.log(result.outputFile);
|
|
21
|
+
}
|