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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
3
4
|
|
|
4
5
|
import type ApplicationContainer from '..';
|
|
6
|
+
import context from '@server/context';
|
|
7
|
+
import type { ChannelInfos } from '../console';
|
|
5
8
|
import {
|
|
6
9
|
traceCaptureModes,
|
|
7
10
|
type TTraceCaptureMode,
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
type TTraceMemorySnapshot,
|
|
18
21
|
type TRequestTraceListItem,
|
|
19
22
|
} from '@common/dev/requestTrace';
|
|
23
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
20
24
|
|
|
21
25
|
export type Config = {
|
|
22
26
|
enable: boolean;
|
|
@@ -33,6 +37,10 @@ const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve
|
|
|
33
37
|
const sensitiveKeyPattern =
|
|
34
38
|
/(^|\.)(authorization|cookie|set-cookie|password|pass|pwd|secret|token|refreshToken|accessToken|apiKey|apiSecret|secretAccessKey|accessKeyId|privateKey|session|jwt|rawBody)$/i;
|
|
35
39
|
const maxStringLength = 240;
|
|
40
|
+
const normalizeFilepath = (value: string) => value.replace(/\\/g, '/');
|
|
41
|
+
const sqlCommentPattern = /\/\*[\s\S]*?\*\//g;
|
|
42
|
+
const sqlLineCommentPattern = /--.*$/gm;
|
|
43
|
+
const stackFilepathPatterns = [/\((\/.+?):\d+:\d+\)$/, /at (\/.+?):\d+:\d+$/];
|
|
36
44
|
|
|
37
45
|
const isTraceCaptureMode = (value: string): value is TTraceCaptureMode =>
|
|
38
46
|
traceCaptureModes.includes(value as TTraceCaptureMode);
|
|
@@ -176,6 +184,11 @@ export default class Trace {
|
|
|
176
184
|
private requests = new Map<string, TRequestTrace>();
|
|
177
185
|
private order: string[] = [];
|
|
178
186
|
private armedCapture?: TTraceCaptureMode;
|
|
187
|
+
private manifestCache?: {
|
|
188
|
+
manifest: TProteumManifest;
|
|
189
|
+
mtimeMs: number;
|
|
190
|
+
serviceByFilepath: Map<string, string>;
|
|
191
|
+
};
|
|
179
192
|
private activeMeasurements = new Map<
|
|
180
193
|
string,
|
|
181
194
|
{
|
|
@@ -193,6 +206,86 @@ export default class Trace {
|
|
|
193
206
|
return __DEV__ && this.config.enable && this.container.Environment.profile === 'dev';
|
|
194
207
|
}
|
|
195
208
|
|
|
209
|
+
private getContextChannel() {
|
|
210
|
+
return context.getStore() as ChannelInfos | undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private readManifestCache() {
|
|
214
|
+
const manifestFilepath = path.join(this.container.path.root, '.proteum', 'manifest.json');
|
|
215
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
216
|
+
|
|
217
|
+
const stats = fs.statSync(manifestFilepath);
|
|
218
|
+
if (this.manifestCache && this.manifestCache.mtimeMs === stats.mtimeMs) return this.manifestCache;
|
|
219
|
+
|
|
220
|
+
const manifest = fs.readJSONSync(manifestFilepath) as TProteumManifest;
|
|
221
|
+
const serviceByFilepath = new Map<string, string>();
|
|
222
|
+
for (const service of [...manifest.services.app, ...manifest.services.routerPlugins]) {
|
|
223
|
+
if (!service.sourceFilepath) continue;
|
|
224
|
+
serviceByFilepath.set(normalizeFilepath(path.resolve(service.sourceFilepath)), service.registeredName);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.manifestCache = {
|
|
228
|
+
manifest,
|
|
229
|
+
mtimeMs: stats.mtimeMs,
|
|
230
|
+
serviceByFilepath,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return this.manifestCache;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private getStackFilepaths(stack?: string) {
|
|
237
|
+
if (!stack) return [];
|
|
238
|
+
|
|
239
|
+
const filepaths: string[] = [];
|
|
240
|
+
for (const line of stack.split('\n')) {
|
|
241
|
+
const trimmedLine = line.trim();
|
|
242
|
+
let matchedFilepath: string | undefined;
|
|
243
|
+
|
|
244
|
+
for (const pattern of stackFilepathPatterns) {
|
|
245
|
+
const match = trimmedLine.match(pattern);
|
|
246
|
+
if (match?.[1]) {
|
|
247
|
+
matchedFilepath = match[1];
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!matchedFilepath) continue;
|
|
253
|
+
if (matchedFilepath.includes('/node_modules/')) continue;
|
|
254
|
+
|
|
255
|
+
filepaths.push(normalizeFilepath(path.resolve(matchedFilepath)));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return filepaths;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private inferServiceLabelFromStack(stack?: string) {
|
|
262
|
+
const manifestCache = this.readManifestCache();
|
|
263
|
+
if (!manifestCache) return undefined;
|
|
264
|
+
|
|
265
|
+
for (const filepath of this.getStackFilepaths(stack)) {
|
|
266
|
+
const serviceLabel = manifestCache.serviceByFilepath.get(filepath);
|
|
267
|
+
if (serviceLabel) return serviceLabel;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private createSqlFingerprint(query: string) {
|
|
274
|
+
const normalized = query
|
|
275
|
+
.replace(sqlCommentPattern, ' ')
|
|
276
|
+
.replace(sqlLineCommentPattern, ' ')
|
|
277
|
+
.replace(/'([^']|'')*'/g, '?')
|
|
278
|
+
.replace(/"([^"]|"")*"/g, '?')
|
|
279
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, '?')
|
|
280
|
+
.replace(/\s+/g, ' ')
|
|
281
|
+
.trim()
|
|
282
|
+
.toUpperCase();
|
|
283
|
+
|
|
284
|
+
if (!normalized) return undefined;
|
|
285
|
+
|
|
286
|
+
return createHash('sha1').update(normalized).digest('hex').slice(0, 12);
|
|
287
|
+
}
|
|
288
|
+
|
|
196
289
|
public armNextRequest(capture: string) {
|
|
197
290
|
if (!isTraceCaptureMode(capture)) {
|
|
198
291
|
throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
|
|
@@ -330,6 +423,11 @@ export default class Trace {
|
|
|
330
423
|
fetcherId?: string;
|
|
331
424
|
connectedProjectNamespace?: string;
|
|
332
425
|
connectedControllerAccessor?: string;
|
|
426
|
+
ownerLabel?: string;
|
|
427
|
+
ownerFilepath?: string;
|
|
428
|
+
serviceLabel?: string;
|
|
429
|
+
cacheKey?: string;
|
|
430
|
+
cachePhase?: string;
|
|
333
431
|
parentId?: string;
|
|
334
432
|
requestDataKeys?: string[];
|
|
335
433
|
requestData?: TTraceInspectable;
|
|
@@ -337,6 +435,8 @@ export default class Trace {
|
|
|
337
435
|
) {
|
|
338
436
|
const trace = this.requests.get(requestId);
|
|
339
437
|
if (!trace) return undefined;
|
|
438
|
+
const channel = this.getContextChannel();
|
|
439
|
+
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
340
440
|
|
|
341
441
|
const call: TTraceCall = {
|
|
342
442
|
id: `${requestId}:call:${trace.calls.length}`,
|
|
@@ -348,6 +448,11 @@ export default class Trace {
|
|
|
348
448
|
fetcherId: input.fetcherId,
|
|
349
449
|
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
350
450
|
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
451
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
452
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
453
|
+
serviceLabel: inferredServiceLabel,
|
|
454
|
+
cacheKey: input.cacheKey || channel?.cacheKey,
|
|
455
|
+
cachePhase: input.cachePhase || channel?.cachePhase,
|
|
351
456
|
startedAt: nowIso(),
|
|
352
457
|
requestDataKeys: input.requestDataKeys || [],
|
|
353
458
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
@@ -405,6 +510,10 @@ export default class Trace {
|
|
|
405
510
|
kind: TTraceSqlQueryKind;
|
|
406
511
|
model?: string;
|
|
407
512
|
operation: string;
|
|
513
|
+
ownerLabel?: string;
|
|
514
|
+
ownerFilepath?: string;
|
|
515
|
+
serviceLabel?: string;
|
|
516
|
+
connectedNamespace?: string;
|
|
408
517
|
paramsJson?: unknown;
|
|
409
518
|
paramsText?: string;
|
|
410
519
|
query: string;
|
|
@@ -413,12 +522,15 @@ export default class Trace {
|
|
|
413
522
|
) {
|
|
414
523
|
const trace = this.requests.get(requestId);
|
|
415
524
|
if (!trace) return;
|
|
525
|
+
const channel = this.getContextChannel();
|
|
416
526
|
|
|
417
527
|
const durationMs = Math.max(0, input.durationMs || 0);
|
|
418
528
|
const finishedAt = input.finishedAt || nowIso();
|
|
419
529
|
const finishedAtMs = Date.parse(finishedAt);
|
|
420
530
|
const startedAt =
|
|
421
531
|
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
532
|
+
const fingerprint = this.createSqlFingerprint(input.query);
|
|
533
|
+
const inferredServiceLabel = input.serviceLabel || channel?.serviceLabel || this.inferServiceLabelFromStack(new Error().stack);
|
|
422
534
|
|
|
423
535
|
const sqlQuery: TTraceSqlQuery = {
|
|
424
536
|
id: `${requestId}:sql:${trace.sqlQueries.length}`,
|
|
@@ -433,6 +545,11 @@ export default class Trace {
|
|
|
433
545
|
kind: input.kind,
|
|
434
546
|
model: input.model,
|
|
435
547
|
operation: input.operation,
|
|
548
|
+
fingerprint,
|
|
549
|
+
ownerLabel: input.ownerLabel || channel?.ownerLabel,
|
|
550
|
+
ownerFilepath: input.ownerFilepath || channel?.ownerFilepath,
|
|
551
|
+
serviceLabel: inferredServiceLabel,
|
|
552
|
+
connectedNamespace: input.connectedNamespace || channel?.connectedNamespace,
|
|
436
553
|
paramsJson: input.paramsJson,
|
|
437
554
|
paramsText: input.paramsText,
|
|
438
555
|
query: input.query.trim(),
|
|
@@ -186,6 +186,6 @@ export default class DevDiagnosticsRegistry<TApplication extends Application = A
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
public perfRequest(requestIdOrPath: string): TPerfRequestResponse {
|
|
189
|
-
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath) };
|
|
189
|
+
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath, this.readManifest()) };
|
|
190
190
|
}
|
|
191
191
|
}
|
package/server/app/index.ts
CHANGED
|
@@ -202,7 +202,11 @@ export abstract class Application<
|
|
|
202
202
|
const connectedProject = this.getConnectedProject(namespace);
|
|
203
203
|
if (connectedProject) return connectedProject;
|
|
204
204
|
|
|
205
|
-
throw new Error(
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Proteum connected boundary mismatch: "${namespace}" is not configured on ${this.identity.identifier}. ` +
|
|
207
|
+
`Likely fix: add connect.${namespace} in proteum.config.ts for this app or stop calling that connected namespace from this side. ` +
|
|
208
|
+
`Re-check both SSR and client navigation if the namespace is used from page render or setup code.`,
|
|
209
|
+
);
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
public register(service: AnyService) {
|
|
@@ -853,6 +853,9 @@ export default abstract class AuthService<
|
|
|
853
853
|
return user as TUser;
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
+
/**
|
|
857
|
+
* @deprecated Use `check(request, null, tracking)` to make the authenticated-user requirement explicit.
|
|
858
|
+
*/
|
|
856
859
|
public check(request: TRequest): TUser;
|
|
857
860
|
|
|
858
861
|
public check(request: TRequest, conditions: null, tracking?: TAuthTrackingContext): TUser;
|
|
@@ -861,8 +864,14 @@ export default abstract class AuthService<
|
|
|
861
864
|
|
|
862
865
|
public check(request: TRequest, conditions: false, tracking?: TAuthTrackingContext): null;
|
|
863
866
|
|
|
867
|
+
/**
|
|
868
|
+
* @deprecated Use `check(request, { role }, tracking)` or another explicit conditions object instead.
|
|
869
|
+
*/
|
|
864
870
|
public check(request: TRequest, role?: TUserRole | boolean): TUser | null;
|
|
865
871
|
|
|
872
|
+
/**
|
|
873
|
+
* @deprecated Use `check(request, { role, ...rules }, tracking)` with app-defined auth rules instead of legacy feature/action arguments.
|
|
874
|
+
*/
|
|
866
875
|
public check(request: TRequest, role: TUserRole | boolean, feature: FeatureKeys, action?: string): TUser | null;
|
|
867
876
|
|
|
868
877
|
public check(
|
|
@@ -37,6 +37,7 @@ import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/cont
|
|
|
37
37
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
38
38
|
import { layoutsList, getLayout } from '@common/router/layouts';
|
|
39
39
|
import {
|
|
40
|
+
profilerConnectedNamespaceHeader,
|
|
40
41
|
profilerOriginHeader,
|
|
41
42
|
profilerParentRequestIdHeader,
|
|
42
43
|
profilerSessionIdHeader,
|
|
@@ -221,6 +222,7 @@ export default class ServerRouter<
|
|
|
221
222
|
const methodName = match ? match[2] : '<anonymous>';*/
|
|
222
223
|
|
|
223
224
|
const contextData = context.getStore() || { channelType: 'master' };
|
|
225
|
+
if (contextData.silentLogs) return;
|
|
224
226
|
|
|
225
227
|
const requestPrefix =
|
|
226
228
|
contextData.channelType === 'request'
|
|
@@ -272,21 +274,18 @@ export default class ServerRouter<
|
|
|
272
274
|
|
|
273
275
|
public async renderStatic(url: string, options: TRouteOptions['static'], rendered?: any) {
|
|
274
276
|
// Wildcard: tell that the newly rendered pages should be cached
|
|
275
|
-
if (url === '*' || !url)
|
|
276
|
-
|
|
277
|
-
if (!rendered) {
|
|
278
|
-
console.log('[router] renderStatic: url', url);
|
|
277
|
+
if (url === '*' || !url) throw new Error(`Unable to cache a dynamic or empty URL.`);
|
|
279
278
|
|
|
279
|
+
if (rendered === undefined) {
|
|
280
280
|
const fullUrl = this.url(url, {}, true);
|
|
281
281
|
const response = await got(fullUrl, {
|
|
282
282
|
method: 'GET',
|
|
283
|
-
headers: { Accept: 'text/html', bypasscache: '1' },
|
|
283
|
+
headers: { Accept: 'text/html', bypasscache: '1', 'x-proteum-static-warmup': '1' },
|
|
284
284
|
throwHttpErrors: false,
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
if (response.statusCode !== 200) {
|
|
288
|
-
|
|
289
|
-
return;
|
|
288
|
+
throw new Error(`Static render returned ${response.statusCode} for ${fullUrl}`);
|
|
290
289
|
}
|
|
291
290
|
|
|
292
291
|
rendered = response.body;
|
|
@@ -301,17 +300,50 @@ export default class ServerRouter<
|
|
|
301
300
|
|
|
302
301
|
private initStaticRoutes() {
|
|
303
302
|
this.clearStaticRoutesRefreshInterval();
|
|
303
|
+
const staticEntries: Array<{ routePath: string; url: string; options: TRouteOptions['static'] }> = [];
|
|
304
|
+
const seenStaticUrls = new Set<string>();
|
|
304
305
|
|
|
305
306
|
for (const route of this.routes) {
|
|
307
|
+
if (route.method !== 'GET' || route.options.accept !== 'html') continue;
|
|
308
|
+
|
|
306
309
|
if (!route.options.static) continue;
|
|
307
310
|
|
|
308
311
|
// Add to static pages
|
|
309
312
|
// Should be a GET oage that don't take any parameter
|
|
310
313
|
for (const url of route.options.static.urls) {
|
|
311
|
-
|
|
314
|
+
if (!url || url === '*' || seenStaticUrls.has(url)) continue;
|
|
315
|
+
|
|
316
|
+
staticEntries.push({
|
|
317
|
+
routePath: route.path || '(unknown route)',
|
|
318
|
+
url,
|
|
319
|
+
options: route.options.static,
|
|
320
|
+
});
|
|
321
|
+
seenStaticUrls.add(url);
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
324
|
|
|
325
|
+
void (async () => {
|
|
326
|
+
const warmedUrls: string[] = [];
|
|
327
|
+
let failedCount = 0;
|
|
328
|
+
|
|
329
|
+
for (const entry of staticEntries) {
|
|
330
|
+
try {
|
|
331
|
+
await this.renderStatic(entry.url, entry.options);
|
|
332
|
+
warmedUrls.push(entry.url);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
failedCount += 1;
|
|
335
|
+
console.error('[router] Static warmup failed', entry.url, `route=${entry.routePath}`, error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
console.log(
|
|
340
|
+
'[router] Static warmup finished',
|
|
341
|
+
`warmed=${warmedUrls.length}`,
|
|
342
|
+
`failed=${failedCount}`,
|
|
343
|
+
`urls=${warmedUrls.length > 0 ? warmedUrls.join(', ') : 'none'}`,
|
|
344
|
+
);
|
|
345
|
+
})();
|
|
346
|
+
|
|
315
347
|
// Every hours, refresh static pages
|
|
316
348
|
this.staticRoutesRefreshInterval = setInterval(
|
|
317
349
|
() => {
|
|
@@ -327,7 +359,9 @@ export default class ServerRouter<
|
|
|
327
359
|
for (const pageUrl in this.cache) {
|
|
328
360
|
const page = this.cache[pageUrl];
|
|
329
361
|
if (page.expire && page.expire < Date.now()) {
|
|
330
|
-
this.renderStatic(pageUrl, page.options)
|
|
362
|
+
void this.renderStatic(pageUrl, page.options).catch((error) => {
|
|
363
|
+
console.error('[router] Static refresh failed', pageUrl, error);
|
|
364
|
+
});
|
|
331
365
|
}
|
|
332
366
|
}
|
|
333
367
|
}
|
|
@@ -382,7 +416,7 @@ export default class ServerRouter<
|
|
|
382
416
|
----------------------------------*/
|
|
383
417
|
|
|
384
418
|
public page(...args: TRegisterPageArgs<any, TRouteOptions>) {
|
|
385
|
-
const { path, options,
|
|
419
|
+
const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
|
|
386
420
|
|
|
387
421
|
const { regex, keys } = buildRegex(path);
|
|
388
422
|
|
|
@@ -391,10 +425,10 @@ export default class ServerRouter<
|
|
|
391
425
|
path,
|
|
392
426
|
regex,
|
|
393
427
|
keys,
|
|
428
|
+
data,
|
|
394
429
|
controller: (context: TRouterContext<this>) => new Page(route, renderer, context, layout),
|
|
395
430
|
options: this.buildRouteOptions({
|
|
396
431
|
accept: 'html', // Les pages retournent forcémment du html
|
|
397
|
-
setup,
|
|
398
432
|
...options,
|
|
399
433
|
}),
|
|
400
434
|
};
|
|
@@ -607,6 +641,14 @@ export default class ServerRouter<
|
|
|
607
641
|
profilerParentRequestId: request.headers[profilerParentRequestIdHeader] || undefined,
|
|
608
642
|
});
|
|
609
643
|
if (this.app.container.Trace.isEnabled()) res.setHeader(profilerTraceRequestIdHeader, request.id);
|
|
644
|
+
if (cachedPage) {
|
|
645
|
+
this.app.container.Trace.record(
|
|
646
|
+
request.id,
|
|
647
|
+
'cache.hit',
|
|
648
|
+
{ cacheKey: req.path, cachePhase: 'hit' },
|
|
649
|
+
'summary',
|
|
650
|
+
);
|
|
651
|
+
}
|
|
610
652
|
|
|
611
653
|
let response: ServerResponse<this>;
|
|
612
654
|
try {
|
|
@@ -747,8 +789,10 @@ export default class ServerRouter<
|
|
|
747
789
|
// This is for debugging
|
|
748
790
|
channelType: 'request',
|
|
749
791
|
channelId: request.id,
|
|
792
|
+
silentLogs: request.headers['x-proteum-static-warmup'] === '1',
|
|
750
793
|
method: request.method,
|
|
751
794
|
path: request.path,
|
|
795
|
+
connectedNamespace: request.headers[profilerConnectedNamespaceHeader] || undefined,
|
|
752
796
|
...(request.traceCall
|
|
753
797
|
? {
|
|
754
798
|
traceCallFetcherId: request.traceCall.fetcherId,
|
|
@@ -930,8 +974,6 @@ export default class ServerRouter<
|
|
|
930
974
|
});
|
|
931
975
|
|
|
932
976
|
private async resolvedRoute(route: TMatchedRoute, response: ServerResponse<this>, timeStart: number) {
|
|
933
|
-
route = await response.resolveRouteOptions(route);
|
|
934
|
-
|
|
935
977
|
this.app.container.Trace.record(
|
|
936
978
|
response.request.id,
|
|
937
979
|
'resolve.route-match',
|
|
@@ -964,11 +1006,19 @@ export default class ServerRouter<
|
|
|
964
1006
|
if (!staticUrl) continue;
|
|
965
1007
|
|
|
966
1008
|
console.log('[router] Set in cache', staticUrl);
|
|
1009
|
+
this.app.container.Trace.record(
|
|
1010
|
+
response.request.id,
|
|
1011
|
+
'cache.write',
|
|
1012
|
+
{ cacheKey: staticUrl, cachePhase: 'write' },
|
|
1013
|
+
'summary',
|
|
1014
|
+
);
|
|
967
1015
|
void this.renderStatic(
|
|
968
1016
|
staticUrl,
|
|
969
1017
|
route.options.static,
|
|
970
1018
|
staticUrl === response.request.path ? response.data : undefined,
|
|
971
|
-
)
|
|
1019
|
+
).catch((error) => {
|
|
1020
|
+
console.error('[router] Static cache write failed', staticUrl, error);
|
|
1021
|
+
});
|
|
972
1022
|
}
|
|
973
1023
|
}
|
|
974
1024
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { fromJson as errorFromJson } from '@common/errors';
|
|
8
8
|
import {
|
|
9
|
+
profilerConnectedNamespaceHeader,
|
|
9
10
|
profilerOriginHeader,
|
|
10
11
|
profilerParentRequestIdHeader,
|
|
11
12
|
profilerSessionIdHeader,
|
|
@@ -87,6 +88,7 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
87
88
|
|
|
88
89
|
if (fetcher.options?.connected) {
|
|
89
90
|
headers.set(profilerOriginHeader, this.getTraceCallOrigin());
|
|
91
|
+
headers.set(profilerConnectedNamespaceHeader, fetcher.options.connected.namespace);
|
|
90
92
|
|
|
91
93
|
const profilerSessionId = this.request.headers[profilerSessionIdHeader];
|
|
92
94
|
if (profilerSessionId) headers.set(profilerSessionIdHeader, profilerSessionId);
|
|
@@ -102,7 +104,11 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
102
104
|
|
|
103
105
|
const connectedProject = this.request.router.app.connectedProjects?.[connected.namespace];
|
|
104
106
|
if (!connectedProject) {
|
|
105
|
-
throw new Error(
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Proteum connected boundary mismatch: "${connected.namespace}" is not registered on ${this.request.router.app.identity.identifier}. ` +
|
|
109
|
+
`Likely fix: declare connect.${connected.namespace} in proteum.config.ts for the consumer app or stop using that connected controller accessor here. ` +
|
|
110
|
+
`Re-check both SSR and client navigation if this fetcher is used from a page data or render path.`,
|
|
111
|
+
);
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
const headers = this.buildConnectedRequestHeaders(fetcher);
|
|
@@ -17,7 +17,6 @@ import ServerRequest from '@server/services/router/request';
|
|
|
17
17
|
import { TMatchedRoute, TRoute, TAnyRoute } from '@common/router';
|
|
18
18
|
import { NotFound, Forbidden, Anomaly } from '@common/errors';
|
|
19
19
|
import BaseResponse, { TResponseData } from '@common/router/response';
|
|
20
|
-
import { splitRouteSetupResult } from '@common/router/pageSetup';
|
|
21
20
|
import Page from './page';
|
|
22
21
|
import createControllers from '@generated/common/controllers';
|
|
23
22
|
import type { TControllers } from '@generated/common/controllers';
|
|
@@ -154,11 +153,18 @@ export default class ServerResponse<
|
|
|
154
153
|
// Create response context for controllers
|
|
155
154
|
const requestContext = await this.createContext(route);
|
|
156
155
|
const contextStore = context.getStore() as
|
|
157
|
-
| {
|
|
156
|
+
| {
|
|
157
|
+
requestContext?: TRouterContext<TAnyRouter>;
|
|
158
|
+
inputSchemaUsed?: boolean;
|
|
159
|
+
ownerLabel?: string;
|
|
160
|
+
ownerFilepath?: string;
|
|
161
|
+
}
|
|
158
162
|
| undefined;
|
|
159
163
|
if (contextStore) {
|
|
160
164
|
contextStore.requestContext = requestContext;
|
|
161
165
|
contextStore.inputSchemaUsed = false;
|
|
166
|
+
contextStore.ownerLabel = getRouteTraceTarget(route as TAnyRoute<TRouterContext<TServerRouter>>);
|
|
167
|
+
contextStore.ownerFilepath = route.options.filepath || undefined;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
// Run controller
|
|
@@ -213,32 +219,6 @@ export default class ServerResponse<
|
|
|
213
219
|
- INTERNAL
|
|
214
220
|
----------------------------------*/
|
|
215
221
|
|
|
216
|
-
public async resolveRouteOptions(
|
|
217
|
-
route: TMatchedRoute<TRouterContext<TRouter>>,
|
|
218
|
-
): Promise<TMatchedRoute<TRouterContext<TRouter>>> {
|
|
219
|
-
const setup = route.options.setup;
|
|
220
|
-
if (!setup) return route;
|
|
221
|
-
|
|
222
|
-
const requestContext = await this.createContext(route);
|
|
223
|
-
const { options } = splitRouteSetupResult(((setup as any)({ ...requestContext, data: this.request.data }) as {}) || {});
|
|
224
|
-
|
|
225
|
-
this.app.container.Trace.record(
|
|
226
|
-
this.request.id,
|
|
227
|
-
'setup.options',
|
|
228
|
-
{
|
|
229
|
-
optionKeys: Object.keys(options),
|
|
230
|
-
source: {
|
|
231
|
-
filepath: route.options.filepath || '',
|
|
232
|
-
line: route.options.sourceLocation?.line || 0,
|
|
233
|
-
column: route.options.sourceLocation?.column || 0,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
'resolve',
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
return { ...route, options: { ...route.options, ...options } };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
222
|
// Start controller services
|
|
243
223
|
private async createContext(route: TAnyRoute<TRouterContext<TRouter>>): Promise<TRequestContext> {
|
|
244
224
|
const contextServices = this.router.createContextServices(this.request);
|
|
@@ -3,6 +3,18 @@ declare module 'accepts' {
|
|
|
3
3
|
export default accepts;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
declare module '@babel/generator' {
|
|
7
|
+
const generate: any;
|
|
8
|
+
export default generate;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '@babel/traverse' {
|
|
12
|
+
export type Binding = any;
|
|
13
|
+
export type NodePath<T = any> = any;
|
|
14
|
+
const traverse: any;
|
|
15
|
+
export default traverse;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
declare module 'bytes' {
|
|
7
19
|
const bytes: (value: string | number) => number;
|
|
8
20
|
export default bytes;
|
package/types/vendors.d.ts
CHANGED
|
@@ -3,6 +3,18 @@ declare module 'accepts' {
|
|
|
3
3
|
export default accepts;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
declare module '@babel/generator' {
|
|
7
|
+
const generate: any;
|
|
8
|
+
export default generate;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '@babel/traverse' {
|
|
12
|
+
export type Binding = any;
|
|
13
|
+
export type NodePath<T = any> = any;
|
|
14
|
+
const traverse: any;
|
|
15
|
+
export default traverse;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
declare module 'bytes' {
|
|
7
19
|
const bytes: (value: string | number) => number;
|
|
8
20
|
export default bytes;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- TYPES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
import type { TRouteOptions } from '.';
|
|
6
|
-
|
|
7
|
-
export const routeSetupOptionKeys = [
|
|
8
|
-
'priority',
|
|
9
|
-
'preload',
|
|
10
|
-
'domain',
|
|
11
|
-
'accept',
|
|
12
|
-
'raw',
|
|
13
|
-
'auth',
|
|
14
|
-
'authTracking',
|
|
15
|
-
'redirectLogged',
|
|
16
|
-
'static',
|
|
17
|
-
'whenStatic',
|
|
18
|
-
'canonicalParams',
|
|
19
|
-
'layout',
|
|
20
|
-
'TESTING',
|
|
21
|
-
'logging',
|
|
22
|
-
] as const satisfies (keyof TRouteOptions)[];
|
|
23
|
-
|
|
24
|
-
export const reservedRouteSetupKeys = ['id', 'filepath', 'bodyId', 'data', 'setup'] as const;
|
|
25
|
-
|
|
26
|
-
const routeSetupOptionKeysSet = new Set<string>(routeSetupOptionKeys);
|
|
27
|
-
const reservedRouteSetupKeysSet = new Set<string>(reservedRouteSetupKeys);
|
|
28
|
-
|
|
29
|
-
export const getRouteSetupOptionKey = (key: string) => {
|
|
30
|
-
const normalizedKey = key.startsWith('_') ? key.substring(1) : key;
|
|
31
|
-
|
|
32
|
-
if (reservedRouteSetupKeysSet.has(normalizedKey)) throw new Error(`"${key}" is a reserved Router.page setup key.`);
|
|
33
|
-
|
|
34
|
-
return routeSetupOptionKeysSet.has(normalizedKey) ? (normalizedKey as keyof TRouteOptions) : null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const splitRouteSetupResult = (result: TObjetDonnees | undefined) => {
|
|
38
|
-
const options: Partial<TRouteOptions> = {};
|
|
39
|
-
const data: TObjetDonnees = {};
|
|
40
|
-
|
|
41
|
-
if (!result) return { options, data };
|
|
42
|
-
|
|
43
|
-
for (const key in result) {
|
|
44
|
-
const optionKey = getRouteSetupOptionKey(key);
|
|
45
|
-
|
|
46
|
-
if (optionKey) options[optionKey] = result[key];
|
|
47
|
-
else data[key] = result[key];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return { options, data };
|
|
51
|
-
};
|