proteum 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -55
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/eslint.js +36 -1
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
package/docs/agent-routing.md
CHANGED
|
@@ -68,7 +68,7 @@ proteum mcp
|
|
|
68
68
|
|
|
69
69
|
The machine router discovers live `proteum dev` sessions and offline Proteum app roots under a cwd. `proteum dev` ensures one managed machine MCP daemon is running; terminal `proteum mcp` starts or reuses that daemon and prints a compact central MCP banner with the HTTP client URL, while MCP clients can use stdio. Agents should call MCP `workflow_start` with `cwd` or a known `projectId`, use `project_resolve { cwd }` when routing is ambiguous or offline, and pass the returned live `projectId` to every follow-up app-bound MCP tool. Offline candidates include port-inspected next actions, so agents should follow those instead of guessing the manifest default port. The router forwards to the selected dev-hosted `/__proteum/mcp` endpoint and strips routing fields before the app sees the call.
|
|
70
70
|
|
|
71
|
-
If machine MCP routing returns offline candidates, choose the intended app root and follow that candidate's next action from the app root, not from the monorepo wrapper. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact Start Dev next action from runtime status so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Then retry MCP `workflow_start`.
|
|
71
|
+
If machine MCP routing returns offline candidates, choose the intended app root and follow that candidate's next action from the app root, not from the monorepo wrapper. In `/.codex/worktrees/`, if `workflow_start` returns a worktree bootstrap block, run `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command before any runtime read. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact Start Dev next action from runtime status so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Do not start a second dev server in the same worktree, and do not start a second managed MCP daemon. Then retry MCP `workflow_start`.
|
|
72
72
|
|
|
73
73
|
Prefer CLI over MCP when the result must be reproducible as a shell command, part of verification, or copied into CI/debug instructions.
|
|
74
74
|
|
|
@@ -93,9 +93,12 @@ The router standard is trigger -> canonical instruction file, not trigger -> cop
|
|
|
93
93
|
|
|
94
94
|
Standard triggered reads:
|
|
95
95
|
|
|
96
|
+
- Worktree Preflight (`cwd` inside `/.codex/worktrees/`, newly created Proteum worktree, or before editing in a Codex worktree): root contract fallback, then `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command, `npx proteum runtime status`, and tracked `npx proteum dev` for runtime-visible work.
|
|
96
97
|
- Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): root contract fallback.
|
|
97
|
-
- Before
|
|
98
|
+
- Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change: `DOCUMENTATION.md`.
|
|
99
|
+
- Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md`.
|
|
98
100
|
- Runtime-visible, request-time, router, SSR, browser, or controller behavior: root contract fallback plus `diagnostics.md`.
|
|
101
|
+
- Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: `DOCUMENTATION.md`.
|
|
99
102
|
- Non-trivial feature, product, business-rule, UX, copy, or docs changes: `DOCUMENTATION.md`.
|
|
100
103
|
- Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table.
|
|
101
104
|
|
package/docs/diagnostics.md
CHANGED
|
@@ -112,6 +112,8 @@ Default compact command output follows this shape:
|
|
|
112
112
|
|
|
113
113
|
`proteum runtime status` emits the current app manifest summary, tracked dev sessions, selected live session, MCP URL, health status, configured router/HMR port inspection, and a suggested next command. Use it before starting another dev server, and use its Start Dev command instead of probing page bodies when the default port is occupied. If it reports that the same app already responds on the configured port without a live tracked session, use or repair that runtime instead of starting a second server.
|
|
114
114
|
|
|
115
|
+
Inside `/.codex/worktrees/`, `proteum dev`, `proteum refresh`, `proteum runtime status`, `proteum verify`, and MCP `workflow_start` require a fresh `.proteum/worktree-bootstrap.json`. If the marker is missing, run `npx proteum worktree init --source <source-app-root>`. If hashes, `.env`, `.proteum/manifest.json`, `node_modules`, or the Proteum version are stale, run the returned `npx proteum worktree init --source <source-app-root> --refresh` command. `PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE=1` bypasses the block but remains visible in runtime status, doctor diagnostics, and MCP output.
|
|
116
|
+
|
|
115
117
|
During `proteum dev`, `/__proteum/mcp` exposes compact `workflow_start`, `runtime_status`, `orient`, `instructions_resolve`, `route_candidates`, `explain_summary`, `doctor`, `diagnose`, `trace_*`, `perf_*`, and `logs_tail` tools without spawning CLI commands for each repeated read. `proteum dev` also ensures one managed machine `proteum mcp` daemon is running. Through the machine router, call `workflow_start` with `cwd` or a known `projectId`; if routing is ambiguous or returns offline app candidates, use `project_resolve { cwd }`, follow the selected app root's port-inspected next action when needed, then pass the selected live `projectId` to follow-up app-bound tools.
|
|
116
118
|
|
|
117
119
|
MCP tool/resource output follows compact single-line `proteum-mcp-v1` JSON:
|
package/docs/mcp.md
CHANGED
|
@@ -49,6 +49,8 @@ Example tool calls:
|
|
|
49
49
|
|
|
50
50
|
`workflow_start` is the only app-bound bootstrap tool that may resolve from `cwd` when `projectId` is not known. It may return offline app candidates when no matching dev server is running yet. Other app-bound tools require a live `projectId`; if they omit it, the router returns a compact error that tells the agent to call `projects_list` or `project_resolve`. There is no single-project fallback, because wrong-project reads are worse than an explicit routing retry.
|
|
51
51
|
|
|
52
|
+
When the selected app root is inside `/.codex/worktrees/`, `workflow_start` first checks `.proteum/worktree-bootstrap.json`. If the marker is missing or stale, it returns `ok: false` with a single next action such as `npx proteum worktree init --source <source-app-root>` or the same command with `--refresh`. The router does not forward to the app MCP endpoint until bootstrap is complete, unless `PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE=1` is set; bypasses remain visible in MCP, `runtime status`, and `doctor`.
|
|
53
|
+
|
|
52
54
|
## Dev Runtime Endpoint
|
|
53
55
|
|
|
54
56
|
During `proteum dev`, the app exposes the same app-level MCP contract through the official streamable HTTP transport:
|
|
@@ -76,9 +78,10 @@ If machine MCP routing fails:
|
|
|
76
78
|
|
|
77
79
|
1. Run `proteum mcp status`.
|
|
78
80
|
2. Run `proteum runtime status` from the intended app root. If you are in a monorepo wrapper, use the returned app candidates and exact next action instead of starting dev from the wrapper.
|
|
79
|
-
3. If
|
|
80
|
-
4. If
|
|
81
|
-
5.
|
|
81
|
+
3. If the app root is inside `/.codex/worktrees/` and runtime status or workflow start reports missing/stale bootstrap, run `proteum worktree init --source <source-app-root>` or the returned `--refresh` command first.
|
|
82
|
+
4. If no live app session exists, use the exact Start Dev next action returned by runtime status. It checks the configured router/HMR ports and suggests an alternate free port when the manifest default is occupied.
|
|
83
|
+
5. If a live session exists but runtime/MCP is unreachable, stop the listed session file with `proteum dev stop --session-file <path>`, then start dev again.
|
|
84
|
+
6. Retry MCP `workflow_start` and use the returned `projectId`.
|
|
82
85
|
|
|
83
86
|
Offline `project_resolve` and `workflow_start` candidates also inspect configured router/HMR ports before returning `nextAction`. If the configured port already serves the same app but no live machine project is registered, the next action is runtime tracking repair, not starting a second dev server.
|
|
84
87
|
|
package/eslint.js
CHANGED
|
@@ -139,6 +139,8 @@ const preservingMemberNames = new Set(['captureException', 'error', 'handleError
|
|
|
139
139
|
const isPreservingCall = (callExpression, names) => {
|
|
140
140
|
const propertyName = getCalleePropertyName(callExpression.callee);
|
|
141
141
|
if (!propertyName) return false;
|
|
142
|
+
if (callExpression.callee.type === 'MemberExpression' && callExpression.callee.object?.name === 'console')
|
|
143
|
+
return false;
|
|
142
144
|
|
|
143
145
|
const isKnownPreserver =
|
|
144
146
|
preservingCallNames.has(propertyName) ||
|
|
@@ -170,7 +172,7 @@ const directPromiseCatchHandlers = new Set([
|
|
|
170
172
|
const isDirectPromiseCatchHandler = (node) => {
|
|
171
173
|
const name = getCalleePropertyName(node);
|
|
172
174
|
if (name && directPromiseCatchHandlers.has(name)) return true;
|
|
173
|
-
return
|
|
175
|
+
return false;
|
|
174
176
|
};
|
|
175
177
|
|
|
176
178
|
const createSwallowedErrorRule = () => ({
|
|
@@ -228,6 +230,37 @@ const createSwallowedErrorRule = () => ({
|
|
|
228
230
|
},
|
|
229
231
|
});
|
|
230
232
|
|
|
233
|
+
const createNoAppImportRule = () => ({
|
|
234
|
+
meta: {
|
|
235
|
+
type: 'problem',
|
|
236
|
+
docs: {
|
|
237
|
+
description: 'Disallow Proteum contextual @app imports in user code.',
|
|
238
|
+
},
|
|
239
|
+
messages: {
|
|
240
|
+
noAppImport:
|
|
241
|
+
'`@app` is not a real runtime module. Receive app services through typed route/controller callback context instead.',
|
|
242
|
+
},
|
|
243
|
+
schema: [],
|
|
244
|
+
},
|
|
245
|
+
create(context) {
|
|
246
|
+
return {
|
|
247
|
+
ImportDeclaration(node) {
|
|
248
|
+
if (node.source?.value === '@app') context.report({ node, messageId: 'noAppImport' });
|
|
249
|
+
},
|
|
250
|
+
CallExpression(node) {
|
|
251
|
+
if (
|
|
252
|
+
node.callee?.type === 'Identifier' &&
|
|
253
|
+
node.callee.name === 'require' &&
|
|
254
|
+
node.arguments?.[0]?.type === 'Literal' &&
|
|
255
|
+
node.arguments[0].value === '@app'
|
|
256
|
+
) {
|
|
257
|
+
context.report({ node, messageId: 'noAppImport' });
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
231
264
|
const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
232
265
|
{
|
|
233
266
|
ignores: [...defaultIgnores, ...ignores],
|
|
@@ -253,6 +286,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
|
253
286
|
'@typescript-eslint': tseslint.plugin,
|
|
254
287
|
proteum: {
|
|
255
288
|
rules: {
|
|
289
|
+
'no-app-import': createNoAppImportRule(),
|
|
256
290
|
'no-swallowed-caught-error': createSwallowedErrorRule(),
|
|
257
291
|
},
|
|
258
292
|
},
|
|
@@ -262,6 +296,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
|
262
296
|
},
|
|
263
297
|
rules: {
|
|
264
298
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
299
|
+
'proteum/no-app-import': 'error',
|
|
265
300
|
'proteum/no-swallowed-caught-error': 'error',
|
|
266
301
|
'no-restricted-syntax': [
|
|
267
302
|
'error',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
package/server/app/commands.ts
CHANGED
|
@@ -44,7 +44,11 @@ export abstract class Commands<TApplication extends TCommandApplication = TComma
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
public get models(): object {
|
|
47
|
-
const
|
|
47
|
+
const appRecord = this.app as unknown as Record<string, unknown>;
|
|
48
|
+
const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
|
|
49
|
+
? (appRecord.models as { client?: object } | undefined)
|
|
50
|
+
: undefined;
|
|
51
|
+
const models = explicitModels?.client ?? this.app.Models?.client;
|
|
48
52
|
|
|
49
53
|
if (!models)
|
|
50
54
|
throw new Error(`${this.constructor.name} tried to access models but no Models service is registered.`);
|
|
@@ -539,7 +539,7 @@ Logs: ${
|
|
|
539
539
|
public jsonToHTML(json: unknown): string {
|
|
540
540
|
if (!json) return 'No data';
|
|
541
541
|
|
|
542
|
-
const coloredJson = highlight(stringify(json,
|
|
542
|
+
const coloredJson = highlight(stringify(json, undefined, 4), { language: 'json', ignoreIllegals: true });
|
|
543
543
|
|
|
544
544
|
const html = ansi2Html.toHtml(coloredJson);
|
|
545
545
|
|
|
@@ -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
|
|
|
@@ -78,7 +105,7 @@ export abstract class Application<
|
|
|
78
105
|
public app!: this;
|
|
79
106
|
public servicesContainer!: TServicesContainer;
|
|
80
107
|
public userType!: TUser;
|
|
81
|
-
public declare Router:
|
|
108
|
+
public declare Router: unknown;
|
|
82
109
|
|
|
83
110
|
/*----------------------------------
|
|
84
111
|
- PROPERTIES
|
|
@@ -268,4 +295,68 @@ export abstract class Application<
|
|
|
268
295
|
}
|
|
269
296
|
}
|
|
270
297
|
|
|
298
|
+
const resolveApplicationDefinitionValue = <TValue, TApplication extends object>(
|
|
299
|
+
value: TApplicationDefinitionValue<TValue, TApplication> | undefined,
|
|
300
|
+
app: TApplication,
|
|
301
|
+
): TValue | undefined => (typeof value === 'function' ? (value as (app: TApplication) => TValue)(app) : value);
|
|
302
|
+
|
|
303
|
+
const assignApplicationRecord = (target: object, value: unknown) => {
|
|
304
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return;
|
|
305
|
+
|
|
306
|
+
Object.assign(target, value);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const defineApplication = <
|
|
310
|
+
TServices extends Record<string, unknown> = {},
|
|
311
|
+
TRouter = unknown,
|
|
312
|
+
TModels = unknown,
|
|
313
|
+
TCommands = unknown,
|
|
314
|
+
>(
|
|
315
|
+
definition: TApplicationDefinition<TServices, TRouter, TModels, TCommands>,
|
|
316
|
+
) => {
|
|
317
|
+
type TDefinedApplication = Application &
|
|
318
|
+
TServices & {
|
|
319
|
+
Router: TRouter;
|
|
320
|
+
models?: TModels;
|
|
321
|
+
Models?: TModels;
|
|
322
|
+
commands?: TCommands;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
class DefinedApplication extends Application {
|
|
326
|
+
public constructor() {
|
|
327
|
+
super();
|
|
328
|
+
|
|
329
|
+
const self = this as unknown as TDefinedApplication & Record<string, unknown>;
|
|
330
|
+
const services = resolveApplicationDefinitionValue(
|
|
331
|
+
definition.services,
|
|
332
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
333
|
+
);
|
|
334
|
+
assignApplicationRecord(self, services);
|
|
335
|
+
|
|
336
|
+
const router = resolveApplicationDefinitionValue(
|
|
337
|
+
definition.router,
|
|
338
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
339
|
+
);
|
|
340
|
+
if (router !== undefined) (self as Record<string, unknown>).Router = router;
|
|
341
|
+
|
|
342
|
+
const models = resolveApplicationDefinitionValue(
|
|
343
|
+
definition.models,
|
|
344
|
+
self as TDefinedApplicationContext<TServices, TRouter>,
|
|
345
|
+
);
|
|
346
|
+
if (models !== undefined) {
|
|
347
|
+
(self as Record<string, unknown>).models = models;
|
|
348
|
+
(self as Record<string, unknown>).Models = models;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const commands = resolveApplicationDefinitionValue(
|
|
352
|
+
definition.commands,
|
|
353
|
+
self as TDefinedApplicationContext<TServices, TRouter, TModels>,
|
|
354
|
+
);
|
|
355
|
+
if (commands !== undefined) (self as Record<string, unknown>).commands = commands;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return DefinedApplication as unknown as new () => TDefinedApplication;
|
|
360
|
+
};
|
|
361
|
+
|
|
271
362
|
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;
|