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.
- package/README.md +81 -52
- 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 +5 -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/server/services/AGENTS.md +4 -0
- 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/app/index.ts +22 -5
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +16 -6
- 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/docs/migration-2.5.md +226 -0
- package/eslint.js +89 -42
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +120 -3
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +50 -41
- 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/client-app-error-handling.test.cjs +100 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -31
- package/tests/eslint-rules.test.cjs +185 -8
- package/tests/mcp.test.cjs +90 -0
- package/tests/scaffold-templates.test.cjs +18 -0
- package/tests/server-app-report-error.test.cjs +135 -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
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
// Npm
|
|
6
6
|
import zod from 'zod';
|
|
7
7
|
|
|
8
|
-
// Core
|
|
9
|
-
import context from '@server/context';
|
|
10
8
|
import {
|
|
11
9
|
toValidationSchema,
|
|
12
10
|
type TValidationSchema,
|
|
@@ -63,11 +61,6 @@ export type TControllerRequestContext<
|
|
|
63
61
|
} & (TRouter extends TAnyRouter ? TRouterContextServices<TControllerRouter<TRouter>> : {}) &
|
|
64
62
|
TRequestServices;
|
|
65
63
|
|
|
66
|
-
type TControllerBaseContext<TApplication extends object> = {
|
|
67
|
-
app: TApplication;
|
|
68
|
-
request: { data: TObjetDonnees };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
64
|
type TControllerDefaultContext<TApplication extends object, TRequestServices extends object> = {
|
|
72
65
|
app: TApplication;
|
|
73
66
|
context: object;
|
|
@@ -80,43 +73,108 @@ type TControllerDefaultContext<TApplication extends object, TRequestServices ext
|
|
|
80
73
|
} & TRouterContextServices<TControllerApplicationRouter<TApplication>> &
|
|
81
74
|
TRequestServices;
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
----------------------------------*/
|
|
86
|
-
|
|
87
|
-
export default abstract class Controller<
|
|
76
|
+
export type TControllerActionContext<
|
|
77
|
+
TInput = undefined,
|
|
88
78
|
TApplication extends object = object,
|
|
89
|
-
TRouter extends object = object,
|
|
90
79
|
TRequestServices extends object = {},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return this.request.app as TApplication;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
public get services(): TApplication {
|
|
100
|
-
return this.app;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public get models(): TControllerModelsClient<TApplication> {
|
|
104
|
-
const app = this.app as {
|
|
105
|
-
models?: { client?: TControllerModelsClient<TApplication> };
|
|
106
|
-
Models?: { client?: TControllerModelsClient<TApplication> };
|
|
107
|
-
};
|
|
108
|
-
return (app.models?.client ?? app.Models?.client) as TControllerModelsClient<TApplication>;
|
|
109
|
-
}
|
|
80
|
+
> = TControllerDefaultContext<TApplication, TRequestServices> & {
|
|
81
|
+
input: TInput;
|
|
82
|
+
models: TControllerModelsClient<TApplication>;
|
|
83
|
+
services: TApplication;
|
|
84
|
+
};
|
|
110
85
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
86
|
+
export type TControllerActionDefinition<
|
|
87
|
+
TInput = undefined,
|
|
88
|
+
TResult = unknown,
|
|
89
|
+
TApplication extends object = object,
|
|
90
|
+
TRequestServices extends object = {},
|
|
91
|
+
> = {
|
|
92
|
+
input?: TValidationSchema | TValidationShape;
|
|
93
|
+
handler: (context: TControllerActionContext<TInput, TApplication, TRequestServices>) => TResult;
|
|
94
|
+
};
|
|
115
95
|
|
|
116
|
-
|
|
96
|
+
export type TControllerDefinition<
|
|
97
|
+
TActions extends Record<string, TControllerActionDefinition<any, any, any, any>>,
|
|
98
|
+
> = {
|
|
99
|
+
kind: 'controller';
|
|
100
|
+
path?: string;
|
|
101
|
+
actions: TActions;
|
|
102
|
+
};
|
|
117
103
|
|
|
118
|
-
|
|
104
|
+
export type TControllerActionInput<TController, TMethod extends keyof any> = TController extends { actions: infer TActions }
|
|
105
|
+
? TMethod extends keyof TActions
|
|
106
|
+
? TActions[TMethod] extends TControllerActionDefinition<infer TInput, any, any, any>
|
|
107
|
+
? TInput
|
|
108
|
+
: never
|
|
109
|
+
: never
|
|
110
|
+
: never;
|
|
119
111
|
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
export type TControllerActionResult<TController, TMethod extends keyof any> = TController extends {
|
|
113
|
+
actions: infer TActions;
|
|
114
|
+
}
|
|
115
|
+
? TMethod extends keyof TActions
|
|
116
|
+
? TActions[TMethod] extends TControllerActionDefinition<any, infer TResult, any, any>
|
|
117
|
+
? Awaited<TResult>
|
|
118
|
+
: never
|
|
119
|
+
: never
|
|
120
|
+
: never;
|
|
121
|
+
|
|
122
|
+
export function defineAction<TSchema extends TValidationSchema, TResult>(
|
|
123
|
+
definition: {
|
|
124
|
+
input: TSchema;
|
|
125
|
+
handler: (context: TControllerActionContext<zod.output<TSchema>>) => TResult;
|
|
126
|
+
},
|
|
127
|
+
): TControllerActionDefinition<zod.output<TSchema>, TResult>;
|
|
128
|
+
export function defineAction<TShape extends TValidationShape, TResult>(
|
|
129
|
+
definition: {
|
|
130
|
+
input: TShape;
|
|
131
|
+
handler: (context: TControllerActionContext<zod.output<zod.ZodObject<TShape>>>) => TResult;
|
|
132
|
+
},
|
|
133
|
+
): TControllerActionDefinition<zod.output<zod.ZodObject<TShape>>, TResult>;
|
|
134
|
+
export function defineAction<TResult>(
|
|
135
|
+
definition: {
|
|
136
|
+
handler: (context: TControllerActionContext<undefined>) => TResult;
|
|
137
|
+
},
|
|
138
|
+
): TControllerActionDefinition<undefined, TResult>;
|
|
139
|
+
export function defineAction(definition: {
|
|
140
|
+
input?: TValidationSchema | TValidationShape;
|
|
141
|
+
handler: (context: TControllerActionContext<any>) => unknown;
|
|
142
|
+
}) {
|
|
143
|
+
return definition;
|
|
122
144
|
}
|
|
145
|
+
|
|
146
|
+
export const defineController = <
|
|
147
|
+
TActions extends Record<string, TControllerActionDefinition<any, any, any, any>>,
|
|
148
|
+
>({
|
|
149
|
+
path,
|
|
150
|
+
actions,
|
|
151
|
+
}: {
|
|
152
|
+
path?: string;
|
|
153
|
+
actions: TActions;
|
|
154
|
+
}): TControllerDefinition<TActions> => ({
|
|
155
|
+
kind: 'controller',
|
|
156
|
+
path,
|
|
157
|
+
actions,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const runControllerAction = (
|
|
161
|
+
action: TControllerActionDefinition<any, any, any, any>,
|
|
162
|
+
requestContext: TControllerDefaultContext<any, any>,
|
|
163
|
+
) => {
|
|
164
|
+
const input = action.input === undefined ? undefined : toValidationSchema(action.input).parse(requestContext.request.data);
|
|
165
|
+
const app = requestContext.app as {
|
|
166
|
+
models?: { client?: object };
|
|
167
|
+
Models?: { client?: object };
|
|
168
|
+
};
|
|
169
|
+
const appRecord = app as Record<string, unknown>;
|
|
170
|
+
const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
|
|
171
|
+
? (appRecord.models as { client?: object } | undefined)
|
|
172
|
+
: undefined;
|
|
173
|
+
|
|
174
|
+
return action.handler({
|
|
175
|
+
...requestContext,
|
|
176
|
+
input,
|
|
177
|
+
models: explicitModels?.client ?? app.Models?.client ?? {},
|
|
178
|
+
services: requestContext.app,
|
|
179
|
+
});
|
|
180
|
+
};
|
package/server/app/index.ts
CHANGED
|
@@ -40,6 +40,33 @@ export type TApplicationStartOptions = {
|
|
|
40
40
|
|
|
41
41
|
export const Service = ServicesContainer;
|
|
42
42
|
|
|
43
|
+
type TApplicationDefinitionValue<TValue, TApplication extends object> =
|
|
44
|
+
| TValue
|
|
45
|
+
| ((app: TApplication) => TValue);
|
|
46
|
+
|
|
47
|
+
type TDefinedApplicationContext<
|
|
48
|
+
TServices extends Record<string, unknown>,
|
|
49
|
+
TRouter,
|
|
50
|
+
TModels = unknown,
|
|
51
|
+
> = Application &
|
|
52
|
+
TServices & {
|
|
53
|
+
Router: TRouter;
|
|
54
|
+
models?: TModels;
|
|
55
|
+
Models?: TModels;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type TApplicationDefinition<
|
|
59
|
+
TServices extends Record<string, unknown> = {},
|
|
60
|
+
TRouter = unknown,
|
|
61
|
+
TModels = unknown,
|
|
62
|
+
TCommands = unknown,
|
|
63
|
+
> = {
|
|
64
|
+
services?: TApplicationDefinitionValue<TServices, TDefinedApplicationContext<TServices, TRouter>>;
|
|
65
|
+
router?: TApplicationDefinitionValue<TRouter, TDefinedApplicationContext<TServices, TRouter>>;
|
|
66
|
+
models?: TApplicationDefinitionValue<TModels, TDefinedApplicationContext<TServices, TRouter>>;
|
|
67
|
+
commands?: TApplicationDefinitionValue<TCommands, TDefinedApplicationContext<TServices, TRouter, TModels>>;
|
|
68
|
+
};
|
|
69
|
+
|
|
43
70
|
// Without prettify, we don't get a clear list of the class properties
|
|
44
71
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
45
72
|
|
|
@@ -65,6 +92,24 @@ const createCommandsManager = (app: Application) => new CommandsManager(app, { d
|
|
|
65
92
|
const createDevCommandsRegistry = (app: Application) => new DevCommandsRegistry(app);
|
|
66
93
|
const createDevDiagnosticsRegistry = (app: Application) => new DevDiagnosticsRegistry(app);
|
|
67
94
|
|
|
95
|
+
const normalizeReportedError = (rejection: unknown, fallbackMessage: string): Error => {
|
|
96
|
+
if (rejection instanceof Error) return rejection;
|
|
97
|
+
if (typeof rejection === 'string') return new Error(rejection);
|
|
98
|
+
|
|
99
|
+
return new Error(fallbackMessage);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const getNumericErrorProperty = (error: Error, property: 'http' | 'status' | 'statusCode') => {
|
|
103
|
+
const value = (error as Error & Partial<Record<typeof property, unknown>>)[property];
|
|
104
|
+
return typeof value === 'number' ? value : undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getErrorHttpCode = (error: Error) =>
|
|
108
|
+
getNumericErrorProperty(error, 'http') ??
|
|
109
|
+
getNumericErrorProperty(error, 'status') ??
|
|
110
|
+
getNumericErrorProperty(error, 'statusCode') ??
|
|
111
|
+
500;
|
|
112
|
+
|
|
68
113
|
/*----------------------------------
|
|
69
114
|
- FUNCTIONS
|
|
70
115
|
----------------------------------*/
|
|
@@ -78,7 +123,7 @@ export abstract class Application<
|
|
|
78
123
|
public app!: this;
|
|
79
124
|
public servicesContainer!: TServicesContainer;
|
|
80
125
|
public userType!: TUser;
|
|
81
|
-
public declare Router:
|
|
126
|
+
public declare Router: unknown;
|
|
82
127
|
|
|
83
128
|
/*----------------------------------
|
|
84
129
|
- PROPERTIES
|
|
@@ -111,12 +156,12 @@ export abstract class Application<
|
|
|
111
156
|
// Handle unhandled crash
|
|
112
157
|
this.on('error', (e, request) => this.container.handleBug(e, 'An error occured in the application', request));
|
|
113
158
|
|
|
114
|
-
process.on('unhandledRejection', (error:
|
|
159
|
+
process.on('unhandledRejection', (error: unknown) => {
|
|
115
160
|
// Log so we know it's coming from unhandledRejection
|
|
116
161
|
console.error('unhandledRejection', error);
|
|
117
162
|
|
|
118
163
|
// We don't log the error here because it's the role of the app to decidehiw to log errors
|
|
119
|
-
this.
|
|
164
|
+
void this.reportError(error);
|
|
120
165
|
});
|
|
121
166
|
|
|
122
167
|
// We can't pass this in super so we assign here
|
|
@@ -128,6 +173,14 @@ export abstract class Application<
|
|
|
128
173
|
return this.container.Console.createBugReport(new Anomaly(...anomalyArgs));
|
|
129
174
|
}
|
|
130
175
|
|
|
176
|
+
public async reportError(rejection: unknown, request?: ServerRequest<TServerRouter>) {
|
|
177
|
+
const error = normalizeReportedError(rejection, 'Unknown application error');
|
|
178
|
+
const code = getErrorHttpCode(error);
|
|
179
|
+
|
|
180
|
+
if (code === 500) await this.runHook('error', error, request);
|
|
181
|
+
else await this.runHook('error.' + code, error, request);
|
|
182
|
+
}
|
|
183
|
+
|
|
131
184
|
/*----------------------------------
|
|
132
185
|
- COMMANDS
|
|
133
186
|
----------------------------------*/
|
|
@@ -268,4 +321,68 @@ export abstract class Application<
|
|
|
268
321
|
}
|
|
269
322
|
}
|
|
270
323
|
|
|
324
|
+
const resolveApplicationDefinitionValue = <TValue, TApplication extends object>(
|
|
325
|
+
value: TApplicationDefinitionValue<TValue, TApplication> | undefined,
|
|
326
|
+
app: TApplication,
|
|
327
|
+
): TValue | undefined => (typeof value === 'function' ? (value as (app: TApplication) => TValue)(app) : value);
|
|
328
|
+
|
|
329
|
+
const assignApplicationRecord = (target: object, value: unknown) => {
|
|
330
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return;
|
|
331
|
+
|
|
332
|
+
Object.assign(target, value);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export const defineApplication = <
|
|
336
|
+
TServices extends Record<string, unknown> = {},
|
|
337
|
+
TRouter = unknown,
|
|
338
|
+
TModels = unknown,
|
|
339
|
+
TCommands = unknown,
|
|
340
|
+
>(
|
|
341
|
+
definition: TApplicationDefinition<TServices, TRouter, TModels, TCommands>,
|
|
342
|
+
) => {
|
|
343
|
+
type TDefinedApplication = Application &
|
|
344
|
+
TServices & {
|
|
345
|
+
Router: TRouter;
|
|
346
|
+
models?: TModels;
|
|
347
|
+
Models?: TModels;
|
|
348
|
+
commands?: TCommands;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
class DefinedApplication extends Application {
|
|
352
|
+
public constructor() {
|
|
353
|
+
super();
|
|
354
|
+
|
|
355
|
+
const self = this as unknown as TDefinedApplication & Record<string, unknown>;
|
|
356
|
+
const services = resolveApplicationDefinitionValue(
|
|
357
|
+
definition.services,
|
|
358
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
359
|
+
);
|
|
360
|
+
assignApplicationRecord(self, services);
|
|
361
|
+
|
|
362
|
+
const router = resolveApplicationDefinitionValue(
|
|
363
|
+
definition.router,
|
|
364
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
365
|
+
);
|
|
366
|
+
if (router !== undefined) (self as Record<string, unknown>).Router = router;
|
|
367
|
+
|
|
368
|
+
const models = resolveApplicationDefinitionValue(
|
|
369
|
+
definition.models,
|
|
370
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
371
|
+
);
|
|
372
|
+
if (models !== undefined) {
|
|
373
|
+
(self as Record<string, unknown>).models = models;
|
|
374
|
+
(self as Record<string, unknown>).Models = models;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const commands = resolveApplicationDefinitionValue(
|
|
378
|
+
definition.commands,
|
|
379
|
+
self as TDefinedApplicationContext<TServices, TRouter, TModels>,
|
|
380
|
+
);
|
|
381
|
+
if (commands !== undefined) (self as Record<string, unknown>).commands = commands;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return DefinedApplication as unknown as new () => TDefinedApplication;
|
|
386
|
+
};
|
|
387
|
+
|
|
271
388
|
export default Application;
|
|
@@ -145,7 +145,11 @@ export default abstract class Service<
|
|
|
145
145
|
models?: { client?: TServiceModelsClient<TApplication> };
|
|
146
146
|
Models?: { client?: TServiceModelsClient<TApplication> };
|
|
147
147
|
};
|
|
148
|
-
const
|
|
148
|
+
const appRecord = app as Record<string, unknown>;
|
|
149
|
+
const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
|
|
150
|
+
? (appRecord.models as { client?: TServiceModelsClient<TApplication> } | undefined)
|
|
151
|
+
: undefined;
|
|
152
|
+
const models = explicitModels?.client ?? app.Models?.client;
|
|
149
153
|
|
|
150
154
|
if (!models)
|
|
151
155
|
throw new Error(`${this.constructor.name} tried to access models but no Models service is registered.`);
|
package/server/index.ts
CHANGED
|
@@ -3,6 +3,10 @@ import Application from '@/server/index';
|
|
|
3
3
|
import { isServerHotReloadRequest, serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
4
4
|
|
|
5
5
|
const application = AppContainer.start(Application);
|
|
6
|
+
const router = application.Router as {
|
|
7
|
+
started?: Promise<unknown>;
|
|
8
|
+
reloadGeneratedDefinitions?: (changedFiles: string[]) => Promise<unknown>;
|
|
9
|
+
};
|
|
6
10
|
let shutdownPromise: Promise<void> | undefined;
|
|
7
11
|
|
|
8
12
|
const shutdownApplication = async (reason: string) => {
|
|
@@ -36,8 +40,8 @@ if (__DEV__ && typeof process.send === 'function') {
|
|
|
36
40
|
|
|
37
41
|
void (async () => {
|
|
38
42
|
try {
|
|
39
|
-
await
|
|
40
|
-
await
|
|
43
|
+
await router.started;
|
|
44
|
+
await router.reloadGeneratedDefinitions?.(message.changedFiles);
|
|
41
45
|
|
|
42
46
|
process.send?.({ type: serverHotReloadMessageType.succeeded, changedFiles: message.changedFiles });
|
|
43
47
|
} catch (error) {
|
|
@@ -14,12 +14,12 @@ const { v4: uuid } = require('uuid') as { v4: () => string };
|
|
|
14
14
|
import got from 'got';
|
|
15
15
|
import hInterval from 'human-interval';
|
|
16
16
|
import type express from 'express';
|
|
17
|
-
import type { Request, Response, NextFunction } from 'express';
|
|
18
17
|
import zod, { ZodError } from 'zod';
|
|
19
18
|
export { default as schema } from 'zod';
|
|
20
19
|
|
|
21
20
|
// Core
|
|
22
21
|
import type { Application } from '@server/app/index';
|
|
22
|
+
import { runControllerAction, type TControllerActionDefinition } from '@server/app/controller';
|
|
23
23
|
import Service, { TServiceArgs } from '@server/app/service';
|
|
24
24
|
import context from '@server/context';
|
|
25
25
|
import type DisksManager from '@server/services/disks';
|
|
@@ -30,9 +30,12 @@ import BaseRouter, {
|
|
|
30
30
|
TErrorRoute,
|
|
31
31
|
TRouteModule,
|
|
32
32
|
TRouteOptions,
|
|
33
|
+
type TRouteDefinition,
|
|
34
|
+
type TRouteMetadata,
|
|
33
35
|
defaultOptions,
|
|
34
36
|
matchRoute,
|
|
35
37
|
buildUrl,
|
|
38
|
+
withRouteMetadata,
|
|
36
39
|
} from '@common/router';
|
|
37
40
|
import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
|
|
38
41
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
@@ -76,7 +79,7 @@ type TGeneratedControllerDefinition = {
|
|
|
76
79
|
path: string;
|
|
77
80
|
filepath: string;
|
|
78
81
|
sourceLocation: { line: number; column: number };
|
|
79
|
-
|
|
82
|
+
action: TControllerActionDefinition<any, any, any, any>;
|
|
80
83
|
method: string;
|
|
81
84
|
};
|
|
82
85
|
|
|
@@ -385,7 +388,7 @@ export default class ServerRouter<
|
|
|
385
388
|
}
|
|
386
389
|
}
|
|
387
390
|
|
|
388
|
-
this.
|
|
391
|
+
this.refreshRouteRegistration();
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
private registerControllers(definitions: TGeneratedControllerDefinition[]) {
|
|
@@ -393,10 +396,8 @@ export default class ServerRouter<
|
|
|
393
396
|
const route: TRoute<TRouterContext<this>> = {
|
|
394
397
|
method: 'POST',
|
|
395
398
|
path: definition.path,
|
|
396
|
-
controller: (requestContext: TRouterContext<this>) =>
|
|
397
|
-
|
|
398
|
-
return controller[definition.method]();
|
|
399
|
-
},
|
|
399
|
+
controller: (requestContext: TRouterContext<this>) =>
|
|
400
|
+
runControllerAction(definition.action, requestContext as any),
|
|
400
401
|
options: { ...defaultOptions, filepath: definition.filepath, sourceLocation: definition.sourceLocation },
|
|
401
402
|
};
|
|
402
403
|
|
|
@@ -419,7 +420,31 @@ export default class ServerRouter<
|
|
|
419
420
|
- REGISTER
|
|
420
421
|
----------------------------------*/
|
|
421
422
|
|
|
422
|
-
public
|
|
423
|
+
public registerRouteDefinition(definition: TRouteDefinition, metadata: TRouteMetadata = {}) {
|
|
424
|
+
if (definition.kind === 'page') {
|
|
425
|
+
return this.page(
|
|
426
|
+
definition.path,
|
|
427
|
+
withRouteMetadata(definition.options, metadata),
|
|
428
|
+
definition.data,
|
|
429
|
+
definition.render,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (definition.kind === 'error') {
|
|
434
|
+
return this.error(definition.code, withRouteMetadata(definition.options, metadata), definition.render);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const method = definition.method === '*' ? '*' : (definition.method.toUpperCase() as TRouteHttpMethod);
|
|
438
|
+
|
|
439
|
+
return this.registerApi(
|
|
440
|
+
method,
|
|
441
|
+
definition.path,
|
|
442
|
+
withRouteMetadata(definition.options, metadata),
|
|
443
|
+
definition.handler as TServerController<this>,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
protected page(...args: TRegisterPageArgs<any, TRouteOptions>) {
|
|
423
448
|
const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
|
|
424
449
|
|
|
425
450
|
const { regex, keys } = buildRegex(path);
|
|
@@ -442,7 +467,7 @@ export default class ServerRouter<
|
|
|
442
467
|
return this;
|
|
443
468
|
}
|
|
444
469
|
|
|
445
|
-
|
|
470
|
+
protected error(
|
|
446
471
|
code: number,
|
|
447
472
|
options: Partial<TRouteOptions>,
|
|
448
473
|
renderer: TFrontRenderer<{}, { message: string }>,
|
|
@@ -461,34 +486,13 @@ export default class ServerRouter<
|
|
|
461
486
|
this.errors[code] = route;
|
|
462
487
|
}
|
|
463
488
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
public express(
|
|
473
|
-
middleware: (req: Request, res: Response, next: NextFunction, requestContext: TRouterContext<this>) => void,
|
|
474
|
-
) {
|
|
475
|
-
return (context: TRouterContext<this>) =>
|
|
476
|
-
new Promise((resolve) => {
|
|
477
|
-
context.request.res.on('finish', function () {
|
|
478
|
-
//console.log('the response has been sent', request.res.statusCode);
|
|
479
|
-
resolve(true);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
middleware(
|
|
483
|
-
context.request.req,
|
|
484
|
-
context.request.res,
|
|
485
|
-
() => {
|
|
486
|
-
resolve(true);
|
|
487
|
-
},
|
|
488
|
-
context,
|
|
489
|
-
);
|
|
490
|
-
});
|
|
491
|
-
}
|
|
489
|
+
protected all = (...args: TApiRegisterArgs<this>) => this.registerApi('*', ...args);
|
|
490
|
+
protected options = (...args: TApiRegisterArgs<this>) => this.registerApi('OPTIONS', ...args);
|
|
491
|
+
protected get = (...args: TApiRegisterArgs<this>) => this.registerApi('GET', ...args);
|
|
492
|
+
protected post = (...args: TApiRegisterArgs<this>) => this.registerApi('POST', ...args);
|
|
493
|
+
protected put = (...args: TApiRegisterArgs<this>) => this.registerApi('PUT', ...args);
|
|
494
|
+
protected patch = (...args: TApiRegisterArgs<this>) => this.registerApi('PATCH', ...args);
|
|
495
|
+
protected delete = (...args: TApiRegisterArgs<this>) => this.registerApi('DELETE', ...args);
|
|
492
496
|
|
|
493
497
|
protected registerApi(method: TRouteHttpMethod, ...args: TApiRegisterArgs<this>): this {
|
|
494
498
|
let path: string;
|
|
@@ -565,7 +569,11 @@ export default class ServerRouter<
|
|
|
565
569
|
);
|
|
566
570
|
}
|
|
567
571
|
|
|
568
|
-
|
|
572
|
+
public refreshRouteRegistration() {
|
|
573
|
+
this.afterRegister();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private afterRegister() {
|
|
569
577
|
// Ordonne par ordre de priorité
|
|
570
578
|
this.config.debug && console.info('Loading routes ...');
|
|
571
579
|
this.routes.sort((r1, r2) => {
|
|
@@ -579,6 +587,7 @@ export default class ServerRouter<
|
|
|
579
587
|
return 0;
|
|
580
588
|
});
|
|
581
589
|
// - Génère les définitions de route pour le client
|
|
590
|
+
this.ssrRoutes = [];
|
|
582
591
|
this.config.debug && console.info(`Registered routes:`);
|
|
583
592
|
for (const route of this.routes) {
|
|
584
593
|
const chunkId = route.options.id;
|
|
@@ -628,7 +637,7 @@ export default class ServerRouter<
|
|
|
628
637
|
error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown request.finished hook error');
|
|
629
638
|
|
|
630
639
|
try {
|
|
631
|
-
await this.app.
|
|
640
|
+
await this.app.reportError(typedError, request);
|
|
632
641
|
} catch (hookError) {
|
|
633
642
|
console.error('request.finished hook error', typedError, 'Error hook failure', hookError);
|
|
634
643
|
}
|
|
@@ -1102,7 +1111,7 @@ export default class ServerRouter<
|
|
|
1102
1111
|
console.log(LogPrefix, 'Error catched from the router:', error);
|
|
1103
1112
|
|
|
1104
1113
|
// Report error
|
|
1105
|
-
await this.app.
|
|
1114
|
+
await this.app.reportError(error, request);
|
|
1106
1115
|
|
|
1107
1116
|
// Don't exose technical errors to users
|
|
1108
1117
|
if (this.app.env.profile === 'prod')
|
|
@@ -1114,7 +1123,7 @@ export default class ServerRouter<
|
|
|
1114
1123
|
/*if (this.app.env.profile === "dev")
|
|
1115
1124
|
console.warn(e);*/
|
|
1116
1125
|
|
|
1117
|
-
await this.app.
|
|
1126
|
+
await this.app.reportError(error, request);
|
|
1118
1127
|
}
|
|
1119
1128
|
|
|
1120
1129
|
// Return error based on the request format
|
|
@@ -60,6 +60,7 @@ export type TRouterContext<TRouter extends TServerRouter> =
|
|
|
60
60
|
// Request context
|
|
61
61
|
{
|
|
62
62
|
app: TServerRouterApplication<TRouter>;
|
|
63
|
+
services: TServerRouterApplication<TRouter>;
|
|
63
64
|
context: TRouterContext<TRouter>; // = this
|
|
64
65
|
request: ServerRequest<TRouter>;
|
|
65
66
|
api: ServerRequest<TRouter>['api'];
|
|
@@ -155,14 +156,12 @@ export default class ServerResponse<
|
|
|
155
156
|
const contextStore = context.getStore() as
|
|
156
157
|
| {
|
|
157
158
|
requestContext?: TRouterContext<TAnyRouter>;
|
|
158
|
-
inputSchemaUsed?: boolean;
|
|
159
159
|
ownerLabel?: string;
|
|
160
160
|
ownerFilepath?: string;
|
|
161
161
|
}
|
|
162
162
|
| undefined;
|
|
163
163
|
if (contextStore) {
|
|
164
164
|
contextStore.requestContext = requestContext;
|
|
165
|
-
contextStore.inputSchemaUsed = false;
|
|
166
165
|
contextStore.ownerLabel = getRouteTraceTarget(route as TAnyRoute<TRouterContext<TServerRouter>>);
|
|
167
166
|
contextStore.ownerFilepath = route.options.filepath || undefined;
|
|
168
167
|
}
|
|
@@ -229,6 +228,7 @@ export default class ServerResponse<
|
|
|
229
228
|
const requestContext: TRouterContext<TRouter> = {
|
|
230
229
|
// Router context
|
|
231
230
|
app: this.app,
|
|
231
|
+
services: this.app,
|
|
232
232
|
context: undefined!,
|
|
233
233
|
request: this.request,
|
|
234
234
|
response: this,
|
|
@@ -104,11 +104,23 @@ test('standalone configure creates tracked instruction files with routing contra
|
|
|
104
104
|
assert.match(agentsContent, /\/__proteum\/mcp/);
|
|
105
105
|
assert.match(agentsContent, /central MCP ready banner/);
|
|
106
106
|
assert.match(agentsContent, /proteum-mcp-v1/);
|
|
107
|
+
assert.match(agentsContent, /## Explicit App-Building Contract/);
|
|
108
|
+
assert.match(agentsContent, /defineApplication\(\{ services, router, models, commands \}\)/);
|
|
109
|
+
assert.match(agentsContent, /definePageRoute\(\{ path, options, data, render \}\)/);
|
|
110
|
+
assert.match(agentsContent, /defineController\(\{ path, actions \}\)/);
|
|
111
|
+
assert.match(agentsContent, /Never import `@app` in page, route, or controller files/);
|
|
112
|
+
assert.match(agentsContent, /Never call top-level `Router\.page\(\.\.\.\)`/);
|
|
107
113
|
assert.match(agentsContent, /## Triggered Instruction Reads/);
|
|
114
|
+
assert.match(agentsContent, /Worktree Preflight/);
|
|
115
|
+
assert.match(agentsContent, /npx proteum worktree init --source <source-app-root>/);
|
|
116
|
+
assert.match(agentsContent, /--skip-deps --reason/);
|
|
108
117
|
assert.match(agentsContent, /Git lifecycle/);
|
|
109
118
|
assert.match(agentsContent, /read Root contract fallback before any git write/);
|
|
119
|
+
assert.match(agentsContent, /Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change/);
|
|
110
120
|
assert.match(agentsContent, /add or update focused unit tests/);
|
|
111
|
-
assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
|
|
121
|
+
assert.match(agentsContent, /read Root contract fallback, `DOCUMENTATION\.md`, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
|
|
122
|
+
assert.match(agentsContent, /Bug fixes, regressions, incidents, broken public routes, auth\/OAuth failures/);
|
|
123
|
+
assert.match(agentsContent, /docs\/fixes\/YYYY-MM-DD-short-bug-name\.md/);
|
|
112
124
|
assert.match(agentsContent, /GEO\/SEO\/crawler\/structured-data\/AI-source changes/);
|
|
113
125
|
assert.match(agentsContent, /MCP-selected previews are enough/);
|
|
114
126
|
assert.doesNotMatch(agentsContent, /Conventional Commits/);
|
|
@@ -234,6 +246,7 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
234
246
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
|
|
235
247
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
|
|
236
248
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Do not start `npx proteum dev` from this root/);
|
|
249
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Worktree Preflight/);
|
|
237
250
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
238
251
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
|
|
239
252
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
|