proteum 2.4.3 → 2.5.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/README.md +60 -55
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/eslint.js +36 -1
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/http-client-error-context.test.cjs +10 -1
- package/server/app/container/console/index.ts +2 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -22,19 +22,19 @@ export const createPageTemplate = ({
|
|
|
22
22
|
routePath: string;
|
|
23
23
|
heading: string;
|
|
24
24
|
message: string;
|
|
25
|
-
}) => `import
|
|
25
|
+
}) => `import { definePageRoute } from '@common/router/definitions';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
${JSON.stringify(routePath)},
|
|
29
|
-
{
|
|
27
|
+
export default definePageRoute({
|
|
28
|
+
path: ${JSON.stringify(routePath)},
|
|
29
|
+
options: {
|
|
30
30
|
auth: false,
|
|
31
31
|
layout: false,
|
|
32
32
|
},
|
|
33
|
-
() => ({
|
|
33
|
+
data: () => ({
|
|
34
34
|
heading: ${JSON.stringify(heading)},
|
|
35
35
|
message: ${JSON.stringify(message)},
|
|
36
36
|
}),
|
|
37
|
-
({ heading, message }) => {
|
|
37
|
+
render: ({ heading, message }) => {
|
|
38
38
|
return (
|
|
39
39
|
<main>
|
|
40
40
|
<h1>{heading}</h1>
|
|
@@ -42,7 +42,7 @@ Router.page(
|
|
|
42
42
|
</main>
|
|
43
43
|
);
|
|
44
44
|
},
|
|
45
|
-
);
|
|
45
|
+
});
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
48
|
export const createControllerTemplate = ({
|
|
@@ -53,15 +53,19 @@ export const createControllerTemplate = ({
|
|
|
53
53
|
appIdentifier: string;
|
|
54
54
|
className: string;
|
|
55
55
|
methodName: string;
|
|
56
|
-
}) => `import
|
|
57
|
-
|
|
58
|
-
export default
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
56
|
+
}) => `import { defineAction, defineController } from '@server/app/controller';
|
|
57
|
+
|
|
58
|
+
export default defineController({
|
|
59
|
+
actions: {
|
|
60
|
+
${methodName}: defineAction({
|
|
61
|
+
async handler() {
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
65
69
|
`;
|
|
66
70
|
|
|
67
71
|
export const createCommandTemplate = ({
|
|
@@ -89,12 +93,17 @@ export const createRouteTemplate = ({
|
|
|
89
93
|
}: {
|
|
90
94
|
httpMethod: string;
|
|
91
95
|
routePath: string;
|
|
92
|
-
}) => `import {
|
|
96
|
+
}) => `import { defineServerRoute } from '@common/router/definitions';
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
+
export default defineServerRoute({
|
|
99
|
+
method: ${JSON.stringify(httpMethod.toUpperCase())},
|
|
100
|
+
path: ${JSON.stringify(routePath)},
|
|
101
|
+
options: {},
|
|
102
|
+
async handler() {
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
98
107
|
});
|
|
99
108
|
`;
|
|
100
109
|
|
|
@@ -156,24 +165,26 @@ export const routerBaseConfig = {
|
|
|
156
165
|
} satisfies RouterBaseConfig;
|
|
157
166
|
`;
|
|
158
167
|
|
|
159
|
-
export const createServerIndexTemplate = (
|
|
168
|
+
export const createServerIndexTemplate = (_args: { appIdentifier: string }) => `import { defineApplication } from '@server/app';
|
|
160
169
|
import Router from '@server/services/router';
|
|
161
170
|
import SchemaRouter from '@server/services/schema/router';
|
|
162
171
|
|
|
163
172
|
import * as appConfig from '@/server/config/app';
|
|
164
173
|
|
|
165
|
-
export default
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
export default defineApplication({
|
|
175
|
+
services: (app) => ({
|
|
176
|
+
Router: new Router(
|
|
177
|
+
app,
|
|
178
|
+
{
|
|
179
|
+
...appConfig.routerBaseConfig,
|
|
180
|
+
plugins: {
|
|
181
|
+
schema: new SchemaRouter({}, app),
|
|
182
|
+
},
|
|
172
183
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
}
|
|
184
|
+
app,
|
|
185
|
+
),
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
177
188
|
`;
|
|
178
189
|
|
|
179
190
|
export const createClientTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
|
package/cli/utils/agents.ts
CHANGED
|
@@ -638,19 +638,32 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
|
|
|
638
638
|
'- Do not run `git restore` or `git reset`.',
|
|
639
639
|
'- Keep `proteum dev` sessions tracked with explicit session files and do not replace another live session.',
|
|
640
640
|
'',
|
|
641
|
+
'## Explicit App-Building Contract',
|
|
642
|
+
'',
|
|
643
|
+
'- App roots default-export `defineApplication({ services, router, models, commands })`; `server/index.ts` is the canonical type root for the project app, services, router plugins, request context, and models.',
|
|
644
|
+
'- Client page files default-export `definePageRoute({ path, options, data, render })` or `defineErrorRoute({ code, options, render })`; route paths come from the definition object, not from `Router.page(...)` or the file path.',
|
|
645
|
+
'- Manual HTTP route files default-export `defineServerRoute({ method, path, options, handler })` or `defineServerRoutes(...)`; use `expressHandler(...)` only when raw Express `req`, `res`, or `next` is required.',
|
|
646
|
+
'- Controllers default-export `defineController({ path, actions })`; actions use `defineAction({ input, handler })`, and parsed input is read from the handler context.',
|
|
647
|
+
'- Never import `@app` in page, route, or controller files. Never call top-level `Router.page(...)`, `Router.error(...)`, `Router.get(...)`, `Router.post(...)`, `Router.put(...)`, `Router.patch(...)`, `Router.delete(...)`, or `Router.express(...)` in app source.',
|
|
648
|
+
'- Runtime app, service, request, response, router, auth, and custom router-plugin access belongs only in typed callback parameters such as `data`, `render`, route `handler`, controller action `handler`, `defineServerRoutes((app) => ...)`, or typed service `this.app`/`this.services`.',
|
|
649
|
+
'',
|
|
641
650
|
'## Triggered Instruction Reads',
|
|
642
651
|
'',
|
|
643
652
|
'Keep this root file as a router. MCP-selected previews are enough for read-only discovery and diagnostics. Read the referenced full instruction file only before edits or git writes, when `fullRead`/`fullReadPolicy` requires it, or when the preview is insufficient.',
|
|
644
653
|
'',
|
|
654
|
+
'- Worktree Preflight (`cwd` inside `/.codex/worktrees/`, newly created Proteum worktree, or before editing in a Codex worktree): read Root contract fallback, run `npx proteum worktree init --source <source-app-root>` when the bootstrap marker is missing, run `npx proteum worktree init --source <source-app-root> --refresh` when Proteum reports stale bootstrap state, use `--skip-deps --reason "..."` only for intentional dependency skips, then run `npx proteum runtime status`; for runtime-visible work start or reuse one tracked `npx proteum dev` session using the Task Lifecycle launch workflow.',
|
|
645
655
|
'- Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): read Root contract fallback before any git write.',
|
|
646
|
-
'- Before
|
|
656
|
+
'- Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change: read `DOCUMENTATION.md` and verify required docs, fix notes, or ADRs were updated or explicitly skipped with a reason.',
|
|
657
|
+
'- Before finishing production code changes: read Root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, `tests/AGENTS.md`, and any touched area `AGENTS.md`.',
|
|
647
658
|
'- Runtime-visible, request-time, router, SSR, browser, or controller behavior: read Root contract fallback and `diagnostics.md` for verification routing.',
|
|
659
|
+
'- Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: read `DOCUMENTATION.md` before editing and update the relevant fix/regression docs when required.',
|
|
648
660
|
'- Non-trivial feature, product, business-rule, UX, copy, or docs changes: read `DOCUMENTATION.md` before editing.',
|
|
649
661
|
'- Implementation edits: read `CODING_STYLE.md` before editing, plus the matching area file from the routing table.',
|
|
650
662
|
'',
|
|
651
663
|
'## Routing Table',
|
|
652
664
|
'',
|
|
653
665
|
'- Non-trivial coding tasks, feature docs, product intent, acceptance criteria, or docs updates: read `DOCUMENTATION.md`.',
|
|
666
|
+
'- Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: read `DOCUMENTATION.md`, `diagnostics.md`, `CODING_STYLE.md`, `tests/AGENTS.md`, and the touched area `AGENTS.md`; update or create `docs/fixes/YYYY-MM-DD-short-bug-name.md` and regression-test docs when required, or explain why no docs update was needed.',
|
|
654
667
|
'- GEO/SEO/crawler/structured-data/AI-source changes: read `DOCUMENTATION.md`, `CODING_STYLE.md`, `tests/AGENTS.md`, and update or create a docs page under `docs/` describing the public contract, routes, validation, and operational caveats.',
|
|
655
668
|
'- Raw errors, failing requests, traces, perf, or reproduction: read `diagnostics.md`.',
|
|
656
669
|
'- Implementation edits: read `CODING_STYLE.md` before editing.',
|
|
@@ -21,8 +21,11 @@ import BaseRouter, {
|
|
|
21
21
|
TErrorRoute,
|
|
22
22
|
TRouteOptions,
|
|
23
23
|
TRouteModule,
|
|
24
|
+
type TRouteDefinition,
|
|
25
|
+
type TRouteMetadata,
|
|
24
26
|
matchRoute,
|
|
25
27
|
buildUrl,
|
|
28
|
+
withRouteMetadata,
|
|
26
29
|
} from '@common/router';
|
|
27
30
|
import type { TRegisterPageArgs, TSsrUnresolvedRoute } from '@common/router/contracts';
|
|
28
31
|
import { getLayout } from '@common/router/layouts';
|
|
@@ -241,14 +244,31 @@ export default class ClientRouter<
|
|
|
241
244
|
return currentRoute;
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
public
|
|
247
|
+
public registerRouteDefinition(definition: TRouteDefinition, metadata: TRouteMetadata = {}) {
|
|
248
|
+
if (definition.kind === 'page') {
|
|
249
|
+
return this.page(
|
|
250
|
+
definition.path,
|
|
251
|
+
withRouteMetadata(definition.options, metadata),
|
|
252
|
+
definition.data,
|
|
253
|
+
definition.render,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (definition.kind === 'error') {
|
|
258
|
+
return this.error(definition.code, withRouteMetadata(definition.options, metadata), definition.render);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new Error(`Client router cannot register server route definition: ${definition.method} ${definition.path}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected page<TProvidedData extends {} = {}>(
|
|
245
265
|
path: string,
|
|
246
266
|
options: Partial<TRouteOptions>,
|
|
247
267
|
data: TPageDataProvider<TProvidedData> | null,
|
|
248
268
|
renderer: TFrontRenderer<TProvidedData>,
|
|
249
269
|
): TClientPageRoute<this>;
|
|
250
270
|
|
|
251
|
-
|
|
271
|
+
protected page(...args: TRegisterPageArgs<any, TRouteOptions>): TClientPageRoute<this> {
|
|
252
272
|
const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
|
|
253
273
|
|
|
254
274
|
// Page ids are injected by the generated route wrapper modules.
|
|
@@ -272,7 +292,7 @@ export default class ClientRouter<
|
|
|
272
292
|
return route;
|
|
273
293
|
}
|
|
274
294
|
|
|
275
|
-
|
|
295
|
+
protected error(
|
|
276
296
|
code: number,
|
|
277
297
|
options: Partial<TRouteOptions>,
|
|
278
298
|
renderer: TFrontRenderer<{}, { message: string }>,
|
|
@@ -38,8 +38,19 @@ type TExecuteResult<TData> = { data: TData; durationMs: number; response: Respon
|
|
|
38
38
|
|
|
39
39
|
export type Config = {};
|
|
40
40
|
|
|
41
|
-
const isFileValue = (value: unknown): value is
|
|
42
|
-
typeof
|
|
41
|
+
const isFileValue = (value: unknown): value is Blob =>
|
|
42
|
+
typeof Blob !== 'undefined' && typeof value === 'object' && value instanceof Blob;
|
|
43
|
+
|
|
44
|
+
const isFileListValue = (value: unknown): value is FileList =>
|
|
45
|
+
typeof FileList !== 'undefined' && typeof value === 'object' && value instanceof FileList;
|
|
46
|
+
|
|
47
|
+
const containsFileValue = (value: unknown): boolean => {
|
|
48
|
+
if (isFileValue(value) || isFileListValue(value)) return true;
|
|
49
|
+
if (value instanceof Date) return false;
|
|
50
|
+
if (Array.isArray(value)) return value.some((item) => containsFileValue(item));
|
|
51
|
+
if (value && typeof value === 'object') return Object.values(value).some((item) => containsFileValue(item));
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
43
54
|
|
|
44
55
|
/*----------------------------------
|
|
45
56
|
- FUNCTION
|
|
@@ -286,8 +297,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
286
297
|
// Update options depending on data
|
|
287
298
|
if (data) {
|
|
288
299
|
// If file included in data, need to use multipart
|
|
289
|
-
|
|
290
|
-
const hasFile = Object.values(data).some((value) => isFileValue(value));
|
|
300
|
+
const hasFile = containsFileValue(data);
|
|
291
301
|
if (hasFile) {
|
|
292
302
|
// GET request = Can't send files
|
|
293
303
|
if (method === 'GET') throw new Error('Cannot send file in GET request');
|
|
@@ -314,7 +314,7 @@ const buildHookContractDiagnostics = (manifest: TProteumManifest, sourceFilepath
|
|
|
314
314
|
sourceLocation,
|
|
315
315
|
fixHint:
|
|
316
316
|
importedHook.kind === 'router-context'
|
|
317
|
-
? 'Call the hook only inside a
|
|
317
|
+
? 'Call the hook only inside a definePageRoute render callback, a component rendered under App, or a custom hook used from that tree.'
|
|
318
318
|
: 'Move the hook back under the provider-managed React tree or pass the required values as explicit props or SSR data.',
|
|
319
319
|
relatedFilepaths,
|
|
320
320
|
}),
|
|
@@ -131,6 +131,10 @@ export const resolveTriggeredInstructionReads = ({
|
|
|
131
131
|
normalizedQuery,
|
|
132
132
|
/\b(implement|change|edit|update|modify|fix|add|remove|refactor|increase|decrease|code)\b/,
|
|
133
133
|
);
|
|
134
|
+
const looksLikeBugFixOrDecisionDocs = matchesInstructionTrigger(
|
|
135
|
+
normalizedQuery,
|
|
136
|
+
/\b(bug|regression|incident|broken|outage|fix|fixed|decision|adr|auth|oauth|integration|public route|production behavior)\b/,
|
|
137
|
+
);
|
|
134
138
|
const looksLikeProductOrDocs = matchesInstructionTrigger(
|
|
135
139
|
normalizedQuery,
|
|
136
140
|
/\b(feature|product|business|acceptance|docs|documentation|ux|copy|onboarding|pricing|commercial|semantics)\b/,
|
|
@@ -153,6 +157,9 @@ export const resolveTriggeredInstructionReads = ({
|
|
|
153
157
|
if (looksLikeImplementationEdit) {
|
|
154
158
|
addRead(codingStyle, 'Implementation edit trigger; read coding style before editing.');
|
|
155
159
|
}
|
|
160
|
+
if (looksLikeBugFixOrDecisionDocs) {
|
|
161
|
+
addRead(documentation, 'Bug fix, regression, auth/OAuth, integration, public-route, decision, or production-behavior trigger.');
|
|
162
|
+
}
|
|
156
163
|
if (looksLikeProductOrDocs) {
|
|
157
164
|
addRead(documentation, 'Feature, product, business-rule, UX, copy, or docs trigger.');
|
|
158
165
|
}
|
|
@@ -903,7 +910,7 @@ export const resolveInstructionRouting = ({
|
|
|
903
910
|
|
|
904
911
|
addReadWhen(
|
|
905
912
|
'DOCUMENTATION.md',
|
|
906
|
-
'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
|
|
913
|
+
'Read before non-trivial coding tasks, bug fixes, auth/OAuth, integration, public-route, decision, or production-behavior changes to choose the smallest `/docs` pack and update docs after changes.',
|
|
907
914
|
);
|
|
908
915
|
addReadWhen('diagnostics.md', 'Read for raw errors, failing requests, traces, perf regressions, or reproduction work.');
|
|
909
916
|
addReadWhen('CODING_STYLE.md', 'Read before editing implementation files.');
|
package/common/env/proteumEnv.ts
CHANGED
|
@@ -238,6 +238,16 @@ const parseAbsoluteUrl = ({
|
|
|
238
238
|
return value;
|
|
239
239
|
};
|
|
240
240
|
|
|
241
|
+
const applyRouterPortOverrideToUrl = (value: string, routerPortOverride: number | undefined) => {
|
|
242
|
+
if (routerPortOverride === undefined) return value;
|
|
243
|
+
|
|
244
|
+
const url = new URL(value);
|
|
245
|
+
url.port = String(routerPortOverride);
|
|
246
|
+
const serialized = url.toString();
|
|
247
|
+
|
|
248
|
+
return value.endsWith('/') ? serialized : serialized.replace(/\/$/, '');
|
|
249
|
+
};
|
|
250
|
+
|
|
241
251
|
const parseConnectedProjectAbsoluteUrl = ({
|
|
242
252
|
appDir,
|
|
243
253
|
namespace,
|
|
@@ -323,16 +333,18 @@ export const parseProteumEnvConfig = ({
|
|
|
323
333
|
value: getRequiredEnvValue({ key: 'PORT', context }),
|
|
324
334
|
context,
|
|
325
335
|
});
|
|
326
|
-
const
|
|
336
|
+
const configuredCurrentDomain = parseAbsoluteUrl({
|
|
327
337
|
key: 'URL',
|
|
328
338
|
value: getRequiredEnvValue({ key: 'URL', context }),
|
|
329
339
|
context,
|
|
330
340
|
});
|
|
331
|
-
const
|
|
341
|
+
const configuredInternalUrl = parseAbsoluteUrl({
|
|
332
342
|
key: 'URL_INTERNAL',
|
|
333
343
|
value: getRequiredEnvValue({ key: 'URL_INTERNAL', context }),
|
|
334
344
|
context,
|
|
335
345
|
});
|
|
346
|
+
const currentDomain = applyRouterPortOverrideToUrl(configuredCurrentDomain, routerPortOverride);
|
|
347
|
+
const internalUrl = applyRouterPortOverrideToUrl(configuredInternalUrl, routerPortOverride);
|
|
336
348
|
|
|
337
349
|
const traceEnable = parseBooleanEnvValue({
|
|
338
350
|
key: 'TRACE_ENABLE',
|
|
@@ -10,7 +10,7 @@ import type { TRouteOptions } from '.';
|
|
|
10
10
|
- PUBLIC API
|
|
11
11
|
----------------------------------*/
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Runtime page registration signature used by generated definePageRoute wrappers.
|
|
14
14
|
export type TRegisterPageArgs<TProvidedData extends {} = {}, TPageOptions extends {} = TRouteOptions> =
|
|
15
15
|
[
|
|
16
16
|
path: string,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
7
|
+
|
|
8
|
+
// Core
|
|
9
|
+
import type { TAnyRouter, TRouterContext, TRouteHttpMethod } from '@server/services/router';
|
|
10
|
+
import type { TFrontRenderer, TPageDataProvider } from './response/page';
|
|
11
|
+
import type { TRouteOptions } from '.';
|
|
12
|
+
|
|
13
|
+
/*----------------------------------
|
|
14
|
+
- TYPES
|
|
15
|
+
----------------------------------*/
|
|
16
|
+
|
|
17
|
+
export type TRouteMetadata = {
|
|
18
|
+
filepath?: string;
|
|
19
|
+
sourceLocation?: { line: number; column: number };
|
|
20
|
+
id?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type TRouteDefinitionHttpMethod = TRouteHttpMethod | Lowercase<Exclude<TRouteHttpMethod, '*'>>;
|
|
24
|
+
|
|
25
|
+
export type TPageRouteDefinition<TProvidedData extends {} = {}> = {
|
|
26
|
+
kind: 'page';
|
|
27
|
+
path: string;
|
|
28
|
+
options: Partial<TRouteOptions>;
|
|
29
|
+
data: TPageDataProvider<TProvidedData> | null;
|
|
30
|
+
render: TFrontRenderer<TProvidedData>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type TErrorRouteDefinition = {
|
|
34
|
+
kind: 'error';
|
|
35
|
+
code: number;
|
|
36
|
+
options: Partial<TRouteOptions>;
|
|
37
|
+
render: TFrontRenderer<{}, { message: string }>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TServerRouteDefinition<TRouter extends TAnyRouter = TAnyRouter> = {
|
|
41
|
+
kind: 'server';
|
|
42
|
+
method: TRouteDefinitionHttpMethod;
|
|
43
|
+
path: string;
|
|
44
|
+
options: Partial<TRouteOptions>;
|
|
45
|
+
handler: (context: TRouterContext<TRouter>) => any;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type TRouteDefinition =
|
|
49
|
+
| TPageRouteDefinition
|
|
50
|
+
| TErrorRouteDefinition
|
|
51
|
+
| TServerRouteDefinition;
|
|
52
|
+
|
|
53
|
+
export type TServerRouteDefinitionsFactory<TApplication extends object = object> = (
|
|
54
|
+
app: TApplication,
|
|
55
|
+
) => TServerRouteDefinition[];
|
|
56
|
+
|
|
57
|
+
export type TRouteDefinitionExport =
|
|
58
|
+
| TRouteDefinition
|
|
59
|
+
| TRouteDefinition[]
|
|
60
|
+
| TServerRouteDefinitionsFactory<any>
|
|
61
|
+
| { default: TRouteDefinition | TRouteDefinition[] | TServerRouteDefinitionsFactory<any> };
|
|
62
|
+
|
|
63
|
+
export type TExpressRouteHandler<TRouter extends TAnyRouter = TAnyRouter> = (
|
|
64
|
+
req: Request,
|
|
65
|
+
res: Response,
|
|
66
|
+
next: NextFunction,
|
|
67
|
+
requestContext: TRouterContext<TRouter>,
|
|
68
|
+
) => void | Promise<void>;
|
|
69
|
+
|
|
70
|
+
export type TRouteDefinitionRegistrar = {
|
|
71
|
+
registerRouteDefinition: (definition: TRouteDefinition, metadata?: TRouteMetadata) => unknown;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/*----------------------------------
|
|
75
|
+
- HELPERS
|
|
76
|
+
----------------------------------*/
|
|
77
|
+
|
|
78
|
+
export const definePageRoute = <TProvidedData extends {} = {}>({
|
|
79
|
+
path,
|
|
80
|
+
options,
|
|
81
|
+
data,
|
|
82
|
+
render,
|
|
83
|
+
}: Omit<TPageRouteDefinition<TProvidedData>, 'kind'>): TPageRouteDefinition<TProvidedData> => ({
|
|
84
|
+
kind: 'page',
|
|
85
|
+
path,
|
|
86
|
+
options,
|
|
87
|
+
data,
|
|
88
|
+
render,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const defineErrorRoute = ({
|
|
92
|
+
code,
|
|
93
|
+
options,
|
|
94
|
+
render,
|
|
95
|
+
}: Omit<TErrorRouteDefinition, 'kind'>): TErrorRouteDefinition => ({
|
|
96
|
+
kind: 'error',
|
|
97
|
+
code,
|
|
98
|
+
options,
|
|
99
|
+
render,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const defineServerRoute = <TRouter extends TAnyRouter = TAnyRouter>({
|
|
103
|
+
method,
|
|
104
|
+
path,
|
|
105
|
+
options,
|
|
106
|
+
handler,
|
|
107
|
+
}: Omit<TServerRouteDefinition<TRouter>, 'kind'>): TServerRouteDefinition<TRouter> => ({
|
|
108
|
+
kind: 'server',
|
|
109
|
+
method,
|
|
110
|
+
path,
|
|
111
|
+
options,
|
|
112
|
+
handler,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const defineServerRoutes = <
|
|
116
|
+
TRouter extends TAnyRouter = TAnyRouter,
|
|
117
|
+
TApplication extends object = object,
|
|
118
|
+
>(
|
|
119
|
+
routes: TServerRouteDefinition<TRouter>[] | TServerRouteDefinitionsFactory<TApplication>,
|
|
120
|
+
) => routes;
|
|
121
|
+
|
|
122
|
+
export const expressHandler = <TRouter extends TAnyRouter = TAnyRouter>(
|
|
123
|
+
middleware: TExpressRouteHandler<TRouter>,
|
|
124
|
+
) => {
|
|
125
|
+
return (requestContext: TRouterContext<TRouter>) =>
|
|
126
|
+
new Promise((resolve, reject) => {
|
|
127
|
+
requestContext.request.res.on('finish', function () {
|
|
128
|
+
resolve(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const result = middleware(
|
|
133
|
+
requestContext.request.req,
|
|
134
|
+
requestContext.request.res,
|
|
135
|
+
() => {
|
|
136
|
+
resolve(true);
|
|
137
|
+
},
|
|
138
|
+
requestContext,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (result && typeof (result as Promise<void>).then === 'function') {
|
|
142
|
+
(result as Promise<void>).catch(reject);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
reject(error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const normalizeRouteDefinitions = (
|
|
151
|
+
value: TRouteDefinition | TRouteDefinition[] | TServerRouteDefinitionsFactory<any>,
|
|
152
|
+
app?: object,
|
|
153
|
+
) => {
|
|
154
|
+
const definitions = typeof value === 'function' ? value(app || {}) : value;
|
|
155
|
+
|
|
156
|
+
return Array.isArray(definitions) ? definitions : [definitions];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const withRouteMetadata = <TOptions extends Partial<TRouteOptions>>(
|
|
160
|
+
options: TOptions,
|
|
161
|
+
metadata: TRouteMetadata,
|
|
162
|
+
): TOptions & TRouteMetadata => ({
|
|
163
|
+
...options,
|
|
164
|
+
...metadata,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const registerRouteDefinition = (
|
|
168
|
+
router: TRouteDefinitionRegistrar,
|
|
169
|
+
definition: TRouteDefinition,
|
|
170
|
+
metadata: TRouteMetadata = {},
|
|
171
|
+
) => {
|
|
172
|
+
if (!router || typeof router.registerRouteDefinition !== 'function') {
|
|
173
|
+
throw new Error('Proteum route definitions require a router with registerRouteDefinition(definition, metadata).');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return router.registerRouteDefinition(definition, metadata);
|
|
177
|
+
};
|
package/common/router/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ import type zod from 'zod';
|
|
|
7
7
|
|
|
8
8
|
// types
|
|
9
9
|
import type { default as ClientRouter, TRouterContext as ClientRouterContext } from '@client/services/router';
|
|
10
|
-
import type { TRegisterPageArgs } from './contracts';
|
|
11
|
-
|
|
12
10
|
import type { TAnyRouter, TRouterContext as ServerRouterContext, TRouteHttpMethod } from '@server/services/router';
|
|
13
11
|
|
|
14
12
|
import type RouterRequest from './request';
|
|
@@ -18,13 +16,34 @@ import type { TAuthCheckInput, TAuthTrackingContext } from '@server/services/aut
|
|
|
18
16
|
import type { TAppArrowFunction } from '@common/app';
|
|
19
17
|
|
|
20
18
|
// Specfic
|
|
21
|
-
import type { default as Page,
|
|
19
|
+
import type { default as Page, TPageDataProvider } from './response/page';
|
|
22
20
|
|
|
23
21
|
/*----------------------------------
|
|
24
22
|
- TYPES: ROUTES
|
|
25
23
|
----------------------------------*/
|
|
26
24
|
|
|
27
25
|
export type { Layout } from './layouts';
|
|
26
|
+
export {
|
|
27
|
+
defineErrorRoute,
|
|
28
|
+
definePageRoute,
|
|
29
|
+
defineServerRoute,
|
|
30
|
+
defineServerRoutes,
|
|
31
|
+
expressHandler,
|
|
32
|
+
normalizeRouteDefinitions,
|
|
33
|
+
registerRouteDefinition,
|
|
34
|
+
withRouteMetadata,
|
|
35
|
+
} from './definitions';
|
|
36
|
+
export type {
|
|
37
|
+
TErrorRouteDefinition,
|
|
38
|
+
TExpressRouteHandler,
|
|
39
|
+
TPageRouteDefinition,
|
|
40
|
+
TRouteDefinition,
|
|
41
|
+
TRouteDefinitionExport,
|
|
42
|
+
TRouteDefinitionHttpMethod,
|
|
43
|
+
TRouteDefinitionRegistrar,
|
|
44
|
+
TRouteMetadata,
|
|
45
|
+
TServerRouteDefinition,
|
|
46
|
+
} from './definitions';
|
|
28
47
|
|
|
29
48
|
export type { default as Request } from './request';
|
|
30
49
|
export type { default as Response } from './response';
|
|
@@ -166,13 +185,5 @@ export const matchRoute = (route: TRouteMatch, request: RouterRequest) => {
|
|
|
166
185
|
----------------------------------*/
|
|
167
186
|
|
|
168
187
|
export default abstract class RouterInterface {
|
|
169
|
-
public abstract
|
|
170
|
-
...args: TRegisterPageArgs<TControllerData, TRouteOptions>
|
|
171
|
-
): unknown;
|
|
172
|
-
|
|
173
|
-
public abstract error(
|
|
174
|
-
code: number,
|
|
175
|
-
options: Partial<TRouteOptions>,
|
|
176
|
-
renderer: TFrontRenderer<{}, { message: string }>,
|
|
177
|
-
): unknown;
|
|
188
|
+
public abstract registerRouteDefinition(definition: unknown, metadata?: unknown): unknown;
|
|
178
189
|
}
|
|
@@ -46,7 +46,7 @@ const formatRouteSource = (route: TAnyRoute) => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export const getRouteOptionKey = (key: string) => {
|
|
49
|
-
if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved
|
|
49
|
+
if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved route option key.`);
|
|
50
50
|
|
|
51
51
|
return routeOptionKeysSet.has(key) ? (key as keyof TRouteOptions) : null;
|
|
52
52
|
};
|
|
@@ -54,8 +54,8 @@ export const getRouteOptionKey = (key: string) => {
|
|
|
54
54
|
export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
|
|
55
55
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
56
56
|
throw new Error(
|
|
57
|
-
`
|
|
58
|
-
`If the page has no data loader,
|
|
57
|
+
`definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
|
|
58
|
+
`If the page has no data loader, set data to null.`,
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,8 +63,8 @@ export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
|
|
|
63
63
|
if (!reservedPageDataKeys.has(key)) continue;
|
|
64
64
|
|
|
65
65
|
throw new Error(
|
|
66
|
-
`
|
|
67
|
-
`Move route behavior into
|
|
66
|
+
`definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
|
|
67
|
+
`Move route behavior into definePageRoute({ path, options, data, render }).options.`,
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -21,12 +21,12 @@ export const getRegisterPageArgs = (...args: TRegisterPageArgs<any, TRouteOption
|
|
|
21
21
|
const [path, options, data, renderer] = args;
|
|
22
22
|
|
|
23
23
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
24
|
-
throw new Error(`
|
|
24
|
+
throw new Error(`definePageRoute(${JSON.stringify(path)}) requires an explicit options object.`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (data !== null && typeof data !== 'function') {
|
|
28
28
|
throw new Error(
|
|
29
|
-
`
|
|
29
|
+
`definePageRoute(${JSON.stringify(path)}) requires a data function or null.`,
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -43,9 +43,19 @@ export type TApiFetchOptions = {
|
|
|
43
43
|
|
|
44
44
|
export type TPostData = TPostDataWithFile;
|
|
45
45
|
|
|
46
|
-
export type
|
|
46
|
+
export type TPostDataJsonValue =
|
|
47
|
+
| PrimitiveValue
|
|
48
|
+
| null
|
|
49
|
+
| undefined
|
|
50
|
+
| Date
|
|
51
|
+
| TPostDataJsonValue[]
|
|
52
|
+
| { [key: string]: TPostDataJsonValue };
|
|
47
53
|
|
|
48
|
-
export type
|
|
54
|
+
export type TPostDataValue = TPostDataJsonValue | Blob | FileList;
|
|
55
|
+
|
|
56
|
+
export type TPostDataWithFile = { [key: string]: TPostDataValue };
|
|
57
|
+
|
|
58
|
+
export type TPostDataWithoutFile = { [key: string]: TPostDataJsonValue };
|
|
49
59
|
|
|
50
60
|
export type TDataReturnedByFetchers<TProvidedData extends TFetcherList = {}> = {
|
|
51
61
|
[Property in keyof TProvidedData]: ThenArg<TProvidedData[Property]>;
|