proteum 2.1.0-5 → 2.1.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.
Files changed (56) hide show
  1. package/AGENTS.md +37 -49
  2. package/README.md +52 -1
  3. package/agents/framework/AGENTS.md +104 -236
  4. package/agents/project/AGENTS.md +36 -70
  5. package/cli/commands/command.ts +243 -0
  6. package/cli/commands/commandLocalRunner.js +198 -0
  7. package/cli/commands/dev.ts +95 -1
  8. package/cli/commands/doctor.ts +8 -74
  9. package/cli/commands/explain.ts +8 -194
  10. package/cli/commands/trace.ts +8 -0
  11. package/cli/compiler/artifacts/commands.ts +217 -0
  12. package/cli/compiler/artifacts/manifest.ts +17 -2
  13. package/cli/compiler/artifacts/services.ts +291 -0
  14. package/cli/compiler/client/index.ts +13 -0
  15. package/cli/compiler/common/commands.ts +175 -0
  16. package/cli/compiler/common/proteumManifest.ts +15 -124
  17. package/cli/compiler/index.ts +25 -2
  18. package/cli/compiler/server/index.ts +3 -0
  19. package/cli/presentation/commands.ts +37 -5
  20. package/cli/runtime/commands.ts +29 -1
  21. package/cli/tsconfig.json +4 -1
  22. package/cli/utils/check.ts +1 -1
  23. package/client/app/component.tsx +11 -0
  24. package/client/dev/profiler/index.tsx +1511 -0
  25. package/client/dev/profiler/noop.tsx +5 -0
  26. package/client/dev/profiler/runtime.noop.ts +116 -0
  27. package/client/dev/profiler/runtime.ts +840 -0
  28. package/client/services/router/components/router.tsx +30 -2
  29. package/client/services/router/index.tsx +25 -0
  30. package/client/services/router/request/api.ts +133 -17
  31. package/commands/proteum/diagnostics.ts +11 -0
  32. package/common/dev/commands.ts +50 -0
  33. package/common/dev/diagnostics.ts +298 -0
  34. package/common/dev/profiler.ts +91 -0
  35. package/common/dev/proteumManifest.ts +135 -0
  36. package/common/dev/requestTrace.ts +28 -1
  37. package/docs/dev-commands.md +86 -0
  38. package/docs/request-tracing.md +2 -0
  39. package/package.json +1 -2
  40. package/server/app/commands.ts +35 -370
  41. package/server/app/commandsManager.ts +393 -0
  42. package/server/app/container/console/index.ts +0 -2
  43. package/server/app/container/trace/index.ts +88 -8
  44. package/server/app/devCommands.ts +192 -0
  45. package/server/app/devDiagnostics.ts +53 -0
  46. package/server/app/index.ts +27 -4
  47. package/server/services/cron/CronTask.ts +73 -5
  48. package/server/services/cron/index.ts +34 -11
  49. package/server/services/fetch/index.ts +3 -10
  50. package/server/services/prisma/index.ts +1 -1
  51. package/server/services/router/http/index.ts +132 -21
  52. package/server/services/router/index.ts +40 -4
  53. package/server/services/router/request/api.ts +30 -1
  54. package/skills/clean-project-code/SKILL.md +7 -2
  55. package/test-results/.last-run.json +4 -0
  56. package/types/aliases.d.ts +6 -0
@@ -28,6 +28,11 @@ export type TProps = { service?: ClientRouter; loaderComponent?: React.Component
28
28
  ----------------------------------*/
29
29
 
30
30
  const LogPrefix = `[router][component]`;
31
+ const withProfiler = <T,>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
32
+ if (!__DEV__) return undefined as T | undefined;
33
+ const profilerModule = require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
34
+ return callback(profilerModule.profilerRuntime);
35
+ };
31
36
 
