proteum 2.1.3-1 → 2.1.7

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 (95) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +109 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/connect.ts +45 -0
  15. package/cli/commands/dev.ts +37 -13
  16. package/cli/commands/diagnose.ts +286 -0
  17. package/cli/commands/doctor.ts +18 -5
  18. package/cli/commands/explain.ts +25 -0
  19. package/cli/commands/perf.ts +243 -0
  20. package/cli/commands/trace.ts +9 -1
  21. package/cli/commands/verify.ts +281 -0
  22. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  23. package/cli/compiler/artifacts/controllers.ts +198 -49
  24. package/cli/compiler/artifacts/discovery.ts +0 -34
  25. package/cli/compiler/artifacts/manifest.ts +95 -6
  26. package/cli/compiler/artifacts/routing.ts +2 -2
  27. package/cli/compiler/artifacts/services.ts +277 -130
  28. package/cli/compiler/client/index.ts +3 -0
  29. package/cli/compiler/common/files/style.ts +52 -0
  30. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  31. package/cli/compiler/common/scripts.ts +11 -5
  32. package/cli/compiler/index.ts +2 -1
  33. package/cli/compiler/server/index.ts +3 -0
  34. package/cli/presentation/commands.ts +110 -7
  35. package/cli/presentation/devSession.ts +32 -7
  36. package/cli/runtime/commands.ts +165 -6
  37. package/cli/scaffold/index.ts +18 -27
  38. package/cli/scaffold/templates.ts +48 -28
  39. package/cli/utils/agents.ts +106 -13
  40. package/cli/utils/keyboard.ts +8 -0
  41. package/client/dev/profiler/ApexChart.tsx +66 -0
  42. package/client/dev/profiler/index.tsx +2508 -302
  43. package/client/dev/profiler/runtime.noop.ts +12 -0
  44. package/client/dev/profiler/runtime.ts +195 -4
  45. package/client/services/router/request/api.ts +6 -1
  46. package/common/applicationConfig.ts +173 -0
  47. package/common/applicationConfigLoader.ts +102 -0
  48. package/common/connectedProjects.ts +113 -0
  49. package/common/dev/connect.ts +267 -0
  50. package/common/dev/console.ts +31 -0
  51. package/common/dev/contractsDoctor.ts +128 -0
  52. package/common/dev/diagnostics.ts +59 -15
  53. package/common/dev/inspection.ts +491 -0
  54. package/common/dev/performance.ts +809 -0
  55. package/common/dev/profiler.ts +3 -0
  56. package/common/dev/proteumManifest.ts +31 -6
  57. package/common/dev/requestTrace.ts +52 -1
  58. package/common/env/proteumEnv.ts +176 -50
  59. package/common/router/index.ts +1 -0
  60. package/common/router/request/api.ts +2 -0
  61. package/config.ts +5 -0
  62. package/docs/dev-commands.md +5 -1
  63. package/docs/dev-sessions.md +90 -0
  64. package/docs/diagnostics.md +74 -11
  65. package/docs/request-tracing.md +50 -3
  66. package/package.json +1 -1
  67. package/server/app/container/config.ts +16 -87
  68. package/server/app/container/console/index.ts +42 -8
  69. package/server/app/container/index.ts +10 -2
  70. package/server/app/container/trace/index.ts +105 -0
  71. package/server/app/devDiagnostics.ts +138 -0
  72. package/server/app/index.ts +18 -8
  73. package/server/app/service/container.ts +0 -12
  74. package/server/app/service/index.ts +0 -2
  75. package/server/services/prisma/index.ts +121 -4
  76. package/server/services/router/http/index.ts +305 -11
  77. package/server/services/router/index.ts +116 -57
  78. package/server/services/router/request/api.ts +160 -19
  79. package/server/services/router/request/index.ts +8 -0
  80. package/server/services/router/response/index.ts +23 -1
  81. package/server/services/router/response/page/document.tsx +31 -14
  82. package/server/services/router/response/page/index.tsx +10 -0
  83. package/agents/framework/AGENTS.md +0 -177
  84. package/server/services/auth/router/service.json +0 -6
  85. package/server/services/auth/service.json +0 -6
  86. package/server/services/cron/service.json +0 -6
  87. package/server/services/disks/drivers/local/service.json +0 -6
  88. package/server/services/disks/drivers/s3/service.json +0 -6
  89. package/server/services/disks/service.json +0 -6
  90. package/server/services/fetch/service.json +0 -7
  91. package/server/services/prisma/service.json +0 -6
  92. package/server/services/router/service.json +0 -6
  93. package/server/services/schema/router/service.json +0 -6
  94. package/server/services/schema/service.json +0 -6
  95. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -24,9 +24,17 @@ import type CronManager from '@server/services/cron';
