@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,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
+ }