@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,129 @@
|
|
|
1
|
+
import { createRouteId, normalizeRoutePath } from './route-id.js';
|
|
2
|
+
import { projectRoutes } from './route-projection.js';
|
|
3
|
+
|
|
4
|
+
export { createRouteId, normalizeRoutePath };
|
|
5
|
+
|
|
6
|
+
export function createRouteManifest(input, options = {}) {
|
|
7
|
+
const projected = projectRoutes(input);
|
|
8
|
+
const diagnostics = [];
|
|
9
|
+
|
|
10
|
+
if (projected.kind !== 'routes') {
|
|
11
|
+
diagnostics.push(createDiagnostic('POTENTIA_MANIFEST_INVALID_ROUTE', 'Manifest input could not be projected as routes', { index: null, routeId: null, routeName: null }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const routes = projected.routes.map((route, index) => manifestRoute(route, index));
|
|
15
|
+
const actions = projected.routes.map((route, index) => manifestAction(route, index)).filter(Boolean);
|
|
16
|
+
const lookupResult = buildLookups(routes);
|
|
17
|
+
diagnostics.push(...lookupResult.diagnostics);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
kind: 'potentia-route-manifest',
|
|
21
|
+
version: 1,
|
|
22
|
+
package: {
|
|
23
|
+
name: typeof options.packageName === 'string' ? options.packageName : null,
|
|
24
|
+
version: typeof options.packageVersion === 'string' ? options.packageVersion : null
|
|
25
|
+
},
|
|
26
|
+
routes: routes,
|
|
27
|
+
actions: actions,
|
|
28
|
+
lookups: lookupResult.lookups,
|
|
29
|
+
diagnostics: diagnostics,
|
|
30
|
+
meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function manifestAction(route, index) {
|
|
35
|
+
if (!route.action) return null;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
kind: 'action',
|
|
39
|
+
id: route.action.id,
|
|
40
|
+
routeId: route.id || createRouteId(route),
|
|
41
|
+
method: route.method,
|
|
42
|
+
path: normalizeRoutePath(route.path),
|
|
43
|
+
input: route.action.input,
|
|
44
|
+
output: route.action.output,
|
|
45
|
+
result: route.action.result,
|
|
46
|
+
contentTypes: ['application/json', 'application/x-www-form-urlencoded'],
|
|
47
|
+
enhancement: {
|
|
48
|
+
plainForm: true,
|
|
49
|
+
fetch: true,
|
|
50
|
+
clientValidation: 'projection-only'
|
|
51
|
+
},
|
|
52
|
+
source: route.action.source || null,
|
|
53
|
+
meta: route.action.meta || null,
|
|
54
|
+
index: index
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function manifestRoute(route, index) {
|
|
59
|
+
return {
|
|
60
|
+
id: route.id || createRouteId(route),
|
|
61
|
+
name: route.name || null,
|
|
62
|
+
method: route.method,
|
|
63
|
+
path: normalizeRoutePath(route.path),
|
|
64
|
+
contracts: route.contracts,
|
|
65
|
+
hooks: route.hooks,
|
|
66
|
+
source: route.source || null,
|
|
67
|
+
meta: route.meta || null,
|
|
68
|
+
index: index
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildLookups(routes) {
|
|
73
|
+
const lookups = emptyLookups();
|
|
74
|
+
const diagnostics = [];
|
|
75
|
+
|
|
76
|
+
for (const route of routes) {
|
|
77
|
+
if (!route.id || !route.method || !route.path) {
|
|
78
|
+
diagnostics.push(createDiagnostic('POTENTIA_MANIFEST_INVALID_ROUTE', 'Manifest route is missing method, path, or id', diagnosticDetail(route)));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(lookups.byId, route.id)) {
|
|
83
|
+
diagnostics.push(createDiagnostic('POTENTIA_MANIFEST_DUPLICATE_ROUTE_ID', `Duplicate route id: ${route.id}`, diagnosticDetail(route)));
|
|
84
|
+
} else {
|
|
85
|
+
lookups.byId[route.id] = route.index;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (route.name) {
|
|
89
|
+
if (Object.prototype.hasOwnProperty.call(lookups.byName, route.name)) {
|
|
90
|
+
diagnostics.push(createDiagnostic('POTENTIA_MANIFEST_DUPLICATE_ROUTE_NAME', `Duplicate route name: ${route.name}`, diagnosticDetail(route)));
|
|
91
|
+
} else {
|
|
92
|
+
lookups.byName[route.name] = route.id;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const methodPath = `${route.method} ${route.path}`;
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(lookups.byMethodPath, methodPath)) {
|
|
98
|
+
diagnostics.push(createDiagnostic('POTENTIA_MANIFEST_DUPLICATE_ROUTE_ID', `Duplicate route method/path: ${methodPath}`, diagnosticDetail(route)));
|
|
99
|
+
} else {
|
|
100
|
+
lookups.byMethodPath[methodPath] = route.id;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { lookups: lookups, diagnostics: diagnostics };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emptyLookups() {
|
|
108
|
+
return {
|
|
109
|
+
byId: {},
|
|
110
|
+
byName: {},
|
|
111
|
+
byMethodPath: {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createDiagnostic(code, message, detail) {
|
|
116
|
+
return {
|
|
117
|
+
code: code,
|
|
118
|
+
message: message,
|
|
119
|
+
detail: detail || null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function diagnosticDetail(route) {
|
|
124
|
+
return {
|
|
125
|
+
index: Number.isInteger(route.index) ? route.index : null,
|
|
126
|
+
routeId: route.id || null,
|
|
127
|
+
routeName: route.name || null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { projectContract } from './contract-projection.js';
|
|
2
|
+
import { isAction } from './action.js';
|
|
3
|
+
import { projectAction } from './action-projection.js';
|
|
4
|
+
import { createRouteId } from './route-id.js';
|
|
5
|
+
import { composeRoutes } from './route-collection.js';
|
|
6
|
+
|
|
7
|
+
const CONTRACT_BOUNDARIES = ['params', 'query', 'headers', 'body', 'response'];
|
|
8
|
+
|
|
9
|
+
export function projectRoute(value) {
|
|
10
|
+
if (!isRoute(value)) {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'unknown-route',
|
|
13
|
+
id: null,
|
|
14
|
+
name: null,
|
|
15
|
+
method: null,
|
|
16
|
+
path: null,
|
|
17
|
+
contracts: emptyProjectedContracts(),
|
|
18
|
+
hooks: emptyHookSummary(),
|
|
19
|
+
action: null,
|
|
20
|
+
source: null,
|
|
21
|
+
meta: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
kind: 'route',
|
|
27
|
+
id: createRouteId(value),
|
|
28
|
+
name: typeof value.name === 'string' ? value.name : null,
|
|
29
|
+
method: value.method,
|
|
30
|
+
path: value.path,
|
|
31
|
+
contracts: projectContractBoundaries(value.options),
|
|
32
|
+
hooks: summarizeHooks(value.hooks),
|
|
33
|
+
action: isAction(value.handler) ? projectAction(value.handler) : null,
|
|
34
|
+
source: value.source || null,
|
|
35
|
+
meta: Object.prototype.hasOwnProperty.call(value, 'meta') ? value.meta : null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function projectRoutes(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return projectRouteSet({
|
|
42
|
+
prefix: null,
|
|
43
|
+
routes: composeRoutes(value),
|
|
44
|
+
hooks: {},
|
|
45
|
+
contracts: {},
|
|
46
|
+
meta: null
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isRouteCollection(value) || isMount(value)) {
|
|
51
|
+
return projectRouteSet({
|
|
52
|
+
prefix: value.prefix,
|
|
53
|
+
routes: composeRoutes([value]),
|
|
54
|
+
hooks: value.hooks,
|
|
55
|
+
contracts: value.contracts,
|
|
56
|
+
meta: Object.prototype.hasOwnProperty.call(value, 'meta') ? value.meta : null
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isApp(value)) {
|
|
61
|
+
return projectRouteSet({
|
|
62
|
+
prefix: null,
|
|
63
|
+
routes: value.routes,
|
|
64
|
+
hooks: value.hooks,
|
|
65
|
+
contracts: {},
|
|
66
|
+
meta: null
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
kind: 'unknown-routes',
|
|
72
|
+
prefix: null,
|
|
73
|
+
routes: [],
|
|
74
|
+
hooks: emptyHookSummary(),
|
|
75
|
+
contracts: emptyProjectedContracts(),
|
|
76
|
+
meta: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function projectContractBoundaries(contracts = {}) {
|
|
81
|
+
const projected = {};
|
|
82
|
+
for (const boundary of CONTRACT_BOUNDARIES) {
|
|
83
|
+
projected[boundary] = contracts && Object.prototype.hasOwnProperty.call(contracts, boundary) ? projectContract(contracts[boundary]) : null;
|
|
84
|
+
}
|
|
85
|
+
return projected;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function summarizeHooks(hooks = {}) {
|
|
89
|
+
return {
|
|
90
|
+
beforeRequest: Array.isArray(hooks.beforeRequest) ? hooks.beforeRequest.length : 0,
|
|
91
|
+
afterResponse: Array.isArray(hooks.afterResponse) ? hooks.afterResponse.length : 0,
|
|
92
|
+
onError: Array.isArray(hooks.onError) ? hooks.onError.length : 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function projectRouteSet(value) {
|
|
97
|
+
return {
|
|
98
|
+
kind: 'routes',
|
|
99
|
+
prefix: value.prefix,
|
|
100
|
+
routes: value.routes.map(projectRoute),
|
|
101
|
+
hooks: summarizeHooks(value.hooks),
|
|
102
|
+
contracts: projectContractBoundaries(value.contracts),
|
|
103
|
+
meta: value.meta
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emptyProjectedContracts() {
|
|
108
|
+
return {
|
|
109
|
+
params: null,
|
|
110
|
+
query: null,
|
|
111
|
+
headers: null,
|
|
112
|
+
body: null,
|
|
113
|
+
response: null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emptyHookSummary() {
|
|
118
|
+
return {
|
|
119
|
+
beforeRequest: 0,
|
|
120
|
+
afterResponse: 0,
|
|
121
|
+
onError: 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isRoute(value) {
|
|
126
|
+
return Boolean(value && typeof value === 'object' && typeof value.method === 'string' && typeof value.path === 'string' && Object.prototype.hasOwnProperty.call(value, 'handler'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isRouteCollection(value) {
|
|
130
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'routes' && Array.isArray(value.routes));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isMount(value) {
|
|
134
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'mount');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isApp(value) {
|
|
138
|
+
return Boolean(value && typeof value === 'object' && Array.isArray(value.routes) && typeof value.fetch === 'function');
|
|
139
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createFrameworkError, ERROR_CODES } from './error.js';
|
|
2
|
+
|
|
3
|
+
export function route(method, path, handler, options = {}) {
|
|
4
|
+
const normalizedSource = normalizeSource(options.source);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
method: String(method).toUpperCase(),
|
|
8
|
+
path: path,
|
|
9
|
+
handler: handler,
|
|
10
|
+
options: options,
|
|
11
|
+
name: typeof options.name === 'string' ? options.name : null,
|
|
12
|
+
source: normalizedSource,
|
|
13
|
+
meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null,
|
|
14
|
+
pattern: parsePathPattern(path)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeSource(source) {
|
|
19
|
+
if (!source || typeof source !== 'object') return null;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
file: typeof source.file === 'string' ? source.file : null,
|
|
23
|
+
line: Number.isInteger(source.line) ? source.line : null,
|
|
24
|
+
column: Number.isInteger(source.column) ? source.column : null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function matchRoute(routes, method, path) {
|
|
29
|
+
const normalizedMethod = String(method).toUpperCase();
|
|
30
|
+
const matches = [];
|
|
31
|
+
const allowed = [];
|
|
32
|
+
let error = null;
|
|
33
|
+
|
|
34
|
+
for (let index = 0; index < routes.length; index += 1) {
|
|
35
|
+
const candidate = routes[index];
|
|
36
|
+
const pathMatch = matchPath(candidate.pattern, path);
|
|
37
|
+
|
|
38
|
+
if (pathMatch.error) {
|
|
39
|
+
error = pathMatch.error;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!pathMatch.matched) continue;
|
|
44
|
+
|
|
45
|
+
allowed.push(candidate.method);
|
|
46
|
+
matches.push({
|
|
47
|
+
route: candidate,
|
|
48
|
+
params: pathMatch.params,
|
|
49
|
+
score: candidate.pattern.score,
|
|
50
|
+
index: index
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (matches.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
found: false,
|
|
57
|
+
methodAllowed: true,
|
|
58
|
+
route: null,
|
|
59
|
+
params: {},
|
|
60
|
+
allowed: [],
|
|
61
|
+
error: error
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
matches.sort(compareMatches);
|
|
66
|
+
|
|
67
|
+
for (const match of matches) {
|
|
68
|
+
if (match.route.method === normalizedMethod) {
|
|
69
|
+
return {
|
|
70
|
+
found: true,
|
|
71
|
+
methodAllowed: true,
|
|
72
|
+
route: match.route,
|
|
73
|
+
params: match.params,
|
|
74
|
+
allowed: allowed,
|
|
75
|
+
error: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
found: false,
|
|
82
|
+
methodAllowed: false,
|
|
83
|
+
route: null,
|
|
84
|
+
params: {},
|
|
85
|
+
allowed: unique(allowed),
|
|
86
|
+
error: null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parsePathPattern(path) {
|
|
91
|
+
const segments = splitPath(path);
|
|
92
|
+
let staticCount = 0;
|
|
93
|
+
let dynamicCount = 0;
|
|
94
|
+
|
|
95
|
+
const parsedSegments = segments.map((segment) => {
|
|
96
|
+
if (segment.startsWith(':') && segment.length > 1) {
|
|
97
|
+
dynamicCount += 1;
|
|
98
|
+
return { type: 'param', value: segment.slice(1) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
staticCount += 1;
|
|
102
|
+
return { type: 'static', value: segment };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
path: path,
|
|
107
|
+
segments: parsedSegments,
|
|
108
|
+
score: staticCount * 10 - dynamicCount
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchPath(pattern, path) {
|
|
113
|
+
const pathSegments = splitPath(path);
|
|
114
|
+
|
|
115
|
+
if (pattern.segments.length !== pathSegments.length) {
|
|
116
|
+
return { matched: false, params: {}, error: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const params = {};
|
|
120
|
+
|
|
121
|
+
for (let index = 0; index < pattern.segments.length; index += 1) {
|
|
122
|
+
const patternSegment = pattern.segments[index];
|
|
123
|
+
const pathSegment = pathSegments[index];
|
|
124
|
+
|
|
125
|
+
if (patternSegment.type === 'static') {
|
|
126
|
+
if (patternSegment.value !== pathSegment) return { matched: false, params: {}, error: null };
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
params[patternSegment.value] = decodeURIComponent(pathSegment);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
matched: false,
|
|
135
|
+
params: {},
|
|
136
|
+
error: createFrameworkError(ERROR_CODES.BAD_REQUEST, 'Invalid route parameter encoding', { status: 400, cause: error, expose: true })
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { matched: true, params: params, error: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function splitPath(path) {
|
|
145
|
+
if (path === '/') return [];
|
|
146
|
+
return String(path).split('/').filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareMatches(a, b) {
|
|
150
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
151
|
+
return a.index - b.index;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function unique(values) {
|
|
155
|
+
return Array.from(new Set(values));
|
|
156
|
+
}
|