proteum 2.1.7 → 2.1.9

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/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.7",
4
+ "version": "2.1.9",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../node_modules/proteum/tsconfig.common.json",
2
+ "extends": "../tsconfig.common.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": "..",
5
5
  "baseUrl": "..",
@@ -14,22 +14,23 @@
14
14
  "@/common/.generated/*": ["./.proteum/common/*"],
15
15
  "@/server/.generated/*": ["./.proteum/server/*"],
16
16
 
17
- "@client/*": ["../node_modules/proteum/client/*"],
18
- "@common/*": ["../node_modules/proteum/common/*"],
19
- "@server/*": ["../node_modules/proteum/server/*"],
17
+ "@client/*": ["./client/*"],
18
+ "@common/*": ["./common/*"],
19
+ "@server/*": ["./server/*"],
20
20
 
21
21
  "@/*": ["./*"],
22
22
 
23
23
  // ATTENTION: Les références à preact doivent toujours pointer vers la même instance
24
- "react": ["preact/compat"],
25
- "react-dom/test-utils": ["preact/test-utils"],
26
- "react-dom": ["preact/compat"], // Must be below test-utils
27
- "react/jsx-runtime": ["preact/jsx-runtime"]
24
+ "react": ["../preact/compat"],
25
+ "react-dom/client": ["../preact/compat/client"],
26
+ "react-dom/test-utils": ["../preact/test-utils"],
27
+ "react-dom": ["../preact/compat"], // Must be below client + test-utils
28
+ "react/jsx-runtime": ["../preact/jsx-runtime"]
28
29
  }
29
30
  },
30
31
 
31
32
  "include": [
32
33
  ".",
33
- "../../node_modules/proteum/types/global"
34
+ "../types/global"
34
35
  ]
35
36
  }
@@ -26,10 +26,12 @@ import type { TBasicUser } from '@server/services/auth';
26
26
  import type { TServerRouter } from '..';
27
27
  import type { TDevConsoleLogLevel } from '@common/dev/console';
28
28
  import type { TPerfGroupBy } from '@common/dev/performance';
29
+ import type { TServerReadyConnectedProject } from '@common/dev/serverHotReload';
29
30
  import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
30
31
  import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
31
32
  import { explainSectionNames } from '@common/dev/diagnostics';
32
33
  import {
34
+ type TConnectedProjectHealthResponse,
33
35
  connectedProjectHealthPath,
34
36
  connectedProjectProxyPathPrefix,
35
37
  parseConnectedProjectProxyPath,
@@ -93,25 +95,48 @@ const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPol
93
95
  };
94
96
 
95
97
  const immutablePublicAssetCacheControl = 'public, max-age=31536000, immutable';
98
+ const devPublicAssetCacheControl = 'no-store';
96
99
  const revalidatedPublicAssetCacheControl = 'public, max-age=0, must-revalidate';
97
100
  const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
98
101
  const connectedProjectBootRetryCount = 10;
99
102
  const connectedProjectBootRetryDelayMs = 5_000;
100
103
 