24
24
  import type CronTask from '@server/services/cron/CronTask';
25
25
  import type { TBasicUser } from '@server/services/auth';
26
26
  import type { TServerRouter } from '..';
27
+ import type { TDevConsoleLogLevel } from '@common/dev/console';
28
+ import type { TPerfGroupBy } from '@common/dev/performance';
27
29
  import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
28
30
  import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
29
31
  import { explainSectionNames } from '@common/dev/diagnostics';
32
+ import {
33
+ connectedProjectHealthPath,
34
+ connectedProjectProxyPathPrefix,
35
+ parseConnectedProjectProxyPath,
36
+ } from '@common/connectedProjects';
37
+ import { profilerTraceRequestIdHeader } from '@common/dev/profiler';
30
38
 
31
39
  // Middlewaees (core)
32
40
  import { isMutipart, MiddlewareFormData } from './multipart';
@@ -84,6 +92,27 @@ const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPol
84
92
  };
85
93
  };
86
94
 
95
+ const immutablePublicAssetCacheControl = 'public, max-age=31536000, immutable';
96
+ const revalidatedPublicAssetCacheControl = 'public, max-age=0, must-revalidate';
97
+ const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
98
+ const connectedProjectBootRetryCount = 10;
99
+ const connectedProjectBootRetryDelayMs = 5_000;
100
+
101
+ const isVersionedPublicAssetRequest = (res: express.Response, filePath: string) => {
102
+ const requestUrl = res.req?.originalUrl || res.req?.url || '';
103
+ const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
104
+ if (searchParams.has('v')) return true;
105
+
106
+ return hashedPublicAssetPattern.test(path.basename(filePath));
107
+ };
108
+
109
+ const resolvePublicAssetCacheControl = (res: express.Response, filePath: string) =>
110
+ isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
111
+ const wait = async (durationMs: number) =>
112
+ await new Promise<void>((resolve) => {
113
+ setTimeout(resolve, durationMs);
114
+ });
115
+
87
116
  /*----------------------------------
88
117
  - FUNCTION
89
118
  ----------------------------------*/
@@ -138,6 +167,143 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
138
167
  };
139
168
  }
140
169
 
