proteum 2.1.9-8 → 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/README.md +11 -8
- package/agents/project/AGENTS.md +7 -7
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +10 -10
- package/agents/project/optimizations.md +5 -6
- package/agents/project/root/AGENTS.md +7 -7
- package/cli/commands/migrate.ts +51 -0
- package/cli/compiler/artifacts/manifest.ts +4 -4
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/common/generatedRouteModules.ts +31 -38
- package/cli/context.ts +1 -1
- package/cli/migrate/pageContract.ts +516 -0
- package/cli/presentation/commands.ts +27 -1
- package/cli/runtime/commands.ts +25 -0
- package/cli/scaffold/templates.ts +4 -2
- package/client/dev/profiler/index.tsx +1 -2
- package/client/services/router/index.tsx +6 -22
- package/common/dev/console.ts +1 -0
- package/common/dev/diagnostics.ts +4 -4
- package/common/dev/inspection.ts +1 -1
- package/common/dev/proteumManifest.ts +4 -4
- package/common/dev/requestTrace.ts +0 -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/package.json +5 -1
- package/server/services/router/index.ts +48 -14
- package/server/services/router/request/api.ts +1 -1
- package/server/services/router/response/index.ts +0 -27
- package/types/global/vendors.d.ts +12 -0
- package/types/vendors.d.ts +12 -0
- package/common/router/pageSetup.ts +0 -51
|
@@ -28,7 +28,7 @@ import type { TRegisterPageArgs, TSsrUnresolvedRoute } from '@common/router/cont
|
|
|
28
28
|
import { getLayout } from '@common/router/layouts';
|
|
29
29
|
import { getRegisterPageArgs, buildRegex } from '@common/router/register';
|
|
30
30
|
import { TFetcherList } from '@common/router/request/api';
|
|
31
|
-
import type { TFrontRenderer,
|
|
31
|
+
import type { TFrontRenderer, TPageDataProvider } from '@common/router/response/page';
|
|
32
32
|
|
|
33
33
|
import App from '@client/app/component';
|
|
34
34
|
import type ClientApplication from '@client/app';
|
|
@@ -241,32 +241,15 @@ export default class ClientRouter<
|
|
|
241
241
|
return currentRoute;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
public page<TProvidedData extends {} = {}>(
|
|
245
|
-
path: string,
|
|
246
|
-
renderer: TFrontRenderer<TProvidedData>,
|
|
247
|
-
): TClientPageRoute<this>;
|
|
248
|
-
|
|
249
|
-
public page<TProvidedData extends {} = {}>(
|
|
250
|
-
path: string,
|
|
251
|
-
setup: TPageSetup<TProvidedData>,
|
|
252
|
-
renderer: TFrontRenderer<TProvidedData>,
|
|
253
|
-
): TClientPageRoute<this>;
|
|
254
|
-
|
|
255
|
-
public page<TProvidedData extends {} = {}>(
|
|
256
|
-
path: string,
|
|
257
|
-
options: Partial<TRouteOptions>,
|
|
258
|
-
renderer: TFrontRenderer<TProvidedData>,
|
|
259
|
-
): TClientPageRoute<this>;
|
|
260
|
-
|
|
261
244
|
public page<TProvidedData extends {} = {}>(
|
|
262
245
|
path: string,
|
|
263
246
|
options: Partial<TRouteOptions>,
|
|
264
|
-
|
|
247
|
+
data: TPageDataProvider<TProvidedData> | null,
|
|
265
248
|
renderer: TFrontRenderer<TProvidedData>,
|
|
266
249
|
): TClientPageRoute<this>;
|
|
267
250
|
|
|
268
251
|
public page(...args: TRegisterPageArgs<any, TRouteOptions>): TClientPageRoute<this> {
|
|
269
|
-
const { path, options,
|
|
252
|
+
const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
|
|
270
253
|
|
|
271
254
|
// Page ids are injected by the generated route wrapper modules.
|
|
272
255
|
const id = options.id;
|
|
@@ -279,7 +262,8 @@ export default class ClientRouter<
|
|
|
279
262
|
path,
|
|
280
263
|
regex,
|
|
281
264
|
keys,
|
|
282
|
-
|
|
265
|
+
data,
|
|
266
|
+
options: { ...defaultOptions, ...options },
|
|
283
267
|
controller: (context) => new ClientPage(route, renderer, context as any, layout),
|
|
284
268
|
};
|
|
285
269
|
|
|
@@ -406,7 +390,7 @@ export default class ClientRouter<
|
|
|
406
390
|
|
|
407
391
|
const response = await this.createResponse(route, request, apiData);
|
|
408
392
|
|
|
409
|
-
ReactDOM.hydrate(<App context={response.context as AppPropsContext} />, document.body, () => {
|
|
393
|
+
ReactDOM.hydrate(<App context={response.context as unknown as AppPropsContext} />, document.body, () => {
|
|
410
394
|
console.log(`Render complete`);
|
|
411
395
|
withProfiler((runtime) => runtime.markInitialHydrated({ chunkId: response.chunkId, title: response.title }));
|
|
412
396
|
|
package/common/dev/console.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type TDevConsoleLogLevel = 'silly' | 'log' | 'info' | 'warn' | 'error';
|
|
|
9
9
|
export type TDevConsoleLogChannel = {
|
|
10
10
|
channelType: 'cron' | 'master' | 'request' | 'socket';
|
|
11
11
|
channelId?: string;
|
|
12
|
+
silentLogs?: boolean;
|
|
12
13
|
method?: string;
|
|
13
14
|
path?: string;
|
|
14
15
|
user?: string;
|
|
@@ -72,11 +72,11 @@ const formatRouteTarget = (route: TProteumManifestRoute) => {
|
|
|
72
72
|
|
|
73
73
|
const formatRouteItem = (manifest: TProteumManifest, route: TProteumManifestRoute) => {
|
|
74
74
|
const chunk = route.chunkId ? ` chunk=${route.chunkId}` : '';
|
|
75
|
-
const
|
|
75
|
+
const data = route.hasData ? ' data=yes' : ' data=no';
|
|
76
76
|
const options = route.normalizedOptionKeys.length > 0 ? ` options=${route.normalizedOptionKeys.join(',')}` : '';
|
|
77
77
|
const resolution = route.targetResolution !== 'literal' ? ` resolution=${route.targetResolution}` : '';
|
|
78
78
|
|
|
79
|
-
return `${route.kind} ${route.methodName} ${formatRouteTarget(route)} [${route.scope}]${chunk}${
|
|
79
|
+
return `${route.kind} ${route.methodName} ${formatRouteTarget(route)} [${route.scope}]${chunk}${data}${options}${resolution} source=${formatManifestFilepath(manifest, route.filepath)}${formatManifestLocation(route.sourceLocation.line, route.sourceLocation.column)}`;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
const formatLayoutItem = (manifest: TProteumManifest, layout: TProteumManifestLayout) =>
|
|
@@ -155,8 +155,8 @@ export const buildExplainBlocks = (manifest: TProteumManifest, sectionNames: TEx
|
|
|
155
155
|
blocks.push({
|
|
156
156
|
title: 'Conventions',
|
|
157
157
|
items: [
|
|
158
|
-
`
|
|
159
|
-
`
|
|
158
|
+
`routeOptionKeys=${manifest.conventions.routeOptionKeys.join(', ')}`,
|
|
159
|
+
`reservedRouteOptionKeys=${manifest.conventions.reservedRouteOptionKeys.join(', ')}`,
|
|
160
160
|
],
|
|
161
161
|
});
|
|
162
162
|
continue;
|
package/common/dev/inspection.ts
CHANGED
|
@@ -358,7 +358,7 @@ const createRouteEntry = (manifest: TProteumManifest, route: TProteumManifestRou
|
|
|
358
358
|
`${route.kind} ${route.methodName}`,
|
|
359
359
|
...(route.path ? [`path=${route.path}`] : []),
|
|
360
360
|
...(route.chunkId ? [`chunk=${route.chunkId}`] : []),
|
|
361
|
-
`
|
|
361
|
+
`data=${route.hasData ? 'yes' : 'no'}`,
|
|
362
362
|
],
|
|
363
363
|
kind: 'route',
|
|
364
364
|
label: route.path || route.pathRaw || route.chunkId || route.filepath,
|
|
@@ -87,7 +87,7 @@ export type TProteumManifestRoute = {
|
|
|
87
87
|
invalidOptionKeys: string[];
|
|
88
88
|
reservedOptionKeys: string[];
|
|
89
89
|
optionsRaw?: string;
|
|
90
|
-
|
|
90
|
+
hasData: boolean;
|
|
91
91
|
chunkId?: string;
|
|
92
92
|
chunkFilepath?: string;
|
|
93
93
|
scope: TProteumManifestScope;
|
|
@@ -102,7 +102,7 @@ export type TProteumManifestLayout = {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
export type TProteumManifest = {
|
|
105
|
-
version:
|
|
105
|
+
version: 10;
|
|
106
106
|
app: {
|
|
107
107
|
root: string;
|
|
108
108
|
coreRoot: string;
|
|
@@ -126,8 +126,8 @@ export type TProteumManifest = {
|
|
|
126
126
|
};
|
|
127
127
|
};
|
|
128
128
|
conventions: {
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
routeOptionKeys: string[];
|
|
130
|
+
reservedRouteOptionKeys: string[];
|
|
131
131
|
};
|
|
132
132
|
env: {
|
|
133
133
|
source: 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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"proteum": "cli/bin.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@babel/generator": "^7.29.0",
|
|
20
|
+
"@babel/parser": "^7.29.0",
|
|
21
|
+
"@babel/traverse": "^7.29.0",
|
|
22
|
+
"@babel/types": "^7.29.0",
|
|
19
23
|
"@inkjs/ui": "^2.0.0",
|
|
20
24
|
"@prisma/adapter-mariadb": "7.2.0",
|
|
21
25
|
"@prisma/client": "7.2.0",
|
|
@@ -222,6 +222,7 @@ export default class ServerRouter<
|
|
|
222
222
|
const methodName = match ? match[2] : '<anonymous>';*/
|
|
223
223
|
|
|
224
224
|
const contextData = context.getStore() || { channelType: 'master' };
|
|
225
|
+
if (contextData.silentLogs) return;
|
|
225
226
|
|
|
226
227
|
const requestPrefix =
|
|
227
228
|
contextData.channelType === 'request'
|
|
@@ -273,21 +274,18 @@ export default class ServerRouter<
|
|
|
273
274
|
|
|
274
275
|
public async renderStatic(url: string, options: TRouteOptions['static'], rendered?: any) {
|
|
275
276
|
// Wildcard: tell that the newly rendered pages should be cached
|
|
276
|
-
if (url === '*' || !url)
|
|
277
|
-
|
|
278
|
-
if (!rendered) {
|
|
279
|
-
console.log('[router] renderStatic: url', url);
|
|
277
|
+
if (url === '*' || !url) throw new Error(`Unable to cache a dynamic or empty URL.`);
|
|
280
278
|
|
|
279
|
+
if (rendered === undefined) {
|
|
281
280
|
const fullUrl = this.url(url, {}, true);
|
|
282
281
|
const response = await got(fullUrl, {
|
|
283
282
|
method: 'GET',
|
|
284
|
-
headers: { Accept: 'text/html', bypasscache: '1' },
|
|
283
|
+
headers: { Accept: 'text/html', bypasscache: '1', 'x-proteum-static-warmup': '1' },
|
|
285
284
|
throwHttpErrors: false,
|
|
286
285
|
});
|
|
287
286
|
|
|
288
287
|
if (response.statusCode !== 200) {
|
|
289
|
-
|
|
290
|
-
return;
|
|
288
|
+
throw new Error(`Static render returned ${response.statusCode} for ${fullUrl}`);
|
|
291
289
|
}
|
|
292
290
|
|
|
293
291
|
rendered = response.body;
|
|
@@ -302,17 +300,50 @@ export default class ServerRouter<
|
|
|
302
300
|
|
|
303
301
|
private initStaticRoutes() {
|
|
304
302
|
this.clearStaticRoutesRefreshInterval();
|
|
303
|
+
const staticEntries: Array<{ routePath: string; url: string; options: TRouteOptions['static'] }> = [];
|
|
304
|
+
const seenStaticUrls = new Set<string>();
|
|
305
305
|
|
|
306
306
|
for (const route of this.routes) {
|
|
307
|
+
if (route.method !== 'GET' || route.options.accept !== 'html') continue;
|
|
308
|
+
|
|
307
309
|
if (!route.options.static) continue;
|
|
308
310
|
|
|
309
311
|
// Add to static pages
|
|
310
312
|
// Should be a GET oage that don't take any parameter
|
|
311
313
|
for (const url of route.options.static.urls) {
|
|
312
|
-
|
|
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);
|
|
313
322
|
}
|
|
314
323
|
}
|
|
315
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
|
+
|
|
316
347
|
// Every hours, refresh static pages
|
|
317
348
|
this.staticRoutesRefreshInterval = setInterval(
|
|
318
349
|
() => {
|
|
@@ -328,7 +359,9 @@ export default class ServerRouter<
|
|
|
328
359
|
for (const pageUrl in this.cache) {
|
|
329
360
|
const page = this.cache[pageUrl];
|
|
330
361
|
if (page.expire && page.expire < Date.now()) {
|
|
331
|
-
this.renderStatic(pageUrl, page.options)
|
|
362
|
+
void this.renderStatic(pageUrl, page.options).catch((error) => {
|
|
363
|
+
console.error('[router] Static refresh failed', pageUrl, error);
|
|
364
|
+
});
|
|
332
365
|
}
|
|
333
366
|
}
|
|
334
367
|
}
|
|
@@ -383,7 +416,7 @@ export default class ServerRouter<
|
|
|
383
416
|
----------------------------------*/
|
|
384
417
|
|
|
385
418
|
public page(...args: TRegisterPageArgs<any, TRouteOptions>) {
|
|
386
|
-
const { path, options,
|
|
419
|
+
const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
|
|
387
420
|
|
|
388
421
|
const { regex, keys } = buildRegex(path);
|
|
389
422
|
|
|
@@ -392,10 +425,10 @@ export default class ServerRouter<
|
|
|
392
425
|
path,
|
|
393
426
|
regex,
|
|
394
427
|
keys,
|
|
428
|
+
data,
|
|
395
429
|
controller: (context: TRouterContext<this>) => new Page(route, renderer, context, layout),
|
|
396
430
|
options: this.buildRouteOptions({
|
|
397
431
|
accept: 'html', // Les pages retournent forcémment du html
|
|
398
|
-
setup,
|
|
399
432
|
...options,
|
|
400
433
|
}),
|
|
401
434
|
};
|
|
@@ -756,6 +789,7 @@ export default class ServerRouter<
|
|
|
756
789
|
// This is for debugging
|
|
757
790
|
channelType: 'request',
|
|
758
791
|
channelId: request.id,
|
|
792
|
+
silentLogs: request.headers['x-proteum-static-warmup'] === '1',
|
|
759
793
|
method: request.method,
|
|
760
794
|
path: request.path,
|
|
761
795
|
connectedNamespace: request.headers[profilerConnectedNamespaceHeader] || undefined,
|
|
@@ -940,8 +974,6 @@ export default class ServerRouter<
|
|
|
940
974
|
});
|
|
941
975
|
|
|
942
976
|
private async resolvedRoute(route: TMatchedRoute, response: ServerResponse<this>, timeStart: number) {
|
|
943
|
-
route = await response.resolveRouteOptions(route);
|
|
944
|
-
|
|
945
977
|
this.app.container.Trace.record(
|
|
946
978
|
response.request.id,
|
|
947
979
|
'resolve.route-match',
|
|
@@ -984,7 +1016,9 @@ export default class ServerRouter<
|
|
|
984
1016
|
staticUrl,
|
|
985
1017
|
route.options.static,
|
|
986
1018
|
staticUrl === response.request.path ? response.data : undefined,
|
|
987
|
-
)
|
|
1019
|
+
).catch((error) => {
|
|
1020
|
+
console.error('[router] Static cache write failed', staticUrl, error);
|
|
1021
|
+
});
|
|
988
1022
|
}
|
|
989
1023
|
}
|
|
990
1024
|
|
|
@@ -107,7 +107,7 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
107
107
|
throw new Error(
|
|
108
108
|
`Proteum connected boundary mismatch: "${connected.namespace}" is not registered on ${this.request.router.app.identity.identifier}. ` +
|
|
109
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
|
|
110
|
+
`Re-check both SSR and client navigation if this fetcher is used from a page data or render path.`,
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|