proteum 2.1.2 → 2.1.6

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 (99) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +112 -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/command.ts +8 -0
  15. package/cli/commands/connect.ts +45 -0
  16. package/cli/commands/dev.ts +26 -11
  17. package/cli/commands/diagnose.ts +286 -0
  18. package/cli/commands/doctor.ts +18 -5
  19. package/cli/commands/explain.ts +25 -0
  20. package/cli/commands/perf.ts +243 -0
  21. package/cli/commands/session.ts +254 -0
  22. package/cli/commands/sessionLocalRunner.js +188 -0
  23. package/cli/commands/trace.ts +17 -1
  24. package/cli/commands/verify.ts +281 -0
  25. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  26. package/cli/compiler/artifacts/controllers.ts +198 -49
  27. package/cli/compiler/artifacts/discovery.ts +0 -34
  28. package/cli/compiler/artifacts/manifest.ts +90 -6
  29. package/cli/compiler/artifacts/routing.ts +2 -2
  30. package/cli/compiler/artifacts/services.ts +277 -130
  31. package/cli/compiler/client/index.ts +3 -0
  32. package/cli/compiler/common/files/style.ts +52 -0
  33. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  34. package/cli/compiler/common/scripts.ts +11 -5
  35. package/cli/compiler/index.ts +2 -1
  36. package/cli/compiler/server/index.ts +3 -0
  37. package/cli/presentation/commands.ts +136 -7
  38. package/cli/presentation/devSession.ts +32 -7
  39. package/cli/runtime/commands.ts +193 -6
  40. package/cli/scaffold/index.ts +14 -25
  41. package/cli/scaffold/templates.ts +41 -27
  42. package/cli/utils/agents.ts +4 -2
  43. package/cli/utils/keyboard.ts +8 -0
  44. package/client/dev/profiler/ApexChart.tsx +66 -0
  45. package/client/dev/profiler/index.tsx +2798 -417
  46. package/client/dev/profiler/runtime.noop.ts +12 -0
  47. package/client/dev/profiler/runtime.ts +195 -4
  48. package/client/services/router/request/api.ts +6 -1
  49. package/common/applicationConfig.ts +173 -0
  50. package/common/applicationConfigLoader.ts +102 -0
  51. package/common/connectedProjects.ts +113 -0
  52. package/common/dev/connect.ts +267 -0
  53. package/common/dev/console.ts +31 -0
  54. package/common/dev/contractsDoctor.ts +128 -0
  55. package/common/dev/diagnostics.ts +59 -15
  56. package/common/dev/inspection.ts +491 -0
  57. package/common/dev/performance.ts +809 -0
  58. package/common/dev/profiler.ts +3 -0
  59. package/common/dev/proteumManifest.ts +31 -6
  60. package/common/dev/requestTrace.ts +56 -1
  61. package/common/dev/session.ts +24 -0
  62. package/common/env/proteumEnv.ts +176 -50
  63. package/common/router/index.ts +1 -0
  64. package/common/router/request/api.ts +2 -0
  65. package/config.ts +5 -0
  66. package/docs/dev-commands.md +5 -1
  67. package/docs/dev-sessions.md +90 -0
  68. package/docs/diagnostics.md +74 -11
  69. package/docs/request-tracing.md +50 -3
  70. package/package.json +1 -1
  71. package/server/app/container/config.ts +16 -87
  72. package/server/app/container/console/index.ts +42 -8
  73. package/server/app/container/index.ts +3 -1
  74. package/server/app/container/trace/index.ts +153 -0
  75. package/server/app/devDiagnostics.ts +138 -0
  76. package/server/app/index.ts +18 -8
  77. package/server/app/service/container.ts +0 -12
  78. package/server/app/service/index.ts +0 -2
  79. package/server/services/prisma/index.ts +121 -4
  80. package/server/services/router/http/index.ts +352 -0
  81. package/server/services/router/index.ts +50 -47
  82. package/server/services/router/request/api.ts +160 -19
  83. package/server/services/router/request/index.ts +8 -0
  84. package/server/services/router/response/index.ts +24 -1
  85. package/server/services/router/response/page/document.tsx +5 -0
  86. package/server/services/router/response/page/index.tsx +10 -0
  87. package/agents/framework/AGENTS.md +0 -177
  88. package/server/services/auth/router/service.json +0 -6
  89. package/server/services/auth/service.json +0 -6
  90. package/server/services/cron/service.json +0 -6
  91. package/server/services/disks/drivers/local/service.json +0 -6
  92. package/server/services/disks/drivers/s3/service.json +0 -6
  93. package/server/services/disks/service.json +0 -6
  94. package/server/services/fetch/service.json +0 -7
  95. package/server/services/prisma/service.json +0 -6
  96. package/server/services/router/service.json +0 -6
  97. package/server/services/schema/router/service.json +0 -6
  98. package/server/services/schema/service.json +0 -6
  99. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -7,6 +7,7 @@ import type {
7
7
  } from '@common/dev/profiler';