170
+ private async verifyConnectedProjectsBeforeStart() {
171
+ for (const connectedProject of Object.values(this.app.connectedProjects || {})) {
172
+ const healthUrl = new URL(connectedProjectHealthPath, connectedProject.urlInternal).toString();
173
+ let lastError: Error | undefined;
174
+
175
+ for (let retryIndex = 0; retryIndex <= connectedProjectBootRetryCount; retryIndex++) {
176
+ try {
177
+ const response = await fetch(healthUrl, {
178
+ headers: { Accept: 'application/json' },
179
+ });
180
+
181
+ if (!response.ok) {
182
+ throw new Error(
183
+ `Connected project "${connectedProject.namespace}" health check failed at ${healthUrl} with status ${response.status}.`,
184
+ );
185
+ }
186
+
187
+ lastError = undefined;
188
+ break;
189
+ } catch (error) {
190
+ lastError =
191
+ error instanceof Error && error.message.startsWith(`Connected project "${connectedProject.namespace}"`)
192
+ ? error
193
+ : new Error(
194
+ `Connected project "${connectedProject.namespace}" is unreachable at ${connectedProject.urlInternal}. ${error instanceof Error ? error.message : String(error)}`,
195
+ );
196
+ }
197
+
198
+ if (!lastError) continue;
199
+ if (retryIndex === connectedProjectBootRetryCount) throw lastError;
200
+
201
+ console.warn(
202
+ `[connect] ${lastError.message} Retrying ${retryIndex + 1}/${connectedProjectBootRetryCount} in ${connectedProjectBootRetryDelayMs / 1000}s.`,
203
+ );
204
+ await wait(connectedProjectBootRetryDelayMs);
205
+ }
206
+ }
207
+ }
208
+
209
+ private registerConnectedProjectRoutes(routes: express.Express) {
210
+ routes.get(connectedProjectHealthPath, (_req, res) => {
211
+ res.json({
212
+ connectedProjects: Object.keys(this.app.connectedProjects || {}),
213
+ identifier: this.app.identity.identifier,
214
+ ok: true,
215
+ });
216
+ });
217
+
218
+ routes.all(`${connectedProjectHealthPath}/*`, (_req, res) => {
219
+ res.status(404).json({ error: 'Unknown Proteum connected-project route.' });
220
+ });
221
+
222
+ routes.all(`${connectedProjectProxyPathPrefix}/:namespace/*`, async (req, res, next) => {
223
+ const parsed = parseConnectedProjectProxyPath(req.path);
224
+ if (!parsed) {
225
+ next();
226
+ return;
227
+ }
228
+
229
+ const connectedProject = this.app.connectedProjects?.[parsed.namespace];
230
+ if (!connectedProject) {
231
+ res.status(404).json({ error: `Unknown connected project "${parsed.namespace}".` });
232
+ return;
233
+ }
234
+
235
+ const search = new URL(req.originalUrl, 'http://proteum.local').search;
236
+ const targetUrl = new URL(`${parsed.httpPath}${search}`, connectedProject.urlInternal).toString();
237
+ const headers = new Headers();
238
+
239
+ for (const [key, value] of Object.entries(req.headers)) {
240
+ if (!value) continue;
241
+ if (key === 'content-length' || key === 'host') continue;
242
+ headers.set(key, Array.isArray(value) ? value.join(', ') : String(value));
243
+ }
244
+
245
+ if (!headers.has('accept')) headers.set('accept', 'application/json');
246
+
247
+ const init: RequestInit = {
248
+ method: req.method,
249
+ headers,
250
+ };
251
+
252
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
253
+ if (req.files && Object.keys(req.files).length > 0) {
254
+ res.status(501).json({
255
+ error: `Connected project proxy does not support multipart payloads for ${parsed.namespace} yet.`,
256
+ });
257
+ return;
258
+ }
259
+
260
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
261
+
262
+ if (contentType.includes('application/json')) {
263
+ headers.set('content-type', 'application/json');
264
+ init.body = JSON.stringify(req.body || {});
265
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
266
+ headers.set('content-type', 'application/x-www-form-urlencoded');
267
+ init.body = new URLSearchParams(req.body as Record<string, string>).toString();
268
+ } else {
269
+ headers.delete('content-type');
270
+ init.body = undefined;
271
+ }
272
+ }
273
+
274
+ let response: Response;
275
+ try {
276
+ response = await fetch(targetUrl, init);
277
+ } catch (error) {
278
+ res.status(502).json({
279
+ error: `Failed to proxy connected request to ${connectedProject.namespace}. ${error instanceof Error ? error.message : String(error)}`,
280
+ });
281
+ return;
282
+ }
283
+
284
+ const traceRequestId = response.headers.get(profilerTraceRequestIdHeader);
285
+ if (traceRequestId) res.setHeader(profilerTraceRequestIdHeader, traceRequestId);
286
+
287
+ const setCookie = typeof (response.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === 'function'
288
+ ? (response.headers as Headers & { getSetCookie: () => string[] }).getSetCookie()
289
+ : [];
290
+ if (setCookie.length > 0) {
291
+ res.setHeader('set-cookie', setCookie);
292
+ }
293
+
294
+ const contentType = response.headers.get('content-type') || '';
295
+ res.status(response.status);
296
+ if (contentType) res.setHeader('content-type', contentType);
297
+
298
+ if (contentType.includes('application/json')) {
299
+ res.json(await response.json());
300
+ return;
301
+ }
302
+
303
+ res.send(await response.text());
304
+ });
305
+ }
306
+
141
307
  /*----------------------------------
142
308
  - HOOKS
143
309
  ----------------------------------*/
