proteum 2.5.0 → 2.5.2
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/AGENTS.md +2 -2
- package/README.md +46 -19
- package/agents/project/AGENTS.md +9 -7
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/diagnostics.md +1 -1
- package/agents/project/root/AGENTS.md +9 -7
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +11 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +14 -6
- package/cli/utils/agents.ts +1 -1
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/request/api.ts +2 -2
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +269 -0
- package/eslint.js +96 -50
- package/package.json +1 -1
- package/server/app/index.ts +28 -2
- package/server/services/router/index.ts +3 -3
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/dev-transpile-watch.test.cjs +3 -6
- package/tests/eslint-rules.test.cjs +246 -7
- package/tests/scaffold-templates.test.cjs +43 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/verify-changed.test.cjs +200 -0
|
@@ -32,6 +32,38 @@ export type TApplicationSetupConfig = {
|
|
|
32
32
|
connect?: TConnectedProjectsConfig;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
export type TVerificationCheckScope = 'targeted' | 'area' | 'full' | 'static';
|
|
36
|
+
|
|
37
|
+
export type TVerificationSuiteConfig =
|
|
38
|
+
| string
|
|
39
|
+
| {
|
|
40
|
+
command: string;
|
|
41
|
+
cwd?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
scope?: TVerificationCheckScope;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TVerificationRuleConfig = {
|
|
47
|
+
id: string;
|
|
48
|
+
match: readonly string[];
|
|
49
|
+
reason: string;
|
|
50
|
+
run: readonly string[];
|
|
51
|
+
scope?: TVerificationCheckScope;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type TVerificationDocsOnlyConfig =
|
|
55
|
+
| boolean
|
|
56
|
+
| {
|
|
57
|
+
reason?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type TVerificationConfig = {
|
|
61
|
+
always?: readonly string[];
|
|
62
|
+
docsOnly?: TVerificationDocsOnlyConfig;
|
|
63
|
+
rules?: readonly TVerificationRuleConfig[];
|
|
64
|
+
suites?: Record<string, TVerificationSuiteConfig>;
|
|
65
|
+
};
|
|
66
|
+
|
|
35
67
|
const isRecord = (value: unknown): value is TObjectRecord =>
|
|
36
68
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
37
69
|
|
|
@@ -87,6 +119,137 @@ const readStringRecord = ({
|
|
|
87
119
|
return output;
|
|
88
120
|
};
|
|
89
121
|
|
|
122
|
+
const readStringArray = ({
|
|
123
|
+
filepath,
|
|
124
|
+
path,
|
|
125
|
+
value,
|
|
126
|
+
}: {
|
|
127
|
+
filepath: string;
|
|
128
|
+
path: string;
|
|
129
|
+
value: unknown;
|
|
130
|
+
}) => {
|
|
131
|
+
if (!Array.isArray(value)) throw new Error(`Invalid ${path} in ${filepath}. Expected an array of strings.`);
|
|
132
|
+
|
|
133
|
+
const output = value.map((entry, index) => {
|
|
134
|
+
if (typeof entry !== 'string' || entry.trim() === '')
|
|
135
|
+
throw new Error(`Invalid ${path}.${index} in ${filepath}. Expected a non-empty string.`);
|
|
136
|
+
|
|
137
|
+
return entry.trim();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return [...new Set(output)];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const verificationScopes = new Set<TVerificationCheckScope>(['targeted', 'area', 'full', 'static']);
|
|
144
|
+
|
|
145
|
+
const readVerificationScope = ({
|
|
146
|
+
filepath,
|
|
147
|
+
path,
|
|
148
|
+
value,
|
|
149
|
+
}: {
|
|
150
|
+
filepath: string;
|
|
151
|
+
path: string;
|
|
152
|
+
value: unknown;
|
|
153
|
+
}) => {
|
|
154
|
+
if (value === undefined) return undefined;
|
|
155
|
+
if (typeof value !== 'string' || !verificationScopes.has(value as TVerificationCheckScope)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Invalid ${path} in ${filepath}. Expected one of ${Array.from(verificationScopes).join(', ')}.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return value as TVerificationCheckScope;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const normalizeVerificationSuiteConfig = ({
|
|
165
|
+
filepath,
|
|
166
|
+
key,
|
|
167
|
+
value,
|
|
168
|
+
}: {
|
|
169
|
+
filepath: string;
|
|
170
|
+
key: string;
|
|
171
|
+
value: unknown;
|
|
172
|
+
}): TVerificationSuiteConfig => {
|
|
173
|
+
if (typeof value === 'string') return readRequiredString({ filepath, path: `suites.${key}`, value });
|
|
174
|
+
if (!isRecord(value)) throw new Error(`Invalid suites.${key} in ${filepath}. Expected a command string or object.`);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
command: readRequiredString({ filepath, path: `suites.${key}.command`, value: value.command }),
|
|
178
|
+
cwd: readOptionalString({ filepath, path: `suites.${key}.cwd`, value: value.cwd }),
|
|
179
|
+
description: readOptionalString({ filepath, path: `suites.${key}.description`, value: value.description }),
|
|
180
|
+
scope: readVerificationScope({ filepath, path: `suites.${key}.scope`, value: value.scope }),
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const normalizeVerificationSuitesConfig = ({
|
|
185
|
+
filepath,
|
|
186
|
+
value,
|
|
187
|
+
}: {
|
|
188
|
+
filepath: string;
|
|
189
|
+
value: unknown;
|
|
190
|
+
}): Record<string, TVerificationSuiteConfig> => {
|
|
191
|
+
if (value === undefined) return {};
|
|
192
|
+
if (!isRecord(value)) throw new Error(`Invalid suites in ${filepath}. Expected an object.`);
|
|
193
|
+
|
|
194
|
+
const output: Record<string, TVerificationSuiteConfig> = {};
|
|
195
|
+
|
|
196
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
197
|
+
if (!key.trim()) throw new Error(`Invalid suites key in ${filepath}. Expected a non-empty string.`);
|
|
198
|
+
output[key] = normalizeVerificationSuiteConfig({ filepath, key, value: entry });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return output;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const normalizeVerificationRuleConfig = ({
|
|
205
|
+
filepath,
|
|
206
|
+
index,
|
|
207
|
+
value,
|
|
208
|
+
}: {
|
|
209
|
+
filepath: string;
|
|
210
|
+
index: number;
|
|
211
|
+
value: unknown;
|
|
212
|
+
}): TVerificationRuleConfig => {
|
|
213
|
+
if (!isRecord(value)) throw new Error(`Invalid rules.${index} in ${filepath}. Expected an object.`);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
id: readRequiredString({ filepath, path: `rules.${index}.id`, value: value.id }),
|
|
217
|
+
match: readStringArray({ filepath, path: `rules.${index}.match`, value: value.match }),
|
|
218
|
+
reason: readRequiredString({ filepath, path: `rules.${index}.reason`, value: value.reason }),
|
|
219
|
+
run: readStringArray({ filepath, path: `rules.${index}.run`, value: value.run }),
|
|
220
|
+
scope: readVerificationScope({ filepath, path: `rules.${index}.scope`, value: value.scope }),
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const normalizeVerificationRulesConfig = ({
|
|
225
|
+
filepath,
|
|
226
|
+
value,
|
|
227
|
+
}: {
|
|
228
|
+
filepath: string;
|
|
229
|
+
value: unknown;
|
|
230
|
+
}): TVerificationRuleConfig[] => {
|
|
231
|
+
if (value === undefined) return [];
|
|
232
|
+
if (!Array.isArray(value)) throw new Error(`Invalid rules in ${filepath}. Expected an array.`);
|
|
233
|
+
|
|
234
|
+
return value.map((entry, index) => normalizeVerificationRuleConfig({ filepath, index, value: entry }));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const normalizeVerificationDocsOnlyConfig = ({
|
|
238
|
+
filepath,
|
|
239
|
+
value,
|
|
240
|
+
}: {
|
|
241
|
+
filepath: string;
|
|
242
|
+
value: unknown;
|
|
243
|
+
}): TVerificationDocsOnlyConfig => {
|
|
244
|
+
if (value === undefined) return true;
|
|
245
|
+
if (typeof value === 'boolean') return value;
|
|
246
|
+
if (!isRecord(value)) throw new Error(`Invalid docsOnly in ${filepath}. Expected a boolean or object.`);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
reason: readOptionalString({ filepath, path: 'docsOnly.reason', value: value.reason }),
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
90
253
|
const readSocialConfig = ({
|
|
91
254
|
filepath,
|
|
92
255
|
value,
|
|
@@ -160,6 +323,18 @@ export const normalizeApplicationSetupConfig = (
|
|
|
160
323
|
};
|
|
161
324
|
};
|
|
162
325
|
|
|
326
|
+
export const normalizeVerificationConfig = (value: unknown, filepath = 'proteum.verify.config.ts'): TVerificationConfig => {
|
|
327
|
+
if (value === undefined) return { always: [], docsOnly: true, rules: [], suites: {} };
|
|
328
|
+
if (!isRecord(value)) throw new Error(`Invalid verification config in ${filepath}. Expected an object export.`);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
always: value.always === undefined ? [] : readStringArray({ filepath, path: 'always', value: value.always }),
|
|
332
|
+
docsOnly: normalizeVerificationDocsOnlyConfig({ filepath, value: value.docsOnly }),
|
|
333
|
+
rules: normalizeVerificationRulesConfig({ filepath, value: value.rules }),
|
|
334
|
+
suites: normalizeVerificationSuitesConfig({ filepath, value: value.suites }),
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
163
338
|
class ApplicationConfigHelpers {
|
|
164
339
|
public static identity<const TIdentity extends TApplicationIdentityConfig>(config: TIdentity) {
|
|
165
340
|
return config;
|
|
@@ -170,4 +345,6 @@ class ApplicationConfigHelpers {
|
|
|
170
345
|
}
|
|
171
346
|
}
|
|
172
347
|
|
|
348
|
+
export const defineVerificationConfig = <const TVerification extends TVerificationConfig>(config: TVerification) => config;
|
|
349
|
+
|
|
173
350
|
export const Application = ApplicationConfigHelpers;
|
|
@@ -5,10 +5,13 @@ import * as ts from 'typescript';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
Application as ApplicationConfig,
|
|
8
|
+
defineVerificationConfig,
|
|
8
9
|
normalizeApplicationIdentityConfig,
|
|
9
10
|
normalizeApplicationSetupConfig,
|
|
11
|
+
normalizeVerificationConfig,
|
|
10
12
|
type TApplicationIdentityConfig,
|
|
11
13
|
type TApplicationSetupConfig,
|
|
14
|
+
type TVerificationConfig,
|
|
12
15
|
} from './applicationConfig';
|
|
13
16
|
import { loadOptionalProteumDotenv } from './env/proteumEnv';
|
|
14
17
|
|
|
@@ -51,7 +54,8 @@ const loadTsModule = (filepath: string): unknown => {
|
|
|
51
54
|
|
|
52
55
|
const requireFromFile = createRequire(normalizedFilepath);
|
|
53
56
|
const runtimeRequire = (specifier: string) => {
|
|
54
|
-
if (specifier === 'proteum/config' || specifier === 'proteum/config.ts')
|
|
57
|
+
if (specifier === 'proteum/config' || specifier === 'proteum/config.ts')
|
|
58
|
+
return { Application: ApplicationConfig, defineVerificationConfig };
|
|
55
59
|
|
|
56
60
|
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
57
61
|
const resolved = resolveLocalModulePath(specifier, normalizedFilepath);
|
|
@@ -81,9 +85,11 @@ const getDefaultExport = <T>(value: unknown): T => {
|
|
|
81
85
|
|
|
82
86
|
export const identityConfigFilename = 'identity.config.ts';
|
|
83
87
|
export const setupConfigFilename = 'proteum.config.ts';
|
|
88
|
+
export const verificationConfigFilename = 'proteum.verify.config.ts';
|
|
84
89
|
|
|
85
90
|
export const resolveIdentityConfigFilepath = (appDir: string) => path.join(appDir, identityConfigFilename);
|
|
86
91
|
export const resolveSetupConfigFilepath = (appDir: string) => path.join(appDir, setupConfigFilename);
|
|
92
|
+
export const resolveVerificationConfigFilepath = (rootDir: string) => path.join(rootDir, verificationConfigFilename);
|
|
87
93
|
|
|
88
94
|
export const loadApplicationIdentityConfig = (appDir: string): TApplicationIdentityConfig => {
|
|
89
95
|
const filepath = resolveIdentityConfigFilepath(appDir);
|
|
@@ -100,3 +106,29 @@ export const loadApplicationSetupConfig = (appDir: string): TApplicationSetupCon
|
|
|
100
106
|
|
|
101
107
|
return normalizeApplicationSetupConfig(getDefaultExport(loadTsModule(filepath)), filepath);
|
|
102
108
|
};
|
|
109
|
+
|
|
110
|
+
export const findVerificationConfigFilepath = (startDir: string) => {
|
|
111
|
+
let currentDir = path.resolve(startDir);
|
|
112
|
+
|
|
113
|
+
while (true) {
|
|
114
|
+
const candidate = resolveVerificationConfigFilepath(currentDir);
|
|
115
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
116
|
+
|
|
117
|
+
const parentDir = path.dirname(currentDir);
|
|
118
|
+
if (parentDir === currentDir) return undefined;
|
|
119
|
+
currentDir = parentDir;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const loadVerificationConfig = (
|
|
124
|
+
startDir: string,
|
|
125
|
+
): { config: TVerificationConfig; filepath?: string; root: string } => {
|
|
126
|
+
const filepath = findVerificationConfigFilepath(startDir);
|
|
127
|
+
if (!filepath) return { config: normalizeVerificationConfig(undefined), root: path.resolve(startDir) };
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
config: normalizeVerificationConfig(getDefaultExport(loadTsModule(filepath)), filepath),
|
|
131
|
+
filepath,
|
|
132
|
+
root: path.dirname(filepath),
|
|
133
|
+
};
|
|
134
|
+
};
|
|
@@ -152,8 +152,24 @@ const isRouterRenderCallback = (node: ts.FunctionLikeDeclaration) => {
|
|
|
152
152
|
return lastFunctionArgument === unwrappedNode;
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
+
const isDefinitionRouteRenderCallback = (node: ts.FunctionLikeDeclaration) => {
|
|
156
|
+
const unwrappedNode = unwrapExpression(node);
|
|
157
|
+
const parent = unwrappedNode.parent;
|
|
158
|
+
if (!ts.isPropertyAssignment(parent)) return false;
|
|
159
|
+
if (!ts.isIdentifier(parent.name) || parent.name.text !== 'render') return false;
|
|
160
|
+
if (!ts.isObjectLiteralExpression(parent.parent)) return false;
|
|
161
|
+
|
|
162
|
+
const callExpression = parent.parent.parent;
|
|
163
|
+
if (!ts.isCallExpression(callExpression)) return false;
|
|
164
|
+
if (!ts.isIdentifier(callExpression.expression)) return false;
|
|
165
|
+
|
|
166
|
+
const helperName = callExpression.expression.text;
|
|
167
|
+
return helperName === 'definePageRoute' || helperName === 'defineErrorRoute';
|
|
168
|
+
};
|
|
169
|
+
|
|
155
170
|
const isValidHookContainer = (node: ts.FunctionLikeDeclaration) => {
|
|
156
171
|
if (isRouterRenderCallback(node)) return true;
|
|
172
|
+
if (isDefinitionRouteRenderCallback(node)) return true;
|
|
157
173
|
|
|
158
174
|
const functionName = getFunctionName(node);
|
|
159
175
|
if (!functionName) return false;
|
package/config.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export { Application } from './common/applicationConfig';
|
|
1
|
+
export { Application, defineVerificationConfig } from './common/applicationConfig';
|
|
2
2
|
export type {
|
|
3
3
|
TApplicationIdentityConfig as ApplicationIdentityConfig,
|
|
4
4
|
TApplicationSetupConfig as ApplicationSetupConfig,
|
|
5
|
+
TVerificationCheckScope as VerificationCheckScope,
|
|
6
|
+
TVerificationConfig as VerificationConfig,
|
|
7
|
+
TVerificationRuleConfig as VerificationRuleConfig,
|
|
8
|
+
TVerificationSuiteConfig as VerificationSuiteConfig,
|
|
5
9
|
} from './common/applicationConfig';
|
|
@@ -0,0 +1,269 @@
|
|
|
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.1`, 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 createProjectServices = (app: ProjectApp): ProjectServices => ({
|
|
75
|
+
Billing: new BillingService(app, {}, app),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const ProjectApplication = defineApplication({
|
|
79
|
+
services: createProjectServices,
|
|
80
|
+
router: createProjectRouter,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export default ProjectApplication;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Generated app globals read the named app type exported by `server/index.ts`. Derive that named type from the default `defineApplication` export when the app does not need a more explicit bootstrap type:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import type ProjectApplication from '@/server/index';
|
|
90
|
+
|
|
91
|
+
type ProjectApp = InstanceType<typeof ProjectApplication>;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
In app roots with service constructors that need earlier services during boot, use an explicit exported app type plus a progressively assigned service factory:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
export type ProjectServices = {
|
|
98
|
+
Models: ModelsService;
|
|
99
|
+
Domains: DomainsService;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ProjectApp = Application & ProjectServices & {
|
|
103
|
+
Router: ProjectRouter;
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 4. Migrate Pages
|
|
108
|
+
|
|
109
|
+
Replace legacy page registration with a default-exported page definition.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { definePageRoute } from '@common/router/definitions';
|
|
113
|
+
|
|
114
|
+
export default definePageRoute({
|
|
115
|
+
path: '/dashboard',
|
|
116
|
+
options: { auth: true },
|
|
117
|
+
data: ({ AccountController }) => ({
|
|
118
|
+
account: AccountController.accountPage(),
|
|
119
|
+
}),
|
|
120
|
+
render: ({ account }) => <Dashboard account={account} />,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Rules:
|
|
125
|
+
|
|
126
|
+
- `path`, `options`, and error `code` must be static and compiler-readable.
|
|
127
|
+
- Route behavior belongs in `options`, not in data.
|
|
128
|
+
- Use `data: null` when no SSR data is needed.
|
|
129
|
+
- Runtime references are allowed only inside `data` and `render`.
|
|
130
|
+
|
|
131
|
+
## 5. Migrate Manual Server Routes
|
|
132
|
+
|
|
133
|
+
Replace top-level `Router.get(...)`, `Router.post(...)`, `Router.express(...)`, and similar calls with definition exports.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { defineServerRoute, defineServerRoutes, expressHandler } from '@common/router/definitions';
|
|
137
|
+
import type { ProjectApp } from '@/server/index';
|
|
138
|
+
|
|
139
|
+
export default defineServerRoutes((app: ProjectApp) => [
|
|
140
|
+
defineServerRoute({
|
|
141
|
+
method: 'GET',
|
|
142
|
+
path: '/health',
|
|
143
|
+
options: {},
|
|
144
|
+
handler: ({ response }) => response.json({ ok: true }),
|
|
145
|
+
}),
|
|
146
|
+
defineServerRoute({
|
|
147
|
+
method: 'POST',
|
|
148
|
+
path: '/webhook',
|
|
149
|
+
options: {},
|
|
150
|
+
handler: expressHandler((request, response) => {
|
|
151
|
+
app.Billing.recordWebhook(request.body);
|
|
152
|
+
response.status(204).send('');
|
|
153
|
+
}),
|
|
154
|
+
}),
|
|
155
|
+
]);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use `defineServerRoutes((app) => [...])` only when the route definitions need app services at registration time. Otherwise export one `defineServerRoute(...)`.
|
|
159
|
+
|
|
160
|
+
## 6. Migrate Controllers
|
|
161
|
+
|
|
162
|
+
Replace controller classes and `this.input(schema)` with explicit actions.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { defineAction, defineController, schema } from '@generated/server/controller';
|
|
166
|
+
|
|
167
|
+
export default defineController({
|
|
168
|
+
path: 'Billing',
|
|
169
|
+
actions: {
|
|
170
|
+
read: defineAction({
|
|
171
|
+
input: schema.object({ accountId: schema.string() }),
|
|
172
|
+
handler: ({ input, services }) => services.Billing.read(input.accountId),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Rules:
|
|
179
|
+
|
|
180
|
+
- `input` is parsed before the handler runs.
|
|
181
|
+
- Read parsed input from `context.input`.
|
|
182
|
+
- Read request state from `request`, `response`, `api`, `auth`, and router-plugin context.
|
|
183
|
+
- Call business logic through `services`, `models`, or `app`.
|
|
184
|
+
- Import controller definition helpers from `@generated/server/controller` when app-specific services, models, router, or request plugins are needed. The generated helper binds context to the concrete app type exported by `server/index.ts`.
|
|
185
|
+
- Keep action schemas aligned with existing typed service methods. If a service method accepts `{ domainIds?: string[]; entries?: Entry[] }`, the `defineAction({ input })` schema must mark those fields optional too. Proteum 2.5 generates strict client types from these schemas, so stale schemas that were previously hidden by loose clients become compile errors.
|
|
186
|
+
|
|
187
|
+
## 7. Migrate Commands
|
|
188
|
+
|
|
189
|
+
Commands should no longer import a default `App` type from `server/index.ts`. The default export is now the application constructor returned by `defineApplication`.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { Commands } from '@server/app/commands';
|
|
193
|
+
import type ProjectApplication from '@/server/index';
|
|
194
|
+
|
|
195
|
+
type ProjectApp = InstanceType<typeof ProjectApplication>;
|
|
196
|
+
|
|
197
|
+
export default class BillingCommands extends Commands<ProjectApp> {
|
|
198
|
+
public async sync() {
|
|
199
|
+
return this.app.Billing.sync();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 8. Remove Legacy Magic
|
|
205
|
+
|
|
206
|
+
Search user source for old contracts and remove every match.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
rg -n "from ['\"]@app['\"]|Router\\.(page|error|get|post|put|patch|delete|express)\\(|this\\.input\\(" client server common commands
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Expected result: no user-source matches.
|
|
213
|
+
|
|
214
|
+
Allowed replacements:
|
|
215
|
+
|
|
216
|
+
- `ctx.app`, `ctx.services`, `ctx.Router`, `ctx.request`, `ctx.response`, `ctx.auth`, and custom router-plugin context inside handlers.
|
|
217
|
+
- `this.app`, `this.services`, and `this.models` inside typed services.
|
|
218
|
+
- `defineServerRoutes((app) => [...])` when server route definitions need app services.
|
|
219
|
+
|
|
220
|
+
## 9. Standardize Caught Error Handling
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
Server rules:
|
|
225
|
+
|
|
226
|
+
- Use `throw error` when the request/router/controller should fail and let Proteum render the HTTP error response.
|
|
227
|
+
- 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.
|
|
228
|
+
- Do not use raw `app.runHook('error', error, request)` in app code. `app.reportError(...)` keeps the `error` versus `error.<code>` routing centralized.
|
|
229
|
+
|
|
230
|
+
Client rules:
|
|
231
|
+
|
|
232
|
+
- Use `throw error` when the action should fail and reach the app-level unhandled rejection path.
|
|
233
|
+
- Use `useContext().app.handleError(error)` or `context.app.handleError(error)` when the UI catches and continues.
|
|
234
|
+
- `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.
|
|
235
|
+
- If an app overrides `handleError`, update it to `handleError(error: unknown, fallbackMessage?: string): string` and return the display message.
|
|
236
|
+
- Toasts, form errors, or `setError(...)` are local feedback only. Route the original caught value through `app.handleError(error)` or `throw error`.
|
|
237
|
+
|
|
238
|
+
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.
|
|
239
|
+
|
|
240
|
+
## 10. Refresh Generated Artifacts
|
|
241
|
+
|
|
242
|
+
Do not edit `.proteum/**` manually. Regenerate it from source.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npx proteum refresh
|
|
246
|
+
npx proteum typecheck
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
If connected local projects are used through `file:` sources, start or validate producer apps before validating the consumer.
|
|
250
|
+
|
|
251
|
+
## 11. Validate Runtime Behavior
|
|
252
|
+
|
|
253
|
+
Run the smallest trustworthy checks first, then broaden when the touched surface requires it.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npx proteum diagnose /
|
|
257
|
+
npx proteum build --prod
|
|
258
|
+
npx proteum e2e
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
For protected flows, prefer Proteum session helpers over automating login unless login is the feature under test.
|
|
262
|
+
|
|
263
|
+
## Common Fixes
|
|
264
|
+
|
|
265
|
+
- Production route-generation errors where top-level `Router.express(...)` was lifted outside registration are fixed by moving the route into `defineServerRoute({ handler: expressHandler(...) })`.
|
|
266
|
+
- `@app` import errors are fixed by moving runtime access into `data`, `render`, route handlers, controller action handlers, or typed services.
|
|
267
|
+
- Missing `this.app.Router`, service, or `env` typings are fixed by exporting the project app type from `server/index.ts`, letting `defineApplication({ services, router })` infer from typed factories, and refreshing generated artifacts.
|
|
268
|
+
- Static metadata errors are fixed by moving runtime-dependent values out of `path`, `method`, `options`, and error `code`.
|
|
269
|
+
- Shared package interfaces should match the server controller schemas they wrap. A shared client type that allows `null` or extra enum values while the server schema rejects them will now fail assignment against the generated controller client.
|