8
8
  import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
9
9
  import type { TDoctorResponse } from '@common/dev/diagnostics';
10
+ import type { TDiagnoseResponse } from '@common/dev/inspection';
10
11
  import type { TProteumManifest } from '@common/dev/proteumManifest';
11
12
 
12
13
  type TProfilerState = {
@@ -25,7 +26,14 @@ type TProfilerState = {
25
26
  status: 'idle' | 'loading' | 'ready' | 'error';
26
27
  tasks: TProfilerCronTask[];
27
28
  };
29
+ diagnose: {
30
+ errorMessage?: string;
31
+ lastLoadedAt?: string;
32
+ response?: TDiagnoseResponse;
33
+ status: 'idle' | 'loading' | 'ready' | 'error';
34
+ };
28
35
  doctor: {
36
+ contracts?: TDoctorResponse;
29
37
  errorMessage?: string;
30
38
  lastLoadedAt?: string;
31
39
  response?: TDoctorResponse;
@@ -55,6 +63,9 @@ const noopState: TProfilerState = {
55
63
  status: 'idle',
56
64
  tasks: [],
57
65
  },
66
+ diagnose: {
67
+ status: 'idle',
68
+ },
58
69
  doctor: {
59
70
  status: 'idle',
60
71
  },
@@ -75,6 +86,7 @@ export const profilerRuntime = {
75
86
  runCommand: async (_path: string) => undefined,
76
87
  refreshCronTasks: async () => undefined,
77
88
  runCronTask: async (_name: string) => undefined,
89
+ refreshDiagnose: async (_sessionId?: string) => undefined,
78
90
  refreshDoctor: async () => undefined,
79
91
  refreshExplain: async () => undefined,
80
92
  ensureInitialSession: (_input: { path: string; requestId?: string; url: string }) => undefined,
@@ -1,3 +1,9 @@
1
+ import {
2
+ type TPerfCompareResponse,
3
+ type TPerfGroupBy,
4
+ type TPerfMemoryResponse,
5
+ type TPerfTopResponse,
6
+ } from '@common/dev/performance';
1
7
  import {
2
8
  profilerOriginHeader,
3
9
  profilerParentRequestIdHeader,
@@ -13,6 +19,7 @@ import {
13
19
  } from '@common/dev/profiler';
14
20
  import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
15
21
  import type { TDoctorResponse } from '@common/dev/diagnostics';
22
+ import type { TDiagnoseResponse } from '@common/dev/inspection';
16
23
  import type { TProteumManifest } from '@common/dev/proteumManifest';
17
24
  import type { TRequestTrace } from '@common/dev/requestTrace';
18
25
 
@@ -33,12 +40,20 @@ type TProfilerCronState = {
33
40
  };
34
41
 
35
42
  type TProfilerDoctorState = {
43
+ contracts?: TDoctorResponse;
36
44
  errorMessage?: string;
37
45
  lastLoadedAt?: string;
38
46
  response?: TDoctorResponse;
39
47
  status: 'idle' | 'loading' | 'ready' | 'error';
40
48
  };
41
49
 
50
+ type TProfilerDiagnoseState = {
51
+ errorMessage?: string;
52
+ lastLoadedAt?: string;
53
+ response?: TDiagnoseResponse;
54
+ status: 'idle' | 'loading' | 'ready' | 'error';
55
+ };
56
+
42
57
  type TProfilerExplainState = {
43
58
  errorMessage?: string;
44
59
  lastLoadedAt?: string;
@@ -46,12 +61,27 @@ type TProfilerExplainState = {
46
61
  status: 'idle' | 'loading' | 'ready' | 'error';
47
62
  };
48
63
 
64
+ type TProfilerPerfState = {
65
+ baseline: string;
66
+ compare?: TPerfCompareResponse;
67
+ errorMessage?: string;
68
+ groupBy: TPerfGroupBy;
69
+ lastLoadedAt?: string;
70
+ memory?: TPerfMemoryResponse;
71
+ since: string;
72
+ status: 'idle' | 'loading' | 'ready' | 'error';
73
+ target: string;
74
+ top?: TPerfTopResponse;
75
+ };
76
+
49
77
  type TProfilerState = {
50
78
  activePanel: TProfilerPanel;
51
79
  commands: TProfilerCommandsState;
52
80
  cron: TProfilerCronState;
81
+ diagnose: TProfilerDiagnoseState;
53
82
  doctor: TProfilerDoctorState;
54
83
  explain: TProfilerExplainState;
84
+ perf: TProfilerPerfState;
55
85
  currentSessionId?: string;
56
86
  selectedSessionId?: string;
57
87
  sessions: TProfilerNavigationSession[];
@@ -124,12 +154,23 @@ const cloneCronState = (cron: TProfilerCronState) => ({
124
154
  });
125
155
  const cloneDoctorState = (doctor: TProfilerDoctorState) => ({
126
156
  ...doctor,
157
+ contracts: doctor.contracts ? cloneDoctorResponse(doctor.contracts) : undefined,
127
158
  response: doctor.response ? cloneDoctorResponse(doctor.response) : undefined,
128
159
  });
160
+ const cloneDiagnoseState = (diagnose: TProfilerDiagnoseState) => ({
161
+ ...diagnose,
162
+ response: diagnose.response ? (JSON.parse(JSON.stringify(diagnose.response)) as TDiagnoseResponse) : undefined,
163
+ });
129
164
  const cloneExplainState = (explain: TProfilerExplainState) => ({
130
165
  ...explain,
131
166
  manifest: explain.manifest ? cloneManifest(explain.manifest) : undefined,
132
167
  });
168
+ const clonePerfState = (perf: TProfilerPerfState) => ({
169
+ ...perf,
170
+ compare: perf.compare ? (JSON.parse(JSON.stringify(perf.compare)) as TPerfCompareResponse) : undefined,
171
+ memory: perf.memory ? (JSON.parse(JSON.stringify(perf.memory)) as TPerfMemoryResponse) : undefined,
172
+ top: perf.top ? (JSON.parse(JSON.stringify(perf.top)) as TPerfTopResponse) : undefined,
173
+ });
133
174
  const cloneCommandsState = (commands: TProfilerCommandsState) => ({
134
175
  ...commands,
135
176
  commands: commands.commands.map(cloneCommand),
@@ -156,12 +197,22 @@ class ProfilerRuntime {
156
197
  status: 'idle',
157
198
  tasks: [],
158
199
  },
200
+ diagnose: {
201
+ status: 'idle',
202
+ },
159
203
  doctor: {
160
204
  status: 'idle',
161
205
  },
162
206
  explain: {
163
207
  status: 'idle',
164
208
  },
209
+ perf: {
210
+ baseline: 'yesterday',
211
+ groupBy: 'path',
212
+ since: 'today',
213
+ status: 'idle',
214
+ target: 'today',
215
+ },
165
216
  sessions: [],
166
217
  uiState: initialUiState(),
167
218
  };
@@ -183,8 +234,10 @@ class ProfilerRuntime {
183
234
  this.state = { ...this.state, activePanel: panel, uiState: 'expanded' };
184
235
  safeSessionStorage.set(profilerStorageKey, 'expanded');
185
236
  this.emit();
237
+ if (panel === 'perf') void this.refreshPerf();
186
238
  if (panel === 'commands') void this.refreshCommands();
187
239
  if (panel === 'cron') void this.refreshCronTasks();
240
+ if (panel === 'diagnose') void this.refreshDiagnose();
188
241
  if (panel === 'doctor') void this.refreshDoctor();
189
242
  if (panel === 'doctor' || panel === 'explain') void this.refreshExplain();
190
243
  }
@@ -240,6 +293,89 @@ class ProfilerRuntime {
240
293
  }
241
294
  }
242
295
 
296
+ public setPerfFilters(filters: Partial<Pick<TProfilerPerfState, 'baseline' | 'groupBy' | 'since' | 'target'>>) {
297
+ this.state = {
298
+ ...this.state,
299
+ perf: {
300
+ ...this.state.perf,
301
+ ...filters,
302
+ },
303
+ };
304
+ this.emit();
305
+ }
306
+
307
+ public async refreshPerf(overrides: Partial<Pick<TProfilerPerfState, 'baseline' | 'groupBy' | 'since' | 'target'>> = {}) {
308
+ const nextPerf = {
309
+ ...this.state.perf,
310
+ ...overrides,
311
+ };
312
+ const topParams = new URLSearchParams({
313
+ groupBy: nextPerf.groupBy,
314
+ limit: '8',
315
+ since: nextPerf.since,
316
+ });
317
+ const compareParams = new URLSearchParams({
318
+ baseline: nextPerf.baseline,
319
+ groupBy: nextPerf.groupBy,
320
+ limit: '8',
321
+ target: nextPerf.target,
322
+ });
323
+ const memoryParams = new URLSearchParams({
324
+ groupBy: nextPerf.groupBy,
325
+ limit: '8',
326
+ since: nextPerf.since,
327
+ });
328
+
329
+ this.state = {
330
+ ...this.state,
331
+ perf: {
332
+ ...nextPerf,
333
+ errorMessage: undefined,
334
+ status: 'loading',
335
+ },
336
+ };
337
+ this.emit();
338
+
339
+ try {
340
+ const [topResponse, compareResponse, memoryResponse] = await Promise.all([
341
+ fetch(`/__proteum/perf/top?${topParams.toString()}`, { cache: 'no-store' }),
342
+ fetch(`/__proteum/perf/compare?${compareParams.toString()}`, { cache: 'no-store' }),
343
+ fetch(`/__proteum/perf/memory?${memoryParams.toString()}`, { cache: 'no-store' }),
344
+ ]);
345
+ const topBody = (await topResponse.json()) as TPerfTopResponse & { error?: string };
346
+ const compareBody = (await compareResponse.json()) as TPerfCompareResponse & { error?: string };
347
+ const memoryBody = (await memoryResponse.json()) as TPerfMemoryResponse & { error?: string };
348
+
349
+ if (!topResponse.ok) throw new Error(topBody.error || 'Failed to load perf top data.');
350
+ if (!compareResponse.ok) throw new Error(compareBody.error || 'Failed to load perf compare data.');
351
+ if (!memoryResponse.ok) throw new Error(memoryBody.error || 'Failed to load perf memory data.');
352
+
353
+ this.state = {
354
+ ...this.state,
355
+ perf: {
356
+ ...nextPerf,
357
+ compare: JSON.parse(JSON.stringify(compareBody)) as TPerfCompareResponse,
358
+ errorMessage: undefined,
359
+ lastLoadedAt: nowIso(),
360
+ memory: JSON.parse(JSON.stringify(memoryBody)) as TPerfMemoryResponse,
361
+ status: 'ready',
362
+ top: JSON.parse(JSON.stringify(topBody)) as TPerfTopResponse,
363
+ },
364
+ };
365
+ this.emit();
366
+ } catch (error) {
367
+ this.state = {
368
+ ...this.state,
369
+ perf: {
370
+ ...nextPerf,
371
+ errorMessage: error instanceof Error ? error.message : String(error),
372
+ status: 'error',
373
+ },
374
+ };
375
+ this.emit();
376
+ }
377
+ }
378
+
243
379
  public async runCommand(commandPath: string) {
244
380
  this.state = {
245
381
  ...this.state,
@@ -353,16 +489,20 @@ class ProfilerRuntime {
353
489
  this.emit();
354
490
 
355
491
  try {
356
- const response = await fetch('/__proteum/doctor', { cache: 'no-store' });
492
+ const [response, contractsResponse] = await Promise.all([
493
+ fetch('/__proteum/doctor', { cache: 'no-store' }),
494
+ fetch('/__proteum/doctor/contracts', { cache: 'no-store' }),
495
+ ]);
357
496
  const body = (await response.json()) as TDoctorResponse & { error?: string };
497
+ const contractsBody = (await contractsResponse.json()) as TDoctorResponse & { error?: string };
358
498
 
359
- if (!response.ok) {
360
- throw new Error(body.error || 'Failed to load doctor diagnostics.');
361
- }
499
+ if (!response.ok) throw new Error(body.error || 'Failed to load doctor diagnostics.');
500
+ if (!contractsResponse.ok) throw new Error(contractsBody.error || 'Failed to load doctor contract diagnostics.');
362
501
 
363
502
  this.state = {
364
503
  ...this.state,
365
504
  doctor: {
505
+ contracts: cloneDoctorResponse(contractsBody),
366
506
  errorMessage: undefined,
367
507
  lastLoadedAt: nowIso(),
368
508
  response: cloneDoctorResponse(body),
@@ -383,6 +523,55 @@ class ProfilerRuntime {
383
523
  }
384
524
  }
385
525
 
526
+ public async refreshDiagnose(sessionId?: string) {
527
+ const session = this.getSession(sessionId || this.state.selectedSessionId || this.state.currentSessionId);
528
+ const params = new URLSearchParams();
529
+ if (session?.requestId) params.set('requestId', session.requestId);
530
+ else if (session?.path) params.set('query', session.path);
531
+
532
+ this.state = {
533
+ ...this.state,
534
+ diagnose: {
535
+ ...this.state.diagnose,
536
+ errorMessage: undefined,
537
+ status: 'loading',
538
+ },
539
+ };
540
+ this.emit();
541
+
542
+ try {
543
+ const response = await fetch(`/__proteum/diagnose${params.size > 0 ? `?${params.toString()}` : ''}`, {
544
+ cache: 'no-store',
545
+ });
546
+ const body = (await response.json()) as TDiagnoseResponse & { error?: string };
547
+
548
+ if (!response.ok) {
549
+ throw new Error(body.error || 'Failed to load diagnose data.');
550
+ }
551
+
552
+ this.state = {
553
+ ...this.state,
554
+ diagnose: {
555
+ errorMessage: undefined,
556
+ lastLoadedAt: nowIso(),
557
+ response: JSON.parse(JSON.stringify(body)) as TDiagnoseResponse,
558
+ status: 'ready',
559
+ },
560
+ };
561
+ this.emit();
562
+ } catch (error) {
563
+ this.state = {
564
+ ...this.state,
565
+ diagnose: {
566
+ ...this.state.diagnose,
567
+ errorMessage: error instanceof Error ? error.message : String(error),
568
+ status: 'error',
569
+ },
570
+ };
571
+ this.emit();
572
+ }
573
+ }
574
+
386
575
  public async refreshExplain() {
387
576
  this.state = {
388
577
  ...this.state,
@@ -784,8 +973,10 @@ class ProfilerRuntime {
784
973
  ...this.state,
785
974
  commands: cloneCommandsState(this.state.commands),
786
975
  cron: cloneCronState(this.state.cron),
976
+ diagnose: cloneDiagnoseState(this.state.diagnose),
787
977
  doctor: cloneDoctorState(this.state.doctor),
788
978
  explain: cloneExplainState(this.state.explain),
979
+ perf: clonePerfState(this.state.perf),
789
980
  sessions: this.state.sessions.map((candidate) => (candidate.id === session.id ? cloneSession(session) : candidate)),
790
981
  };
791
982
  this.emit();
@@ -4,6 +4,7 @@
4
4
 
5
5
  // Core
6
6
  import type ClientApplication from '@client/app';
7
+ import { buildConnectedProjectProxyPath } from '@common/connectedProjects';
7
8
  import { fromJson as errorFromJson, NetworkError } from '@common/errors';
8
9
  import ApiClientService, {
9
10
  TPostData,
@@ -270,7 +271,11 @@ export default class ApiClient implements ApiClientService {
270
271
  }
271
272
 
272
273
  public configure = (...[method, path, data, options = {}]: TFetcherArgs) => {
273
- let url = this.router.url(path, {}, false);
274
+ const requestPath =
275
+ options.connected !== undefined
276
+ ? buildConnectedProjectProxyPath(options.connected.namespace, path)
277
+ : path;
278
+ let url = this.router.url(requestPath, {}, false);
274
279
 
275
280
  debug && console.log(`[api] Sending request`, method, url, data);
276
281
 
@@ -0,0 +1,173 @@
1
+ import { normalizeConnectedProjectsConfig, type TConnectedProjectsConfig } from './connectedProjects';
2
+
3
+ type TObjectRecord = Record<string, unknown>;
4
+
5
+ export type TApplicationIdentityConfig = {
6
+ name: string;
7
+ identifier: string;
8
+ description: string;
9
+ author: {
10
+ name: string;
11
+ url: string;
12
+ email: string;
13
+ };
14
+ social?: TObjectRecord;
15
+ locale?: string;
16
+ language: string;
17
+ maincolor: string;
18
+ iconsPack?: string;
19
+ web: {
20
+ title: string;
21
+ titleSuffix: string;
22
+ fullTitle: string;
23
+ description: string;
24
+ version: string;
25
+ metas?: Record<string, string>;
26
+ jsonld?: Record<string, string>;
27
+ };
28
+ };
29
+
30
+ export type TApplicationSetupConfig = {
31
+ transpile?: string[];
32
+ connect?: TConnectedProjectsConfig;
33
+ };
34
+
35
+ const isRecord = (value: unknown): value is TObjectRecord =>
36
+ value !== null && typeof value === 'object' && !Array.isArray(value);
37
+
38
+ const readRequiredString = ({
39
+ filepath,
40
+ path,
41
+ value,
42
+ }: {
43
+ filepath: string;
44
+ path: string;
45
+ value: unknown;
46
+ }) => {
47
+ if (typeof value === 'string' && value.trim()) return value;
48
+
49
+ throw new Error(`Invalid ${path} in ${filepath}. Expected a non-empty string.`);
50
+ };
51
+
52
+ const readOptionalString = ({
53
+ filepath,
54
+ path,
55
+ value,
56
+ }: {
57
+ filepath: string;
58
+ path: string;
59
+ value: unknown;
60
+ }) => {
61
+ if (value === undefined) return undefined;
62
+
63
+ return readRequiredString({ filepath, path, value });
64
+ };
65
+
66
+ const readStringRecord = ({
67
+ filepath,
68
+ path,
69
+ value,
70
+ }: {
71
+ filepath: string;
72
+ path: string;
73
+ value: unknown;
74
+ }) => {
75
+ if (value === undefined) return undefined;
76
+ if (!isRecord(value)) throw new Error(`Invalid ${path} in ${filepath}. Expected an object of string values.`);
77
+
78
+ const output: Record<string, string> = {};
79
+
80
+ for (const [key, entry] of Object.entries(value)) {
81
+ if (typeof entry !== 'string')
82
+ throw new Error(`Invalid ${path}.${key} in ${filepath}. Expected a string value.`);
83
+
84
+ output[key] = entry;
85
+ }
86
+
87
+ return output;
88
+ };
89
+
90
+ const readSocialConfig = ({
91
+ filepath,
92
+ value,
93
+ }: {
94
+ filepath: string;
95
+ value: unknown;
96
+ }) => {
97
+ if (value === undefined) return undefined;
98
+ if (!isRecord(value)) throw new Error(`Invalid social in ${filepath}. Expected an object.`);
99
+
100
+ return value;
101
+ };
102
+
103
+ export const normalizeTranspileConfig = (value: unknown): string[] => {
104
+ if (!Array.isArray(value)) return [];
105
+
106
+ return Array.from(new Set(value.map((entry) => (typeof entry === 'string' ? entry.trim() : '')).filter(Boolean)));
107
+ };
108
+
109
+ export const normalizeApplicationIdentityConfig = (
110
+ value: unknown,
111
+ filepath = 'identity.config.ts',
112
+ ): TApplicationIdentityConfig => {
113
+ if (!isRecord(value)) throw new Error(`Invalid identity config in ${filepath}. Expected an object export.`);
114
+
115
+ const author = value.author;
116
+ const web = value.web;
117
+
118
+ if (!isRecord(author)) throw new Error(`Invalid author in ${filepath}. Expected an object.`);
119
+ if (!isRecord(web)) throw new Error(`Invalid web in ${filepath}. Expected an object.`);
120
+
121
+ return {
122
+ name: readRequiredString({ filepath, path: 'name', value: value.name }),
123
+ identifier: readRequiredString({ filepath, path: 'identifier', value: value.identifier }),
124
+ description: readRequiredString({ filepath, path: 'description', value: value.description }),
125
+ author: {
126
+ name: readRequiredString({ filepath, path: 'author.name', value: author.name }),
127
+ url: readRequiredString({ filepath, path: 'author.url', value: author.url }),
128
+ email: readRequiredString({ filepath, path: 'author.email', value: author.email }),
129
+ },
130
+ social: readSocialConfig({ filepath, value: value.social }),
131
+ locale: readOptionalString({ filepath, path: 'locale', value: value.locale }),
132
+ language: readRequiredString({ filepath, path: 'language', value: value.language }),
133
+ maincolor: readRequiredString({ filepath, path: 'maincolor', value: value.maincolor }),
134
+ iconsPack: readOptionalString({ filepath, path: 'iconsPack', value: value.iconsPack }),
135
+ web: {
136
+ title: readRequiredString({ filepath, path: 'web.title', value: web.title }),
137
+ titleSuffix: readRequiredString({ filepath, path: 'web.titleSuffix', value: web.titleSuffix }),
138
+ fullTitle: readRequiredString({ filepath, path: 'web.fullTitle', value: web.fullTitle }),
139
+ description: readRequiredString({ filepath, path: 'web.description', value: web.description }),
140
+ version: readRequiredString({ filepath, path: 'web.version', value: web.version }),
141
+ metas: readStringRecord({ filepath, path: 'web.metas', value: web.metas }),
142
+ jsonld: readStringRecord({ filepath, path: 'web.jsonld', value: web.jsonld }),
143
+ },
144
+ };
145
+ };
146
+
147
+ export const normalizeApplicationSetupConfig = (
148
+ value: unknown,
149
+ filepath = 'proteum.config.ts',
150
+ ): TApplicationSetupConfig => {
151
+ if (value === undefined) return {};
152
+ if (!isRecord(value)) throw new Error(`Invalid setup config in ${filepath}. Expected an object export.`);
153
+ if ('transpileModules' in value) {
154
+ throw new Error(`Invalid setup config in ${filepath}. Use "transpile" instead of "transpileModules".`);
155
+ }
156
+
157
+ return {
158
+ transpile: normalizeTranspileConfig(value.transpile),
159
+ connect: normalizeConnectedProjectsConfig(value.connect),
160
+ };
161
+ };
162
+
163
+ class ApplicationConfigHelpers {
164
+ public static identity<const TIdentity extends TApplicationIdentityConfig>(config: TIdentity) {
165
+ return config;
166
+ }
167
+
168
+ public static setup<const TSetup extends TApplicationSetupConfig>(config: TSetup) {
169
+ return config;
170
+ }
171
+ }
172
+
173
+ export const Application = ApplicationConfigHelpers;
@@ -0,0 +1,102 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createRequire } from 'module';
4
+ import * as ts from 'typescript';
5
+
6
+ import {
7
+ Application as ApplicationConfig,
8
+ normalizeApplicationIdentityConfig,
9
+ normalizeApplicationSetupConfig,
10
+ type TApplicationIdentityConfig,
11
+ type TApplicationSetupConfig,
12
+ } from './applicationConfig';
13
+ import { loadOptionalProteumDotenv } from './env/proteumEnv';
14
+
15
+ const moduleCache = new Map<string, unknown>();
16
+ const supportedModuleExtensions = ['.ts', '.tsx', '.js', '.cjs', '.mjs', '.json'];
17
+
18
+ const resolveLocalModulePath = (specifier: string, fromFilepath: string) => {
19
+ const basePath = path.resolve(path.dirname(fromFilepath), specifier);
20
+ const candidates = [
21
+ basePath,
22
+ ...supportedModuleExtensions.map((extension) => `${basePath}${extension}`),
23
+ ...supportedModuleExtensions.map((extension) => path.join(basePath, `index${extension}`)),
24
+ ];
25
+
26
+ for (const candidate of candidates) {
27
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
28
+ }
29
+
30
+ throw new Error(`Unable to resolve module "${specifier}" from ${fromFilepath}.`);
31
+ };
32
+
33
+ const loadTsModule = (filepath: string): unknown => {
34
+ const normalizedFilepath = path.resolve(filepath);
35
+ if (moduleCache.has(normalizedFilepath)) return moduleCache.get(normalizedFilepath);
36
+
37
+ const source = fs.readFileSync(normalizedFilepath, 'utf8');
38
+ const transpiled = ts.transpileModule(source, {
39
+ fileName: normalizedFilepath,
40
+ compilerOptions: {
41
+ module: ts.ModuleKind.CommonJS,
42
+ target: ts.ScriptTarget.ES2020,
43
+ esModuleInterop: true,
44
+ resolveJsonModule: true,
45
+ jsx: ts.JsxEmit.ReactJSX,
46
+ },
47
+ }).outputText;
48
+
49
+ const module = { exports: {} as Record<string, unknown> };
50
+ moduleCache.set(normalizedFilepath, module.exports);
51
+
52
+ const requireFromFile = createRequire(normalizedFilepath);
53
+ const runtimeRequire = (specifier: string) => {
54
+ if (specifier === 'proteum/config' || specifier === 'proteum/config.ts') return { Application: ApplicationConfig };
55
+
56
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
57
+ const resolved = resolveLocalModulePath(specifier, normalizedFilepath);
58
+
59
+ if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) return loadTsModule(resolved);
60
+
61
+ return requireFromFile(resolved);
62
+ }
63
+
64
+ return requireFromFile(specifier);
65
+ };
66
+
67
+ const evaluate = new Function('require', 'module', 'exports', '__filename', '__dirname', transpiled);
68
+ evaluate(runtimeRequire, module, module.exports, normalizedFilepath, path.dirname(normalizedFilepath));
69
+
70
+ moduleCache.set(normalizedFilepath, module.exports);
71
+
72
+ return module.exports;
73
+ };
74
+
75
+ const getDefaultExport = <T>(value: unknown): T => {
76
+ if (value && typeof value === 'object' && 'default' in (value as Record<string, unknown>))
77
+ return (value as { default: T }).default;
78
+
79
+ return value as T;
80
+ };
81
+
82
+ export const identityConfigFilename = 'identity.config.ts';
83
+ export const setupConfigFilename = 'proteum.config.ts';
84
+
85
+ export const resolveIdentityConfigFilepath = (appDir: string) => path.join(appDir, identityConfigFilename);
86
+ export const resolveSetupConfigFilepath = (appDir: string) => path.join(appDir, setupConfigFilename);
87
+
88
+ export const loadApplicationIdentityConfig = (appDir: string): TApplicationIdentityConfig => {
89
+ const filepath = resolveIdentityConfigFilepath(appDir);
90
+ if (!fs.existsSync(filepath)) throw new Error(`Missing ${identityConfigFilename} in ${appDir}.`);
91
+ loadOptionalProteumDotenv(appDir);
92
+
93
+ return normalizeApplicationIdentityConfig(getDefaultExport(loadTsModule(filepath)), filepath);
94
+ };
95
+
96
+ export const loadApplicationSetupConfig = (appDir: string): TApplicationSetupConfig => {
97
+ const filepath = resolveSetupConfigFilepath(appDir);
98
+ if (!fs.existsSync(filepath)) throw new Error(`Missing ${setupConfigFilename} in ${appDir}.`);
99
+ loadOptionalProteumDotenv(appDir);
100
+
101
+ return normalizeApplicationSetupConfig(getDefaultExport(loadTsModule(filepath)), filepath);
102
+ };