proteum 2.4.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -55
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/eslint.js +36 -1
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/http-client-error-context.test.cjs +10 -1
- package/server/app/container/console/index.ts +2 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { EventEmitter } = require('node:events');
|
|
6
|
+
|
|
7
|
+
const coreRoot = path.join(__dirname, '..');
|
|
8
|
+
require('module-alias').addAliases({
|
|
9
|
+
'@client': path.join(coreRoot, 'client'),
|
|
10
|
+
'@common': path.join(coreRoot, 'common'),
|
|
11
|
+
'@server': path.join(coreRoot, 'server'),
|
|
12
|
+
});
|
|
13
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
14
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
15
|
+
require('ts-node/register/transpile-only');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
indexRouteDefinitions,
|
|
19
|
+
writeGeneratedRouteModule,
|
|
20
|
+
} = require('../cli/compiler/common/generatedRouteModules.ts');
|
|
21
|
+
const { findClientRouteFiles } = require('../cli/compiler/artifacts/discovery.ts');
|
|
22
|
+
const { indexControllers } = require('../cli/compiler/common/controllers.ts');
|
|
23
|
+
const {
|
|
24
|
+
defineAction,
|
|
25
|
+
defineController,
|
|
26
|
+
runControllerAction,
|
|
27
|
+
schema,
|
|
28
|
+
} = require('../server/app/controller/index.ts');
|
|
29
|
+
const Service = require('../server/app/service/index.ts').default;
|
|
30
|
+
const { expressHandler, registerRouteDefinition } = require('../common/router/definitions.ts');
|
|
31
|
+
const { parseProteumEnvConfig } = require('../common/env/proteumEnv.ts');
|
|
32
|
+
|
|
33
|
+
const createTempDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-definition-contracts-'));
|
|
34
|
+
|
|
35
|
+
const writeFile = (filepath, content) => {
|
|
36
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
37
|
+
fs.writeFileSync(filepath, content);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const loadServiceArtifactsForAppRoot = (appRoot) => {
|
|
41
|
+
const Module = require('node:module');
|
|
42
|
+
const modulePath = require.resolve('../cli/compiler/artifacts/services.ts');
|
|
43
|
+
const originalLoad = Module._load;
|
|
44
|
+
const appStub = { paths: { root: appRoot }, identity: { identifier: 'TestApp' }, containerServices: [] };
|
|
45
|
+
const cliStub = { paths: { core: { root: coreRoot } } };
|
|
46
|
+
|
|
47
|
+
delete require.cache[modulePath];
|
|
48
|
+
Module._load = function patchedLoad(request, parent, isMain) {
|
|
49
|
+
if (parent?.filename === modulePath && request === '../../app') {
|
|
50
|
+
return { __esModule: true, default: appStub };
|
|
51
|
+
}
|
|
52
|
+
if (parent?.filename === modulePath && request === '../..') {
|
|
53
|
+
return { __esModule: true, default: cliStub };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return require(modulePath);
|
|
61
|
+
} finally {
|
|
62
|
+
Module._load = originalLoad;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const withProteumEnv = (env, run) => {
|
|
67
|
+
const previous = {};
|
|
68
|
+
for (const key of Object.keys(env)) previous[key] = process.env[key];
|
|
69
|
+
|
|
70
|
+
Object.assign(process.env, env);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return run();
|
|
74
|
+
} finally {
|
|
75
|
+
for (const key of Object.keys(env)) {
|
|
76
|
+
if (previous[key] === undefined) delete process.env[key];
|
|
77
|
+
else process.env[key] = previous[key];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
test('route indexer reads explicit page definitions with static metadata', () => {
|
|
83
|
+
const root = createTempDir();
|
|
84
|
+
const filepath = path.join(root, 'client/pages/index.tsx');
|
|
85
|
+
|
|
86
|
+
writeFile(
|
|
87
|
+
filepath,
|
|
88
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
89
|
+
|
|
90
|
+
const routePath = '/demo';
|
|
91
|
+
|
|
92
|
+
export default definePageRoute({
|
|
93
|
+
path: routePath,
|
|
94
|
+
options: { auth: false },
|
|
95
|
+
data: null,
|
|
96
|
+
render: () => null,
|
|
97
|
+
});
|
|
98
|
+
`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const [definition] = indexRouteDefinitions({ side: 'client', sourceFilepath: filepath });
|
|
102
|
+
|
|
103
|
+
assert.equal(definition.methodName, 'page');
|
|
104
|
+
assert.equal(definition.path, '/demo');
|
|
105
|
+
assert.equal(definition.targetResolution, 'static-expression');
|
|
106
|
+
assert.equal(definition.hasData, false);
|
|
107
|
+
assert.deepEqual(definition.normalizedOptionKeys, ['auth']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('router port override updates absolute runtime URLs', () => {
|
|
111
|
+
const root = createTempDir();
|
|
112
|
+
|
|
113
|
+
const config = withProteumEnv(
|
|
114
|
+
{
|
|
115
|
+
ENV_NAME: 'local',
|
|
116
|
+
ENV_PROFILE: 'dev',
|
|
117
|
+
PORT: '3010',
|
|
118
|
+
URL: 'http://localhost:3010',
|
|
119
|
+
URL_INTERNAL: 'http://localhost:3010',
|
|
120
|
+
},
|
|
121
|
+
() =>
|
|
122
|
+
parseProteumEnvConfig({
|
|
123
|
+
appDir: root,
|
|
124
|
+
routerPortOverride: 3100,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assert.equal(config.router.port, 3100);
|
|
129
|
+
assert.equal(config.router.currentDomain, 'http://localhost:3100');
|
|
130
|
+
assert.equal(config.router.internalUrl, 'http://localhost:3100');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('route wrapper imports explicit definitions without lifting source helpers', () => {
|
|
134
|
+
const root = createTempDir();
|
|
135
|
+
const sourceFilepath = path.join(root, 'client/pages/index.tsx');
|
|
136
|
+
const outputFilepath = path.join(root, '.proteum/client/route-modules/client/pages/index.tsx');
|
|
137
|
+
|
|
138
|
+
writeFile(
|
|
139
|
+
sourceFilepath,
|
|
140
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
141
|
+
|
|
142
|
+
const helper = 'must stay in the source module';
|
|
143
|
+
|
|
144
|
+
export default definePageRoute({
|
|
145
|
+
path: '/wrapped',
|
|
146
|
+
options: {},
|
|
147
|
+
data: null,
|
|
148
|
+
render: () => helper,
|
|
149
|
+
});
|
|
150
|
+
`,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
writeGeneratedRouteModule({
|
|
154
|
+
outputFilepath,
|
|
155
|
+
runtime: 'client',
|
|
156
|
+
side: 'client',
|
|
157
|
+
sourceFilepath,
|
|
158
|
+
clientRoute: { chunkId: 'client_pages_index' },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const generated = fs.readFileSync(outputFilepath, 'utf8');
|
|
162
|
+
|
|
163
|
+
assert.match(generated, /import __routeDefinition/);
|
|
164
|
+
assert.match(generated, /registerRouteDefinition/);
|
|
165
|
+
assert.doesNotMatch(generated, /must stay in the source module/);
|
|
166
|
+
assert.doesNotMatch(generated, /const \{ Router/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('route discovery includes explicit route definitions', () => {
|
|
170
|
+
const root = createTempDir();
|
|
171
|
+
const routeFilepath = path.join(root, 'client/pages/index.tsx');
|
|
172
|
+
const helperFilepath = path.join(root, 'client/pages/helpers.tsx');
|
|
173
|
+
|
|
174
|
+
writeFile(
|
|
175
|
+
routeFilepath,
|
|
176
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
177
|
+
|
|
178
|
+
export default definePageRoute({
|
|
179
|
+
path: '/',
|
|
180
|
+
options: {},
|
|
181
|
+
data: null,
|
|
182
|
+
render: () => null,
|
|
183
|
+
});
|
|
184
|
+
`,
|
|
185
|
+
);
|
|
186
|
+
writeFile(
|
|
187
|
+
helperFilepath,
|
|
188
|
+
`export const helper = true;
|
|
189
|
+
`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(findClientRouteFiles(path.join(root, 'client/pages')), [routeFilepath]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('route indexer rejects legacy contextual route imports and top-level router calls', () => {
|
|
196
|
+
const root = createTempDir();
|
|
197
|
+
const filepath = path.join(root, 'server/routes/legacy.ts');
|
|
198
|
+
|
|
199
|
+
writeFile(
|
|
200
|
+
filepath,
|
|
201
|
+
`import { Router } from '@app';
|
|
202
|
+
|
|
203
|
+
Router.get('/legacy', {}, async () => ({ ok: true }));
|
|
204
|
+
`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
assert.throws(
|
|
208
|
+
() => indexRouteDefinitions({ side: 'server', sourceFilepath: filepath }),
|
|
209
|
+
/imports @app/,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('route indexer rejects runtime metadata expressions', () => {
|
|
214
|
+
const root = createTempDir();
|
|
215
|
+
const filepath = path.join(root, 'client/pages/dynamic.tsx');
|
|
216
|
+
|
|
217
|
+
writeFile(
|
|
218
|
+
filepath,
|
|
219
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
220
|
+
import runtimeConfig from '../runtimeConfig';
|
|
221
|
+
|
|
222
|
+
export default definePageRoute({
|
|
223
|
+
path: runtimeConfig.path,
|
|
224
|
+
options: {},
|
|
225
|
+
data: null,
|
|
226
|
+
render: () => null,
|
|
227
|
+
});
|
|
228
|
+
`,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
assert.throws(
|
|
232
|
+
() => indexRouteDefinitions({ side: 'client', sourceFilepath: filepath }),
|
|
233
|
+
/definePageRoute path must be a serializable static literal or const-only expression/,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('server route factories keep app references inside runtime registration', () => {
|
|
238
|
+
const root = createTempDir();
|
|
239
|
+
const filepath = path.join(root, 'server/routes/api.ts');
|
|
240
|
+
|
|
241
|
+
writeFile(
|
|
242
|
+
filepath,
|
|
243
|
+
`import { defineServerRoute, defineServerRoutes } from '@common/router/definitions';
|
|
244
|
+
|
|
245
|
+
const base = '/api';
|
|
246
|
+
|
|
247
|
+
export default defineServerRoutes(({ Users }) => [
|
|
248
|
+
defineServerRoute({
|
|
249
|
+
method: 'GET',
|
|
250
|
+
path: base + '/users',
|
|
251
|
+
options: {},
|
|
252
|
+
handler: async () => Users.list(),
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const [definition] = indexRouteDefinitions({ side: 'server', sourceFilepath: filepath });
|
|
259
|
+
|
|
260
|
+
assert.equal(definition.methodName, 'get');
|
|
261
|
+
assert.equal(definition.path, '/api/users');
|
|
262
|
+
assert.equal(definition.targetResolution, 'static-expression');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('route definitions register through explicit router registrar', () => {
|
|
266
|
+
const calls = [];
|
|
267
|
+
|
|
268
|
+
registerRouteDefinition(
|
|
269
|
+
{
|
|
270
|
+
registerRouteDefinition: (definition, metadata) => {
|
|
271
|
+
calls.push({ definition, metadata });
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{ kind: 'server', method: 'GET', path: '/ready', options: {}, handler: () => true },
|
|
275
|
+
{ id: 'route-ready' },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
assert.equal(calls.length, 1);
|
|
279
|
+
assert.equal(calls[0].definition.path, '/ready');
|
|
280
|
+
assert.equal(calls[0].metadata.id, 'route-ready');
|
|
281
|
+
assert.throws(
|
|
282
|
+
() =>
|
|
283
|
+
registerRouteDefinition(
|
|
284
|
+
{ get: () => true },
|
|
285
|
+
{ kind: 'server', method: 'GET', path: '/ready', options: {}, handler: () => true },
|
|
286
|
+
),
|
|
287
|
+
/registerRouteDefinition/,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('expressHandler preserves next-based middleware behavior', async () => {
|
|
292
|
+
const response = new EventEmitter();
|
|
293
|
+
let called = false;
|
|
294
|
+
|
|
295
|
+
const handler = expressHandler((request, _response, next, context) => {
|
|
296
|
+
assert.equal(request.url, '/health');
|
|
297
|
+
assert.equal(context.marker, 'ctx');
|
|
298
|
+
called = true;
|
|
299
|
+
next();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await handler({
|
|
303
|
+
marker: 'ctx',
|
|
304
|
+
request: {
|
|
305
|
+
req: { url: '/health' },
|
|
306
|
+
res: response,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
assert.equal(called, true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('controller definitions parse input through explicit actions', () => {
|
|
314
|
+
const controller = defineController({
|
|
315
|
+
path: 'Example',
|
|
316
|
+
actions: {
|
|
317
|
+
Save: defineAction({
|
|
318
|
+
input: schema.object({ name: schema.string() }),
|
|
319
|
+
handler: ({ input }) => ({ greeting: `Hello ${input.name}` }),
|
|
320
|
+
}),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = runControllerAction(controller.actions.Save, {
|
|
325
|
+
app: {},
|
|
326
|
+
request: { data: { name: 'Proteum' } },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
assert.deepEqual(result, { greeting: 'Hello Proteum' });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('service model accessor prefers explicit Models service without recursing through inherited app getter', () => {
|
|
333
|
+
class ModelConsumer extends Service {}
|
|
334
|
+
|
|
335
|
+
const app = Object.create(Service.prototype);
|
|
336
|
+
app.app = app;
|
|
337
|
+
app.Models = { client: { model: true } };
|
|
338
|
+
|
|
339
|
+
const service = new ModelConsumer(app, {}, app);
|
|
340
|
+
|
|
341
|
+
assert.deepEqual(service.models, { model: true });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('controller action context reads Models service without recursing through inherited app getter', () => {
|
|
345
|
+
const app = Object.create(Service.prototype);
|
|
346
|
+
app.app = app;
|
|
347
|
+
app.Models = { client: { model: true } };
|
|
348
|
+
|
|
349
|
+
const controller = defineController({
|
|
350
|
+
path: 'Example',
|
|
351
|
+
actions: {
|
|
352
|
+
ReadModels: defineAction({
|
|
353
|
+
handler: ({ models }) => models,
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const result = runControllerAction(controller.actions.ReadModels, {
|
|
359
|
+
app,
|
|
360
|
+
request: { data: {} },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
assert.deepEqual(result, { model: true });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('controller indexer rejects legacy controller classes', () => {
|
|
367
|
+
const root = createTempDir();
|
|
368
|
+
const controllersRoot = path.join(root, 'server/controllers');
|
|
369
|
+
const filepath = path.join(controllersRoot, 'Legacy.ts');
|
|
370
|
+
|
|
371
|
+
writeFile(
|
|
372
|
+
filepath,
|
|
373
|
+
`import Controller from '@server/app/controller';
|
|
374
|
+
|
|
375
|
+
export default class Legacy extends Controller {
|
|
376
|
+
public Test() {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
`,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
assert.throws(
|
|
384
|
+
() => indexControllers([{ importPrefix: '@/server/controllers/', root: controllersRoot }]),
|
|
385
|
+
/legacy controller class/,
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('service artifact parser reads defineApplication router factories', () => {
|
|
390
|
+
const root = createTempDir();
|
|
391
|
+
const filepath = path.join(root, 'server/index.ts');
|
|
392
|
+
const { parseAppBootstrapSource } = loadServiceArtifactsForAppRoot(root);
|
|
393
|
+
|
|
394
|
+
writeFile(
|
|
395
|
+
filepath,
|
|
396
|
+
`import { defineApplication } from '@server/app';
|
|
397
|
+
import Router from '@server/services/router';
|
|
398
|
+
import SchemaRouter from '@server/services/schema/router';
|
|
399
|
+
|
|
400
|
+
export default defineApplication({
|
|
401
|
+
router: (app) => new Router(
|
|
402
|
+
app,
|
|
403
|
+
{
|
|
404
|
+
plugins: {
|
|
405
|
+
schema: new SchemaRouter({}, app),
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
app,
|
|
409
|
+
),
|
|
410
|
+
});
|
|
411
|
+
`,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const bootstrap = parseAppBootstrapSource(filepath);
|
|
415
|
+
|
|
416
|
+
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Router']);
|
|
417
|
+
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('service artifact parser reads named defineApplication router factories', () => {
|
|
421
|
+
const root = createTempDir();
|
|
422
|
+
const filepath = path.join(root, 'server/index.ts');
|
|
423
|
+
const { parseAppBootstrapSource } = loadServiceArtifactsForAppRoot(root);
|
|
424
|
+
|
|
425
|
+
writeFile(
|
|
426
|
+
filepath,
|
|
427
|
+
`import { defineApplication } from '@server/app';
|
|
428
|
+
import Router from '@server/services/router';
|
|
429
|
+
import SchemaRouter from '@server/services/schema/router';
|
|
430
|
+
|
|
431
|
+
const createRouter = (app) => new Router(
|
|
432
|
+
app,
|
|
433
|
+
{
|
|
434
|
+
plugins: {
|
|
435
|
+
schema: new SchemaRouter({}, app),
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
app,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const App = defineApplication({
|
|
442
|
+
router: createRouter,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
export default App;
|
|
446
|
+
`,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const bootstrap = parseAppBootstrapSource(filepath);
|
|
450
|
+
|
|
451
|
+
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Router']);
|
|
452
|
+
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
453
|
+
});
|
|
@@ -370,24 +370,26 @@ ${cacheConfigSource}
|
|
|
370
370
|
);
|
|
371
371
|
writeFile(
|
|
372
372
|
path.join(appRoot, 'server', 'index.ts'),
|
|
373
|
-
`import {
|
|
373
|
+
`import { defineApplication } from '@server/app';
|
|
374
374
|
import Router from '@server/services/router';
|
|
375
375
|
import SchemaRouter from '@server/services/schema/router';
|
|
376
376
|
|
|
377
377
|
import * as appConfig from '@/server/config/app';
|
|
378
378
|
|
|
379
|
-
export default
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
export default defineApplication({
|
|
380
|
+
services: (app) => ({
|
|
381
|
+
Router: new Router(
|
|
382
|
+
app,
|
|
383
|
+
{
|
|
384
|
+
...appConfig.routerBaseConfig,
|
|
385
|
+
plugins: {
|
|
386
|
+
schema: new SchemaRouter({}, app),
|
|
387
|
+
},
|
|
386
388
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
}
|
|
389
|
+
app,
|
|
390
|
+
),
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
391
393
|
`,
|
|
392
394
|
);
|
|
393
395
|
writeFile(
|
|
@@ -411,48 +413,48 @@ export default class TranspileWatchClient extends ClientApplication {
|
|
|
411
413
|
);
|
|
412
414
|
writeFile(
|
|
413
415
|
path.join(appRoot, 'client', 'pages', 'index.tsx'),
|
|
414
|
-
`import
|
|
416
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
415
417
|
import { SharedMarker } from '@test/shared';
|
|
416
418
|
|
|
417
|
-
|
|
418
|
-
'/',
|
|
419
|
-
{
|
|
419
|
+
export default definePageRoute({
|
|
420
|
+
path: '/',
|
|
421
|
+
options: {
|
|
420
422
|
auth: false,
|
|
421
423
|
layout: false,
|
|
422
424
|
},
|
|
423
|
-
null,
|
|
424
|
-
() => {
|
|
425
|
+
data: null,
|
|
426
|
+
render: () => {
|
|
425
427
|
return (
|
|
426
428
|
<main>
|
|
427
429
|
<SharedMarker />
|
|
428
430
|
</main>
|
|
429
431
|
);
|
|
430
432
|
},
|
|
431
|
-
);
|
|
433
|
+
});
|
|
432
434
|
`,
|
|
433
435
|
);
|
|
434
436
|
if (options.staticPage) {
|
|
435
437
|
writeFile(
|
|
436
438
|
path.join(appRoot, 'client', 'pages', 'static-cache.tsx'),
|
|
437
|
-
`import
|
|
439
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
438
440
|
import { SharedMarker } from '@test/shared';
|
|
439
441
|
|
|
440
|
-
|
|
441
|
-
'/static-cache',
|
|
442
|
-
{
|
|
442
|
+
export default definePageRoute({
|
|
443
|
+
path: '/static-cache',
|
|
444
|
+
options: {
|
|
443
445
|
auth: false,
|
|
444
446
|
layout: false,
|
|
445
447
|
static: { urls: ['/static-cache'] },
|
|
446
448
|
},
|
|
447
|
-
null,
|
|
448
|
-
() => {
|
|
449
|
+
data: null,
|
|
450
|
+
render: () => {
|
|
449
451
|
return (
|
|
450
452
|
<main>
|
|
451
453
|
<SharedMarker />
|
|
452
454
|
</main>
|
|
453
455
|
);
|
|
454
456
|
},
|
|
455
|
-
);
|
|
457
|
+
});
|
|
456
458
|
`,
|
|
457
459
|
);
|
|
458
460
|
}
|
|
@@ -539,8 +541,13 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
539
541
|
|
|
540
542
|
try {
|
|
541
543
|
await waitForSessionReady(sessionFile, child, getOutput);
|
|
544
|
+
await request(port, '/', { Accept: 'text/html' });
|
|
542
545
|
|
|
543
|
-
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
546
|
+
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch(
|
|
547
|
+
(error) => {
|
|
548
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
549
|
+
},
|
|
550
|
+
);
|
|
544
551
|
const initialScriptContent = fs.readFileSync(initialScriptAsset, 'utf8');
|
|
545
552
|
const scriptReloadStream = await connectToReloadStream(port + 1);
|
|
546
553
|
|
|
@@ -608,7 +615,9 @@ test('proteum dev applies router HTTP cache config to HTML and public assets', {
|
|
|
608
615
|
const staticResponse = await waitForHeader(port, '/static-cache', 'cache-control', staticHtmlCacheControl);
|
|
609
616
|
assert.equal(staticResponse.headers.get('surrogate-control'), 'static-surrogate');
|
|
610
617
|
|
|
611
|
-
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
618
|
+
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch((error) => {
|
|
619
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
620
|
+
});
|
|
612
621
|
const { response: assetResponse } = await request(port, toPublicAssetUrl(appRoot, asset));
|
|
613
622
|
|
|
614
623
|
assert.equal(assetResponse.headers.get('cache-control'), publicAssetCacheControl);
|
|
@@ -11,6 +11,17 @@ const lint = (code) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const swallowedErrorRuleId = 'proteum/no-swallowed-caught-error';
|
|
14
|
+
const noAppImportRuleId = 'proteum/no-app-import';
|
|
15
|
+
|
|
16
|
+
test('proteum lint rejects contextual @app imports', () => {
|
|
17
|
+
const messages = lint(`
|
|
18
|
+
import { Router } from '@app';
|
|
19
|
+
|
|
20
|
+
export const route = Router;
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
assert.equal(messages.some((message) => message.ruleId === noAppImportRuleId), true);
|
|
24
|
+
});
|
|
14
25
|
|
|
15
26
|
test('proteum lint rejects empty catch blocks', () => {
|
|
16
27
|
const messages = lint(`
|
|
@@ -50,6 +61,32 @@ test('proteum lint rejects generic catch feedback that drops original error deta
|
|
|
50
61
|
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
51
62
|
});
|
|
52
63
|
|
|
64
|
+
test('proteum lint rejects console calls as caught error preservation', () => {
|
|
65
|
+
for (const method of ['error', 'warn']) {
|
|
66
|
+
const messages = lint(`
|
|
67
|
+
export const run = async () => {
|
|
68
|
+
try {
|
|
69
|
+
await Investor.api.getDashboard();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.${method}(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('proteum lint rejects direct console promise catch handlers', () => {
|
|
81
|
+
const messages = lint(`
|
|
82
|
+
export const run = () => {
|
|
83
|
+
Investor.api.ensureApiKey().catch(console.log);
|
|
84
|
+
};
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
88
|
+
});
|
|
89
|
+
|
|
53
90
|
test('proteum lint allows rethrowing the caught error', () => {
|
|
54
91
|
const messages = lint(`
|
|
55
92
|
export const run = async () => {
|
|
@@ -99,8 +136,9 @@ test('proteum lint allows surfacing a message derived from the caught error', ()
|
|
|
99
136
|
test('proteum lint allows routing promise failures to app error handling', () => {
|
|
100
137
|
const messages = lint(`
|
|
101
138
|
export const run = () => {
|
|
139
|
+
const context = useContext();
|
|
102
140
|
Investor.api.ensureApiKey().catch((error) => {
|
|
103
|
-
|
|
141
|
+
context.app.handleError(error);
|
|
104
142
|
});
|
|
105
143
|
};
|
|
106
144
|
`);
|