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
|
@@ -46,7 +46,7 @@ const formatRouteSource = (route: TAnyRoute) => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export const getRouteOptionKey = (key: string) => {
|
|
49
|
-
if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved
|
|
49
|
+
if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved route option key.`);
|
|
50
50
|
|
|
51
51
|
return routeOptionKeysSet.has(key) ? (key as keyof TRouteOptions) : null;
|
|
52
52
|
};
|
|
@@ -54,8 +54,8 @@ export const getRouteOptionKey = (key: string) => {
|
|
|
54
54
|
export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
|
|
55
55
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
56
56
|
throw new Error(
|
|
57
|
-
`
|
|
58
|
-
`If the page has no data loader,
|
|
57
|
+
`definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
|
|
58
|
+
`If the page has no data loader, set data to null.`,
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,8 +63,8 @@ export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
|
|
|
63
63
|
if (!reservedPageDataKeys.has(key)) continue;
|
|
64
64
|
|
|
65
65
|
throw new Error(
|
|
66
|
-
`
|
|
67
|
-
`Move route behavior into
|
|
66
|
+
`definePageRoute data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
|
|
67
|
+
`Move route behavior into definePageRoute({ path, options, data, render }).options.`,
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -21,12 +21,12 @@ export const getRegisterPageArgs = (...args: TRegisterPageArgs<any, TRouteOption
|
|
|
21
21
|
const [path, options, data, renderer] = args;
|
|
22
22
|
|
|
23
23
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
24
|
-
throw new Error(`
|
|
24
|
+
throw new Error(`definePageRoute(${JSON.stringify(path)}) requires an explicit options object.`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (data !== null && typeof data !== 'function') {
|
|
28
28
|
throw new Error(
|
|
29
|
-
`
|
|
29
|
+
`definePageRoute(${JSON.stringify(path)}) requires a data function or null.`,
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -43,9 +43,19 @@ export type TApiFetchOptions = {
|
|
|
43
43
|
|
|
44
44
|
export type TPostData = TPostDataWithFile;
|
|
45
45
|
|
|
46
|
-
export type
|
|
46
|
+
export type TPostDataJsonValue =
|
|
47
|
+
| PrimitiveValue
|
|
48
|
+
| null
|
|
49
|
+
| undefined
|
|
50
|
+
| Date
|
|
51
|
+
| TPostDataJsonValue[]
|
|
52
|
+
| { [key: string]: TPostDataJsonValue };
|
|
47
53
|
|
|
48
|
-
export type
|
|
54
|
+
export type TPostDataValue = TPostDataJsonValue | Blob | FileList;
|
|
55
|
+
|
|
56
|
+
export type TPostDataWithFile = { [key: string]: TPostDataValue };
|
|
57
|
+
|
|
58
|
+
export type TPostDataWithoutFile = { [key: string]: TPostDataJsonValue };
|
|
49
59
|
|
|
50
60
|
export type TDataReturnedByFetchers<TProvidedData extends TFetcherList = {}> = {
|
|
51
61
|
[Property in keyof TProvidedData]: ThenArg<TProvidedData[Property]>;
|
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
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Migrating To Proteum 2.5
|
|
2
|
+
|
|
3
|
+
Proteum 2.5 is a breaking cleanup release. It removes contextual app/router magic from user source and makes routes, controllers, pages, services, and app bootstrap machine-readable through explicit definition objects.
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
- App roots default-export `defineApplication({ services, router, models, commands })`.
|
|
8
|
+
- Page files default-export `definePageRoute({ path, options, data, render })` or `defineErrorRoute({ code, options, render })`.
|
|
9
|
+
- Manual HTTP route files default-export `defineServerRoute({ method, path, options, handler })` or `defineServerRoutes(...)`.
|
|
10
|
+
- Raw Express handlers are wrapped with `expressHandler(...)`.
|
|
11
|
+
- Controller files default-export `defineController({ path, actions })`; actions use `defineAction({ input, handler })`.
|
|
12
|
+
- Runtime app, service, router, request, response, auth, and router-plugin access comes from typed callback context.
|
|
13
|
+
- `@app` imports, top-level `Router.page(...)`, top-level server `Router.*(...)`, controller classes, and `this.input(...)` are no longer supported.
|
|
14
|
+
|
|
15
|
+
## 1. Install The Published Package
|
|
16
|
+
|
|
17
|
+
Update every Proteum app package in the repo to `proteum@^2.5.0`, then reinstall from npm.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm ls proteum
|
|
22
|
+
node -p "require('./node_modules/proteum/package.json').version + ' ' + require.resolve('proteum/package.json')"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The resolved path must point inside the app repo `node_modules/proteum`, not to a local framework checkout. If a project was using `npm link`, the reinstall should replace the symlink.
|
|
26
|
+
|
|
27
|
+
## 2. Refresh Agent Instructions
|
|
28
|
+
|
|
29
|
+
Regenerate project instructions so LLMs receive the explicit 2.5 contracts.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx proteum configure agents
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Generated instructions should mention `defineApplication`, `definePageRoute`, `defineServerRoute`, `defineController`, and the ban on `@app` imports in route, page, and controller files.
|
|
36
|
+
|
|
37
|
+
## 3. Migrate `server/index.ts`
|
|
38
|
+
|
|
39
|
+
Move from an `Application` subclass or service-returned `Router` to an explicit app definition. Keep `server/index.ts` as the canonical type root for services, router plugins, models, and request context.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { defineApplication, type Application } from '@server/app';
|
|
43
|
+
import Router from '@server/services/router';
|
|
44
|
+
import SchemaRouter from '@server/services/schema/router';
|
|
45
|
+
import BillingService from '@/server/services/Billing';
|
|
46
|
+
|
|
47
|
+
import * as appConfig from '@/server/config/app';
|
|
48
|
+
|
|
49
|
+
type ProjectServices = {
|
|
50
|
+
Billing: BillingService;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ProjectRouterPlugins = {
|
|
54
|
+
schema: SchemaRouter;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ProjectRouter = Router<ProjectApp, ProjectRouterPlugins>;
|
|
58
|
+
export interface ProjectApp extends Application, ProjectServices {
|
|
59
|
+
Router: ProjectRouter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const createProjectRouter = (app: ProjectApp): ProjectRouter =>
|
|
63
|
+
new Router<ProjectApp, ProjectRouterPlugins>(
|
|
64
|
+
app,
|
|
65
|
+
{
|
|
66
|
+
...appConfig.routerBaseConfig,
|
|
67
|
+
plugins: {
|
|
68
|
+
schema: new SchemaRouter({}, app),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
app,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const ProjectApplication = defineApplication<ProjectServices, ProjectRouter>({
|
|
75
|
+
services: (app) => ({
|
|
76
|
+
Billing: new BillingService(app, {}, app),
|
|
77
|
+
}),
|
|
78
|
+
router: createProjectRouter,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export default ProjectApplication;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 4. Migrate Pages
|
|
85
|
+
|
|
86
|
+
Replace legacy page registration with a default-exported page definition.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { definePageRoute } from '@common/router/definitions';
|
|
90
|
+
|
|
91
|
+
export default definePageRoute({
|
|
92
|
+
path: '/dashboard',
|
|
93
|
+
options: { auth: true },
|
|
94
|
+
data: ({ AccountController }) => ({
|
|
95
|
+
account: AccountController.accountPage(),
|
|
96
|
+
}),
|
|
97
|
+
render: ({ account }) => <Dashboard account={account} />,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Rules:
|
|
102
|
+
|
|
103
|
+
- `path`, `options`, and error `code` must be static and compiler-readable.
|
|
104
|
+
- Route behavior belongs in `options`, not in data.
|
|
105
|
+
- Use `data: null` when no SSR data is needed.
|
|
106
|
+
- Runtime references are allowed only inside `data` and `render`.
|
|
107
|
+
|
|
108
|
+
## 5. Migrate Manual Server Routes
|
|
109
|
+
|
|
110
|
+
Replace top-level `Router.get(...)`, `Router.post(...)`, `Router.express(...)`, and similar calls with definition exports.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { defineServerRoute, defineServerRoutes, expressHandler } from '@common/router/definitions';
|
|
114
|
+
import type { ProjectApp } from '@/server/index';
|
|
115
|
+
|
|
116
|
+
export default defineServerRoutes((app: ProjectApp) => [
|
|
117
|
+
defineServerRoute({
|
|
118
|
+
method: 'GET',
|
|
119
|
+
path: '/health',
|
|
120
|
+
options: {},
|
|
121
|
+
handler: ({ response }) => response.json({ ok: true }),
|
|
122
|
+
}),
|
|
123
|
+
defineServerRoute({
|
|
124
|
+
method: 'POST',
|
|
125
|
+
path: '/webhook',
|
|
126
|
+
options: {},
|
|
127
|
+
handler: expressHandler((request, response) => {
|
|
128
|
+
app.Billing.recordWebhook(request.body);
|
|
129
|
+
response.status(204).send('');
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use `defineServerRoutes((app) => [...])` only when the route definitions need app services at registration time. Otherwise export one `defineServerRoute(...)`.
|
|
136
|
+
|
|
137
|
+
## 6. Migrate Controllers
|
|
138
|
+
|
|
139
|
+
Replace controller classes and `this.input(schema)` with explicit actions.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { defineAction, defineController, schema } from '@server/app/controller';
|
|
143
|
+
|
|
144
|
+
export default defineController({
|
|
145
|
+
path: 'Billing',
|
|
146
|
+
actions: {
|
|
147
|
+
read: defineAction({
|
|
148
|
+
input: schema.object({ accountId: schema.string() }),
|
|
149
|
+
handler: ({ input, services }) => services.Billing.read(input.accountId),
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Rules:
|
|
156
|
+
|
|
157
|
+
- `input` is parsed before the handler runs.
|
|
158
|
+
- Read parsed input from `context.input`.
|
|
159
|
+
- Read request state from `request`, `response`, `api`, `auth`, and router-plugin context.
|
|
160
|
+
- Call business logic through `services`, `models`, or `app`.
|
|
161
|
+
|
|
162
|
+
## 7. Remove Legacy Magic
|
|
163
|
+
|
|
164
|
+
Search user source for old contracts and remove every match.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
rg -n "from ['\"]@app['\"]|Router\\.(page|error|get|post|put|patch|delete|express)\\(|this\\.input\\(" client server common commands
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Expected result: no user-source matches.
|
|
171
|
+
|
|
172
|
+
Allowed replacements:
|
|
173
|
+
|
|
174
|
+
- `ctx.app`, `ctx.services`, `ctx.Router`, `ctx.request`, `ctx.response`, `ctx.auth`, and custom router-plugin context inside handlers.
|
|
175
|
+
- `this.app`, `this.services`, and `this.models` inside typed services.
|
|
176
|
+
- `defineServerRoutes((app) => [...])` when server route definitions need app services.
|
|
177
|
+
|
|
178
|
+
## 8. Standardize Caught Error Handling
|
|
179
|
+
|
|
180
|
+
Every caught error must end at the same framework error surface. Local UI feedback or protocol responses can still happen, but they are not the terminal error handling step by themselves.
|
|
181
|
+
|
|
182
|
+
Server rules:
|
|
183
|
+
|
|
184
|
+
- Use `throw error` when the request/router/controller should fail and let Proteum render the HTTP error response.
|
|
185
|
+
- Use `await app.reportError(error, request)` when a Proteum request is available, or `await app.reportError(error)` for detached/custom Express paths, catch-and-continue server work, and jobs that intentionally keep running.
|
|
186
|
+
- Do not use raw `app.runHook('error', error, request)` in app code. `app.reportError(...)` keeps the `error` versus `error.<code>` routing centralized.
|
|
187
|
+
|
|
188
|
+
Client rules:
|
|
189
|
+
|
|
190
|
+
- Use `throw error` when the action should fail and reach the app-level unhandled rejection path.
|
|
191
|
+
- Use `useContext().app.handleError(error)` or `context.app.handleError(error)` when the UI catches and continues.
|
|
192
|
+
- `handleError` accepts unknown caught values and returns a displayable message. Prefer `setError(context.app.handleError(error, 'Unable to finish this action.'))` over local `instanceof Error` filtering.
|
|
193
|
+
- If an app overrides `handleError`, update it to `handleError(error: unknown, fallbackMessage?: string): string` and return the display message.
|
|
194
|
+
- Toasts, form errors, or `setError(...)` are local feedback only. Route the original caught value through `app.handleError(error)` or `throw error`.
|
|
195
|
+
|
|
196
|
+
Do not treat `console.error(error)`, `console.warn(error)`, or any other `console.*(error)` call as error handling. Console calls can be temporary diagnostics, but they must not be the last stop for a caught error.
|
|
197
|
+
|
|
198
|
+
## 9. Refresh Generated Artifacts
|
|
199
|
+
|
|
200
|
+
Do not edit `.proteum/**` manually. Regenerate it from source.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx proteum refresh
|
|
204
|
+
npx proteum typecheck
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
If connected local projects are used through `file:` sources, start or validate producer apps before validating the consumer.
|
|
208
|
+
|
|
209
|
+
## 10. Validate Runtime Behavior
|
|
210
|
+
|
|
211
|
+
Run the smallest trustworthy checks first, then broaden when the touched surface requires it.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx proteum diagnose /
|
|
215
|
+
npx proteum build --prod
|
|
216
|
+
npx proteum e2e
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For protected flows, prefer Proteum session helpers over automating login unless login is the feature under test.
|
|
220
|
+
|
|
221
|
+
## Common Fixes
|
|
222
|
+
|
|
223
|
+
- Production route-generation errors where top-level `Router.express(...)` was lifted outside registration are fixed by moving the route into `defineServerRoute({ handler: expressHandler(...) })`.
|
|
224
|
+
- `@app` import errors are fixed by moving runtime access into `data`, `render`, route handlers, controller action handlers, or typed services.
|
|
225
|
+
- Missing `this.app.Router` typings are fixed by exporting the concrete app and router types from `server/index.ts`.
|
|
226
|
+
- Static metadata errors are fixed by moving runtime-dependent values out of `path`, `method`, `options`, and error `code`.
|
package/eslint.js
CHANGED
|
@@ -121,75 +121,89 @@ const getCalleePropertyName = (callee) => {
|
|
|
121
121
|
return null;
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
const
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
124
|
+
const getErrorHandlingSide = (filename) => {
|
|
125
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
126
|
+
if (/(^|\/)client\//.test(normalized)) return 'client';
|
|
127
|
+
if (/(^|\/)(server|commands)\//.test(normalized)) return 'server';
|
|
128
|
+
|
|
129
|
+
return 'shared';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getMemberPropertyName = (node) => {
|
|
133
|
+
if (node?.type !== 'MemberExpression') return null;
|
|
134
|
+
if (node.property.type === 'Identifier') return node.property.name;
|
|
135
|
+
if (node.property.type === 'Literal') return String(node.property.value);
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const isConsoleMember = (node) =>
|
|
141
|
+
node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
|
|
142
|
+
|
|
143
|
+
const isAppReceiver = (node) => {
|
|
144
|
+
if (!node) return false;
|
|
145
|
+
if (node.type === 'Identifier' && node.name === 'app') return true;
|
|
146
|
+
if (node.type === 'MemberExpression' && getMemberPropertyName(node) === 'app') return true;
|
|
147
|
+
|
|
148
|
+
return false;
|
|
148
149
|
};
|
|
149
150
|
|
|
150
|
-
const
|
|
151
|
+
const isClientErrorHandlerCall = (callExpression) =>
|
|
152
|
+
callExpression.callee.type === 'MemberExpression' &&
|
|
153
|
+
getMemberPropertyName(callExpression.callee) === 'handleError' &&
|
|
154
|
+
isAppReceiver(callExpression.callee.object);
|
|
155
|
+
|
|
156
|
+
const isServerErrorReporterCall = (callExpression) =>
|
|
157
|
+
callExpression.callee.type === 'MemberExpression' &&
|
|
158
|
+
getMemberPropertyName(callExpression.callee) === 'reportError' &&
|
|
159
|
+
isAppReceiver(callExpression.callee.object);
|
|
160
|
+
|
|
161
|
+
const isPromiseRejectCall = (callExpression) => getCalleePropertyName(callExpression.callee) === 'reject';
|
|
162
|
+
|
|
163
|
+
const isPreservingCall = (callExpression, names, side) => {
|
|
164
|
+
if (!nodeReferencesName(callExpression, names)) return false;
|
|
165
|
+
if (isConsoleMember(callExpression.callee)) return false;
|
|
166
|
+
if (isPromiseRejectCall(callExpression)) return true;
|
|
167
|
+
if (side === 'client') return isClientErrorHandlerCall(callExpression);
|
|
168
|
+
if (side === 'server') return isServerErrorReporterCall(callExpression);
|
|
169
|
+
|
|
170
|
+
return isClientErrorHandlerCall(callExpression) || isServerErrorReporterCall(callExpression);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handlerPreservesCaughtError = (node, names, side) => {
|
|
151
174
|
let preserves = false;
|
|
152
175
|
|
|
153
176
|
traverseNode(node, (child) => {
|
|
154
177
|
if (child.type === 'ThrowStatement' && nodeReferencesName(child.argument, names)) preserves = true;
|
|
155
|
-
if (child.type === 'CallExpression' && isPreservingCall(child, names)) preserves = true;
|
|
178
|
+
if (child.type === 'CallExpression' && isPreservingCall(child, names, side)) preserves = true;
|
|
156
179
|
});
|
|
157
180
|
|
|
158
181
|
return preserves;
|
|
159
182
|
};
|
|
160
183
|
|
|
161
|
-
const directPromiseCatchHandlers = new Set([
|
|
162
|
-
'captureError',
|
|
163
|
-
'captureException',
|
|
164
|
-
'consoleError',
|
|
165
|
-
'handleError',
|
|
166
|
-
'logError',
|
|
167
|
-
'reportError',
|
|
168
|
-
]);
|
|
169
|
-
|
|
170
184
|
const isDirectPromiseCatchHandler = (node) => {
|
|
171
185
|
const name = getCalleePropertyName(node);
|
|
172
|
-
|
|
173
|
-
return node?.type === 'MemberExpression' && node.object?.type === 'Identifier' && node.object.name === 'console';
|
|
186
|
+
return name === 'reject';
|
|
174
187
|
};
|
|
175
188
|
|
|
176
189
|
const createSwallowedErrorRule = () => ({
|
|
177
190
|
meta: {
|
|
178
191
|
type: 'problem',
|
|
179
192
|
docs: {
|
|
180
|
-
description: 'Require caught errors to
|
|
193
|
+
description: 'Require caught errors to reach the standard app error path or be rethrown.',
|
|
181
194
|
},
|
|
182
195
|
messages: {
|
|
183
196
|
missingParam:
|
|
184
|
-
'Caught errors must be bound and
|
|
197
|
+
'Caught errors must be bound and routed through the standard error path. Use `catch (error)` and rethrow, call app.reportError on the server, or call app.handleError on the client.',
|
|
185
198
|
unusedParam:
|
|
186
|
-
'Caught error `{{name}}` is discarded. Rethrow it,
|
|
199
|
+
'Caught error `{{name}}` is discarded. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
|
|
187
200
|
unpreserved:
|
|
188
|
-
'Caught error `{{name}}` is used but not
|
|
201
|
+
'Caught error `{{name}}` is used but not routed through the standard error path. Rethrow it, call app.reportError on the server, or call app.handleError on the client.',
|
|
189
202
|
},
|
|
190
203
|
schema: [],
|
|
191
204
|
},
|
|
192
205
|
create(context) {
|
|
206
|
+
const side = getErrorHandlingSide(context.filename || context.getFilename?.() || '');
|
|
193
207
|
const reportHandler = (node, params, body) => {
|
|
194
208
|
const names = params.flatMap((param) => collectPatternNames(param));
|
|
195
209
|
if (names.length === 0) {
|
|
@@ -203,7 +217,7 @@ const createSwallowedErrorRule = () => ({
|
|
|
203
217
|
return;
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names))) {
|
|
220
|
+
if (!handlerPreservesCaughtError(body, collectDerivedErrorNames(body, names), side)) {
|
|
207
221
|
context.report({ node, messageId: 'unpreserved', data: { name: referencedName } });
|
|
208
222
|
}
|
|
209
223
|
};
|
|
@@ -228,6 +242,37 @@ const createSwallowedErrorRule = () => ({
|
|
|
228
242
|
},
|
|
229
243
|
});
|
|
230
244
|
|
|
245
|
+
const createNoAppImportRule = () => ({
|
|
246
|
+
meta: {
|
|
247
|
+
type: 'problem',
|
|
248
|
+
docs: {
|
|
249
|
+
description: 'Disallow Proteum contextual @app imports in user code.',
|
|
250
|
+
},
|
|
251
|
+
messages: {
|
|
252
|
+
noAppImport:
|
|
253
|
+
'`@app` is not a real runtime module. Receive app services through typed route/controller callback context instead.',
|
|
254
|
+
},
|
|
255
|
+
schema: [],
|
|
256
|
+
},
|
|
257
|
+
create(context) {
|
|
258
|
+
return {
|
|
259
|
+
ImportDeclaration(node) {
|
|
260
|
+
if (node.source?.value === '@app') context.report({ node, messageId: 'noAppImport' });
|
|
261
|
+
},
|
|
262
|
+
CallExpression(node) {
|
|
263
|
+
if (
|
|
264
|
+
node.callee?.type === 'Identifier' &&
|
|
265
|
+
node.callee.name === 'require' &&
|
|
266
|
+
node.arguments?.[0]?.type === 'Literal' &&
|
|
267
|
+
node.arguments[0].value === '@app'
|
|
268
|
+
) {
|
|
269
|
+
context.report({ node, messageId: 'noAppImport' });
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
231
276
|
const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
232
277
|
{
|
|
233
278
|
ignores: [...defaultIgnores, ...ignores],
|
|
@@ -253,6 +298,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
|
253
298
|
'@typescript-eslint': tseslint.plugin,
|
|
254
299
|
proteum: {
|
|
255
300
|
rules: {
|
|
301
|
+
'no-app-import': createNoAppImportRule(),
|
|
256
302
|
'no-swallowed-caught-error': createSwallowedErrorRule(),
|
|
257
303
|
},
|
|
258
304
|
},
|
|
@@ -262,6 +308,7 @@ const createProteumEslintConfig = ({ ignores = [] } = {}) => [
|
|
|
262
308
|
},
|
|
263
309
|
rules: {
|
|
264
310
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
311
|
+
'proteum/no-app-import': 'error',
|
|
265
312
|
'proteum/no-swallowed-caught-error': 'error',
|
|
266
313
|
'no-restricted-syntax': [
|
|
267
314
|
'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.1",
|
|
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
|
|