@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -0
  4. package/bin/potentia.js +14 -0
  5. package/examples/file-routing-basic/README.md +156 -0
  6. package/examples/file-routing-basic/app.js +16 -0
  7. package/examples/file-routing-basic/generate.js +21 -0
  8. package/examples/file-routing-basic/routes/health.js +3 -0
  9. package/examples/file-routing-basic/routes/index.js +3 -0
  10. package/examples/file-routing-basic/routes/users/[id].js +6 -0
  11. package/examples/file-routing-basic/routes/users/_routes.js +15 -0
  12. package/examples/form-rendering-basic/README.md +43 -0
  13. package/examples/form-rendering-basic/index.js +120 -0
  14. package/examples/full-flow-basic/README.md +141 -0
  15. package/examples/full-flow-basic/app.js +16 -0
  16. package/examples/full-flow-basic/form.js +205 -0
  17. package/examples/full-flow-basic/generate.js +21 -0
  18. package/examples/full-flow-basic/routes/index.js +5 -0
  19. package/examples/full-flow-basic/routes/users/[id].js +5 -0
  20. package/examples/full-flow-basic/routes/users/_routes.js +8 -0
  21. package/examples/full-flow-basic/routes/users/index.js +5 -0
  22. package/examples/full-flow-basic/routes/users/new.js +5 -0
  23. package/package.json +90 -0
  24. package/src/cli.js +407 -0
  25. package/src/dev/file-routing/diagnostics.js +24 -0
  26. package/src/dev/file-routing/generator.js +126 -0
  27. package/src/dev/file-routing/index.js +5 -0
  28. package/src/dev/file-routing/path-mapping.js +125 -0
  29. package/src/dev/file-routing/scanner.js +154 -0
  30. package/src/dev/file-routing/writer.js +100 -0
  31. package/src/file-routing.d.ts +36 -0
  32. package/src/file-routing.js +1 -0
  33. package/src/forms.d.ts +9 -0
  34. package/src/forms.js +334 -0
  35. package/src/index.d.ts +185 -0
  36. package/src/index.js +15 -0
  37. package/src/index.mjs +25 -0
  38. package/src/kernel/action-projection.js +35 -0
  39. package/src/kernel/action.js +172 -0
  40. package/src/kernel/app.js +144 -0
  41. package/src/kernel/context.js +37 -0
  42. package/src/kernel/contract-projection.js +129 -0
  43. package/src/kernel/contract.js +135 -0
  44. package/src/kernel/diagnostics.js +147 -0
  45. package/src/kernel/effect.js +119 -0
  46. package/src/kernel/error.js +93 -0
  47. package/src/kernel/form-projection.js +251 -0
  48. package/src/kernel/form-state.js +173 -0
  49. package/src/kernel/plugin.js +46 -0
  50. package/src/kernel/response.js +187 -0
  51. package/src/kernel/result.js +45 -0
  52. package/src/kernel/route-collection.js +148 -0
  53. package/src/kernel/route-id.js +11 -0
  54. package/src/kernel/route-manifest.js +129 -0
  55. package/src/kernel/route-projection.js +139 -0
  56. 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,3 @@
1
+ import { json, ok, route } from '@potentiajs/core';
2
+
3
+ export default route('GET', '/health', () => ok(json({ ok: true })));
@@ -0,0 +1,3 @@
1
+ import { ok, route, text } from '@potentiajs/core';
2
+
3
+ export default route('GET', '/', () => ok(text('file routing home')));
@@ -0,0 +1,6 @@
1
+ import { json, ok, route } from '@potentiajs/core';
2
+
3
+ export default route('GET', '/:id', (ctx) => ok(json({
4
+ id: ctx.params.id,
5
+ from: 'file-routing-basic'
6
+ })));
@@ -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, '&amp;')
196
+ .replace(/</g, '&lt;')
197
+ .replace(/>/g, '&gt;')
198
+ .replace(/"/g, '&quot;')
199
+ .replace(/'/g, '&#39;');
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
+ }
@@ -0,0 +1,5 @@
1
+ import { route } from '@potentiajs/core';
2
+
3
+ import { renderHomePage } from '../form.js';
4
+
5
+ export default route('GET', '/', () => renderHomePage());
@@ -0,0 +1,5 @@
1
+ import { route } from '@potentiajs/core';
2
+
3
+ import { renderUserPage } from '../../form.js';
4
+
5
+ export default route('GET', '/:id', (ctx) => renderUserPage(ctx.params.id));
@@ -0,0 +1,8 @@
1
+ import { createRoutes } from '@potentiajs/core';
2
+
3
+ export default createRoutes({
4
+ meta: {
5
+ example: 'full-flow-basic',
6
+ scope: 'users'
7
+ }
8
+ });
@@ -0,0 +1,5 @@
1
+ import { route } from '@potentiajs/core';
2
+
3
+ import { handleCreateUser } from '../../form.js';
4
+
5
+ export default route('POST', '/', (ctx) => handleCreateUser(ctx.request));
@@ -0,0 +1,5 @@
1
+ import { route } from '@potentiajs/core';
2
+
3
+ import { renderCreateUserPage } from '../../form.js';
4
+
5
+ export default route('GET', '/new', () => renderCreateUserPage());