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.
Files changed (74) hide show
  1. package/README.md +60 -55
  2. package/agents/project/AGENTS.md +112 -31
  3. package/agents/project/CODING_STYLE.md +2 -2
  4. package/agents/project/app-root/AGENTS.md +1 -3
  5. package/agents/project/client/AGENTS.md +1 -1
  6. package/agents/project/client/pages/AGENTS.md +21 -9
  7. package/agents/project/diagnostics.md +2 -2
  8. package/agents/project/optimizations.md +1 -1
  9. package/agents/project/root/AGENTS.md +105 -22
  10. package/agents/project/server/routes/AGENTS.md +30 -1
  11. package/agents/project/tests/AGENTS.md +1 -1
  12. package/cli/commands/doctor.ts +54 -3
  13. package/cli/commands/runtime.ts +6 -0
  14. package/cli/commands/worktree.ts +116 -0
  15. package/cli/compiler/artifacts/controllers.ts +16 -15
  16. package/cli/compiler/artifacts/discovery.ts +129 -17
  17. package/cli/compiler/artifacts/routing.ts +0 -5
  18. package/cli/compiler/artifacts/services.ts +253 -76
  19. package/cli/compiler/common/controllers.ts +159 -57
  20. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  21. package/cli/mcp/router.ts +47 -3
  22. package/cli/presentation/commands.ts +25 -15
  23. package/cli/runtime/commands.ts +39 -12
  24. package/cli/runtime/worktreeBootstrap.ts +608 -0
  25. package/cli/scaffold/index.ts +28 -18
  26. package/cli/scaffold/templates.ts +44 -33
  27. package/cli/utils/agents.ts +14 -1
  28. package/client/services/router/index.tsx +23 -3
  29. package/client/services/router/request/api.ts +14 -4
  30. package/common/dev/contractsDoctor.ts +1 -1
  31. package/common/dev/mcpPayloads.ts +8 -1
  32. package/common/env/proteumEnv.ts +14 -2
  33. package/common/router/contracts.ts +1 -1
  34. package/common/router/definitions.ts +177 -0
  35. package/common/router/index.ts +23 -12
  36. package/common/router/pageData.ts +5 -5
  37. package/common/router/register.ts +2 -2
  38. package/common/router/request/api.ts +12 -2
  39. package/docs/agent-routing.md +5 -2
  40. package/docs/diagnostics.md +2 -0
  41. package/docs/mcp.md +6 -3
  42. package/eslint.js +36 -1
  43. package/package.json +1 -1
  44. package/server/app/commands.ts +5 -1
  45. package/server/app/container/console/http-client-error-context.test.cjs +10 -1
  46. package/server/app/container/console/index.ts +2 -1
  47. package/server/app/controller/index.ts +98 -40
  48. package/server/app/index.ts +92 -1
  49. package/server/app/service/index.ts +5 -1
  50. package/server/index.ts +6 -2
  51. package/server/services/router/index.ts +47 -38
  52. package/server/services/router/response/index.ts +2 -2
  53. package/tests/agents-utils.test.cjs +14 -1
  54. package/tests/cli-mcp-command.test.cjs +84 -0
  55. package/tests/definition-contracts.test.cjs +453 -0
  56. package/tests/dev-transpile-watch.test.cjs +37 -28
  57. package/tests/eslint-rules.test.cjs +39 -1
  58. package/tests/mcp.test.cjs +90 -0
  59. package/tests/worktree-bootstrap.test.cjs +206 -0
  60. package/types/aliases.d.ts +0 -5
  61. package/types/controller-input.test.ts +23 -17
  62. package/types/controller-request-context.test.ts +10 -11
  63. package/cli/commands/migrate.ts +0 -51
  64. package/cli/migrate/pageContract.ts +0 -516
  65. package/docs/migrate-from-2.1.3.md +0 -396
  66. package/scripts/cleanup-generated-controllers.ts +0 -62
  67. package/scripts/fix-reference-app-typing.ts +0 -490
  68. package/scripts/format-router-registrations.ts +0 -119
  69. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  70. package/scripts/refactor-client-app-imports.ts +0 -244
  71. package/scripts/refactor-client-pages.ts +0 -587
  72. package/scripts/refactor-server-controllers.ts +0 -471
  73. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  74. 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 Router from '@/client/router';
25
+ }) => `import { definePageRoute } from '@common/router/definitions';
26
26
 
27
- Router.page(
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 Controller from '@server/app/controller';
57
-
58
- export default class ${className} extends Controller<${appIdentifier}> {
59
- public async ${methodName}() {
60
- return {
61
- ok: true,
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 { Router } from '@app';
96
+ }) => `import { defineServerRoute } from '@common/router/definitions';
93
97
 
94
- Router.${httpMethod}(${JSON.stringify(routePath)}, {}, async () => {
95
- return {
96
- ok: true,
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 = ({ appIdentifier }: { appIdentifier: string }) => `import { Application } from '@server/app';
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 class ${appIdentifier} extends Application {
166
- public Router = new Router(
167
- this,
168
- {
169
- ...appConfig.routerBaseConfig,
170
- plugins: {
171
- schema: new SchemaRouter({}, this),
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
- this,
175
- );
176
- }
184
+ app,
185
+ ),
186
+ }),
187
+ });
177
188
  `;
178
189
 
179
190
  export const createClientTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
@@ -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 finishing production code changes: read Root contract fallback, `CODING_STYLE.md`, `tests/AGENTS.md`, and any touched area `AGENTS.md`.',
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 page<TProvidedData extends {} = {}>(
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
- public page(...args: TRegisterPageArgs<any, TRouteOptions>): TClientPageRoute<this> {
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
- public error(
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 File =>
42
- typeof File !== 'undefined' && typeof value === 'object' && value instanceof File;
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
- // TODO: deep check
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 Router.page render callback, a component rendered under App, or a custom hook used from that tree.'
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.');
@@ -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 currentDomain = parseAbsoluteUrl({
336
+ const configuredCurrentDomain = parseAbsoluteUrl({
327
337
  key: 'URL',
328
338
  value: getRequiredEnvValue({ key: 'URL', context }),
329
339
  context,
330
340
  });
331
- const internalUrl = parseAbsoluteUrl({
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
- // Supported `Router.page(...)` registration signature shared by client and compiler code.
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
+ };
@@ -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, TFrontRenderer, TPageDataProvider } from './response/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 page<TControllerData extends TObjetDonnees = {}>(
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 Router.page option key.`);
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
- `Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
58
- `If the page has no data loader, pass null as the third argument.`,
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
- `Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
67
- `Move route behavior into the explicit Router.page(path, options, data, render) options argument.`,
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(`Router.page(${JSON.stringify(path)}) requires an explicit options object as its second argument.`);
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
- `Router.page(${JSON.stringify(path)}) requires a data function or null as its third argument.`,
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 TPostDataWithFile = { [key: string]: PrimitiveValue };
46
+ export type TPostDataJsonValue =
47
+ | PrimitiveValue
48
+ | null
49
+ | undefined
50
+ | Date
51
+ | TPostDataJsonValue[]
52
+ | { [key: string]: TPostDataJsonValue };
47
53
 
48
- export type TPostDataWithoutFile = { [key: string]: PrimitiveValue };
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]>;