proteum 2.1.9 → 2.2.0
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/.codex/environments/environment.toml +11 -0
- package/AGENTS.md +25 -11
- package/README.md +19 -9
- package/agents/project/AGENTS.md +165 -120
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/app-root/AGENTS.md +16 -0
- package/agents/project/client/AGENTS.md +5 -5
- package/agents/project/client/pages/AGENTS.md +13 -13
- package/agents/project/diagnostics.md +19 -10
- package/agents/project/optimizations.md +5 -6
- package/agents/project/root/AGENTS.md +295 -0
- package/agents/project/server/routes/AGENTS.md +2 -2
- package/agents/project/server/services/AGENTS.md +4 -2
- package/agents/project/tests/AGENTS.md +2 -2
- package/cli/app/index.ts +31 -7
- package/cli/commands/configure.ts +226 -0
- package/cli/commands/dev.ts +0 -2
- package/cli/commands/diagnose.ts +33 -1
- package/cli/commands/explain.ts +1 -1
- package/cli/commands/migrate.ts +51 -0
- package/cli/commands/orient.ts +169 -0
- package/cli/commands/perf.ts +8 -1
- package/cli/commands/verify.ts +1003 -49
- package/cli/compiler/artifacts/manifest.ts +4 -4
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +12 -3
- package/cli/compiler/client/index.ts +65 -19
- package/cli/compiler/common/files/style.ts +47 -2
- package/cli/compiler/common/generatedRouteModules.ts +31 -38
- package/cli/compiler/common/index.ts +10 -0
- package/cli/compiler/common/proteumManifest.ts +1 -0
- package/cli/compiler/server/index.ts +34 -9
- package/cli/context.ts +6 -1
- package/cli/index.ts +7 -8
- package/cli/migrate/pageContract.ts +516 -0
- package/cli/paths.ts +47 -6
- package/cli/presentation/commands.ts +100 -10
- package/cli/presentation/devSession.ts +4 -6
- package/cli/presentation/help.ts +2 -2
- package/cli/presentation/ink.ts +10 -5
- package/cli/presentation/welcome.ts +2 -4
- package/cli/runtime/commands.ts +94 -1
- package/cli/scaffold/index.ts +2 -2
- package/cli/scaffold/templates.ts +4 -2
- package/cli/utils/agents.ts +273 -58
- package/client/dev/profiler/index.tsx +3 -2
- package/client/router.ts +10 -2
- package/client/services/router/index.tsx +6 -22
- package/common/dev/connect.ts +20 -4
- package/common/dev/console.ts +7 -0
- package/common/dev/contractsDoctor.ts +354 -0
- package/common/dev/diagnostics.ts +10 -7
- package/common/dev/inspection.ts +830 -38
- package/common/dev/performance.ts +19 -5
- package/common/dev/profiler.ts +1 -0
- package/common/dev/proteumManifest.ts +5 -4
- package/common/dev/requestTrace.ts +12 -1
- package/common/router/contracts.ts +8 -11
- package/common/router/index.ts +2 -2
- package/common/router/pageData.ts +72 -0
- package/common/router/register.ts +10 -46
- package/common/router/response/page.ts +28 -16
- package/docs/dev-sessions.md +8 -4
- package/docs/diagnostics.md +77 -11
- package/docs/migrate-from-2.1.3.md +388 -0
- package/docs/request-tracing.md +25 -6
- package/package.json +6 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/app/container/console/index.ts +11 -1
- package/server/app/container/trace/index.ts +117 -0
- package/server/app/devDiagnostics.ts +1 -1
- package/server/app/index.ts +5 -1
- package/server/services/auth/index.ts +9 -0
- package/server/services/router/index.ts +64 -14
- package/server/services/router/request/api.ts +7 -1
- package/server/services/router/response/index.ts +8 -28
- package/types/global/vendors.d.ts +12 -0
- package/types/vendors.d.ts +12 -0
- package/common/router/pageSetup.ts +0 -51
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
TTraceSqlQuery,
|
|
8
8
|
TTraceSummaryValue,
|
|
9
9
|
} from './requestTrace';
|
|
10
|
+
import { buildRequestChain, explainOwner, type TDiagnoseChainItem } from './inspection';
|
|
11
|
+
import type { TProteumManifest } from './proteumManifest';
|
|
10
12
|
|
|
11
13
|
export const perfGroupByValues = ['path', 'route', 'controller'] as const;
|
|
12
14
|
export const perfWindowPresets = ['1h', '6h', '24h', 'today', 'yesterday'] as const;
|
|
@@ -38,11 +40,14 @@ export type TRequestPerfCall = {
|
|
|
38
40
|
export type TRequestPerfSql = {
|
|
39
41
|
callerLabel: string;
|
|
40
42
|
durationMs: number;
|
|
43
|
+
fingerprint?: string;
|
|
41
44
|
id: string;
|
|
42
45
|
kind: TTraceSqlQuery['kind'];
|
|
46
|
+
connectedNamespace?: string;
|
|
43
47
|
model?: string;
|
|
44
48
|
operation: string;
|
|
45
49
|
query: string;
|
|
50
|
+
serviceLabel?: string;
|
|
46
51
|
target?: string;
|
|
47
52
|
};
|
|
48
53
|
|
|
@@ -53,6 +58,7 @@ export type TRequestPerformance = {
|
|
|
53
58
|
callDurationMs: number;
|
|
54
59
|
capture: TRequestTrace['capture'];
|
|
55
60
|
controllerLabel: string;
|
|
61
|
+
chain?: TDiagnoseChainItem[];
|
|
56
62
|
cpuSystemMs?: number;
|
|
57
63
|
cpuTotalMs?: number;
|
|
58
64
|
cpuUserMs?: number;
|
|
@@ -321,12 +327,16 @@ const formatCallLabel = (call: TTraceCall) => {
|
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
const reference = `${call.method} ${call.path}`.trim();
|
|
330
|
+
if (call.serviceLabel && call.label && reference) return `${call.serviceLabel} -> ${call.label} (${reference})`;
|
|
331
|
+
if (call.serviceLabel && reference) return `${call.serviceLabel} -> ${reference}`;
|
|
324
332
|
if (call.label && reference) return `${call.label} (${reference})`;
|
|
325
333
|
return call.label || reference || call.origin;
|
|
326
334
|
};
|
|
327
335
|
|
|
328
336
|
const formatSqlCallerLabel = (query: TTraceSqlQuery) => {
|
|
329
337
|
const reference = `${query.callerMethod} ${query.callerPath}`.trim();
|
|
338
|
+
if (query.serviceLabel && query.callerLabel && reference) return `${query.serviceLabel} -> ${query.callerLabel} (${reference})`;
|
|
339
|
+
if (query.serviceLabel && reference) return `${query.serviceLabel} -> ${reference}`;
|
|
330
340
|
if (query.callerLabel && reference) return `${query.callerLabel} (${reference})`;
|
|
331
341
|
return query.callerLabel || reference || query.operation;
|
|
332
342
|
};
|
|
@@ -472,7 +482,7 @@ const summarizeProfiles = (profiles: TRequestPerformance[]): TPerfTopSummary =>
|
|
|
472
482
|
};
|
|
473
483
|
};
|
|
474
484
|
|
|
475
|
-
export const buildRequestPerformance = (trace: TRequestTrace): TRequestPerformance => {
|
|
485
|
+
export const buildRequestPerformance = (trace: TRequestTrace, manifest?: TProteumManifest): TRequestPerformance => {
|
|
476
486
|
const performance = trace.performance;
|
|
477
487
|
const cpuUserMs = performance ? performance.cpu.userMicros / 1000 : undefined;
|
|
478
488
|
const cpuSystemMs = performance ? performance.cpu.systemMicros / 1000 : undefined;
|
|
@@ -503,6 +513,7 @@ export const buildRequestPerformance = (trace: TRequestTrace): TRequestPerforman
|
|
|
503
513
|
callDurationMs,
|
|
504
514
|
capture: trace.capture,
|
|
505
515
|
controllerLabel: findControllerLabel(trace),
|
|
516
|
+
chain: manifest ? buildRequestChain({ manifest, owner: explainOwner(manifest, trace.path), request: trace }) : undefined,
|
|
506
517
|
cpuSystemMs,
|
|
507
518
|
cpuTotalMs,
|
|
508
519
|
cpuUserMs,
|
|
@@ -534,11 +545,14 @@ export const buildRequestPerformance = (trace: TRequestTrace): TRequestPerforman
|
|
|
534
545
|
.map((query) => ({
|
|
535
546
|
callerLabel: formatSqlCallerLabel(query),
|
|
536
547
|
durationMs: query.durationMs,
|
|
548
|
+
fingerprint: query.fingerprint,
|
|
537
549
|
id: query.id,
|
|
538
550
|
kind: query.kind,
|
|
551
|
+
connectedNamespace: query.connectedNamespace,
|
|
539
552
|
model: query.model,
|
|
540
553
|
operation: query.operation,
|
|
541
554
|
query: query.query,
|
|
555
|
+
serviceLabel: query.serviceLabel,
|
|
542
556
|
target: query.target,
|
|
543
557
|
})),
|
|
544
558
|
htmlBytes: readNumber(renderEnd?.details.htmlLength),
|
|
@@ -582,7 +596,7 @@ export const buildPerfTopResponse = ({
|
|
|
582
596
|
}): TPerfTopResponse => {
|
|
583
597
|
const selectedGroupBy = perfGroupByValues.includes(groupBy) ? groupBy : 'path';
|
|
584
598
|
const { requests: filteredRequests, window } = selectRequestsInWindow(requests, since);
|
|
585
|
-
const profiles = filteredRequests.map(buildRequestPerformance);
|
|
599
|
+
const profiles = filteredRequests.map((trace) => buildRequestPerformance(trace));
|
|
586
600
|
const groups = new Map<string, TRequestPerformance[]>();
|
|
587
601
|
|
|
588
602
|
for (const profile of profiles) {
|
|
@@ -738,7 +752,7 @@ export const buildPerfMemoryResponse = ({
|
|
|
738
752
|
}): TPerfMemoryResponse => {
|
|
739
753
|
const selectedGroupBy = perfGroupByValues.includes(groupBy) ? groupBy : 'path';
|
|
740
754
|
const { requests: filteredRequests, window } = selectRequestsInWindow(requests, since);
|
|
741
|
-
const profiles = filteredRequests.map(buildRequestPerformance);
|
|
755
|
+
const profiles = filteredRequests.map((trace) => buildRequestPerformance(trace));
|
|
742
756
|
const groups = new Map<string, TRequestPerformance[]>();
|
|
743
757
|
|
|
744
758
|
for (const profile of profiles) {
|
|
@@ -793,7 +807,7 @@ export const buildPerfMemoryResponse = ({
|
|
|
793
807
|
};
|
|
794
808
|
};
|
|
795
809
|
|
|
796
|
-
export const resolvePerfRequest = (requests: TRequestTrace[], requestOrPath: string) => {
|
|
810
|
+
export const resolvePerfRequest = (requests: TRequestTrace[], requestOrPath: string, manifest?: TProteumManifest) => {
|
|
797
811
|
const normalized = requestOrPath.trim();
|
|
798
812
|
if (!normalized) throw new Error('Perf request id or path is required.');
|
|
799
813
|
|
|
@@ -805,5 +819,5 @@ export const resolvePerfRequest = (requests: TRequestTrace[], requestOrPath: str
|
|
|
805
819
|
throw new Error(`Could not find a traced request for "${requestOrPath}".`);
|
|
806
820
|
}
|
|
807
821
|
|
|
808
|
-
return buildRequestPerformance(request);
|
|
822
|
+
return buildRequestPerformance(request, manifest);
|
|
809
823
|
};
|
package/common/dev/profiler.ts
CHANGED
|
@@ -4,6 +4,7 @@ export const profilerTraceRequestIdHeader = 'x-proteum-trace-request-id';
|
|
|
4
4
|
export const profilerSessionIdHeader = 'x-proteum-profiler-session-id';
|
|
5
5
|
export const profilerOriginHeader = 'x-proteum-profiler-origin';
|
|
6
6
|
export const profilerParentRequestIdHeader = 'x-proteum-profiler-parent-request-id';
|
|
7
|
+
export const profilerConnectedNamespaceHeader = 'x-proteum-profiler-connected-namespace';
|
|
7
8
|
|
|
8
9
|
export type TProfilerUiState = 'expanded' | 'minimized' | 'pinned-handle';
|
|
9
10
|
export type TProfilerPanel =
|
|
@@ -14,6 +14,7 @@ export type TProteumManifestDiagnostic = {
|
|
|
14
14
|
message: string;
|
|
15
15
|
filepath: string;
|
|
16
16
|
sourceLocation?: TProteumManifestSourceLocation;
|
|
17
|
+
fixHint?: string;
|
|
17
18
|
relatedFilepaths?: string[];
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -86,7 +87,7 @@ export type TProteumManifestRoute = {
|
|
|
86
87
|
invalidOptionKeys: string[];
|
|
87
88
|
reservedOptionKeys: string[];
|
|
88
89
|
optionsRaw?: string;
|
|
89
|
-
|
|
90
|
+
hasData: boolean;
|
|
90
91
|
chunkId?: string;
|
|
91
92
|
chunkFilepath?: string;
|
|
92
93
|
scope: TProteumManifestScope;
|
|
@@ -101,7 +102,7 @@ export type TProteumManifestLayout = {
|
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
export type TProteumManifest = {
|
|
104
|
-
version:
|
|
105
|
+
version: 10;
|
|
105
106
|
app: {
|
|
106
107
|
root: string;
|
|
107
108
|
coreRoot: string;
|
|
@@ -125,8 +126,8 @@ export type TProteumManifest = {
|
|
|
125
126
|
};
|
|
126
127
|
};
|
|
127
128
|
conventions: {
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
routeOptionKeys: string[];
|
|
130
|
+
reservedRouteOptionKeys: string[];
|
|
130
131
|
};
|
|
131
132
|
env: {
|
|
132
133
|
source: string;
|
|
@@ -28,7 +28,6 @@ export const traceEventTypes = [
|
|
|
28
28
|
'resolve.not-found',
|
|
29
29
|
'controller.start',
|
|
30
30
|
'controller.result',
|
|
31
|
-
'setup.options',
|
|
32
31
|
'context.create',
|
|
33
32
|
'page.data',
|
|
34
33
|
'ssr.payload',
|
|
@@ -36,6 +35,8 @@ export const traceEventTypes = [
|
|
|
36
35
|
'render.end',
|
|
37
36
|
'response.send',
|
|
38
37
|
'request.finish',
|
|
38
|
+
'cache.hit',
|
|
39
|
+
'cache.write',
|
|
39
40
|
'error',
|
|
40
41
|
] as const;
|
|
41
42
|
|
|
@@ -81,6 +82,11 @@ export type TTraceCall = {
|
|
|
81
82
|
fetcherId?: string;
|
|
82
83
|
connectedProjectNamespace?: string;
|
|
83
84
|
connectedControllerAccessor?: string;
|
|
85
|
+
ownerLabel?: string;
|
|
86
|
+
ownerFilepath?: string;
|
|
87
|
+
serviceLabel?: string;
|
|
88
|
+
cacheKey?: string;
|
|
89
|
+
cachePhase?: string;
|
|
84
90
|
startedAt: string;
|
|
85
91
|
finishedAt?: string;
|
|
86
92
|
durationMs?: number;
|
|
@@ -107,6 +113,11 @@ export type TTraceSqlQuery = {
|
|
|
107
113
|
kind: TTraceSqlQueryKind;
|
|
108
114
|
model?: string;
|
|
109
115
|
operation: string;
|
|
116
|
+
fingerprint?: string;
|
|
117
|
+
ownerLabel?: string;
|
|
118
|
+
ownerFilepath?: string;
|
|
119
|
+
serviceLabel?: string;
|
|
120
|
+
connectedNamespace?: string;
|
|
110
121
|
paramsJson?: unknown;
|
|
111
122
|
paramsText?: string;
|
|
112
123
|
query: string;
|
|
@@ -3,24 +3,21 @@
|
|
|
3
3
|
----------------------------------*/
|
|
4
4
|
|
|
5
5
|
// Core
|
|
6
|
-
import type { TFrontRenderer,
|
|
6
|
+
import type { TFrontRenderer, TPageDataProvider } from './response/page';
|
|
7
7
|
import type { TRouteOptions } from '.';
|
|
8
8
|
|
|
9
9
|
/*----------------------------------
|
|
10
10
|
- PUBLIC API
|
|
11
11
|
----------------------------------*/
|
|
12
12
|
|
|
13
|
-
// Supported `Router.page(...)` registration
|
|
13
|
+
// Supported `Router.page(...)` registration signature shared by client and compiler code.
|
|
14
14
|
export type TRegisterPageArgs<TProvidedData extends {} = {}, TPageOptions extends {} = TRouteOptions> =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
setup: TPageSetup<TProvidedData>,
|
|
22
|
-
renderer: TFrontRenderer<TProvidedData>,
|
|
23
|
-
];
|
|
15
|
+
[
|
|
16
|
+
path: string,
|
|
17
|
+
options: Partial<TPageOptions>,
|
|
18
|
+
data: TPageDataProvider<TProvidedData> | null,
|
|
19
|
+
renderer: TFrontRenderer<TProvidedData>,
|
|
20
|
+
];
|
|
24
21
|
|
|
25
22
|
// Serialized SSR route description exchanged between build output and runtime.
|
|
26
23
|
export type TSsrUnresolvedRoute<TKey = number | string> = { chunk: string } & (
|
package/common/router/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { TAuthCheckInput, TAuthTrackingContext } from '@server/services/aut
|
|
|
18
18
|
import type { TAppArrowFunction } from '@common/app';
|
|
19
19
|
|
|
20
20
|
// Specfic
|
|
21
|
-
import type { default as Page, TFrontRenderer,
|
|
21
|
+
import type { default as Page, TFrontRenderer, TPageDataProvider } from './response/page';
|
|
22
22
|
|
|
23
23
|
/*----------------------------------
|
|
24
24
|
- TYPES: ROUTES
|
|
@@ -40,6 +40,7 @@ type TRouteBase<RouterContext = unknown, TResult = any> = {
|
|
|
40
40
|
|
|
41
41
|
// Execute
|
|
42
42
|
schema?: zod.ZodSchema;
|
|
43
|
+
data?: TPageDataProvider | null;
|
|
43
44
|
controller: TRouteController<RouterContext, TResult>;
|
|
44
45
|
options: TRouteOptions;
|
|
45
46
|
};
|
|
@@ -76,7 +77,6 @@ export type TRouteOptions = {
|
|
|
76
77
|
id?: string;
|
|
77
78
|
filepath?: string;
|
|
78
79
|
sourceLocation?: { line: number; column: number };
|
|
79
|
-
setup?: TPageSetup;
|
|
80
80
|
|
|
81
81
|
// Indexing
|
|
82
82
|
bodyId?: string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- TYPES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
import type { TAnyRoute, TRouteOptions } from '.';
|
|
6
|
+
|
|
7
|
+
export const routeOptionKeys = [
|
|
8
|
+
'bodyId',
|
|
9
|
+
'priority',
|
|
10
|
+
'preload',
|
|
11
|
+
'domain',
|
|
12
|
+
'accept',
|
|
13
|
+
'raw',
|
|
14
|
+
'auth',
|
|
15
|
+
'authTracking',
|
|
16
|
+
'redirectLogged',
|
|
17
|
+
'static',
|
|
18
|
+
'whenStatic',
|
|
19
|
+
'canonicalParams',
|
|
20
|
+
'layout',
|
|
21
|
+
'TESTING',
|
|
22
|
+
'logging',
|
|
23
|
+
] as const satisfies (keyof TRouteOptions)[];
|
|
24
|
+
|
|
25
|
+
export const reservedRouteOptionKeys = ['id', 'filepath', 'sourceLocation', 'data'] as const;
|
|
26
|
+
|
|
27
|
+
const routeOptionKeysSet = new Set<string>(routeOptionKeys);
|
|
28
|
+
const reservedRouteOptionKeysSet = new Set<string>(reservedRouteOptionKeys);
|
|
29
|
+
const reservedPageDataKeys = new Set<string>([
|
|
30
|
+
...routeOptionKeys,
|
|
31
|
+
...reservedRouteOptionKeys,
|
|
32
|
+
...routeOptionKeys.map((key) => `_${key}`),
|
|
33
|
+
...reservedRouteOptionKeys.map((key) => `_${key}`),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const formatRouteTarget = (route: TAnyRoute) => ('code' in route ? String(route.code) : route.path || '(unknown route)');
|
|
37
|
+
|
|
38
|
+
const formatRouteSource = (route: TAnyRoute) => {
|
|
39
|
+
const filepath = route.options.filepath || 'unknown file';
|
|
40
|
+
const line = route.options.sourceLocation?.line;
|
|
41
|
+
const column = route.options.sourceLocation?.column;
|
|
42
|
+
|
|
43
|
+
if (!line) return filepath;
|
|
44
|
+
if (!column) return `${filepath}:${line}`;
|
|
45
|
+
return `${filepath}:${line}:${column}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getRouteOptionKey = (key: string) => {
|
|
49
|
+
if (reservedRouteOptionKeysSet.has(key)) throw new Error(`"${key}" is a reserved Router.page option key.`);
|
|
50
|
+
|
|
51
|
+
return routeOptionKeysSet.has(key) ? (key as keyof TRouteOptions) : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const validatePageDataResult = (route: TAnyRoute, result: unknown) => {
|
|
55
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} must return an object. ` +
|
|
58
|
+
`If the page has no data loader, pass null as the third argument.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const key of Object.keys(result)) {
|
|
63
|
+
if (!reservedPageDataKeys.has(key)) continue;
|
|
64
|
+
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Router.page data for ${formatRouteTarget(route)} in ${formatRouteSource(route)} cannot return reserved key "${key}". ` +
|
|
67
|
+
`Move route behavior into the explicit Router.page(path, options, data, render) options argument.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result as TObjetDonnees;
|
|
72
|
+
};
|
|
@@ -11,65 +11,29 @@ import type { TRegisterPageArgs } from './contracts';
|
|
|
11
11
|
|
|
12
12
|
// types
|
|
13
13
|
import type { TRouteOptions } from '.';
|
|
14
|
-
import type {
|
|
14
|
+
import type { TPageDataProvider } from './response/page';
|
|
15
15
|
|
|
16
16
|
/*----------------------------------
|
|
17
17
|
- UTILS
|
|
18
18
|
----------------------------------*/
|
|
19
19
|
|
|
20
20
|
export const getRegisterPageArgs = (...args: TRegisterPageArgs<any, TRouteOptions>) => {
|
|
21
|
-
|
|
22
|
-
let options: Partial<TRouteOptions> = {};
|
|
23
|
-
let setup: TPageSetup | undefined;
|
|
24
|
-
let renderer: TFrontRenderer;
|
|
21
|
+
const [path, options, data, renderer] = args;
|
|
25
22
|
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const [pathArg, optionsOrSetupArg, rendererArg] = args;
|
|
30
|
-
path = pathArg;
|
|
31
|
-
renderer = rendererArg;
|
|
23
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
24
|
+
throw new Error(`Router.page(${JSON.stringify(path)}) requires an explicit options object as its second argument.`);
|
|
25
|
+
}
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
path = pathArg;
|
|
38
|
-
options = optionsArg;
|
|
39
|
-
setup = setupArg;
|
|
40
|
-
renderer = rendererArg;
|
|
27
|
+
if (data !== null && typeof data !== 'function') {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Router.page(${JSON.stringify(path)}) requires a data function or null as its third argument.`,
|
|
30
|
+
);
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
// Automatic layout form the nearest _layout folder using static options only.
|
|
44
34
|
const layout = getLayout(path, options);
|
|
45
35
|
|
|
46
|
-
return { path, options,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const getRegisterPageOptions = (...args: TRegisterPageArgs<any, TRouteOptions>) => {
|
|
50
|
-
let path: string;
|
|
51
|
-
let options: Partial<TRouteOptions> = {};
|
|
52
|
-
let setup: TPageSetup | undefined;
|
|
53
|
-
let renderer: TFrontRenderer;
|
|
54
|
-
|
|
55
|
-
if (args.length === 2) {
|
|
56
|
-
[path, renderer] = args;
|
|
57
|
-
} else if (args.length === 3) {
|
|
58
|
-
const [pathArg, optionsOrSetupArg, rendererArg] = args;
|
|
59
|
-
path = pathArg;
|
|
60
|
-
renderer = rendererArg;
|
|
61
|
-
|
|
62
|
-
if (typeof optionsOrSetupArg === 'function') setup = optionsOrSetupArg;
|
|
63
|
-
else options = optionsOrSetupArg;
|
|
64
|
-
} else {
|
|
65
|
-
const [pathArg, optionsArg, setupArg, rendererArg] = args;
|
|
66
|
-
path = pathArg;
|
|
67
|
-
options = optionsArg;
|
|
68
|
-
setup = setupArg;
|
|
69
|
-
renderer = rendererArg;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { path, options, setup, renderer };
|
|
36
|
+
return { path, options, data: data as TPageDataProvider | null, renderer, layout };
|
|
73
37
|
};
|
|
74
38
|
|
|
75
39
|
export const buildRegex = (path: string) => {
|
|
@@ -8,27 +8,38 @@ import type { Thing } from 'schema-dts';
|
|
|
8
8
|
|
|
9
9
|
// Core libs
|
|
10
10
|
import type { ClientContext } from '@/client/context';
|
|
11
|
-
import { ClientOrServerRouter, TErrorRoute, TPageErrorRoute, TPageRoute, TRoute } from '@common/router';
|
|
11
|
+
import { ClientOrServerRouter, TErrorRoute, TPageErrorRoute, TPageRoute, TRoute, TRouteOptions } from '@common/router';
|
|
12
12
|
import type { TFetcher, TFetcherList } from '@common/router/request/api';
|
|
13
|
-
import {
|
|
13
|
+
import { validatePageDataResult } from '@common/router/pageData';
|
|
14
14
|
|
|
15
15
|
/*----------------------------------
|
|
16
16
|
- TYPES
|
|
17
17
|
----------------------------------*/
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type TPageDataContext = ClientContext;
|
|
20
20
|
|
|
21
21
|
export type TPageRenderContext = With<ClientContext, 'page'>;
|
|
22
22
|
|
|
23
|
+
type TPageResponseContext = {
|
|
24
|
+
route: { options: Partial<TRouteOptions> };
|
|
25
|
+
request: {
|
|
26
|
+
url: string;
|
|
27
|
+
data: TObjetDonnees;
|
|
28
|
+
api: {
|
|
29
|
+
fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
23
34
|
export type TResolvedPageData<TProvidedData extends {} = {}> = {
|
|
24
35
|
[Property in keyof TProvidedData]: TProvidedData[Property] extends TFetcher<infer TData>
|
|
25
36
|
? TData
|
|
26
37
|
: Awaited<TProvidedData[Property]>;
|
|
27
38
|
};
|
|
28
39
|
|
|
29
|
-
// The function that prepares
|
|
30
|
-
export type
|
|
31
|
-
context:
|
|
40
|
+
// The function that prepares SSR data before rendering.
|
|
41
|
+
export type TPageDataProvider<TProvidedData extends {} = {}> = (
|
|
42
|
+
context: TPageDataContext & {
|
|
32
43
|
// URL query parameters
|
|
33
44
|
// TODO: typings
|
|
34
45
|
data: { [key: string]: string | number };
|
|
@@ -36,7 +47,7 @@ export type TPageSetup<TProvidedData extends {} = {}> = (
|
|
|
36
47
|
) => TProvidedData;
|
|
37
48
|
|
|
38
49
|
export type TDataProvider<TProvidedData extends {} = TFetcherList> = (
|
|
39
|
-
context:
|
|
50
|
+
context: TPageDataContext & { data: { [key: string]: PrimitiveValue } },
|
|
40
51
|
) => TProvidedData;
|
|
41
52
|
|
|
42
53
|
// The function that renders routes
|
|
@@ -68,7 +79,7 @@ const debug = false;
|
|
|
68
79
|
export default abstract class PageResponse<
|
|
69
80
|
TRouter extends ClientOrServerRouter = ClientOrServerRouter,
|
|
70
81
|
TRouteLike extends TRoute | TErrorRoute = TPageRoute | TPageErrorRoute,
|
|
71
|
-
TContext extends
|
|
82
|
+
TContext extends TPageResponseContext = TPageResponseContext,
|
|
72
83
|
> {
|
|
73
84
|
// Metadata
|
|
74
85
|
public chunkId?: string;
|
|
@@ -100,18 +111,19 @@ export default abstract class PageResponse<
|
|
|
100
111
|
this.url = context.request.url;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
private
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
114
|
+
private resolveDataProviderResult() {
|
|
115
|
+
const dataProvider = 'data' in this.route ? this.route.data : undefined;
|
|
116
|
+
if (!dataProvider) return {};
|
|
106
117
|
|
|
107
|
-
const
|
|
118
|
+
const dataContext = { ...this.context, data: this.context.request.data } as unknown as Parameters<
|
|
119
|
+
typeof dataProvider
|
|
120
|
+
>[0];
|
|
108
121
|
|
|
109
|
-
return
|
|
122
|
+
return validatePageDataResult(this.route, dataProvider(dataContext));
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
private createFetchers() {
|
|
113
|
-
const
|
|
114
|
-
this.route.options = { ...this.route.options, ...options };
|
|
126
|
+
const data = this.resolveDataProviderResult();
|
|
115
127
|
this.chunkId = this.route.options.id;
|
|
116
128
|
|
|
117
129
|
return data as TFetcherList;
|
|
@@ -126,7 +138,7 @@ export default abstract class PageResponse<
|
|
|
126
138
|
const layoutContext = {
|
|
127
139
|
...this.context,
|
|
128
140
|
data: this.context.request.data,
|
|
129
|
-
} as Parameters<typeof this.layout.data>[0];
|
|
141
|
+
} as unknown as Parameters<typeof this.layout.data>[0];
|
|
130
142
|
const fetchers = this.layout.data(layoutContext);
|
|
131
143
|
this.fetchers = { ...this.fetchers, ...fetchers };
|
|
132
144
|
}
|
package/docs/dev-sessions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dev Sessions
|
|
2
2
|
|
|
3
|
-
Proteum ships a dev-only auth bootstrap command so
|
|
3
|
+
Proteum ships a dev-only auth bootstrap command so `proteum verify browser`, Playwright runs, and local debugging can start from an authenticated state without driving the login UI.
|
|
4
4
|
|
|
5
5
|
## When To Use It
|
|
6
6
|
|
|
@@ -37,6 +37,7 @@ Behavior:
|
|
|
37
37
|
- remote mode talks to an already running `proteum dev` instance
|
|
38
38
|
- the command requires an explicit email and optionally asserts a role before returning the session
|
|
39
39
|
- the command is available only in dev mode
|
|
40
|
+
- browser verification flows should keep browser state app-local and disposable through `proteum verify browser` or direct Playwright instead of reusing a shared temp profile
|
|
40
41
|
|
|
41
42
|
## Output Contract
|
|
42
43
|
|
|
@@ -67,20 +68,23 @@ curl -H "$(jq -r '.curlCookieHeader' session.json)" http://localhost:3101/api/Au
|
|
|
67
68
|
## Agent Guidance
|
|
68
69
|
|
|
69
70
|
- Prefer `proteum session` over UI login automation when the goal is to test or debug protected application behavior.
|
|
71
|
+
- Prefer `proteum verify browser` for focused browser-visible verification. When lower-level control is required, use direct Playwright with a disposable profile.
|
|
70
72
|
- Use UI login automation only when the auth UX itself is the feature under test.
|
|
71
73
|
- Pair it with `proteum diagnose` for a fast protected-route summary, `proteum perf request` for a one-request timing breakdown, then use `proteum trace` when you need lower-level request events.
|
|
74
|
+
- Only the final verifier agent should usually run browser flows. Earlier agents should stay on `orient`, `verify owner`, `verify request`, and request-level diagnostics unless browser execution is required.
|
|
72
75
|
|
|
73
76
|
Typical flow:
|
|
74
77
|
|
|
75
78
|
```bash
|
|
76
|
-
proteum
|
|
79
|
+
proteum orient /dashboard
|
|
77
80
|
proteum session admin@example.com --role ADMIN --port 3101 --json > session.json
|
|
78
|
-
|
|
79
|
-
proteum diagnose /dashboard --port 3101
|
|
81
|
+
proteum diagnose /dashboard --hit /dashboard --port 3101
|
|
80
82
|
proteum perf request /dashboard --port 3101
|
|
81
83
|
proteum trace latest --port 3101
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
When `proteum verify browser <path>` is available in the target app, it uses the same fresh per-run browser workspace model under `var/proteum/browser/<run-id>` and should be preferred over ad hoc shared Playwright profile reuse.
|
|
87
|
+
|
|
84
88
|
## Dev HTTP Endpoint
|
|
85
89
|
|
|
86
90
|
The CLI uses the same dev-only endpoint exposed by the running app:
|