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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict');
|
|
2
|
+
const crypto = require('node:crypto');
|
|
2
3
|
const { spawn, spawnSync } = require('node:child_process');
|
|
3
4
|
const fs = require('node:fs');
|
|
4
5
|
const http = require('node:http');
|
|
@@ -55,6 +56,42 @@ const createProteumApp = (appRoot, { routerPort = 3020 } = {}) => {
|
|
|
55
56
|
);
|
|
56
57
|
};
|
|
57
58
|
|
|
59
|
+
const hashFile = (filepath) => crypto.createHash('sha256').update(fs.readFileSync(filepath)).digest('hex');
|
|
60
|
+
|
|
61
|
+
const writeFreshWorktreeBootstrapMarker = (appRoot) => {
|
|
62
|
+
const timestamp = new Date().toISOString();
|
|
63
|
+
fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
|
|
64
|
+
writeFile(path.join(appRoot, '.env'), 'PORT=3020\n');
|
|
65
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
66
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# Agents\n');
|
|
67
|
+
|
|
68
|
+
writeFile(
|
|
69
|
+
path.join(appRoot, '.proteum', 'worktree-bootstrap.json'),
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
version: 1,
|
|
73
|
+
createdAt: timestamp,
|
|
74
|
+
updatedAt: timestamp,
|
|
75
|
+
proteumVersion: require('../package.json').version,
|
|
76
|
+
packageLockHash: hashFile(path.join(appRoot, 'package-lock.json')),
|
|
77
|
+
proteumConfigHash: hashFile(path.join(appRoot, 'proteum.config.ts')),
|
|
78
|
+
agentsHash: hashFile(path.join(appRoot, 'AGENTS.md')),
|
|
79
|
+
env: { present: true, copied: false },
|
|
80
|
+
refresh: { status: 'ok', ranAt: timestamp },
|
|
81
|
+
dependencies: {
|
|
82
|
+
status: 'up-to-date',
|
|
83
|
+
ranAt: timestamp,
|
|
84
|
+
nodeModulesPresent: true,
|
|
85
|
+
packageLockHash: hashFile(path.join(appRoot, 'package-lock.json')),
|
|
86
|
+
},
|
|
87
|
+
runtimeStatus: { status: 'ok', checkedAt: timestamp, summary: 'runtime ok' },
|
|
88
|
+
},
|
|
89
|
+
null,
|
|
90
|
+
2,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
58
95
|
const listen = async (server, port = 0) =>
|
|
59
96
|
await new Promise((resolve, reject) => {
|
|
60
97
|
server.once('error', reject);
|
|
@@ -150,6 +187,7 @@ test('top-level help lists the machine-scope mcp router', () => {
|
|
|
150
187
|
|
|
151
188
|
assert.equal(result.status, 0);
|
|
152
189
|
assert.match(result.stdout, /proteum mcp\b/);
|
|
190
|
+
assert.match(result.stdout, /proteum worktree\b/);
|
|
153
191
|
assert.match(result.stdout, /machine-scope MCP router/);
|
|
154
192
|
});
|
|
155
193
|
|
|
@@ -287,6 +325,52 @@ test('runtime status manifest guard points to explain manifest', () => {
|
|
|
287
325
|
assert.match(payload.nextActions[0].command, /proteum explain --manifest/);
|
|
288
326
|
});
|
|
289
327
|
|
|
328
|
+
test('runtime status blocks unbootstrapped Codex worktrees', () => {
|
|
329
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-worktree-block-'));
|
|
330
|
+
const appRoot = path.join(root, '.codex', 'worktrees', 'product');
|
|
331
|
+
createProteumApp(appRoot);
|
|
332
|
+
|
|
333
|
+
const result = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
|
|
334
|
+
cwd: appRoot,
|
|
335
|
+
encoding: 'utf8',
|
|
336
|
+
});
|
|
337
|
+
const payload = JSON.parse(result.stdout);
|
|
338
|
+
|
|
339
|
+
assert.equal(result.status, 1);
|
|
340
|
+
assert.equal(payload.ok, false);
|
|
341
|
+
assert.match(payload.summary, /has not completed Proteum worktree bootstrap/);
|
|
342
|
+
assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('runtime status allows fresh Codex worktree bootstrap markers and reports stale refresh action', () => {
|
|
346
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-worktree-fresh-'));
|
|
347
|
+
const appRoot = path.join(root, '.codex', 'worktrees', 'product');
|
|
348
|
+
createProteumApp(appRoot);
|
|
349
|
+
writeFreshWorktreeBootstrapMarker(appRoot);
|
|
350
|
+
|
|
351
|
+
const fresh = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
|
|
352
|
+
cwd: appRoot,
|
|
353
|
+
encoding: 'utf8',
|
|
354
|
+
});
|
|
355
|
+
const freshPayload = JSON.parse(fresh.stdout);
|
|
356
|
+
|
|
357
|
+
assert.equal(fresh.status, 0);
|
|
358
|
+
assert.equal(freshPayload.ok, true);
|
|
359
|
+
assert.equal(freshPayload.data.worktreeBootstrap.state, 'fresh');
|
|
360
|
+
|
|
361
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3,"changed":true}\n');
|
|
362
|
+
|
|
363
|
+
const stale = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
|
|
364
|
+
cwd: appRoot,
|
|
365
|
+
encoding: 'utf8',
|
|
366
|
+
});
|
|
367
|
+
const stalePayload = JSON.parse(stale.stdout);
|
|
368
|
+
|
|
369
|
+
assert.equal(stale.status, 1);
|
|
370
|
+
assert.equal(stalePayload.ok, false);
|
|
371
|
+
assert.match(stalePayload.nextActions[0].command, /--refresh/);
|
|
372
|
+
});
|
|
373
|
+
|
|
290
374
|
test('runtime status reports occupied configured port without probing page bodies', async () => {
|
|
291
375
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-port-'));
|
|
292
376
|
const otherRoot = path.join(repoRoot, 'apps', 'other');
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
const previousLessExtension = require.extensions['.less'];
|
|
15
|
+
require.extensions['.less'] = () => {};
|
|
16
|
+
|
|
17
|
+
const clientContextStub = () => ({ side: 'client' });
|
|
18
|
+
clientContextStub.default = clientContextStub;
|
|
19
|
+
const originalLoad = Module._load;
|
|
20
|
+
Module._load = function load(request, parent, isMain) {
|
|
21
|
+
if (request === '@/client/context') return clientContextStub;
|
|
22
|
+
|
|
23
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const previousWindow = global.window;
|
|
27
|
+
const previousDocument = global.document;
|
|
28
|
+
global.window = {
|
|
29
|
+
dev: false,
|
|
30
|
+
addEventListener: () => {},
|
|
31
|
+
removeEventListener: () => {},
|
|
32
|
+
history: {
|
|
33
|
+
state: {},
|
|
34
|
+
pushState: () => {},
|
|
35
|
+
replaceState: () => {},
|
|
36
|
+
},
|
|
37
|
+
location: {
|
|
38
|
+
hash: '',
|
|
39
|
+
pathname: '/',
|
|
40
|
+
search: '',
|
|
41
|
+
assign: () => {},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
global.document = { defaultView: global.window };
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
default: Application,
|
|
48
|
+
getClientErrorMessage,
|
|
49
|
+
normalizeClientError,
|
|
50
|
+
} = require('../client/app/index.ts');
|
|
51
|
+
|
|
52
|
+
class TestApplication extends Application {
|
|
53
|
+
boot() {}
|
|
54
|
+
handleUpdate() {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('client error message normalization accepts unknown caught values', () => {
|
|
58
|
+
assert.equal(getClientErrorMessage(new Error('boom'), 'fallback'), 'boom');
|
|
59
|
+
assert.equal(getClientErrorMessage('string failure', 'fallback'), 'string failure');
|
|
60
|
+
assert.equal(getClientErrorMessage({ message: 'object failure' }, 'fallback'), 'object failure');
|
|
61
|
+
assert.equal(getClientErrorMessage({ code: 'UNKNOWN' }, 'fallback'), 'fallback');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('client error normalization returns an Error instance', () => {
|
|
65
|
+
const existing = new Error('existing');
|
|
66
|
+
|
|
67
|
+
assert.equal(normalizeClientError(existing), existing);
|
|
68
|
+
assert.equal(normalizeClientError('string failure').message, 'string failure');
|
|
69
|
+
assert.equal(normalizeClientError({ code: 'UNKNOWN' }, 'fallback').message, 'fallback');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('client app handleError logs and returns displayable message', () => {
|
|
73
|
+
const app = new TestApplication();
|
|
74
|
+
const logged = [];
|
|
75
|
+
const originalConsoleError = console.error;
|
|
76
|
+
console.error = (error) => {
|
|
77
|
+
logged.push(error);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const message = app.handleError({ code: 'UNKNOWN' }, 'Unable to finish action.');
|
|
82
|
+
|
|
83
|
+
assert.equal(message, 'Unable to finish action.');
|
|
84
|
+
assert.equal(logged.length, 1);
|
|
85
|
+
assert.equal(logged[0] instanceof Error, true);
|
|
86
|
+
assert.equal(logged[0].message, 'Unable to finish action.');
|
|
87
|
+
} finally {
|
|
88
|
+
console.error = originalConsoleError;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
Module._load = originalLoad;
|
|
94
|
+
if (previousLessExtension === undefined) delete require.extensions['.less'];
|
|
95
|
+
else require.extensions['.less'] = previousLessExtension;
|
|
96
|
+
if (previousWindow === undefined) delete global.window;
|
|
97
|
+
else global.window = previousWindow;
|
|
98
|
+
if (previousDocument === undefined) delete global.document;
|
|
99
|
+
else global.document = previousDocument;
|
|
100
|
+
});
|
|
@@ -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
|
+
});
|