32
37
  const PageLoading = ({
33
38
  clientRouter,
@@ -91,6 +96,12 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
91
96
  }
92
97
 
93
98
  // Set loading state
99
+ const sessionId = withProfiler((runtime) =>
100
+ runtime.startNavigationSession({
101
+ path: request.path,
102
+ url: request.url,
103
+ }),
104
+ );
94
105
  clientRouter.runHook('page.change', request);
95
106
  window.scrollTo({ top: 0, behavior: 'smooth' });
96
107
  clientRouter.setLoading(true);
@@ -99,21 +110,25 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
99
110
  // Unable to load (no connection, server error, ....)
100
111
  if (newpage === null) return;
101
112
 
102
- return await changePage(newpage, data, request);
113
+ return await changePage(newpage, data, request, sessionId);
103
114
  };
104
115
 
105
- async function changePage(newpage: Page, data?: {}, request?: ClientRequest) {
116
+ async function changePage(newpage: Page, data?: {}, request?: ClientRequest, sessionId?: string) {
106
117
  // Fetch API data to hydrate the page
107
118
  try {
108
119
  await newpage.preRender();
109
120
  } catch (error) {
110
121
  console.error(LogPrefix, 'Unable to fetch data:', error);
122
+ withProfiler((runtime) =>
123
+ runtime.failNavigation(error instanceof Error ? error.message : String(error), sessionId),
124
+ );
111
125
  clientRouter?.setLoading(false);
112
126
  return;
113
127
  }
114
128
 
115
129
  // Add additional data
116
130
  if (data) newpage.data = { ...newpage.data, ...data };
131
+ withProfiler((runtime) => runtime.startRenderStep(sessionId));
117
132
 
118
133
  // Add page container
119
134
  setCurrentPage((page) => {
@@ -172,6 +187,19 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
172
187
  currentPage?.updateClient();
173
188
  // Scroll to the selected content via url hash
174
189
  restoreScroll(currentPage);
190
+ const routeLabel =
191
+ currentPage && 'path' in currentPage.route && currentPage.route.path
192
+ ? currentPage.route.path
193
+ : currentPage && 'code' in currentPage.route
194
+ ? String(currentPage.route.code)
195
+ : undefined;
196
+ withProfiler((runtime) =>
197
+ runtime.finishNavigation({
198
+ chunkId: currentPage?.chunkId,
199
+ routeLabel,
200
+ title: currentPage?.title,
201
+ }),
202
+ );
175
203
 
176
204
  // Hooks
177
205
  clientRouter.runHook('page.changed', (currentPage?.context.request || context.request) as ClientRequest);
@@ -52,6 +52,11 @@ import appRoutes from '@generated/client/routes';
52
52
  const debug = false;
53
53
  const LogPrefix = '[router]';
54
54
  const browserWindow = window as Window & { routes?: TSsrUnresolvedRoute[]; ssr?: TBasicSSrData };
55
+ const withProfiler = <T,>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
56
+ if (!__DEV__) return undefined as T | undefined;
57
+ const profilerModule = require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
58
+ return callback(profilerModule.profilerRuntime);
59
+ };
55
60
 
56
61
  /*----------------------------------
57
62
  - TYPES
@@ -320,12 +325,19 @@ export default class ClientRouter<
320
325
 
321
326
  // Create response
322
327
  debug && console.log(LogPrefix, 'Resolved request', request.path, '| Route:', route);
328
+ withProfiler((runtime) =>
329
+ runtime.completeResolveStep({
330
+ chunkId: 'chunk' in route ? route.chunk : route.options.id,
331
+ routeLabel: request.path,
332
+ }),
333
+ );
323
334
  const page = await this.createResponse(route, request);
324
335
 
325
336
  return page;
326
337
  }
327
338
 
328
339
  const notFoundRoute = this.errors[404];
340
+ withProfiler((runtime) => runtime.completeResolveStep({ routeLabel: '404' }));
329
341
  return await this.createResponse(notFoundRoute, request, { error: new Error('Page not found') });
330
342
  }
331
343
 
@@ -337,16 +349,21 @@ export default class ClientRouter<
337
349
  //throw new Error(`Failed to load route: ${route.chunk}`);
338
350
 
339
351
  debug && console.log(`Fetching route ${route.chunk} ...`, route);
352
+ const stepId = withProfiler((runtime) => runtime.startChunkStep(route.chunk));
340
353
  try {
341
354
  const loaded = await route.load();
342
355
  const fetched = loaded.__register(this.app);
343
356
 
344
357
  debug && console.log(`Route fetched: ${route.chunk}`, fetched);
358
+ withProfiler((runtime) => runtime.finishStep(stepId));
345
359
 
346
360
  if ('code' in route) return fetched as TClientPageErrorRoute<this>;
347
361
 
348
362
  return { ...(fetched as TClientPageRoute<this>), regex: route.regex, keys: route.keys };
349
363
  } catch (e) {
364
+ withProfiler((runtime) =>
365
+ runtime.finishStep(stepId, 'error', e instanceof Error ? e.message : String(e)),
366
+ );
350
367
  console.error(`Failed to fetch the route ${route.chunk}`, e);
351
368
  try {
352
369
  this.app.handleUpdate();
@@ -367,6 +384,13 @@ export default class ClientRouter<
367
384
  if (!route) throw new Error(`Unable to resolve route.`);
368
385
 
369
386
  const request = new ClientRequest(location, this);
387
+ withProfiler((runtime) =>
388
+ runtime.ensureInitialSession({
389
+ path: request.path,
390
+ requestId: this.ssrContext?.request.id,
391
+ url: request.url,
392
+ }),
393
+ );
370
394
 
371
395
  // Restituate SSR response
372
396
  let apiData: {} = {};
@@ -384,6 +408,7 @@ export default class ClientRouter<
384
408
 
385
409
  ReactDOM.hydrate(<App context={response.context as AppPropsContext} />, document.body, () => {
386
410
  console.log(`Render complete`);
411
+ withProfiler((runtime) => runtime.markInitialHydrated({ chunkId: response.chunkId, title: response.title }));
387
412
 
388
413
  this.runHook('page.rendered', request);
389
414
  });
@@ -24,6 +24,16 @@ import { toMultipart } from './multipart';
24
24
  ----------------------------------*/
25
25
 
26
26
  const debug = false;
27
+ const getProfilerModule = () => {
28
+ if (!__DEV__) return undefined;
29
+ return require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
30
+ };
31
+ const withProfiler = <T>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
32
+ const profilerModule = getProfilerModule();
33
+ return profilerModule ? callback(profilerModule.profilerRuntime) : undefined;
34
+ };
35
+
36
+ type TExecuteResult<TData> = { data: TData; durationMs: number; response: Response };
27
37
 
28
38
  export type Config = {};
29
39
 
@@ -126,8 +136,54 @@ export default class ApiClient implements ApiClientService {
126
136
  ): Promise<TData> {
127
137
  /*if (options?.captcha !== undefined)
128
138
  await this.gui.captcha.check(options?.captcha);*/
139
+ const pendingTrace = withProfiler((runtime) =>
140
+ runtime.startTrace('async', {
141
+ label: `${method} ${path}`,
142
+ method,
143
+ path,
144
+ }),
145
+ );
146
+
147
+ try {
148
+ const result = await this.executeDetailed<TData>('client-async', method, path, data, options);
149
+ const profilerModule = getProfilerModule();
150
+ const traceRequestId = profilerModule?.readProfilerTraceRequestId(result.response);
151
+
152
+ if (pendingTrace && traceRequestId) {
153
+ await profilerModule?.profilerRuntime.attachTraceByRequestId(
154
+ pendingTrace.sessionId,
155
+ pendingTrace.traceId,
156
+ traceRequestId,
157
+ );
158
+ } else if (pendingTrace) {
159
+ withProfiler((runtime) =>
160
+ runtime.completeTrace(pendingTrace.traceId, {
161
+ durationMs: result.durationMs,
162
+ status: 'completed',
163
+ }),
164
+ );
165
+ }
129
166
 
130
- return await this.execute<TData>(method, path, data, options);
167
+ return result.data;
168
+ } catch (error) {
169
+ const profilerModule = getProfilerModule();
170
+ const errorResponse = (error as Error & { response?: Response }).response;
171
+ const traceRequestId = errorResponse ? profilerModule?.readProfilerTraceRequestId(errorResponse) : undefined;
172
+ if (pendingTrace && traceRequestId) {
173
+ await profilerModule?.profilerRuntime.attachTraceByRequestId(
174
+ pendingTrace.sessionId,
175
+ pendingTrace.traceId,
176
+ traceRequestId,
177
+ );
178
+ }
179
+ withProfiler((runtime) =>
180
+ runtime.completeTrace(pendingTrace?.traceId, {
181
+ errorMessage: error instanceof Error ? error.message : String(error),
182
+ status: 'error',
183
+ }),
184
+ );
185
+ throw error;
186
+ }
131
187
  }
132
188
 
133
189
  public async fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees> {
@@ -145,23 +201,68 @@ export default class ApiClient implements ApiClientService {
145
201
  const fetchedData =
146
202
  fetchersCount === 0
147
203
  ? {}
148
- : await this.execute<TObjetDonnees>(
149
- 'POST',
150
- '/api',
151
- ({ fetchers: fetchersToRun } as unknown) as TPostData,
152
- )
153
- .then((res: TObjetDonnees) => {
154
- const data: TObjetDonnees = {};
155
- for (const id in res) data[id] = res[id];
156
-
157
- return data;
158
- })
159
- .catch((e: Error) => {
204
+ : await (async () => {
205
+ const pendingTrace = withProfiler((runtime) =>
206
+ runtime.startTrace('navigation-data', {
207
+ fetcherIds: Object.keys(fetchersToRun),
208
+ label: 'Navigation data',
209
+ method: 'POST',
210
+ path: '/api',
211
+ }),
212
+ );
213
+
214
+ try {
215
+ const result = await this.executeDetailed<TObjetDonnees>(
216
+ 'client-navigation',
217
+ 'POST',
218
+ '/api',
219
+ ({ fetchers: fetchersToRun } as unknown) as TPostData,
220
+ );
221
+ const profilerModule = getProfilerModule();
222
+ const traceRequestId = profilerModule?.readProfilerTraceRequestId(result.response);
223
+
224
+ if (pendingTrace && traceRequestId) {
225
+ await profilerModule?.profilerRuntime.attachTraceByRequestId(
226
+ pendingTrace.sessionId,
227
+ pendingTrace.traceId,
228
+ traceRequestId,
229
+ );
230
+ } else if (pendingTrace) {
231
+ withProfiler((runtime) =>
232
+ runtime.completeTrace(pendingTrace.traceId, {
233
+ durationMs: result.durationMs,
234
+ status: 'completed',
235
+ }),
236
+ );
237
+ }
238
+
239
+ const responseData: TObjetDonnees = {};
240
+ for (const id in result.data) responseData[id] = result.data[id];
241
+ return responseData;
242
+ } catch (e) {
243
+ const profilerModule = getProfilerModule();
244
+ const errorResponse = (e as Error & { response?: Response }).response;
245
+ const traceRequestId = errorResponse ? profilerModule?.readProfilerTraceRequestId(errorResponse) : undefined;
246
+ if (pendingTrace && traceRequestId) {
247
+ await profilerModule?.profilerRuntime.attachTraceByRequestId(
248
+ pendingTrace.sessionId,
249
+ pendingTrace.traceId,
250
+ traceRequestId,
251
+ );
252
+ }
253
+ withProfiler((runtime) =>
254
+ runtime.completeTrace(pendingTrace?.traceId, {
255
+ errorMessage: e instanceof Error ? e.message : String(e),
256
+ status: 'error',
257
+ }),
258
+ );
259
+
160
260
  // API Error hook
161
- this.app.handleError(e);
261
+ this.app.handleError(e as Error);
162
262
 
163
263
  throw e;
164
- });
264
+ }
265
+ })();
165
266
 
166
267
  // Errors will be catched in the caller
167
268
 
@@ -214,21 +315,36 @@ export default class ApiClient implements ApiClientService {
214
315
  };
215
316
 
216
317
  public execute<TData = unknown>(...args: TFetcherArgs): Promise<TData> {
318
+ return this.executeDetailed<TData>('client-async', ...args).then((result) => result.data);
319
+ }
320
+
321
+ private async executeDetailed<TData = unknown>(
322
+ profilerOrigin: string,
323
+ ...args: TFetcherArgs
324
+ ): Promise<TExecuteResult<TData>> {
217
325
  const { url, config } = this.configure(...args);
326
+ const startedAt = Date.now();
327
+ const headers = config.headers instanceof Headers ? config.headers : new Headers(config.headers as HeadersInit);
328
+ const profilerHeaders = withProfiler((runtime) => runtime.getRequestHeaders(profilerOrigin)) || {};
329
+ for (const [key, value] of Object.entries(profilerHeaders)) headers.set(key, value);
330
+ config.headers = headers;
218
331
 
219
332
  console.log(`[api] Fetching`, url, config);
220
333
 
221
334
  return fetch(url, config)
222
335
  .then(async (response) => {
336
+ const requestDurationMs = Math.max(0, Date.now() - startedAt);
223
337
  if (!response.ok) {
224
338
  const errorData = await response.json();
225
339
  console.warn(`[api] Failure:`, response.status, errorData);
226
- const error = errorFromJson(errorData);
340
+ const error = errorFromJson(errorData) as Error & { durationMs?: number; response?: Response };
341
+ error.durationMs = requestDurationMs;
342
+ error.response = response;
227
343
  throw error;
228
344
  }
229
345
  const json = (await response.json()) as TData;
230
346
  debug && console.log(`[api] Success:`, json);
231
- return json;
347
+ return { data: json, durationMs: requestDurationMs, response };
232
348
  })
233
349
  .catch((error) => {
234
350
  if (error instanceof TypeError) {
@@ -0,0 +1,11 @@
1
+ import { Commands } from '@server/app/commands';
2
+
3
+ export default class ProteumDiagnosticsCommands extends Commands {
4
+ public async ping() {
5
+ return {
6
+ app: this.app.identity.identifier,
7
+ envProfile: this.app.env.profile,
8
+ services: Object.keys(this.app.getRootServices()),
9
+ };
10
+ }
11
+ }
@@ -0,0 +1,50 @@
1
+ import type { TTraceSummaryValue } from './requestTrace';
2
+
3
+ export type TDevCommandScope = 'app' | 'framework';
4
+ export type TDevCommandSourceLocation = { line: number; column: number };
5
+ export type TDevCommandExecutionStatus = 'completed' | 'error';
6
+
7
+ export type TDevCommandDefinition = {
8
+ path: string;
9
+ className: string;
10
+ methodName: string;
11
+ importPath: string;
12
+ filepath: string;
13
+ sourceLocation: TDevCommandSourceLocation;
14
+ scope: TDevCommandScope;
15
+ };
16
+
17
+ export type TDevCommandSerializedResult = {
18
+ json?: unknown;
19
+ summary: TTraceSummaryValue;
20
+ };
21
+
22
+ export type TDevCommandExecution = {
23
+ command: TDevCommandDefinition;
24
+ startedAt: string;
25
+ finishedAt: string;
26
+ durationMs: number;
27
+ status: TDevCommandExecutionStatus;
28
+ result?: TDevCommandSerializedResult;
29
+ errorMessage?: string;
30
+ };
31
+
32
+ export type TDevCommandListResponse = {
33
+ commands: TDevCommandDefinition[];
34
+ };
35
+
36
+ export type TDevCommandRunResponse = {
37
+ execution: TDevCommandExecution;
38
+ };
39
+
40
+ export type TDevCommandErrorResponse = {
41
+ error: string;
42
+ execution?: TDevCommandExecution;
43
+ };
44
+
45
+ export const normalizeDevCommandPath = (value: string) =>
46
+ value
47
+ .trim()
48
+ .replace(/^\/+/, '')
49
+ .replace(/\/+$/, '')
50
+ .replace(/\/{2,}/g, '/');