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.
- package/AGENTS.md +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|