@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,173 @@
1
+ import { createRootIssue, normalizeIssue } from './diagnostics.js';
2
+
3
+ const DEFAULT_ROOT_KEY = '_form';
4
+ const DEFAULT_SENSITIVE_FIELDS = new Set([
5
+ 'password',
6
+ 'confirmpassword',
7
+ 'currentpassword',
8
+ 'newpassword',
9
+ 'token',
10
+ 'secret',
11
+ 'apikey',
12
+ 'authorization',
13
+ 'auth',
14
+ 'credential',
15
+ 'session',
16
+ 'cookie'
17
+ ]);
18
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
19
+
20
+ export function createFormState(input = {}) {
21
+ const normalizedInput = isPlainObject(input) ? input : {};
22
+ const ok = Object.prototype.hasOwnProperty.call(normalizedInput, 'ok') ? Boolean(normalizedInput.ok) : false;
23
+ const error = normalizeFormError(normalizedInput.error, ok);
24
+ const issues = normalizeFormIssues(normalizedInput.issues, ok, error);
25
+
26
+ return {
27
+ ok: ok,
28
+ kind: 'form',
29
+ values: preserveFormValues(normalizedInput.values),
30
+ errors: groupIssuesByField(issues),
31
+ issues: issues,
32
+ error: error,
33
+ value: Object.prototype.hasOwnProperty.call(normalizedInput, 'value') ? normalizedInput.value : null,
34
+ meta: Object.prototype.hasOwnProperty.call(normalizedInput, 'meta') ? normalizedInput.meta : null
35
+ };
36
+ }
37
+
38
+ export function preserveFormValues(input, options = {}) {
39
+ if (!isPlainObject(input)) return {};
40
+
41
+ const sensitiveFields = createSensitiveSet(options.sensitiveFields);
42
+ const result = {};
43
+
44
+ for (const key of Object.keys(input)) {
45
+ if (!isSafeKey(key) || isSensitiveSegment(key, sensitiveFields)) continue;
46
+
47
+ const value = preserveValue(input[key], [key], sensitiveFields, new Set());
48
+ if (value !== undefined) result[key] = value;
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ export function groupIssuesByField(issues, options = {}) {
55
+ if (!Array.isArray(issues) || issues.length === 0) return {};
56
+
57
+ const rootKey = typeof options.rootKey === 'string' && options.rootKey.length > 0 ? options.rootKey : DEFAULT_ROOT_KEY;
58
+ const errors = {};
59
+
60
+ for (const issue of issues) {
61
+ const key = issue && typeof issue === 'object' && typeof issue.field === 'string' && issue.field.length > 0 ? issue.field : rootKey;
62
+ if (!Array.isArray(errors[key])) errors[key] = [];
63
+ errors[key].push(issue);
64
+ }
65
+
66
+ return errors;
67
+ }
68
+
69
+ function normalizeFormIssues(issues, ok, error) {
70
+ if (Array.isArray(issues) && issues.length > 0) {
71
+ return issues.map((issue) => normalizeIssue(issue, { boundary: 'input', source: 'framework' }));
72
+ }
73
+
74
+ if (!ok && error) {
75
+ return [createRootIssue({
76
+ code: error.code || 'form_failed',
77
+ message: error.message || 'Form submission failed',
78
+ boundary: 'handler',
79
+ source: 'framework'
80
+ })];
81
+ }
82
+
83
+ return [];
84
+ }
85
+
86
+ function normalizeFormError(error, ok) {
87
+ if (ok && !error) return null;
88
+ if (!error || typeof error !== 'object') {
89
+ return ok ? null : { code: 'FORM_FAILED', message: 'Form submission failed' };
90
+ }
91
+
92
+ return {
93
+ code: typeof error.code === 'string' && error.code.length > 0 ? error.code : 'FORM_FAILED',
94
+ message: typeof error.message === 'string' && error.message.length > 0 ? error.message : 'Form submission failed'
95
+ };
96
+ }
97
+
98
+ function preserveValue(value, path, sensitiveFields, seen) {
99
+ if (isSensitivePath(path, sensitiveFields)) return undefined;
100
+ if (value === null) return null;
101
+
102
+ const type = typeof value;
103
+ if (type === 'string' || type === 'number' || type === 'boolean') return value;
104
+ if (type === 'function' || type === 'symbol' || type === 'bigint' || type === 'undefined') return undefined;
105
+
106
+ if (Array.isArray(value)) {
107
+ const preserved = [];
108
+ for (let index = 0; index < value.length; index += 1) {
109
+ const item = preserveValue(value[index], path.concat(index), sensitiveFields, seen);
110
+ if (item !== undefined && isSafeArrayValue(item)) preserved.push(item);
111
+ }
112
+ return preserved;
113
+ }
114
+
115
+ if (!isPlainObject(value)) return undefined;
116
+ if (seen.has(value)) return undefined;
117
+
118
+ seen.add(value);
119
+ const result = {};
120
+ for (const key of Object.keys(value)) {
121
+ if (!isSafeKey(key) || isSensitiveSegment(key, sensitiveFields)) continue;
122
+ const preserved = preserveValue(value[key], path.concat(key), sensitiveFields, seen);
123
+ if (preserved !== undefined) result[key] = preserved;
124
+ }
125
+ seen.delete(value);
126
+
127
+ return result;
128
+ }
129
+
130
+ function isSafeArrayValue(value) {
131
+ if (value === null) return true;
132
+ if (Array.isArray(value)) return value.every(isSafeArrayValue);
133
+ if (isPlainObject(value)) return true;
134
+ const type = typeof value;
135
+ return type === 'string' || type === 'number' || type === 'boolean';
136
+ }
137
+
138
+ function createSensitiveSet(extra = []) {
139
+ const fields = new Set(DEFAULT_SENSITIVE_FIELDS);
140
+ if (Array.isArray(extra)) {
141
+ for (const field of extra) {
142
+ if (typeof field === 'string' && field.length > 0) fields.add(normalizeFieldName(field));
143
+ }
144
+ }
145
+ return fields;
146
+ }
147
+
148
+ function isSensitivePath(path, sensitiveFields) {
149
+ return path.some((segment) => typeof segment === 'string' && isSensitiveSegment(segment, sensitiveFields));
150
+ }
151
+
152
+ function isSensitiveSegment(segment, sensitiveFields) {
153
+ const normalized = normalizeFieldName(segment);
154
+ if (sensitiveFields.has(normalized)) return true;
155
+ for (const field of sensitiveFields) {
156
+ if (normalized.includes(field)) return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ function normalizeFieldName(value) {
162
+ return String(value).replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
163
+ }
164
+
165
+ function isSafeKey(key) {
166
+ return typeof key === 'string' && key.length > 0 && !DANGEROUS_KEYS.has(key);
167
+ }
168
+
169
+ function isPlainObject(value) {
170
+ if (!value || typeof value !== 'object') return false;
171
+ const prototype = Object.getPrototypeOf(value);
172
+ return prototype === Object.prototype || prototype === null;
173
+ }
@@ -0,0 +1,46 @@
1
+ import { createRoutes, normalizeHooks } from './route-collection.js';
2
+
3
+ export function createPlugin(options = {}) {
4
+ return {
5
+ kind: 'plugin',
6
+ name: typeof options.name === 'string' ? options.name : 'anonymous',
7
+ routes: Array.isArray(options.routes) ? options.routes.slice() : [],
8
+ hooks: normalizeHooks(options.hooks),
9
+ contracts: normalizeContracts(options.contracts),
10
+ setup: typeof options.setup === 'function' ? options.setup : null,
11
+ meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
12
+ };
13
+ }
14
+
15
+ export function pluginRoutes(plugins = [], appContext = {}) {
16
+ if (!Array.isArray(plugins)) return [];
17
+
18
+ const routes = [];
19
+ for (const plugin of plugins) {
20
+ const normalized = plugin && plugin.kind === 'plugin' ? plugin : createPlugin(plugin);
21
+ runSetup(normalized, appContext);
22
+ routes.push(createRoutes({
23
+ routes: normalized.routes,
24
+ hooks: normalized.hooks,
25
+ contracts: normalized.contracts,
26
+ meta: { plugin: normalized.name }
27
+ }));
28
+ }
29
+
30
+ return routes;
31
+ }
32
+
33
+ function runSetup(plugin, appContext) {
34
+ if (!plugin.setup) return;
35
+
36
+ plugin.setup(Object.freeze({
37
+ name: plugin.name,
38
+ state: Object.prototype.hasOwnProperty.call(appContext, 'state') ? appContext.state : null,
39
+ meta: plugin.meta
40
+ }));
41
+ }
42
+
43
+ function normalizeContracts(contracts = {}) {
44
+ if (!contracts || typeof contracts !== 'object') return {};
45
+ return { ...contracts };
46
+ }
@@ -0,0 +1,187 @@
1
+ import { fail, normalizeError } from './result.js';
2
+
3
+ export function json(value, init = {}) {
4
+ return createResponseDescriptor('json', value, init);
5
+ }
6
+
7
+ export function text(value, init = {}) {
8
+ return createResponseDescriptor('text', String(value), init);
9
+ }
10
+
11
+ export function redirect(location, status = 302) {
12
+ return createResponseDescriptor('redirect', null, {
13
+ status: status,
14
+ headers: { location: location }
15
+ });
16
+ }
17
+
18
+ export function toResponse(value) {
19
+ if (value instanceof Response) return value;
20
+
21
+ if (isResult(value)) {
22
+ if (value.ok) return toResponse(value.value);
23
+ return errorResponse(value.error);
24
+ }
25
+
26
+ if (isResponseDescriptor(value)) {
27
+ return descriptorToResponse(value);
28
+ }
29
+
30
+ if (value instanceof Error) {
31
+ return errorResponse(normalizeError(value));
32
+ }
33
+
34
+ if (value && typeof value === 'object' && value.name === 'PotentiaError') {
35
+ return errorResponse(normalizeError(value));
36
+ }
37
+
38
+ if (value === undefined) {
39
+ return new Response(null, { status: 204 });
40
+ }
41
+
42
+ if (value === null) {
43
+ return new Response(null, { status: 204 });
44
+ }
45
+
46
+ if (typeof value === 'string') {
47
+ return descriptorToResponse(text(value));
48
+ }
49
+
50
+ return descriptorToResponse(json(value));
51
+ }
52
+
53
+ export function errorResponse(error) {
54
+ const normalized = normalizeError(error);
55
+
56
+ if (normalized.details && typeof normalized.details === 'object' && normalized.details.kind === 'form') {
57
+ return descriptorToResponse(json(normalized.details, { status: normalized.status }));
58
+ }
59
+
60
+ const diagnosticShaped = hasDiagnosticDetails(normalized) || isActionOrDomainCode(normalized.code);
61
+ const body = diagnosticShaped ? {
62
+ ok: false,
63
+ error: {
64
+ code: normalized.code,
65
+ message: normalized.message
66
+ },
67
+ boundary: null,
68
+ issues: []
69
+ } : {
70
+ error: {
71
+ code: normalized.code,
72
+ message: normalized.message
73
+ }
74
+ };
75
+
76
+ if (normalized.details && typeof normalized.details === 'object') {
77
+ if (typeof normalized.details.boundary === 'string') {
78
+ if (diagnosticShaped) body.boundary = normalized.details.boundary;
79
+ body.error.boundary = normalized.details.boundary;
80
+ }
81
+ if (Array.isArray(normalized.details.issues)) {
82
+ if (diagnosticShaped) body.issues = normalized.details.issues;
83
+ body.error.issues = normalized.details.issues;
84
+ }
85
+ }
86
+
87
+ return descriptorToResponse(json(body, { status: normalized.status }));
88
+ }
89
+
90
+ function hasDiagnosticDetails(error) {
91
+ return Boolean(error && error.details && typeof error.details === 'object' && (typeof error.details.boundary === 'string' || Array.isArray(error.details.issues)));
92
+ }
93
+
94
+ function isActionOrDomainCode(code) {
95
+ return typeof code === 'string' && (code.startsWith('POTENTIA_ACTION_') || !code.startsWith('POTENTIA_'));
96
+ }
97
+
98
+ export function internalFailure(error) {
99
+ return fail(normalizeError(error));
100
+ }
101
+
102
+ export function isResponseDescriptor(value) {
103
+ return Boolean(value && typeof value === 'object' && value.kind === 'response');
104
+ }
105
+
106
+ export function responseBody(value) {
107
+ if (isResult(value)) return responseBody(value.value);
108
+ if (isResponseDescriptor(value)) return value.body;
109
+ return value;
110
+ }
111
+
112
+ export function replaceResponseBody(value, body) {
113
+ if (isResult(value)) {
114
+ return {
115
+ ok: value.ok,
116
+ value: replaceResponseBody(value.value, body),
117
+ error: value.error,
118
+ meta: value.meta
119
+ };
120
+ }
121
+
122
+ if (isResponseDescriptor(value)) {
123
+ return {
124
+ kind: value.kind,
125
+ type: value.type,
126
+ body: body,
127
+ status: value.status,
128
+ headers: value.headers
129
+ };
130
+ }
131
+
132
+ return body;
133
+ }
134
+
135
+ export function isRedirectResponse(value) {
136
+ if (isResult(value)) return value.ok && isRedirectResponse(value.value);
137
+ return isResponseDescriptor(value) && value.type === 'redirect';
138
+ }
139
+
140
+ function createResponseDescriptor(type, body, init) {
141
+ const headers = new Headers(init.headers || {});
142
+ const status = init.status || (type === 'redirect' ? 302 : 200);
143
+
144
+ return {
145
+ kind: 'response',
146
+ type: type,
147
+ body: body,
148
+ status: status,
149
+ headers: headers
150
+ };
151
+ }
152
+
153
+ function descriptorToResponse(descriptor) {
154
+ const headers = new Headers(descriptor.headers);
155
+
156
+ if (descriptor.type === 'json') {
157
+ if (!headers.has('content-type')) headers.set('content-type', 'application/json');
158
+ return new Response(JSON.stringify(descriptor.body), {
159
+ status: descriptor.status,
160
+ headers: headers
161
+ });
162
+ }
163
+
164
+ if (descriptor.type === 'text') {
165
+ if (!headers.has('content-type')) headers.set('content-type', 'text/plain; charset=utf-8');
166
+ return new Response(descriptor.body, {
167
+ status: descriptor.status,
168
+ headers: headers
169
+ });
170
+ }
171
+
172
+ if (descriptor.type === 'redirect') {
173
+ return new Response(null, {
174
+ status: descriptor.status,
175
+ headers: headers
176
+ });
177
+ }
178
+
179
+ return new Response(String(descriptor.body), {
180
+ status: descriptor.status,
181
+ headers: headers
182
+ });
183
+ }
184
+
185
+ function isResult(value) {
186
+ return Boolean(value && typeof value === 'object' && typeof value.ok === 'boolean' && 'value' in value && 'error' in value && 'meta' in value);
187
+ }
@@ -0,0 +1,45 @@
1
+ import { normalizeFrameworkError } from './error.js';
2
+
3
+ export function ok(value, meta = null) {
4
+ return {
5
+ ok: true,
6
+ value: value,
7
+ error: null,
8
+ meta: meta
9
+ };
10
+ }
11
+
12
+ export function fail(error, meta = null) {
13
+ const normalizedInput = typeof meta === 'number' && error && typeof error === 'object' ? { ...error, status: meta } : error;
14
+
15
+ return {
16
+ ok: false,
17
+ value: null,
18
+ error: normalizeError(normalizedInput),
19
+ meta: typeof meta === 'number' ? null : meta
20
+ };
21
+ }
22
+
23
+ export function normalizeError(error) {
24
+ if (isFormState(error)) {
25
+ return {
26
+ code: error.error.code,
27
+ message: error.error.message,
28
+ status: Number.isInteger(error.status) ? error.status : 400,
29
+ details: error
30
+ };
31
+ }
32
+
33
+ const normalized = normalizeFrameworkError(error);
34
+
35
+ return {
36
+ code: normalized.code,
37
+ message: normalized.message,
38
+ status: normalized.status,
39
+ details: normalized.detail
40
+ };
41
+ }
42
+
43
+ function isFormState(value) {
44
+ return Boolean(value && typeof value === 'object' && value.kind === 'form' && typeof value.ok === 'boolean' && value.error && typeof value.error === 'object' && typeof value.error.code === 'string');
45
+ }
@@ -0,0 +1,148 @@
1
+ import { route } from './route.js';
2
+
3
+ export function createRoutes(options = {}) {
4
+ return {
5
+ kind: 'routes',
6
+ prefix: normalizePrefix(options.prefix || ''),
7
+ routes: Array.isArray(options.routes) ? options.routes.slice() : [],
8
+ hooks: normalizeHooks(options.hooks),
9
+ contracts: normalizeContracts(options.contracts),
10
+ meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
11
+ };
12
+ }
13
+
14
+ export function mount(source, options = {}) {
15
+ return {
16
+ kind: 'mount',
17
+ source: source,
18
+ prefix: normalizePrefix(options.prefix || ''),
19
+ hooks: normalizeHooks(options.hooks),
20
+ contracts: normalizeContracts(options.contracts),
21
+ meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
22
+ };
23
+ }
24
+
25
+ export function composeRoutes(entries = []) {
26
+ const routes = Array.isArray(entries) ? entries : [];
27
+ return flattenRoutes(routes, emptyScope());
28
+ }
29
+
30
+ function flattenRoutes(entries, scope) {
31
+ const flattened = [];
32
+
33
+ for (const entry of entries) {
34
+ if (isMount(entry)) {
35
+ flattened.push(...flattenMounted(entry, scope));
36
+ continue;
37
+ }
38
+
39
+ if (isRouteCollection(entry)) {
40
+ flattened.push(...flattenCollection(entry, scope));
41
+ continue;
42
+ }
43
+
44
+ if (isRoute(entry)) {
45
+ flattened.push(cloneRoute(entry, scope));
46
+ }
47
+ }
48
+
49
+ return flattened;
50
+ }
51
+
52
+ function flattenMounted(entry, scope) {
53
+ const nextScope = mergeScope(scope, {
54
+ prefix: entry.prefix,
55
+ contracts: entry.contracts,
56
+ hooks: entry.hooks
57
+ });
58
+
59
+ if (isRoute(entry.source)) return [cloneRoute(entry.source, nextScope)];
60
+ if (isRouteCollection(entry.source)) return flattenCollection(entry.source, nextScope);
61
+ if (Array.isArray(entry.source)) return flattenRoutes(entry.source, nextScope);
62
+ return [];
63
+ }
64
+
65
+ function flattenCollection(collection, scope) {
66
+ const nextScope = mergeScope(scope, {
67
+ prefix: collection.prefix,
68
+ contracts: collection.contracts,
69
+ hooks: collection.hooks
70
+ });
71
+
72
+ return flattenRoutes(collection.routes, nextScope);
73
+ }
74
+
75
+ function cloneRoute(source, scope) {
76
+ const cloned = route(source.method, composePath(scope.prefix, source.path), source.handler, {
77
+ ...scope.contracts,
78
+ ...(source.options || {})
79
+ });
80
+ cloned.hooks = normalizeHooks(scope.hooks);
81
+ return cloned;
82
+ }
83
+
84
+ function emptyScope() {
85
+ return {
86
+ prefix: '',
87
+ contracts: {},
88
+ hooks: normalizeHooks()
89
+ };
90
+ }
91
+
92
+ function mergeScope(parent, child) {
93
+ const childHooks = normalizeHooks(child.hooks);
94
+ return {
95
+ prefix: composePath(parent.prefix, child.prefix || ''),
96
+ contracts: {
97
+ ...parent.contracts,
98
+ ...normalizeContracts(child.contracts)
99
+ },
100
+ hooks: {
101
+ beforeRequest: parent.hooks.beforeRequest.concat(childHooks.beforeRequest),
102
+ afterResponse: childHooks.afterResponse.concat(parent.hooks.afterResponse),
103
+ onError: childHooks.onError.concat(parent.hooks.onError)
104
+ }
105
+ };
106
+ }
107
+
108
+ export function normalizeHooks(hooks = {}) {
109
+ return {
110
+ beforeRequest: Array.isArray(hooks.beforeRequest) ? hooks.beforeRequest.slice() : [],
111
+ afterResponse: Array.isArray(hooks.afterResponse) ? hooks.afterResponse.slice() : [],
112
+ onError: Array.isArray(hooks.onError) ? hooks.onError.slice() : []
113
+ };
114
+ }
115
+
116
+ function normalizeContracts(contracts = {}) {
117
+ if (!contracts || typeof contracts !== 'object') return {};
118
+ return { ...contracts };
119
+ }
120
+
121
+ function normalizePrefix(prefix) {
122
+ if (!prefix) return '';
123
+ const normalized = `/${String(prefix).split('/').filter(Boolean).join('/')}`;
124
+ return normalized === '/' ? '' : normalized;
125
+ }
126
+
127
+ function composePath(prefix, path) {
128
+ const parts = [];
129
+ for (const value of [prefix, path]) {
130
+ const segment = String(value || '').split('/').filter(Boolean).join('/');
131
+ if (segment) parts.push(segment);
132
+ }
133
+
134
+ if (parts.length === 0) return '/';
135
+ return `/${parts.join('/')}`;
136
+ }
137
+
138
+ function isMount(value) {
139
+ return Boolean(value && typeof value === 'object' && value.kind === 'mount');
140
+ }
141
+
142
+ function isRouteCollection(value) {
143
+ return Boolean(value && typeof value === 'object' && value.kind === 'routes' && Array.isArray(value.routes));
144
+ }
145
+
146
+ function isRoute(value) {
147
+ return Boolean(value && typeof value === 'object' && typeof value.method === 'string' && typeof value.path === 'string' && Object.prototype.hasOwnProperty.call(value, 'handler'));
148
+ }
@@ -0,0 +1,11 @@
1
+ export function createRouteId(value) {
2
+ if (!value || typeof value !== 'object') return null;
3
+ if (typeof value.method !== 'string' || typeof value.path !== 'string') return null;
4
+
5
+ return `${String(value.method).toUpperCase()} ${normalizeRoutePath(value.path)}`;
6
+ }
7
+
8
+ export function normalizeRoutePath(path) {
9
+ const normalized = `/${String(path || '').split('/').filter(Boolean).join('/')}`;
10
+ return normalized === '' ? '/' : normalized;
11
+ }