proteum 2.1.0-2 → 2.1.0-3
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 +51 -93
- package/README.md +44 -1
- package/agents/framework/AGENTS.md +155 -788
- package/agents/project/AGENTS.md +81 -110
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/commands/dev.ts +1 -0
- package/cli/commands/trace.ts +210 -0
- package/cli/compiler/client/index.ts +30 -8
- package/cli/compiler/server/index.ts +28 -6
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +23 -1
- package/cli/presentation/devSession.ts +5 -0
- package/cli/runtime/commands.ts +31 -0
- package/common/dev/requestTrace.ts +81 -0
- package/docs/request-tracing.md +115 -0
- package/package.json +1 -1
- package/server/app/container/config.ts +15 -0
- package/server/app/container/index.ts +3 -0
- package/server/app/container/trace/index.ts +284 -0
- package/server/services/prisma/index.ts +61 -5
- package/server/services/router/http/index.ts +40 -0
- package/server/services/router/index.ts +159 -6
- package/server/services/router/response/index.ts +80 -7
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- package/server/services/router/http/session.ts.old +0 -40
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# E2E Tests
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
3
|
+
This file adds test-area local rules on top of the canonical framework contract:
|
|
4
|
+
|
|
5
|
+
- framework repo: `agents/framework/AGENTS.md`
|
|
6
|
+
- installed app: `./node_modules/proteum/agents/framework/AGENTS.md`
|
|
7
|
+
|
|
8
|
+
- Understand the real user flow and the main feature branches before writing tests.
|
|
9
|
+
- Test the current controller/page runtime model, not legacy `@Route` or `api.fetch` behavior.
|
|
10
|
+
- Locate elements with `data-testid`.
|
|
11
|
+
- Add `data-testid` where needed instead of relying on brittle selectors.
|
|
12
|
+
- Reuse root catalog files from `/client/catalogs/**`, `/server/catalogs/**`, or `/common/catalogs/**` instead of duplicating catalog constants in tests.
|
package/cli/commands/dev.ts
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import got from 'got';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'yaml';
|
|
5
|
+
import { UsageError } from 'clipanion';
|
|
6
|
+
|
|
7
|
+
import cli from '..';
|
|
8
|
+
import type {
|
|
9
|
+
TRequestTrace,
|
|
10
|
+
TRequestTraceArmResponse,
|
|
11
|
+
TRequestTraceErrorResponse,
|
|
12
|
+
TRequestTraceListItem,
|
|
13
|
+
TRequestTraceListResponse,
|
|
14
|
+
TRequestTraceResponse,
|
|
15
|
+
} from '@common/dev/requestTrace';
|
|
16
|
+
|
|
17
|
+
type TTraceAction = 'latest' | 'show' | 'requests' | 'arm' | 'export';
|
|
18
|
+
|
|
19
|
+
const allowedActions = new Set<TTraceAction>(['latest', 'show', 'requests', 'arm', 'export']);
|
|
20
|
+
|
|
21
|
+
class TraceResponseError extends UsageError {}
|
|
22
|
+
|
|
23
|
+
const getAction = () => {
|
|
24
|
+
const action = typeof cli.args.action === 'string' && cli.args.action ? cli.args.action : 'latest';
|
|
25
|
+
if (!allowedActions.has(action as TTraceAction)) {
|
|
26
|
+
throw new UsageError(`Unsupported trace action "${action}". Expected one of: ${[...allowedActions].join(', ')}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return action as TTraceAction;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
33
|
+
|
|
34
|
+
const getRouterPort = () => {
|
|
35
|
+
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
36
|
+
if (overridePort) return overridePort;
|
|
37
|
+
|
|
38
|
+
const envFilepath = path.join(cli.args.workdir as string, 'env.yaml');
|
|
39
|
+
if (!fs.existsSync(envFilepath)) {
|
|
40
|
+
throw new UsageError(`Could not find env.yaml in ${cli.args.workdir as string}. Pass --port or --url explicitly.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const envFile = yaml.parse(fs.readFileSync(envFilepath, 'utf8')) as { router?: { port?: number } };
|
|
44
|
+
const port = envFile.router?.port;
|
|
45
|
+
if (!port) {
|
|
46
|
+
throw new UsageError(`Could not determine the router port from ${envFilepath}. Pass --port or --url explicitly.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return String(port);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getRouterBaseUrls = () => {
|
|
53
|
+
const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
|
|
54
|
+
if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
|
|
55
|
+
|
|
56
|
+
const port = getRouterPort();
|
|
57
|
+
return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getTraceErrorMessage = (body: TRequestTraceErrorResponse | object | string | undefined, statusCode: number) => {
|
|
61
|
+
if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
|
|
62
|
+
return body.error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `Trace request failed with status ${statusCode}.`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
69
|
+
const attempts: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const baseUrl of getRouterBaseUrls()) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await got(`${baseUrl}${pathname}`, {
|
|
74
|
+
method: options?.method || 'GET',
|
|
75
|
+
json: options?.json,
|
|
76
|
+
responseType: 'json',
|
|
77
|
+
throwHttpErrors: false,
|
|
78
|
+
retry: { limit: 0 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (response.statusCode >= 400) {
|
|
82
|
+
throw new TraceResponseError(
|
|
83
|
+
getTraceErrorMessage(response.body as TRequestTraceErrorResponse | object | string | undefined, response.statusCode),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return response.body as TResponse;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof TraceResponseError) throw error;
|
|
90
|
+
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
attempts.push(`${baseUrl}${pathname}: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new UsageError(
|
|
97
|
+
[
|
|
98
|
+
'Could not reach the Proteum trace server.',
|
|
99
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
100
|
+
'Make sure the app is running with `proteum dev`, or pass `--url http://host:port` if it is bound elsewhere.',
|
|
101
|
+
].join('\n'),
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const renderTraceSummary = (request: TRequestTraceListItem) =>
|
|
106
|
+
[
|
|
107
|
+
`${request.id} ${request.method} ${request.path}`,
|
|
108
|
+
`status=${request.statusCode ?? 'pending'}`,
|
|
109
|
+
`capture=${request.capture}`,
|
|
110
|
+
`events=${request.eventCount}`,
|
|
111
|
+
request.user ? `user=${request.user}` : '',
|
|
112
|
+
request.errorMessage ? `error=${request.errorMessage}` : '',
|
|
113
|
+
]
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join(' | ');
|
|
116
|
+
|
|
117
|
+
const renderTrace = (request: TRequestTrace) =>
|
|
118
|
+
[
|
|
119
|
+
`Request ${request.id}`,
|
|
120
|
+
`- ${request.method} ${request.path} status=${request.statusCode ?? 'pending'} capture=${request.capture}`,
|
|
121
|
+
`- started=${request.startedAt} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} dropped=${request.droppedEvents}`,
|
|
122
|
+
...(request.user ? [`- user=${request.user}`] : []),
|
|
123
|
+
...(request.persistedFilepath ? [`- persisted=${request.persistedFilepath}`] : []),
|
|
124
|
+
'Events',
|
|
125
|
+
...request.events.map(
|
|
126
|
+
(event) =>
|
|
127
|
+
`- [${event.elapsedMs}ms] ${event.type} ${Object.entries(event.details)
|
|
128
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
129
|
+
.join(' ')}`,
|
|
130
|
+
),
|
|
131
|
+
].join('\n');
|
|
132
|
+
|
|
133
|
+
const printJson = (value: object) => {
|
|
134
|
+
console.log(JSON.stringify(value, null, 2));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const run = async () => {
|
|
138
|
+
const action = getAction();
|
|
139
|
+
const requestId = typeof cli.args.id === 'string' ? cli.args.id : '';
|
|
140
|
+
const shouldPrintJson = cli.args.json === true;
|
|
141
|
+
|
|
142
|
+
if (action === 'requests') {
|
|
143
|
+
const response = await requestJson<TRequestTraceListResponse>('/__proteum/trace/requests');
|
|
144
|
+
if (shouldPrintJson) {
|
|
145
|
+
printJson(response);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(['Proteum trace', ...response.requests.map(renderTraceSummary)].join('\n'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (action === 'arm') {
|
|
154
|
+
const capture = typeof cli.args.capture === 'string' && cli.args.capture ? cli.args.capture : 'deep';
|
|
155
|
+
const response = await requestJson<TRequestTraceArmResponse>('/__proteum/trace/arm', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
json: { capture },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (shouldPrintJson) {
|
|
161
|
+
printJson(response);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`Armed next request trace with capture=${response.capture}.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (action === 'latest') {
|
|
170
|
+
const response = await requestJson<TRequestTraceResponse>('/__proteum/trace/latest');
|
|
171
|
+
if (shouldPrintJson) {
|
|
172
|
+
printJson(response);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(renderTrace(response.request));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!requestId) {
|
|
181
|
+
throw new UsageError(`Trace action "${action}" requires a request id.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const response = await requestJson<TRequestTraceResponse>(`/__proteum/trace/requests/${requestId}`);
|
|
185
|
+
|
|
186
|
+
if (action === 'show') {
|
|
187
|
+
if (shouldPrintJson) {
|
|
188
|
+
printJson(response);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(renderTrace(response.request));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const output =
|
|
197
|
+
typeof cli.args.output === 'string' && cli.args.output
|
|
198
|
+
? cli.args.output
|
|
199
|
+
: path.join(cli.args.workdir as string, 'var', 'traces', 'exports', `${response.request.id}.json`);
|
|
200
|
+
|
|
201
|
+
fs.ensureDirSync(path.dirname(output));
|
|
202
|
+
fs.writeJSONSync(output, response.request, { spaces: 2 });
|
|
203
|
+
|
|
204
|
+
if (shouldPrintJson) {
|
|
205
|
+
printJson({ output, request: response.request });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`Exported trace ${response.request.id} to ${output}`);
|
|
210
|
+
};
|
|
@@ -27,6 +27,24 @@ const hmrClientEntry = path.join(cli.paths.core.root, 'client', 'dev', 'hmr.ts')
|
|
|
27
27
|
const normalizeModulePath = (value?: string) => (value || '').replace(/\\/g, '/');
|
|
28
28
|
const resolveFromAppOrCore = (app: App, request: string) =>
|
|
29
29
|
require.resolve(request, { paths: [app.paths.root, cli.paths.core.root] });
|
|
30
|
+
const rewriteFrameworkAliasTargets = (app: App, aliases: Record<string, string | string[]>) => {
|
|
31
|
+
const installedCoreRoot = normalizeModulePath(path.join(app.paths.root, 'node_modules', 'proteum'));
|
|
32
|
+
const activeCoreRoot = normalizeModulePath(cli.paths.core.root);
|
|
33
|
+
|
|
34
|
+
if (installedCoreRoot === activeCoreRoot) return aliases;
|
|
35
|
+
|
|
36
|
+
const rewriteCandidate = (candidate: string) =>
|
|
37
|
+
normalizeModulePath(candidate).startsWith(installedCoreRoot + '/')
|
|
38
|
+
? activeCoreRoot + normalizeModulePath(candidate).substring(installedCoreRoot.length)
|
|
39
|
+
: candidate;
|
|
40
|
+
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.entries(aliases).map(([alias, value]) => [
|
|
43
|
+
alias,
|
|
44
|
+
Array.isArray(value) ? value.map(rewriteCandidate) : rewriteCandidate(value),
|
|
45
|
+
]),
|
|
46
|
+
);
|
|
47
|
+
};
|
|
30
48
|
|
|
31
49
|
const getModulePath = (module: Module) => {
|
|
32
50
|
const resource = typeof module.nameForCondition === 'function' ? module.nameForCondition() : undefined;
|
|
@@ -58,6 +76,8 @@ export default function createCompiler(
|
|
|
58
76
|
logVerbose(`Creating compiler for client (${mode}).`);
|
|
59
77
|
const dev = mode === 'dev';
|
|
60
78
|
const outputPath = app.outputPath(outputTarget);
|
|
79
|
+
const installedCoreRoot = path.join(app.paths.root, 'node_modules', 'proteum');
|
|
80
|
+
const frameworkRoots = [cli.paths.core.root, installedCoreRoot];
|
|
61
81
|
|
|
62
82
|
const commonConfig = createCommonConfig(app, 'client', mode, outputTarget);
|
|
63
83
|
|
|
@@ -73,11 +93,12 @@ export default function createCompiler(
|
|
|
73
93
|
|
|
74
94
|
// Convert tsconfig paths into bundler aliases.
|
|
75
95
|
const { aliases } = app.aliases.client.forWebpack({ modulesPath: app.paths.root + '/node_modules' });
|
|
96
|
+
const resolvedAliases = rewriteFrameworkAliasTargets(app, aliases);
|
|
76
97
|
|
|
77
98
|
// We're not supposed in any case to import server libs from client
|
|
78
|
-
delete
|
|
79
|
-
delete
|
|
80
|
-
const rspackAliases = toRspackAliases(
|
|
99
|
+
delete resolvedAliases['@server'];
|
|
100
|
+
delete resolvedAliases['@/server'];
|
|
101
|
+
const rspackAliases = toRspackAliases(resolvedAliases);
|
|
81
102
|
rspackAliases['@/client/router$'] = cli.paths.core.root + '/client/router.ts';
|
|
82
103
|
rspackAliases['preact/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
|
|
83
104
|
rspackAliases['react/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
|
|
@@ -133,16 +154,16 @@ export default function createCompiler(
|
|
|
133
154
|
test: ssrScriptPattern,
|
|
134
155
|
include: [
|
|
135
156
|
app.paths.root + '/client',
|
|
136
|
-
cli.paths.core.root + '/client',
|
|
137
157
|
app.paths.client.generated,
|
|
138
158
|
|
|
139
159
|
app.paths.root + '/common',
|
|
140
|
-
cli.paths.core.root + '/common',
|
|
141
160
|
app.paths.common.generated,
|
|
142
161
|
|
|
143
162
|
app.paths.root + '/server',
|
|
144
|
-
cli.paths.core.root + '/server',
|
|
145
163
|
app.paths.server.generated,
|
|
164
|
+
...frameworkRoots.map((rootPath) => rootPath + '/client'),
|
|
165
|
+
...frameworkRoots.map((rootPath) => rootPath + '/common'),
|
|
166
|
+
...frameworkRoots.map((rootPath) => rootPath + '/server'),
|
|
146
167
|
],
|
|
147
168
|
loader: path.join(
|
|
148
169
|
cli.paths.core.root,
|
|
@@ -157,17 +178,18 @@ export default function createCompiler(
|
|
|
157
178
|
test: regex.scripts,
|
|
158
179
|
include: [
|
|
159
180
|
app.paths.root + '/client',
|
|
160
|
-
cli.paths.core.root + '/client',
|
|
161
181
|
app.paths.client.generated,
|
|
162
182
|
|
|
163
183
|
app.paths.root + '/common',
|
|
164
|
-
cli.paths.core.root + '/common',
|
|
165
184
|
app.paths.common.generated,
|
|
166
185
|
|
|
167
186
|
// Prisma 7 generates browser-safe TypeScript entrypoints under var/prisma.
|
|
168
187
|
app.paths.root + '/var/prisma',
|
|
169
188
|
|
|
170
189
|
app.paths.server.generated + '/models.ts',
|
|
190
|
+
...frameworkRoots.map((rootPath) => rootPath + '/client'),
|
|
191
|
+
...frameworkRoots.map((rootPath) => rootPath + '/common'),
|
|
192
|
+
...frameworkRoots.map((rootPath) => rootPath + '/server'),
|
|
171
193
|
],
|
|
172
194
|
rules: require('../common/scripts')({ app, side: 'client', dev }),
|
|
173
195
|
},
|
|
@@ -49,6 +49,25 @@ const getDevGeneratedRuntimeEntries = (app: App) => ({
|
|
|
49
49
|
__proteum_dev_routes: [app.paths.server.generated + '/routes.ts'],
|
|
50
50
|
__proteum_dev_controllers: [app.paths.server.generated + '/controllers.ts'],
|
|
51
51
|
});
|
|
52
|
+
const normalizeModulePath = (value?: string) => (value || '').replace(/\\/g, '/');
|
|
53
|
+
const rewriteFrameworkAliasTargets = (app: App, aliases: Record<string, string | string[]>) => {
|
|
54
|
+
const installedCoreRoot = normalizeModulePath(app.paths.root + '/node_modules/proteum');
|
|
55
|
+
const activeCoreRoot = normalizeModulePath(cli.paths.core.root);
|
|
56
|
+
|
|
57
|
+
if (installedCoreRoot === activeCoreRoot) return aliases;
|
|
58
|
+
|
|
59
|
+
const rewriteCandidate = (candidate: string) =>
|
|
60
|
+
normalizeModulePath(candidate).startsWith(installedCoreRoot + '/')
|
|
61
|
+
? activeCoreRoot + normalizeModulePath(candidate).substring(installedCoreRoot.length)
|
|
62
|
+
: candidate;
|
|
63
|
+
|
|
64
|
+
return Object.fromEntries(
|
|
65
|
+
Object.entries(aliases).map(([alias, value]) => [
|
|
66
|
+
alias,
|
|
67
|
+
Array.isArray(value) ? value.map(rewriteCandidate) : rewriteCandidate(value),
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
};
|
|
52
71
|
|
|
53
72
|
/*----------------------------------
|
|
54
73
|
- CONFIG
|
|
@@ -61,14 +80,17 @@ export default function createCompiler(
|
|
|
61
80
|
debug && console.info(`Creating compiler for server (${mode}).`);
|
|
62
81
|
const dev = mode === 'dev';
|
|
63
82
|
const outputPath = app.outputPath(outputTarget);
|
|
83
|
+
const installedCoreRoot = app.paths.root + '/node_modules/proteum';
|
|
84
|
+
const frameworkRoots = [cli.paths.core.root, installedCoreRoot];
|
|
64
85
|
|
|
65
86
|
const commonConfig = createCommonConfig(app, 'server', mode, outputTarget);
|
|
66
87
|
const { aliases } = app.aliases.server.forWebpack({ modulesPath: app.paths.root + '/node_modules' });
|
|
88
|
+
const resolvedAliases = rewriteFrameworkAliasTargets(app, aliases);
|
|
67
89
|
|
|
68
90
|
// We're not supposed in any case to import client services from server
|
|
69
|
-
delete
|
|
70
|
-
delete
|
|
71
|
-
const rspackAliases = toRspackAliases(
|
|
91
|
+
delete resolvedAliases['@client/services'];
|
|
92
|
+
delete resolvedAliases['@/client/services'];
|
|
93
|
+
const rspackAliases = toRspackAliases(resolvedAliases);
|
|
72
94
|
rspackAliases['@/client/router$'] = cli.paths.core.root + '/client/router.ts';
|
|
73
95
|
|
|
74
96
|
debug &&
|
|
@@ -154,11 +176,9 @@ export default function createCompiler(
|
|
|
154
176
|
test: regex.scripts,
|
|
155
177
|
include: [
|
|
156
178
|
app.paths.root + '/client',
|
|
157
|
-
cli.paths.core.root + '/client',
|
|
158
179
|
app.paths.client.generated,
|
|
159
180
|
|
|
160
181
|
app.paths.root + '/common',
|
|
161
|
-
cli.paths.core.root + '/common',
|
|
162
182
|
app.paths.common.generated,
|
|
163
183
|
|
|
164
184
|
// Prisma 7 generates TypeScript entrypoints under var/prisma.
|
|
@@ -166,8 +186,10 @@ export default function createCompiler(
|
|
|
166
186
|
|
|
167
187
|
// Dossiers server uniquement pour le bundle server
|
|
168
188
|
app.paths.root + '/server',
|
|
169
|
-
cli.paths.core.root + '/server',
|
|
170
189
|
app.paths.server.generated,
|
|
190
|
+
...frameworkRoots.map((rootPath) => rootPath + '/client'),
|
|
191
|
+
...frameworkRoots.map((rootPath) => rootPath + '/common'),
|
|
192
|
+
...frameworkRoots.map((rootPath) => rootPath + '/server'),
|
|
171
193
|
|
|
172
194
|
// Complle 5HTP modules so they can refer to the framework instance and aliases
|
|
173
195
|
// Temp disabled because compile issue on vercel
|
package/cli/paths.ts
CHANGED
|
@@ -43,13 +43,28 @@ export const staticAssetName = /*isDebug ? '[name].[ext].[hash:8]' :*/ '[hash:8]
|
|
|
43
43
|
|
|
44
44
|
const pathInfosDefaultOpts = { shortenExtensions: ['ts', 'js', 'tsx', 'jsx'], trimIndex: true };
|
|
45
45
|
|
|
46
|
+
const safeRealpath = (filepath: string) => {
|
|
47
|
+
try {
|
|
48
|
+
return fs.realpathSync(filepath);
|
|
49
|
+
} catch {
|
|
50
|
+
return path.resolve(filepath);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
46
54
|
const resolveCoreRoot = (appRoot: string) => {
|
|
55
|
+
const currentPackageRoot = path.resolve(__dirname, '..');
|
|
56
|
+
const currentBin = path.join(currentPackageRoot, 'cli', 'bin.js');
|
|
57
|
+
const invokedScript = process.argv[1] ? safeRealpath(process.argv[1]) : '';
|
|
58
|
+
const invokedCurrentPackage = invokedScript === safeRealpath(currentBin);
|
|
59
|
+
|
|
60
|
+
if (invokedCurrentPackage) return currentPackageRoot;
|
|
61
|
+
|
|
47
62
|
const localInstall = path.join(appRoot, 'node_modules', 'proteum');
|
|
48
63
|
if (fs.existsSync(localInstall)) return localInstall;
|
|
49
64
|
|
|
50
65
|
// When running `npx`/global installs, there may be no local `node_modules/proteum` yet.
|
|
51
66
|
// Fall back to the installed package root (this file lives in `<root>/cli`).
|
|
52
|
-
return
|
|
67
|
+
return currentPackageRoot;
|
|
53
68
|
};
|
|
54
69
|
|
|
55
70
|
const normalizeImportPath = (value: string) => value.replace(/\\/g, '/');
|
|
@@ -13,6 +13,7 @@ export const proteumCommandNames = [
|
|
|
13
13
|
'check',
|
|
14
14
|
'doctor',
|
|
15
15
|
'explain',
|
|
16
|
+
'trace',
|
|
16
17
|
] as const;
|
|
17
18
|
|
|
18
19
|
export type TProteumCommandName = (typeof proteumCommandNames)[number];
|
|
@@ -45,7 +46,7 @@ export const proteumRecommendedFlow: TRow[] = [
|
|
|
45
46
|
export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
|
|
46
47
|
{ title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
|
|
47
48
|
{ title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
|
|
48
|
-
{ title: 'Manifest and contracts', names: ['doctor', 'explain'] },
|
|
49
|
+
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace'] },
|
|
49
50
|
{ title: 'Project scaffolding', names: ['init'] },
|
|
50
51
|
];
|
|
51
52
|
|
|
@@ -184,6 +185,27 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
184
185
|
notes: ['Legacy positional section selection remains supported, for example `proteum explain routes services`.'],
|
|
185
186
|
status: 'stable',
|
|
186
187
|
},
|
|
188
|
+
trace: {
|
|
189
|
+
name: 'trace',
|
|
190
|
+
category: 'Manifest and contracts',
|
|
191
|
+
summary: 'Inspect live in-memory request traces from a running Proteum dev server.',
|
|
192
|
+
usage: 'proteum trace [latest|show <requestId>|requests|arm|export <requestId>] [--port <port>|--url <baseUrl>] [--json]',
|
|
193
|
+
bestFor:
|
|
194
|
+
'Debugging route resolution, context creation, SSR payloads, renders, and runtime errors without attaching a debugger.',
|
|
195
|
+
examples: [
|
|
196
|
+
{ description: 'Show the latest request trace', command: 'proteum trace latest' },
|
|
197
|
+
{ description: 'List recent trace summaries', command: 'proteum trace requests' },
|
|
198
|
+
{ description: 'Arm the next request for deep capture', command: 'proteum trace arm --capture deep' },
|
|
199
|
+
{ description: 'Export a request trace to disk', command: 'proteum trace export <requestId>' },
|
|
200
|
+
{ description: 'Target a custom dev base URL directly', command: 'proteum trace latest --url http://127.0.0.1:3010' },
|
|
201
|
+
],
|
|
202
|
+
notes: [
|
|
203
|
+
'This command talks to the running app over the dev-only `__proteum/trace` HTTP endpoints.',
|
|
204
|
+
'Traces are stored in a bounded in-memory buffer with payload summarization and sensitive-field redaction.',
|
|
205
|
+
'Use `--port` when the app is not running on the router port declared in `env.yaml`, or `--url` when the host itself is non-standard.',
|
|
206
|
+
],
|
|
207
|
+
status: 'experimental',
|
|
208
|
+
},
|
|
187
209
|
};
|
|
188
210
|
|
|
189
211
|
export const isLikelyProteumAppRoot = (workdir: string) =>
|
|
@@ -43,6 +43,8 @@ export const renderDevSession = async ({
|
|
|
43
43
|
{ label: 'root', value: appRoot },
|
|
44
44
|
{ label: 'router', value: `http://localhost:${routerPort}` },
|
|
45
45
|
{ label: 'hmr', value: `http://localhost:${devEventPort}/__proteum_hmr` },
|
|
46
|
+
{ label: 'trace', value: `proteum trace latest --port ${routerPort}` },
|
|
47
|
+
{ label: 'trace deep', value: `proteum trace arm --capture deep --port ${routerPort}` },
|
|
46
48
|
{ label: 'hotkeys', value: 'Ctrl+R reload, Ctrl+C stop' },
|
|
47
49
|
],
|
|
48
50
|
{ minLabelWidth: 12, maxLabelWidth: 12 },
|
|
@@ -52,9 +54,11 @@ export const renderDevSession = async ({
|
|
|
52
54
|
export const renderServerReadyBanner = async ({
|
|
53
55
|
appName,
|
|
54
56
|
publicUrl,
|
|
57
|
+
routerPort,
|
|
55
58
|
}: {
|
|
56
59
|
appName: string;
|
|
57
60
|
publicUrl: string;
|
|
61
|
+
routerPort: number;
|
|
58
62
|
}) =>
|
|
59
63
|
renderInk(({ Box, Text }) => {
|
|
60
64
|
const createElement = React.createElement;
|
|
@@ -66,5 +70,6 @@ export const renderServerReadyBanner = async ({
|
|
|
66
70
|
createElement(Text, { bold: true, color: 'green' }, appName),
|
|
67
71
|
createElement(Text, { bold: true }, publicUrl),
|
|
68
72
|
createElement(Text, { dimColor: true }, 'SSR server is listening for requests and hot reloads.'),
|
|
73
|
+
createElement(Text, { dimColor: true }, `Trace latest: proteum trace latest --port ${routerPort}`),
|
|
69
74
|
);
|
|
70
75
|
});
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -185,6 +185,35 @@ class ExplainCommand extends ProteumCommand {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
class TraceCommand extends ProteumCommand {
|
|
189
|
+
public static paths = [['trace']];
|
|
190
|
+
|
|
191
|
+
public static usage = buildUsage('trace');
|
|
192
|
+
|
|
193
|
+
public port = Option.String('--port', { description: 'Override the router port used to query the running dev server.' });
|
|
194
|
+
public url = Option.String('--url', { description: 'Override the full base URL used to query the running dev server.' });
|
|
195
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
196
|
+
public capture = Option.String('--capture', { description: 'Capture mode used by `proteum trace arm`.' });
|
|
197
|
+
public output = Option.String('--output', { description: 'Output filepath used by `proteum trace export`.' });
|
|
198
|
+
public args = Option.Rest();
|
|
199
|
+
|
|
200
|
+
public async execute() {
|
|
201
|
+
const [action = 'latest', id = ''] = this.args;
|
|
202
|
+
|
|
203
|
+
this.setCliArgs({
|
|
204
|
+
action,
|
|
205
|
+
id,
|
|
206
|
+
port: this.port ?? '',
|
|
207
|
+
url: this.url ?? '',
|
|
208
|
+
json: this.json,
|
|
209
|
+
capture: this.capture ?? '',
|
|
210
|
+
output: this.output ?? '',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await runCommandModule(() => import('../commands/trace'));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
export const registeredCommands = {
|
|
189
218
|
init: InitCommand,
|
|
190
219
|
dev: DevCommand,
|
|
@@ -195,6 +224,7 @@ export const registeredCommands = {
|
|
|
195
224
|
check: CheckCommand,
|
|
196
225
|
doctor: DoctorCommand,
|
|
197
226
|
explain: ExplainCommand,
|
|
227
|
+
trace: TraceCommand,
|
|
198
228
|
} as const;
|
|
199
229
|
|
|
200
230
|
export const createCli = (version: string) => {
|
|
@@ -216,6 +246,7 @@ export const createCli = (version: string) => {
|
|
|
216
246
|
clipanion.register(CheckCommand);
|
|
217
247
|
clipanion.register(DoctorCommand);
|
|
218
248
|
clipanion.register(ExplainCommand);
|
|
249
|
+
clipanion.register(TraceCommand);
|
|
219
250
|
|
|
220
251
|
return clipanion;
|
|
221
252
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const traceCaptureModes = ['summary', 'resolve', 'deep'] as const;
|
|
2
|
+
|
|
3
|
+
export type TTraceCaptureMode = (typeof traceCaptureModes)[number];
|
|
4
|
+
|
|
5
|
+
export const traceEventTypes = [
|
|
6
|
+
'request.start',
|
|
7
|
+
'request.user',
|
|
8
|
+
'resolve.start',
|
|
9
|
+
'resolve.controller-route',
|
|
10
|
+
'resolve.routes-evaluated',
|
|
11
|
+
'resolve.route-skip',
|
|
12
|
+
'resolve.route-match',
|
|
13
|
+
'resolve.not-found',
|
|
14
|
+
'controller.start',
|
|
15
|
+
'controller.result',
|
|
16
|
+
'setup.options',
|
|
17
|
+
'context.create',
|
|
18
|
+
'page.data',
|
|
19
|
+
'ssr.payload',
|
|
20
|
+
'render.start',
|
|
21
|
+
'render.end',
|
|
22
|
+
'response.send',
|
|
23
|
+
'request.finish',
|
|
24
|
+
'error',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type TTraceEventType = (typeof traceEventTypes)[number];
|
|
28
|
+
|
|
29
|
+
export type TTraceSummaryValue =
|
|
30
|
+
| PrimitiveValue
|
|
31
|
+
| null
|
|
32
|
+
| { kind: 'undefined' }
|
|
33
|
+
| { kind: 'redacted'; reason: string }
|
|
34
|
+
| { kind: 'bigint'; value: string }
|
|
35
|
+
| { kind: 'symbol'; value: string }
|
|
36
|
+
| { kind: 'function'; name: string }
|
|
37
|
+
| { kind: 'date'; value: string }
|
|
38
|
+
| { kind: 'error'; name: string; message: string; stack?: string }
|
|
39
|
+
| { kind: 'buffer'; byteLength: number }
|
|
40
|
+
| { kind: 'array'; length: number; items: TTraceSummaryValue[]; truncated: boolean }
|
|
41
|
+
| {
|
|
42
|
+
kind: 'object';
|
|
43
|
+
constructorName: string;
|
|
44
|
+
keys: string[];
|
|
45
|
+
entries: { [key: string]: TTraceSummaryValue };
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
}
|
|
48
|
+
| { kind: 'map'; size: number }
|
|
49
|
+
| { kind: 'set'; size: number };
|
|
50
|
+
|
|
51
|
+
export type TTraceEvent = {
|
|
52
|
+
index: number;
|
|
53
|
+
at: string;
|
|
54
|
+
elapsedMs: number;
|
|
55
|
+
type: TTraceEventType;
|
|
56
|
+
details: { [key: string]: TTraceSummaryValue };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TRequestTrace = {
|
|
60
|
+
id: string;
|
|
61
|
+
method: string;
|
|
62
|
+
path: string;
|
|
63
|
+
url: string;
|
|
64
|
+
capture: TTraceCaptureMode;
|
|
65
|
+
startedAt: string;
|
|
66
|
+
finishedAt?: string;
|
|
67
|
+
durationMs?: number;
|
|
68
|
+
statusCode?: number;
|
|
69
|
+
user?: string;
|
|
70
|
+
droppedEvents: number;
|
|
71
|
+
persistedFilepath?: string;
|
|
72
|
+
errorMessage?: string;
|
|
73
|
+
events: TTraceEvent[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type TRequestTraceListItem = Omit<TRequestTrace, 'events'> & { eventCount: number };
|
|
77
|
+
|
|
78
|
+
export type TRequestTraceListResponse = { requests: TRequestTraceListItem[] };
|
|
79
|
+
export type TRequestTraceResponse = { request: TRequestTrace };
|
|
80
|
+
export type TRequestTraceArmResponse = { armed: true; capture: TTraceCaptureMode };
|
|
81
|
+
export type TRequestTraceErrorResponse = { error: string };
|