proteum 2.4.4 → 2.5.1

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 (79) hide show
  1. package/README.md +81 -52
  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 +5 -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/server/services/AGENTS.md +4 -0
  12. package/agents/project/tests/AGENTS.md +1 -1
  13. package/cli/commands/doctor.ts +54 -3
  14. package/cli/commands/runtime.ts +6 -0
  15. package/cli/commands/worktree.ts +116 -0
  16. package/cli/compiler/artifacts/controllers.ts +16 -15
  17. package/cli/compiler/artifacts/discovery.ts +129 -17
  18. package/cli/compiler/artifacts/routing.ts +0 -5
  19. package/cli/compiler/artifacts/services.ts +253 -76
  20. package/cli/compiler/common/controllers.ts +159 -57
  21. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  22. package/cli/mcp/router.ts +47 -3
  23. package/cli/presentation/commands.ts +25 -15
  24. package/cli/runtime/commands.ts +39 -12
  25. package/cli/runtime/worktreeBootstrap.ts +608 -0
  26. package/cli/scaffold/index.ts +28 -18
  27. package/cli/scaffold/templates.ts +44 -33
  28. package/cli/utils/agents.ts +14 -1
  29. package/client/app/index.ts +22 -5
  30. package/client/services/router/index.tsx +23 -3
  31. package/client/services/router/request/api.ts +16 -6
  32. package/common/dev/contractsDoctor.ts +1 -1
  33. package/common/dev/mcpPayloads.ts +8 -1
  34. package/common/env/proteumEnv.ts +14 -2
  35. package/common/router/contracts.ts +1 -1
  36. package/common/router/definitions.ts +177 -0
  37. package/common/router/index.ts +23 -12
  38. package/common/router/pageData.ts +5 -5
  39. package/common/router/register.ts +2 -2
  40. package/common/router/request/api.ts +12 -2
  41. package/docs/agent-routing.md +5 -2
  42. package/docs/diagnostics.md +2 -0
  43. package/docs/mcp.md +6 -3
  44. package/docs/migration-2.5.md +226 -0
  45. package/eslint.js +89 -42
  46. package/package.json +1 -1
  47. package/server/app/commands.ts +5 -1
  48. package/server/app/container/console/index.ts +1 -1
  49. package/server/app/controller/index.ts +98 -40
  50. package/server/app/index.ts +120 -3
  51. package/server/app/service/index.ts +5 -1
  52. package/server/index.ts +6 -2
  53. package/server/services/router/index.ts +50 -41
  54. package/server/services/router/response/index.ts +2 -2
  55. package/tests/agents-utils.test.cjs +14 -1
  56. package/tests/cli-mcp-command.test.cjs +84 -0
  57. package/tests/client-app-error-handling.test.cjs +100 -0
  58. package/tests/definition-contracts.test.cjs +453 -0
  59. package/tests/dev-transpile-watch.test.cjs +37 -31
  60. package/tests/eslint-rules.test.cjs +185 -8
  61. package/tests/mcp.test.cjs +90 -0
  62. package/tests/scaffold-templates.test.cjs +18 -0
  63. package/tests/server-app-report-error.test.cjs +135 -0
  64. package/tests/worktree-bootstrap.test.cjs +206 -0
  65. package/types/aliases.d.ts +0 -5
  66. package/types/controller-input.test.ts +23 -17
  67. package/types/controller-request-context.test.ts +10 -11
  68. package/cli/commands/migrate.ts +0 -51
  69. package/cli/migrate/pageContract.ts +0 -516
  70. package/docs/migrate-from-2.1.3.md +0 -396
  71. package/scripts/cleanup-generated-controllers.ts +0 -62
  72. package/scripts/fix-reference-app-typing.ts +0 -490
  73. package/scripts/format-router-registrations.ts +0 -119
  74. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  75. package/scripts/refactor-client-app-imports.ts +0 -244
  76. package/scripts/refactor-client-pages.ts +0 -587
  77. package/scripts/refactor-server-controllers.ts +0 -471
  78. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  79. 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: () => ({}),
176
+ router: (app) =>
177
+ new Router(
178
+ app,
179
+ {
180
+ ...appConfig.routerBaseConfig,
181
+ plugins: {
182
+ schema: new SchemaRouter({}, app),
183
+ },
172
184
  },
173
- },
174
- this,
175
- );
176
- }
185
+ app,
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.',
@@ -7,8 +7,6 @@ if (typeof window === 'undefined') throw new Error(`This file shouldn't be loade
7
7
 
8
8
  window.dev && require('preact/debug');
9
9
 
10
- // Core
11
- import { CoreError } from '@common/errors';
12
10
  import type { Layout } from '@common/router';
13
11
  import { createDialog } from '@client/components/Dialog/Manager';
14
12
 
@@ -53,6 +51,21 @@ const isIgnorableBrowserErrorMessage = (message: string) =>
53
51
  message === 'ResizeObserver loop completed with undelivered notifications.' ||
54
52
  message === 'ResizeObserver loop limit exceeded';
55
53
 
54
+ export const getClientErrorMessage = (error: unknown, fallbackMessage = 'Unknown client error'): string => {
55
+ if (error instanceof Error && error.message) return error.message;
56
+ if (typeof error === 'string' && error.trim()) return error;
57
+ if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string' && error.message)
58
+ return error.message;
59
+
60
+ return fallbackMessage;
61
+ };
62
+
63
+ export const normalizeClientError = (error: unknown, fallbackMessage?: string): Error => {
64
+ if (error instanceof Error) return error;
65
+
66
+ return new Error(getClientErrorMessage(error, fallbackMessage));
67
+ };
68
+
56
69
  /*----------------------------------
57
70
  - CLASS
58
71
  ----------------------------------*/
@@ -88,8 +101,7 @@ export default abstract class Application {
88
101
  public bindErrorHandlers() {
89
102
  // Impossible de recup le stacktrace ...
90
103
  window.addEventListener('unhandledrejection', (e) => {
91
- const error = new Error(e.reason); // How to get stacktrace ?
92
- this.handleError(error);
104
+ this.handleError(e.reason);
93
105
  });
94
106
 
95
107
  window.onerror = (message, file, line, col, stacktrace) => {
@@ -109,7 +121,12 @@ export default abstract class Application {
109
121
  };
110
122
  }
111
123
 
112
- public abstract handleError(error: CoreError | Error): void;
124
+ public handleError(error: unknown, fallbackMessage?: string): string {
125
+ const normalizedError = normalizeClientError(error, fallbackMessage);
126
+ console.error(normalizedError);
127
+
128
+ return getClientErrorMessage(error, fallbackMessage);
129
+ }
113
130
 
114
131
  public abstract handleUpdate(): void;
115
132
 
@@ -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
@@ -98,7 +109,7 @@ export default class ApiClient implements ApiClientService {
98
109
  .then((data: TObjetDonnees) => {
99
110
  this.set(data);
100
111
  })
101
- .catch((error: Error) => {
112
+ .catch((error: unknown) => {
102
113
  this.app.handleError(error);
103
114
  });
104
115
  }
@@ -259,7 +270,7 @@ export default class ApiClient implements ApiClientService {
259
270
  );
260
271
 
261
272
  // API Error hook
262
- this.app.handleError(e as Error);
273
+ this.app.handleError(e);
263
274
 
264
275
  throw e;
265
276
  }
@@ -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
  }