@@ -180,6 +346,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
180
346
  );
181
347
  routes.use(apiMultipartOnly(MiddlewareFormData));
182
348
  if (this.config.cors !== undefined) routes.use(apiOnly(cors(this.config.cors)));
349
+ this.registerConnectedProjectRoutes(routes);
183
350
  routes.use(apiOnly(routeRequest));
184
351
 
185
352
  // Diverses protections (dont le disable x-powered-by)
@@ -199,17 +366,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
199
366
  '/public',
200
367
  express.static(path.join(Container.path.root, APP_OUTPUT_DIR, 'public'), {
201
368
  dotfiles: 'deny',
202
- setHeaders: function setCustomCacheControl(res, path) {
203
- const dontCache = ['/public/icons', '/public/client'];
204
-
205
- res.setHeader('Cache-Control', 'public, max-age=0');
206
-
207
- // Set long term cache, except for non-hashed filenames
208
- /*if (dontCache.some( p => path.startsWith( p ))) {
209
- res.setHeader('Cache-Control', 'public, max-age=0');
210
- } else {
211
- res.setHeader('Cache-Control', 'public, max-age=604800000'); // 7 Days
212
- }*/
369
+ setHeaders: function setCustomCacheControl(res, filePath) {
370
+ res.setHeader('Cache-Control', resolvePublicAssetCacheControl(res, filePath));
213
371
  },
214
372
  }),
215
373
  (req, res) => {
@@ -268,6 +426,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
268
426
  /*----------------------------------
269
427
  - BOOT SERVICES
270
428
  ----------------------------------*/
429
+ await this.verifyConnectedProjectsBeforeStart();
430
+
271
431
  this.http.listen(this.config.port, () => {
272
432
  if (__DEV__ && typeof process.send === 'function') {
273
433
  process.send({
@@ -348,6 +508,17 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
348
508
  }
349
509
  });
350
510
 
511
+ routes.get('/__proteum/explain/owner', (req, res) => {
512
+ const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
513
+
514
+ try {
515
+ res.json(this.app.getDevDiagnostics().explainOwner(typeof query === 'string' ? query : ''));
516
+ } catch (error) {
517
+ const message = error instanceof Error ? error.message : String(error);
518
+ res.status(message.includes('required') ? 400 : 500).json({ error: message });
519
+ }
520
+ });
521
+
351
522
  routes.get('/__proteum/doctor', (req, res) => {
352
523
  const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
353
524
  const strict = rawStrict === '1' || rawStrict === 'true';
@@ -359,6 +530,129 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
359
530
  }
360
531
  });
361
532
 
533
+ routes.get('/__proteum/doctor/contracts', (req, res) => {
534
+ const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
535
+ const strict = rawStrict === '1' || rawStrict === 'true';
536
+
537
+ try {
538
+ res.json(this.app.getDevDiagnostics().doctorContracts(strict));
539
+ } catch (error) {
540
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
541
+ }
542
+ });
543
+
544
+ routes.get('/__proteum/logs', (req, res) => {
545
+ const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
546
+ const rawLevel = Array.isArray(req.query.level) ? req.query.level[0] : req.query.level;
547
+ const limit = Math.max(0, Math.min(500, Number(rawLimit) || 100));
548
+ const level = typeof rawLevel === 'string' ? (rawLevel as TDevConsoleLogLevel) : 'log';
549
+
550
+ try {
551
+ res.json(this.app.getDevDiagnostics().readLogs(limit, level));
552
+ } catch (error) {
553
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
554
+ }
555
+ });
556
+
557
+ routes.get('/__proteum/diagnose', (req, res) => {
558
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
559
+ const readNumber = (value: unknown, fallback: number) => {
560
+ const parsed = Number(readString(value));
561
+ return Number.isFinite(parsed) ? parsed : fallback;
562
+ };
563
+
564
+ try {
565
+ res.json(
566
+ this.app.getDevDiagnostics().diagnose({
567
+ logsLevel:
568
+ typeof readString(req.query.logsLevel) === 'string'
569
+ ? (readString(req.query.logsLevel) as TDevConsoleLogLevel)
570
+ : 'warn',
571
+ logsLimit: readNumber(req.query.logsLimit, 40),
572
+ path: typeof readString(req.query.path) === 'string' ? readString(req.query.path) : undefined,
573
+ query: typeof readString(req.query.query) === 'string' ? readString(req.query.query) : undefined,
574
+ requestId: typeof readString(req.query.requestId) === 'string' ? readString(req.query.requestId) : undefined,
575
+ strict: readString(req.query.strict) === '1' || readString(req.query.strict) === 'true',
576
+ }),
577
+ );
578
+ } catch (error) {
579
+ const message = error instanceof Error ? error.message : String(error);
580
+ res.status(message.includes('required') || message.includes('Diagnose requires') ? 400 : 500).json({ error: message });
581
+ }
582
+ });
583
+
584
+ routes.get('/__proteum/perf/top', (req, res) => {
585
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
586
+ const readNumber = (value: unknown, fallback: number) => {
587
+ const parsed = Number(readString(value));
588
+ return Number.isFinite(parsed) ? parsed : fallback;
589
+ };
590
+
591
+ try {
592
+ res.json(
593
+ this.app.getDevDiagnostics().perfTop({
594
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
595
+ limit: readNumber(req.query.limit, 12),
596
+ since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
597
+ }),
598
+ );
599
+ } catch (error) {
600
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
601
+ }
602
+ });
603
+
604
+ routes.get('/__proteum/perf/compare', (req, res) => {
605
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
606
+ const readNumber = (value: unknown, fallback: number) => {
607
+ const parsed = Number(readString(value));
608
+ return Number.isFinite(parsed) ? parsed : fallback;
609
+ };
610
+
611
+ try {
612
+ res.json(
613
+ this.app.getDevDiagnostics().perfCompare({
614
+ baseline: typeof readString(req.query.baseline) === 'string' ? readString(req.query.baseline) : undefined,
615
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
616
+ limit: readNumber(req.query.limit, 12),
617
+ target: typeof readString(req.query.target) === 'string' ? readString(req.query.target) : undefined,
618
+ }),
619
+ );
620
+ } catch (error) {
621
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
622
+ }
623
+ });
624
+
625
+ routes.get('/__proteum/perf/memory', (req, res) => {
626
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
627
+ const readNumber = (value: unknown, fallback: number) => {
628
+ const parsed = Number(readString(value));
629
+ return Number.isFinite(parsed) ? parsed : fallback;
630
+ };
631
+
632
+ try {
633
+ res.json(
634
+ this.app.getDevDiagnostics().perfMemory({
635
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
636
+ limit: readNumber(req.query.limit, 12),
637
+ since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
638
+ }),
639
+ );
640
+ } catch (error) {
641
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
642
+ }
643
+ });
644
+
645
+ routes.get('/__proteum/perf/request', (req, res) => {
646
+ const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
647
+
648
+ try {
649
+ res.json(this.app.getDevDiagnostics().perfRequest(typeof query === 'string' ? query : ''));
650
+ } catch (error) {
651
+ const message = error instanceof Error ? error.message : String(error);
652
+ res.status(message.includes('Could not find') || message.includes('required') ? 404 : 400).json({ error: message });
653
+ }
654
+ });
655
+
362
656
  routes.get('/__proteum/cron/tasks', (_req, res) => {
363
657
  const cron = this.getCronManager();
364
658
  res.json({