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.
Files changed (35) hide show
  1. package/README.md +11 -8
  2. package/agents/project/AGENTS.md +7 -7
  3. package/agents/project/CODING_STYLE.md +1 -1
  4. package/agents/project/client/AGENTS.md +1 -1
  5. package/agents/project/client/pages/AGENTS.md +10 -10
  6. package/agents/project/optimizations.md +5 -6
  7. package/agents/project/root/AGENTS.md +7 -7
  8. package/cli/commands/migrate.ts +51 -0
  9. package/cli/compiler/artifacts/manifest.ts +4 -4
  10. package/cli/compiler/artifacts/routing.ts +2 -2
  11. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  12. package/cli/context.ts +1 -1
  13. package/cli/migrate/pageContract.ts +516 -0
  14. package/cli/presentation/commands.ts +27 -1
  15. package/cli/runtime/commands.ts +25 -0
  16. package/cli/scaffold/templates.ts +4 -2
  17. package/client/dev/profiler/index.tsx +1 -2
  18. package/client/services/router/index.tsx +6 -22
  19. package/common/dev/console.ts +1 -0
  20. package/common/dev/diagnostics.ts +4 -4
  21. package/common/dev/inspection.ts +1 -1
  22. package/common/dev/proteumManifest.ts +4 -4
  23. package/common/dev/requestTrace.ts +0 -1
  24. package/common/router/contracts.ts +8 -11
  25. package/common/router/index.ts +2 -2
  26. package/common/router/pageData.ts +72 -0
  27. package/common/router/register.ts +10 -46
  28. package/common/router/response/page.ts +28 -16
  29. package/package.json +5 -1
  30. package/server/services/router/index.ts +48 -14
  31. package/server/services/router/request/api.ts +1 -1
  32. package/server/services/router/response/index.ts +0 -27
  33. package/types/global/vendors.d.ts +12 -0
  34. package/types/vendors.d.ts +12 -0
  35. 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, TPageSetup } from '@common/router/response/page';
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
- setup: TPageSetup<TProvidedData>,
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, setup, renderer, layout } = getRegisterPageArgs(...args);
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
- options: { ...defaultOptions, setup, ...options },
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
 
@@ -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 setup = route.hasSetup ? ' setup=yes' : ' setup=no';
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}${setup}${options}${resolution} source=${formatManifestFilepath(manifest, route.filepath)}${formatManifestLocation(route.sourceLocation.line, route.sourceLocation.column)}`;
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
- `routeSetupOptionKeys=${manifest.conventions.routeSetupOptionKeys.join(', ')}`,
159
- `reservedRouteSetupKeys=${manifest.conventions.reservedRouteSetupKeys.join(', ')}`,
158
+ `routeOptionKeys=${manifest.conventions.routeOptionKeys.join(', ')}`,
159
+ `reservedRouteOptionKeys=${manifest.conventions.reservedRouteOptionKeys.join(', ')}`,
160
160
  ],
161
161
  });
162
162
  continue;
@@ -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
- `setup=${route.hasSetup ? 'yes' : 'no'}`,
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
- hasSetup: boolean;
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: 9;
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
- routeSetupOptionKeys: string[];
130
- reservedRouteSetupKeys: string[];
129
+ routeOptionKeys: string[];
130
+ reservedRouteOptionKeys: string[];
131
131
  };
132
132
  env: {
133
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',
@@ -3,24 +3,21 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  // Core
6
- import type { TFrontRenderer, TPageSetup } from './response/page';
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 signatures shared by client and compiler code.
13
+ // Supported `Router.page(...)` registration signature shared by client and compiler code.
14
14
  export type TRegisterPageArgs<TProvidedData extends {} = {}, TPageOptions extends {} = TRouteOptions> =
15
- | [path: string, renderer: TFrontRenderer<TProvidedData>]
16
- | [path: string, setup: TPageSetup<TProvidedData>, renderer: TFrontRenderer<TProvidedData>]
17
- | [path: string, options: Partial<TPageOptions>, renderer: TFrontRenderer<TProvidedData>]
18
- | [
19
- path: string,
20
- options: Partial<TPageOptions>,
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 } & (
@@ -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, TPageSetup } from './response/page';
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 { TFrontRenderer, TPageSetup } from './response/page';
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
- let path: string;
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 (args.length === 2) {
27
- [path, renderer] = args;
28
- } else if (args.length === 3) {
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
- if (typeof optionsOrSetupArg === 'function') setup = optionsOrSetupArg;
34
- else options = optionsOrSetupArg;
35
- } else {
36
- const [pathArg, optionsArg, setupArg, rendererArg] = args;
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, setup, renderer, layout };
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 { splitRouteSetupResult } from '@common/router/pageSetup';
13
+ import { validatePageDataResult } from '@common/router/pageData';
14
14
 
15
15
  /*----------------------------------
16
16
  - TYPES
17
17
  ----------------------------------*/
18
18
 
19
- export type TPageSetupContext = ClientContext;
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 route config and SSR data before rendering.
30
- export type TPageSetup<TProvidedData extends {} = {}> = (
31
- context: TPageSetupContext & {
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: TPageSetupContext & { data: { [key: string]: PrimitiveValue } },
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 TPageRenderContext = TPageRenderContext,
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 resolveSetup() {
104
- const setup = this.route.options.setup;
105
- if (!setup) return { options: {}, data: {} };
114
+ private resolveDataProviderResult() {
115
+ const dataProvider = 'data' in this.route ? this.route.data : undefined;
116
+ if (!dataProvider) return {};
106
117
 
107
- const setupContext = { ...this.context, data: this.context.request.data } as Parameters<typeof setup>[0];
118
+ const dataContext = { ...this.context, data: this.context.request.data } as unknown as Parameters<
119
+ typeof dataProvider
120
+ >[0];
108
121
 
109
- return splitRouteSetupResult(setup(setupContext) || {});
122
+ return validatePageDataResult(this.route, dataProvider(dataContext));
110
123
  }
111
124
 
112
125
  private createFetchers() {
113
- const { options, data } = this.resolveSetup();
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.1.9-8",
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) return;
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
- console.error('[router] renderStatic: page returned code', response.statusCode, fullUrl);
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
- this.renderStatic(url, route.options.static);
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, setup, renderer, layout } = getRegisterPageArgs(...args);
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 setup or render path.`,
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