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
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
indexRouteDefinitions,
|
|
19
19
|
writeGeneratedRouteModule,
|
|
20
20
|
} = require('../cli/compiler/common/generatedRouteModules.ts');
|
|
21
|
+
const { createTypedControllerHelperContent } = require('../cli/compiler/artifacts/controllerHelper.ts');
|
|
21
22
|
const { findClientRouteFiles } = require('../cli/compiler/artifacts/discovery.ts');
|
|
22
23
|
const { indexControllers } = require('../cli/compiler/common/controllers.ts');
|
|
23
24
|
const {
|
|
@@ -107,6 +108,51 @@ export default definePageRoute({
|
|
|
107
108
|
assert.deepEqual(definition.normalizedOptionKeys, ['auth']);
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
test('route indexer accepts imported const-only option metadata', () => {
|
|
112
|
+
const root = createTempDir();
|
|
113
|
+
const filepath = path.join(root, 'client/pages/feed.tsx');
|
|
114
|
+
const catalogFilepath = path.join(root, 'client/catalogs/routes.ts');
|
|
115
|
+
|
|
116
|
+
writeFile(
|
|
117
|
+
catalogFilepath,
|
|
118
|
+
`export const PAGE_AUTH = {
|
|
119
|
+
feed: {
|
|
120
|
+
auth: {
|
|
121
|
+
hasFeature: 'opportunitiesFreq',
|
|
122
|
+
},
|
|
123
|
+
authTracking: {
|
|
124
|
+
source: 'investor_feed',
|
|
125
|
+
feature: 'opportunitiesFreq',
|
|
126
|
+
action: 'open-feed',
|
|
127
|
+
triggerKind: 'open',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
} as const;
|
|
131
|
+
`,
|
|
132
|
+
);
|
|
133
|
+
writeFile(
|
|
134
|
+
filepath,
|
|
135
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
136
|
+
import { PAGE_AUTH } from '@/client/catalogs/routes';
|
|
137
|
+
|
|
138
|
+
export default definePageRoute({
|
|
139
|
+
path: '/feed',
|
|
140
|
+
options: {
|
|
141
|
+
auth: PAGE_AUTH.feed.auth,
|
|
142
|
+
authTracking: PAGE_AUTH.feed.authTracking,
|
|
143
|
+
},
|
|
144
|
+
data: null,
|
|
145
|
+
render: () => null,
|
|
146
|
+
});
|
|
147
|
+
`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const [definition] = indexRouteDefinitions({ side: 'client', sourceFilepath: filepath });
|
|
151
|
+
|
|
152
|
+
assert.equal(definition.path, '/feed');
|
|
153
|
+
assert.deepEqual(definition.normalizedOptionKeys, ['auth', 'authTracking']);
|
|
154
|
+
});
|
|
155
|
+
|
|
110
156
|
test('router port override updates absolute runtime URLs', () => {
|
|
111
157
|
const root = createTempDir();
|
|
112
158
|
|
|
@@ -262,6 +308,37 @@ export default defineServerRoutes(({ Users }) => [
|
|
|
262
308
|
assert.equal(definition.targetResolution, 'static-expression');
|
|
263
309
|
});
|
|
264
310
|
|
|
311
|
+
test('server route factories can push explicit route definitions from a block body', () => {
|
|
312
|
+
const root = createTempDir();
|
|
313
|
+
const filepath = path.join(root, 'server/routes/api.ts');
|
|
314
|
+
|
|
315
|
+
writeFile(
|
|
316
|
+
filepath,
|
|
317
|
+
`import { defineServerRoute, defineServerRoutes } from '@common/router/definitions';
|
|
318
|
+
|
|
319
|
+
export default defineServerRoutes((app) => {
|
|
320
|
+
const { Users } = app;
|
|
321
|
+
const routeOptions = {};
|
|
322
|
+
const routes = [];
|
|
323
|
+
|
|
324
|
+
routes.push(defineServerRoute({
|
|
325
|
+
method: 'GET',
|
|
326
|
+
path: '/api/users',
|
|
327
|
+
options: routeOptions,
|
|
328
|
+
handler: async () => Users.list(),
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
return routes;
|
|
332
|
+
});
|
|
333
|
+
`,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const [definition] = indexRouteDefinitions({ side: 'server', sourceFilepath: filepath });
|
|
337
|
+
|
|
338
|
+
assert.equal(definition.methodName, 'get');
|
|
339
|
+
assert.equal(definition.path, '/api/users');
|
|
340
|
+
});
|
|
341
|
+
|
|
265
342
|
test('route definitions register through explicit router registrar', () => {
|
|
266
343
|
const calls = [];
|
|
267
344
|
|
|
@@ -386,6 +463,17 @@ export default class Legacy extends Controller {
|
|
|
386
463
|
);
|
|
387
464
|
});
|
|
388
465
|
|
|
466
|
+
test('generated typed controller helper binds action context to app root', () => {
|
|
467
|
+
const content = createTypedControllerHelperContent('TestApp');
|
|
468
|
+
|
|
469
|
+
assert.match(content, /@server\/app\/controller/);
|
|
470
|
+
assert.match(content, /@\/server\/index/);
|
|
471
|
+
assert.match(content, /export type TApplication = import\('@\/server\/index'\)\.TestApp/);
|
|
472
|
+
assert.match(content, /export type TControllerRequestServices = import\('@\/server\/index'\)\.TControllerRequestServices/);
|
|
473
|
+
assert.match(content, /TControllerActionContext<\n TInput,\n TApplication,\n TControllerRequestServices\n>/);
|
|
474
|
+
assert.match(content, /handler: \(context: TTypedControllerActionContext<z\.output<TSchema>>\) => TResult/);
|
|
475
|
+
});
|
|
476
|
+
|
|
389
477
|
test('service artifact parser reads defineApplication router factories', () => {
|
|
390
478
|
const root = createTempDir();
|
|
391
479
|
const filepath = path.join(root, 'server/index.ts');
|
|
@@ -451,3 +539,44 @@ export default App;
|
|
|
451
539
|
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Router']);
|
|
452
540
|
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
453
541
|
});
|
|
542
|
+
|
|
543
|
+
test('service artifact parser reads named defineApplication service factories', () => {
|
|
544
|
+
const root = createTempDir();
|
|
545
|
+
const filepath = path.join(root, 'server/index.ts');
|
|
546
|
+
const serviceFilepath = path.join(root, 'server/services/Billing.ts');
|
|
547
|
+
const { parseAppBootstrapSource } = loadServiceArtifactsForAppRoot(root);
|
|
548
|
+
|
|
549
|
+
writeFile(serviceFilepath, 'export default class Billing {}');
|
|
550
|
+
writeFile(
|
|
551
|
+
filepath,
|
|
552
|
+
`import { defineApplication } from '@server/app';
|
|
553
|
+
import Router from '@server/services/router';
|
|
554
|
+
import SchemaRouter from '@server/services/schema/router';
|
|
555
|
+
import Billing from '@/server/services/Billing';
|
|
556
|
+
|
|
557
|
+
const createServices = (app) => ({
|
|
558
|
+
Billing: new Billing(app, {}, app),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const createRouter = (app) => new Router(
|
|
562
|
+
app,
|
|
563
|
+
{
|
|
564
|
+
plugins: {
|
|
565
|
+
schema: new SchemaRouter({}, app),
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
app,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
export default defineApplication({
|
|
572
|
+
services: createServices,
|
|
573
|
+
router: createRouter,
|
|
574
|
+
});
|
|
575
|
+
`,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const bootstrap = parseAppBootstrapSource(filepath);
|
|
579
|
+
|
|
580
|
+
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Billing', 'Router']);
|
|
581
|
+
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
582
|
+
});
|
|
@@ -377,8 +377,9 @@ import SchemaRouter from '@server/services/schema/router';
|
|
|
377
377
|
import * as appConfig from '@/server/config/app';
|
|
378
378
|
|
|
379
379
|
export default defineApplication({
|
|
380
|
-
services: (
|
|
381
|
-
|
|
380
|
+
services: () => ({}),
|
|
381
|
+
router: (app) =>
|
|
382
|
+
new Router(
|
|
382
383
|
app,
|
|
383
384
|
{
|
|
384
385
|
...appConfig.routerBaseConfig,
|
|
@@ -388,7 +389,6 @@ export default defineApplication({
|
|
|
388
389
|
},
|
|
389
390
|
app,
|
|
390
391
|
),
|
|
391
|
-
}),
|
|
392
392
|
});
|
|
393
393
|
`,
|
|
394
394
|
);
|
|
@@ -405,9 +405,6 @@ export default class TranspileWatchClient extends ClientApplication {
|
|
|
405
405
|
|
|
406
406
|
public boot() {}
|
|
407
407
|
public handleUpdate() {}
|
|
408
|
-
public handleError(error: Error) {
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
411
408
|
}
|
|
412
409
|
`,
|
|
413
410
|
);
|
|
@@ -3,10 +3,10 @@ const { Linter } = require('eslint');
|
|
|
3
3
|
|
|
4
4
|
const { createProteumEslintConfig } = require('../eslint.js');
|
|
5
5
|
|
|
6
|
-
const lint = (code) => {
|
|
6
|
+
const lint = (code, filename = 'client/example.tsx') => {
|
|
7
7
|
const linter = new Linter({ configType: 'flat' });
|
|
8
8
|
return linter.verify(code, createProteumEslintConfig(), {
|
|
9
|
-
filename
|
|
9
|
+
filename,
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
@@ -102,7 +102,7 @@ test('proteum lint allows rethrowing the caught error', () => {
|
|
|
102
102
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('proteum lint
|
|
105
|
+
test('proteum lint rejects user feedback that does not route the caught error', () => {
|
|
106
106
|
const messages = lint(`
|
|
107
107
|
export const run = async () => {
|
|
108
108
|
try {
|
|
@@ -115,10 +115,10 @@ test('proteum lint allows surfacing original error details', () => {
|
|
|
115
115
|
};
|
|
116
116
|
`);
|
|
117
117
|
|
|
118
|
-
assert.equal(messages.
|
|
118
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
test('proteum lint
|
|
121
|
+
test('proteum lint rejects derived message state that does not route the caught error', () => {
|
|
122
122
|
const messages = lint(`
|
|
123
123
|
export const run = async () => {
|
|
124
124
|
try {
|
|
@@ -130,10 +130,10 @@ test('proteum lint allows surfacing a message derived from the caught error', ()
|
|
|
130
130
|
};
|
|
131
131
|
`);
|
|
132
132
|
|
|
133
|
-
assert.equal(messages.
|
|
133
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
test('proteum lint allows
|
|
136
|
+
test('proteum lint allows client catches routed to context app error handling', () => {
|
|
137
137
|
const messages = lint(`
|
|
138
138
|
export const run = () => {
|
|
139
139
|
const context = useContext();
|
|
@@ -145,3 +145,242 @@ test('proteum lint allows routing promise failures to app error handling', () =>
|
|
|
145
145
|
|
|
146
146
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
test('proteum lint rejects optional client error handler calls', () => {
|
|
150
|
+
for (const statement of [
|
|
151
|
+
'app?.handleError(error);',
|
|
152
|
+
'context.app?.handleError(error);',
|
|
153
|
+
'window.app?.handleError(error);',
|
|
154
|
+
]) {
|
|
155
|
+
const messages = lint(`
|
|
156
|
+
export const run = async () => {
|
|
157
|
+
const app = useContext();
|
|
158
|
+
const context = useContext();
|
|
159
|
+
try {
|
|
160
|
+
await Investor.api.ensureApiKey();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
${statement}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('proteum lint allows client catches using app error messages for UI feedback', () => {
|
|
172
|
+
const messages = lint(`
|
|
173
|
+
export const run = async () => {
|
|
174
|
+
const context = useContext();
|
|
175
|
+
try {
|
|
176
|
+
await Investor.api.ensureApiKey();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
setError(context.app.handleError(error, 'Unable to finish this action.'));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
`);
|
|
182
|
+
|
|
183
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('proteum lint allows client catches routed to local app error handling', () => {
|
|
187
|
+
const messages = lint(`
|
|
188
|
+
export const run = async () => {
|
|
189
|
+
const app = useContext();
|
|
190
|
+
try {
|
|
191
|
+
await Investor.api.ensureApiKey();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
app.handleError(error);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
`);
|
|
197
|
+
|
|
198
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('proteum lint allows client catches routed to useContext app error handling', () => {
|
|
202
|
+
const messages = lint(`
|
|
203
|
+
export const run = async () => {
|
|
204
|
+
try {
|
|
205
|
+
await Investor.api.ensureApiKey();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
useContext().app.handleError(error);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
`);
|
|
211
|
+
|
|
212
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('proteum lint rejects bare client error handlers', () => {
|
|
216
|
+
const messages = lint(`
|
|
217
|
+
export const run = async () => {
|
|
218
|
+
try {
|
|
219
|
+
await Investor.api.ensureApiKey();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
handleError(error);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('proteum lint allows server catches routed to app error reporting', () => {
|
|
230
|
+
const messages = lint(
|
|
231
|
+
`
|
|
232
|
+
export const run = async (context) => {
|
|
233
|
+
try {
|
|
234
|
+
await context.services.Worker.run();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
await context.app.reportError(error, context.request);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
`,
|
|
240
|
+
'server/example.ts',
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('proteum lint allows server catches routed to instance app error reporting', () => {
|
|
247
|
+
const messages = lint(
|
|
248
|
+
`
|
|
249
|
+
export class WorkerController {
|
|
250
|
+
async run(context) {
|
|
251
|
+
try {
|
|
252
|
+
await context.services.Worker.run();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
await this.app.reportError(error, context.request);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
'server/example.ts',
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('proteum lint rejects optional server error reporter calls', () => {
|
|
266
|
+
for (const statement of [
|
|
267
|
+
'app.reportError?.(error);',
|
|
268
|
+
'app?.reportError(error);',
|
|
269
|
+
'context.app?.reportError(error);',
|
|
270
|
+
]) {
|
|
271
|
+
const messages = lint(
|
|
272
|
+
`
|
|
273
|
+
export const run = async (context) => {
|
|
274
|
+
const app = context.app;
|
|
275
|
+
try {
|
|
276
|
+
await context.services.Worker.run();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
${statement}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
`,
|
|
282
|
+
'server/example.ts',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('proteum lint rejects conditional server error reporter calls', () => {
|
|
290
|
+
const messages = lint(
|
|
291
|
+
`
|
|
292
|
+
export const run = async (context) => {
|
|
293
|
+
const app = context.app;
|
|
294
|
+
try {
|
|
295
|
+
await context.services.Worker.run();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (app) app.reportError(error, context.request);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
`,
|
|
301
|
+
'server/example.ts',
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('proteum lint rejects server catches routed to client app error handling', () => {
|
|
308
|
+
const messages = lint(
|
|
309
|
+
`
|
|
310
|
+
export const run = async (context) => {
|
|
311
|
+
try {
|
|
312
|
+
await context.services.Worker.run();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
context.app.handleError(error);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
`,
|
|
318
|
+
'server/example.ts',
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('proteum lint rejects raw server error hooks as caught error handling', () => {
|
|
325
|
+
const messages = lint(
|
|
326
|
+
`
|
|
327
|
+
export const run = async (context) => {
|
|
328
|
+
try {
|
|
329
|
+
await context.services.Worker.run();
|
|
330
|
+
} catch (error) {
|
|
331
|
+
await context.app.runHook('error', error, context.request);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
`,
|
|
335
|
+
'server/example.ts',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('proteum lint allows manual promise rejection', () => {
|
|
342
|
+
const messages = lint(
|
|
343
|
+
`
|
|
344
|
+
export const run = (input) =>
|
|
345
|
+
new Promise((resolve, reject) => {
|
|
346
|
+
input.load().catch((error) => {
|
|
347
|
+
reject(error);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
`,
|
|
351
|
+
'common/example.ts',
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('proteum lint allows returning manual promise rejection', () => {
|
|
358
|
+
const messages = lint(
|
|
359
|
+
`
|
|
360
|
+
export const run = async (input) => {
|
|
361
|
+
try {
|
|
362
|
+
await input.load();
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return Promise.reject(error);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
`,
|
|
368
|
+
'common/example.ts',
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('proteum lint allows direct reject promise catch handlers', () => {
|
|
375
|
+
const messages = lint(
|
|
376
|
+
`
|
|
377
|
+
export const run = (input) =>
|
|
378
|
+
new Promise((resolve, reject) => {
|
|
379
|
+
input.load().catch(reject);
|
|
380
|
+
});
|
|
381
|
+
`,
|
|
382
|
+
'common/example.ts',
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
386
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
5
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
6
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
|
+
require('ts-node/register/transpile-only');
|
|
8
|
+
|
|
9
|
+
const { createCommandTemplate, createControllerTemplate, createServerIndexTemplate } = require('../cli/scaffold/templates.ts');
|
|
10
|
+
|
|
11
|
+
test('server index scaffold uses explicit defineApplication router property', () => {
|
|
12
|
+
const content = createServerIndexTemplate({ appIdentifier: 'ExampleApp' });
|
|
13
|
+
|
|
14
|
+
assert.match(content, /const ExampleAppApplication = defineApplication\(\{/);
|
|
15
|
+
assert.match(content, /export type ExampleApp = InstanceType<typeof ExampleAppApplication>/);
|
|
16
|
+
assert.match(content, /export default ExampleAppApplication/);
|
|
17
|
+
assert.match(content, /export type TControllerRequestServices = \{\}/);
|
|
18
|
+
assert.match(content, /services: \(\) => \(\{\}\),/);
|
|
19
|
+
assert.match(content, /router: \(app\) =>\s+new Router\(/);
|
|
20
|
+
assert.doesNotMatch(content, /services: \(app\) => \(\{\s+Router: new Router\(/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('controller scaffold imports app-typed generated controller helpers', () => {
|
|
24
|
+
const content = createControllerTemplate({
|
|
25
|
+
appIdentifier: 'ExampleApp',
|
|
26
|
+
className: 'BillingController',
|
|
27
|
+
methodName: 'read',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.match(content, /from '@generated\/server\/controller'/);
|
|
31
|
+
assert.doesNotMatch(content, /from '@server\/app\/controller'/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('command scaffold infers app type from defineApplication default export', () => {
|
|
35
|
+
const content = createCommandTemplate({
|
|
36
|
+
className: 'BillingCommands',
|
|
37
|
+
methodName: 'sync',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.match(content, /import type AppApplication from '@\/server\/index'/);
|
|
41
|
+
assert.match(content, /type App = InstanceType<typeof AppApplication>/);
|
|
42
|
+
assert.doesNotMatch(content, /import type App from '@\/server\/index'/);
|
|
43
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const Module = require('node:module');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const coreRoot = path.join(__dirname, '..');
|
|
6
|
+
require('module-alias').addAliases({
|
|
7
|
+
'@client': path.join(coreRoot, 'client'),
|
|
8
|
+
'@common': path.join(coreRoot, 'common'),
|
|
9
|
+
'@server': path.join(coreRoot, 'server'),
|
|
10
|
+
});
|
|
11
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
12
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
13
|
+
require('ts-node/register/transpile-only');
|
|
14
|
+
|
|
15
|
+
const appContainerStub = {
|
|
16
|
+
Environment: {
|
|
17
|
+
profile: 'test',
|
|
18
|
+
connectedProjects: [],
|
|
19
|
+
},
|
|
20
|
+
Identity: {},
|
|
21
|
+
Setup: {},
|
|
22
|
+
Console: {
|
|
23
|
+
createBugReport: () => {},
|
|
24
|
+
},
|
|
25
|
+
Trace: {
|
|
26
|
+
finishRequest: () => {},
|
|
27
|
+
record: () => {},
|
|
28
|
+
releaseRequest: () => {},
|
|
29
|
+
},
|
|
30
|
+
handleBug: () => {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const applicationModulePath = path.join(coreRoot, 'server/app/index.ts');
|
|
34
|
+
const originalLoad = Module._load;
|
|
35
|
+
Module._load = function patchedLoad(request, parent, isMain) {
|
|
36
|
+
if (parent?.filename === applicationModulePath && request === './container') {
|
|
37
|
+
return { __esModule: true, default: appContainerStub };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let Application;
|
|
44
|
+
try {
|
|
45
|
+
Application = require('../server/app/index.ts').Application;
|
|
46
|
+
} finally {
|
|
47
|
+
Module._load = originalLoad;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class TestApplication extends Application {}
|
|
51
|
+
|
|
52
|
+
const createAppWithHooks = (hooks) => {
|
|
53
|
+
const app = new TestApplication();
|
|
54
|
+
app.hooks = hooks;
|
|
55
|
+
|
|
56
|
+
return app;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
test('server application reports plain errors through the default error hook', async () => {
|
|
60
|
+
const events = [];
|
|
61
|
+
const request = { id: 'request-1' };
|
|
62
|
+
const app = createAppWithHooks({
|
|
63
|
+
error: [
|
|
64
|
+
async (error, hookRequest) => {
|
|
65
|
+
events.push({
|
|
66
|
+
hook: 'error',
|
|
67
|
+
message: error.message,
|
|
68
|
+
requestId: hookRequest.id,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await app.reportError(new Error('boom'), request);
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(events, [{ hook: 'error', message: 'boom', requestId: 'request-1' }]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('server application reports HTTP errors through code-specific hooks', async () => {
|
|
80
|
+
const events = [];
|
|
81
|
+
const error = new Error('missing');
|
|
82
|
+
error.http = 404;
|
|
83
|
+
const request = { id: 'request-404' };
|
|
84
|
+
const app = createAppWithHooks({
|
|
85
|
+
'error.404': [
|
|
86
|
+
async (hookError, hookRequest) => {
|
|
87
|
+
events.push({
|
|
88
|
+
hook: 'error.404',
|
|
89
|
+
message: hookError.message,
|
|
90
|
+
requestId: hookRequest.id,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await app.reportError(error, request);
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(events, [{ hook: 'error.404', message: 'missing', requestId: 'request-404' }]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('server application reports status errors through code-specific hooks', async () => {
|
|
102
|
+
const events = [];
|
|
103
|
+
const error = new Error('forbidden');
|
|
104
|
+
error.status = 403;
|
|
105
|
+
const app = createAppWithHooks({
|
|
106
|
+
'error.403': [
|
|
107
|
+
async (hookError) => {
|
|
108
|
+
events.push({
|
|
109
|
+
hook: 'error.403',
|
|
110
|
+
message: hookError.message,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await app.reportError(error);
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(events, [{ hook: 'error.403', message: 'forbidden' }]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('server application normalizes non-error rejections before reporting', async () => {
|
|
122
|
+
const messages = [];
|
|
123
|
+
const app = createAppWithHooks({
|
|
124
|
+
error: [
|
|
125
|
+
async (error) => {
|
|
126
|
+
messages.push(error.message);
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await app.reportError('string failure');
|
|
132
|
+
await app.reportError({ reason: 'unknown failure' });
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(messages, ['string failure', 'Unknown application error']);
|
|
135
|
+
});
|