101
- const isVersionedPublicAssetRequest = (res: express.Response, filePath: string) => {
102
- const requestUrl = res.req?.originalUrl || res.req?.url || '';
104
+ const isVersionedPublicAssetRequest = (res: undefined | express.Response | http.ServerResponse, filePath: string) => {
105
+ const request =
106
+ res && typeof res === 'object' && 'req' in res
107
+ ? ((res as express.Response | (http.ServerResponse & { req?: express.Request })).req ?? undefined)
108
+ : undefined;
109
+ const requestUrl = request?.originalUrl || request?.url || '';
103
110
  const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
104
111
  if (searchParams.has('v')) return true;
105
112
 
106
113
  return hashedPublicAssetPattern.test(path.basename(filePath));
107
114
  };
108
115
 
109
- const resolvePublicAssetCacheControl = (res: express.Response, filePath: string) =>
110
- isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
116
+ const resolvePublicAssetCacheControl = ({
117
+ res,
118
+ filePath,
119
+ profile,
120
+ }: {
121
+ res: undefined | express.Response | http.ServerResponse;
122
+ filePath: string;
123
+ profile: string;
124
+ }) => {
125
+ if (profile === 'dev') return devPublicAssetCacheControl;
126
+ return isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
127
+ };
111
128
  const wait = async (durationMs: number) =>
112
129
  await new Promise<void>((resolve) => {
113
130
  setTimeout(resolve, durationMs);
114
131
  });
132
+ const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.trim() !== '';
133
+ const isConnectedProjectHealthResponse = (value: unknown): value is TConnectedProjectHealthResponse =>
134
+ typeof value === 'object' &&
135
+ value !== null &&
136
+ (value as TConnectedProjectHealthResponse).ok === true &&
137
+ isNonEmptyString((value as TConnectedProjectHealthResponse).identifier) &&
138
+ isNonEmptyString((value as TConnectedProjectHealthResponse).name) &&
139
+ Array.isArray((value as TConnectedProjectHealthResponse).connectedProjects);
115
140
 
116
141
  /*----------------------------------
117
142
  - FUNCTION
@@ -167,7 +192,9 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
167
192
  };
168
193
  }
169
194
 
170
- private async verifyConnectedProjectsBeforeStart() {
195
+ private async verifyConnectedProjectsBeforeStart(): Promise<TServerReadyConnectedProject[]> {
196
+ const verifiedConnectedProjects: TServerReadyConnectedProject[] = [];
197
+
171
198
  for (const connectedProject of Object.values(this.app.connectedProjects || {})) {
172
199
  const healthUrl = new URL(connectedProjectHealthPath, connectedProject.urlInternal).toString();
173
200
  let lastError: Error | undefined;
@@ -184,7 +211,21 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
184
211
  );
185
212
  }
186
213
 
214
+ const payload = (await response.json()) as unknown;
215
+ if (!isConnectedProjectHealthResponse(payload)) {
216
+ throw new Error(
217
+ `Connected project "${connectedProject.namespace}" health check returned an invalid payload at ${healthUrl}.`,
218
+ );
219
+ }
220
+
187
221
  lastError = undefined;
222
+ verifiedConnectedProjects.push({
223
+ namespace: connectedProject.namespace,
224
+ identifier: payload.identifier.trim(),
225
+ name: payload.name.trim(),
226
+ urlInternal: connectedProject.urlInternal,
227
+ healthUrl,
228
+ });
188
229
  break;
189
230
  } catch (error) {
190
231
  lastError =
@@ -204,15 +245,20 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
204
245
  await wait(connectedProjectBootRetryDelayMs);
205
246
  }
206
247
  }
248
+
249
+ return verifiedConnectedProjects;
207
250
  }
208
251
 
209
252
  private registerConnectedProjectRoutes(routes: express.Express) {
210
253
  routes.get(connectedProjectHealthPath, (_req, res) => {
211
- res.json({
254
+ const response: TConnectedProjectHealthResponse = {
212
255
  connectedProjects: Object.keys(this.app.connectedProjects || {}),
213
256
  identifier: this.app.identity.identifier,
257
+ name: this.app.identity.name,
214
258
  ok: true,
215
- });
259
+ };
260
+
261
+ res.json(response);
216
262
  });
217
263
 
218
264
  routes.all(`${connectedProjectHealthPath}/*`, (_req, res) => {
@@ -360,14 +406,29 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
360
406
  // Normalement, seulement utile pour le mode production,
361
407
  // Quand mode debug, les ressources client semblent servies par le dev middlewae
362
408
  // Sauf que les ressources serveur ne semblent pas trouvées par le dev-middleware
409
+ const disablePublicAssetCaching = this.app.env.profile === 'dev';
363
410
  routes.use(compression());
364
411
  routes.use('/public', cors());
365
412
  routes.use(
366
413
  '/public',
367
414
  express.static(path.join(Container.path.root, APP_OUTPUT_DIR, 'public'), {
368
415
  dotfiles: 'deny',
369
- setHeaders: function setCustomCacheControl(res, filePath) {
370
- res.setHeader('Cache-Control', resolvePublicAssetCacheControl(res, filePath));
416
+ etag: !disablePublicAssetCaching,
417
+ lastModified: !disablePublicAssetCaching,
418
+ setHeaders: (res, filePath) => {
419
+ if (disablePublicAssetCaching) {
420
+ res.removeHeader('ETag');
421
+ res.removeHeader('Last-Modified');
422
+ }
423
+
424
+ res.setHeader(
425
+ 'Cache-Control',
426
+ resolvePublicAssetCacheControl({
427
+ res,
428
+ filePath,
429
+ profile: this.app.env.profile,
430
+ }),
431
+ );
371
432
  },
372
433
  }),
373
434
  (req, res) => {
@@ -426,12 +487,13 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
426
487
  /*----------------------------------
427
488
  - BOOT SERVICES
428
489
  ----------------------------------*/
429
- await this.verifyConnectedProjectsBeforeStart();
490
+ const verifiedConnectedProjects = await this.verifyConnectedProjectsBeforeStart();
430
491
 
431
492
  this.http.listen(this.config.port, () => {
432
493
  if (__DEV__ && typeof process.send === 'function') {
433
494
  process.send({
434
495
  type: serverHotReloadMessageType.ready,
496
+ connectedProjects: verifiedConnectedProjects,
435
497
  publicUrl: this.publicUrl,
436
498
  });
437
499
  return;