proteum 2.1.8 → 2.1.9-2
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/AGENTS.md +11 -5
- package/README.md +1 -1
- package/agents/project/AGENTS.md +7 -2
- package/agents/project/diagnostics.md +1 -1
- package/cli/app/index.ts +33 -9
- package/cli/commands/dev.ts +7 -1
- package/cli/compiler/artifacts/commands.ts +20 -5
- package/cli/compiler/client/index.ts +46 -23
- package/cli/compiler/common/index.ts +16 -5
- package/cli/compiler/index.ts +12 -5
- package/cli/compiler/server/index.ts +39 -13
- package/cli/index.ts +6 -0
- package/cli/paths.ts +341 -10
- package/cli/presentation/devSession.ts +22 -9
- package/cli/presentation/ink.ts +10 -5
- package/cli/presentation/welcome.ts +8 -4
- package/cli/scaffold/index.ts +27 -4
- package/cli/scaffold/templates.ts +34 -20
- package/cli/utils/check.ts +5 -11
- package/client/app/index.ts +17 -2
- package/client/app.tsconfig.json +11 -10
- package/common/connectedProjects.ts +7 -0
- package/common/dev/serverHotReload.ts +22 -1
- package/package.json +2 -1
- package/server/app.tsconfig.json +10 -9
- package/server/services/auth/index.ts +9 -0
- package/server/services/router/http/index.ts +72 -10
|
@@ -2,6 +2,18 @@ import type { TScaffoldInitConfig, TScaffoldResult } from './types';
|
|
|
2
2
|
|
|
3
3
|
const renderJson = (value: unknown) => JSON.stringify(value, null, 4);
|
|
4
4
|
|
|
5
|
+
export type TTsconfigTemplatePaths = {
|
|
6
|
+
frameworkTsconfig: string;
|
|
7
|
+
frameworkClient: string;
|
|
8
|
+
frameworkCommon: string;
|
|
9
|
+
frameworkServer: string;
|
|
10
|
+
frameworkTypesGlobal: string;
|
|
11
|
+
preactCompat: string;
|
|
12
|
+
preactCompatClient: string;
|
|
13
|
+
preactTestUtils: string;
|
|
14
|
+
preactJsxRuntime: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
5
17
|
export const createPageTemplate = ({
|
|
6
18
|
routePath,
|
|
7
19
|
heading,
|
|
@@ -162,8 +174,8 @@ export default class ${appIdentifier} extends Application {
|
|
|
162
174
|
}
|
|
163
175
|
`;
|
|
164
176
|
|
|
165
|
-
export const createClientTsconfigTemplate = () => `{
|
|
166
|
-
"extends":
|
|
177
|
+
export const createClientTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
|
|
178
|
+
"extends": ${JSON.stringify(paths.frameworkTsconfig)},
|
|
167
179
|
"compilerOptions": {
|
|
168
180
|
"rootDir": "..",
|
|
169
181
|
"baseUrl": "..",
|
|
@@ -172,9 +184,9 @@ export const createClientTsconfigTemplate = () => `{
|
|
|
172
184
|
"strictBindCallApply": true,
|
|
173
185
|
"useUnknownInCatchVariables": true,
|
|
174
186
|
"paths": {
|
|
175
|
-
"@client/*": [
|
|
176
|
-
"@common/*": [
|
|
177
|
-
"@server/*": [
|
|
187
|
+
"@client/*": [${JSON.stringify(paths.frameworkClient)}],
|
|
188
|
+
"@common/*": [${JSON.stringify(paths.frameworkCommon)}],
|
|
189
|
+
"@server/*": [${JSON.stringify(paths.frameworkServer)}],
|
|
178
190
|
|
|
179
191
|
"@/client/context": ["./.proteum/client/context.ts"],
|
|
180
192
|
"@generated/client/*": ["./.proteum/client/*"],
|
|
@@ -182,24 +194,25 @@ export const createClientTsconfigTemplate = () => `{
|
|
|
182
194
|
"@generated/server/*": ["./.proteum/server/*"],
|
|
183
195
|
"@/*": ["./*"],
|
|
184
196
|
|
|
185
|
-
"react": [
|
|
186
|
-
"react-dom/
|
|
187
|
-
"react-dom": [
|
|
188
|
-
"react
|
|
197
|
+
"react": [${JSON.stringify(paths.preactCompat)}],
|
|
198
|
+
"react-dom/client": [${JSON.stringify(paths.preactCompatClient)}],
|
|
199
|
+
"react-dom/test-utils": [${JSON.stringify(paths.preactTestUtils)}],
|
|
200
|
+
"react-dom": [${JSON.stringify(paths.preactCompat)}],
|
|
201
|
+
"react/jsx-runtime": [${JSON.stringify(paths.preactJsxRuntime)}]
|
|
189
202
|
}
|
|
190
203
|
},
|
|
191
204
|
"include": [
|
|
192
205
|
".",
|
|
193
206
|
"../var/typings",
|
|
194
|
-
|
|
207
|
+
${JSON.stringify(paths.frameworkTypesGlobal)},
|
|
195
208
|
"../.proteum/client/services.d.ts",
|
|
196
209
|
"../server/index.ts"
|
|
197
210
|
]
|
|
198
211
|
}
|
|
199
212
|
`;
|
|
200
213
|
|
|
201
|
-
export const createServerTsconfigTemplate = () => `{
|
|
202
|
-
"extends":
|
|
214
|
+
export const createServerTsconfigTemplate = (paths: TTsconfigTemplatePaths) => `{
|
|
215
|
+
"extends": ${JSON.stringify(paths.frameworkTsconfig)},
|
|
203
216
|
"compilerOptions": {
|
|
204
217
|
"rootDir": "..",
|
|
205
218
|
"baseUrl": "..",
|
|
@@ -209,9 +222,9 @@ export const createServerTsconfigTemplate = () => `{
|
|
|
209
222
|
"useUnknownInCatchVariables": true,
|
|
210
223
|
"moduleSuffixes": [".ssr", ""],
|
|
211
224
|
"paths": {
|
|
212
|
-
"@client/*": [
|
|
213
|
-
"@common/*": [
|
|
214
|
-
"@server/*": [
|
|
225
|
+
"@client/*": [${JSON.stringify(paths.frameworkClient)}],
|
|
226
|
+
"@common/*": [${JSON.stringify(paths.frameworkCommon)}],
|
|
227
|
+
"@server/*": [${JSON.stringify(paths.frameworkServer)}],
|
|
215
228
|
|
|
216
229
|
"@/client/context": ["./.proteum/client/context.ts"],
|
|
217
230
|
"@generated/client/*": ["./.proteum/client/*"],
|
|
@@ -219,10 +232,11 @@ export const createServerTsconfigTemplate = () => `{
|
|
|
219
232
|
"@generated/server/*": ["./.proteum/server/*"],
|
|
220
233
|
"@/*": ["./*"],
|
|
221
234
|
|
|
222
|
-
"react": [
|
|
223
|
-
"react-dom/
|
|
224
|
-
"react-dom": [
|
|
225
|
-
"react
|
|
235
|
+
"react": [${JSON.stringify(paths.preactCompat)}],
|
|
236
|
+
"react-dom/client": [${JSON.stringify(paths.preactCompatClient)}],
|
|
237
|
+
"react-dom/test-utils": [${JSON.stringify(paths.preactTestUtils)}],
|
|
238
|
+
"react-dom": [${JSON.stringify(paths.preactCompat)}],
|
|
239
|
+
"react/jsx-runtime": [${JSON.stringify(paths.preactJsxRuntime)}]
|
|
226
240
|
}
|
|
227
241
|
},
|
|
228
242
|
"include": [
|
|
@@ -230,7 +244,7 @@ export const createServerTsconfigTemplate = () => `{
|
|
|
230
244
|
"../identity.config.ts",
|
|
231
245
|
"../proteum.config.ts",
|
|
232
246
|
"../var/typings",
|
|
233
|
-
|
|
247
|
+
${JSON.stringify(paths.frameworkTypesGlobal)},
|
|
234
248
|
"../.proteum/server/services.d.ts",
|
|
235
249
|
"../server/index.ts"
|
|
236
250
|
]
|
package/cli/utils/check.ts
CHANGED
|
@@ -8,13 +8,7 @@ import { runProcess } from './runProcess';
|
|
|
8
8
|
const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
|
|
9
9
|
const eslintConfigPaths = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
|
10
10
|
|
|
11
|
-
const resolveInstalledBinary = (
|
|
12
|
-
const binary = path.join(cli.paths.core.root, 'node_modules', '.bin', name);
|
|
13
|
-
|
|
14
|
-
if (!fs.existsSync(binary)) throw new Error(`Missing required binary "${name}" in Proteum dependencies.`);
|
|
15
|
-
|
|
16
|
-
return binary;
|
|
17
|
-
};
|
|
11
|
+
const resolveInstalledBinary = (packageName: string, binName: string) => cli.paths.resolveBinary(packageName, binName);
|
|
18
12
|
|
|
19
13
|
const resolveExistingAppPaths = (paths: string[]) =>
|
|
20
14
|
paths
|
|
@@ -43,10 +37,10 @@ export const runAppTypecheck = async () => {
|
|
|
43
37
|
if (existingProjects.length === 0)
|
|
44
38
|
throw new Error(`No TypeScript app projects found. Expected one of: ${tsconfigPaths.join(', ')}.`);
|
|
45
39
|
|
|
46
|
-
const tsc = resolveInstalledBinary('tsc');
|
|
40
|
+
const tsc = resolveInstalledBinary('typescript', 'tsc');
|
|
47
41
|
|
|
48
42
|
for (const { relativePath } of existingProjects)
|
|
49
|
-
await runProcess(tsc, ['-p', relativePath, '--noEmit', '--pretty', 'false'], {
|
|
43
|
+
await runProcess(tsc.command, [...tsc.args, '-p', relativePath, '--noEmit', '--pretty', 'false'], {
|
|
50
44
|
cwd: cli.paths.appRoot,
|
|
51
45
|
env: getTypecheckEnv(),
|
|
52
46
|
});
|
|
@@ -62,10 +56,10 @@ export const runAppLint = async ({ fix = false } = {}) => {
|
|
|
62
56
|
.join(', ')}.`,
|
|
63
57
|
);
|
|
64
58
|
|
|
65
|
-
const eslint = resolveInstalledBinary('eslint');
|
|
59
|
+
const eslint = resolveInstalledBinary('eslint', 'eslint');
|
|
66
60
|
const args = ['.', '--config', config.absolutePath, '--no-config-lookup'];
|
|
67
61
|
|
|
68
62
|
if (fix) args.push('--fix');
|
|
69
63
|
|
|
70
|
-
await runProcess(eslint, args, { cwd: cli.paths.appRoot });
|
|
64
|
+
await runProcess(eslint.command, [...eslint.args, ...args], { cwd: cli.paths.appRoot });
|
|
71
65
|
};
|
package/client/app/index.ts
CHANGED
|
@@ -41,6 +41,18 @@ type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
|
41
41
|
|
|
42
42
|
export type ApplicationProperties = Prettify<keyof Application>;
|
|
43
43
|
|
|
44
|
+
const normalizeBrowserErrorMessage = (value: unknown): string => {
|
|
45
|
+
if (typeof value === 'string') return value;
|
|
46
|
+
if (value && typeof value === 'object' && 'message' in value && typeof value.message === 'string')
|
|
47
|
+
return value.message;
|
|
48
|
+
|
|
49
|
+
return '';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isIgnorableBrowserErrorMessage = (message: string) =>
|
|
53
|
+
message === 'ResizeObserver loop completed with undelivered notifications.' ||
|
|
54
|
+
message === 'ResizeObserver loop limit exceeded';
|
|
55
|
+
|
|
44
56
|
/*----------------------------------
|
|
45
57
|
- CLASS
|
|
46
58
|
----------------------------------*/
|
|
@@ -81,8 +93,11 @@ export default abstract class Application {
|
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
window.onerror = (message, file, line, col, stacktrace) => {
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
const normalizedMessage = normalizeBrowserErrorMessage(message);
|
|
97
|
+
if (isIgnorableBrowserErrorMessage(normalizedMessage)) return true;
|
|
98
|
+
|
|
99
|
+
console.error(`Exception catched by method B`, normalizedMessage || message);
|
|
100
|
+
this.reportBug({ stacktrace: stacktrace?.stack || JSON.stringify({ message: normalizedMessage || message, file, line, col }) }).then(
|
|
86
101
|
() => {
|
|
87
102
|
// TODO in toas service: app.on('bug', () => toast.warning( ... ))
|
|
88
103
|
/*context?.toast.warning("Bug detected",
|
package/client/app.tsconfig.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "
|
|
2
|
+
"extends": "../tsconfig.common.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"rootDir": "..",
|
|
5
5
|
"baseUrl": "..",
|
|
6
6
|
"paths": {
|
|
7
7
|
|
|
8
|
-
"@client/*": ["
|
|
9
|
-
"@common/*": ["
|
|
8
|
+
"@client/*": ["./client/*"],
|
|
9
|
+
"@common/*": ["./common/*"],
|
|
10
10
|
|
|
11
11
|
// Only used for typings (ex: ServerResponse)
|
|
12
12
|
// Removed before bundling
|
|
13
|
-
"@server/*": ["
|
|
13
|
+
"@server/*": ["./server/*"],
|
|
14
14
|
"@/*": ["./*"],
|
|
15
15
|
|
|
16
16
|
// ATTENTION: Les références à preact doivent toujours pointer vers la même instance
|
|
17
|
-
"react": ["preact/compat"],
|
|
18
|
-
"react-dom/
|
|
19
|
-
"react-dom": ["preact/
|
|
20
|
-
"react
|
|
21
|
-
"
|
|
17
|
+
"react": ["../preact/compat"],
|
|
18
|
+
"react-dom/client": ["../preact/compat/client"],
|
|
19
|
+
"react-dom/test-utils": ["../preact/test-utils"],
|
|
20
|
+
"react-dom": ["../preact/compat"], // Must be below client + test-utils
|
|
21
|
+
"react/jsx-runtime": ["../preact/jsx-runtime"],
|
|
22
|
+
"preact/jsx-runtime": ["../preact/jsx-runtime"]
|
|
22
23
|
}
|
|
23
24
|
},
|
|
24
25
|
"include": [
|
|
25
26
|
".",
|
|
26
|
-
"
|
|
27
|
+
"../types/global"
|
|
27
28
|
]
|
|
28
29
|
}
|
|
@@ -44,6 +44,13 @@ export type TConnectedProjectEnvConfig = {
|
|
|
44
44
|
urlInternal: string;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
export type TConnectedProjectHealthResponse = {
|
|
48
|
+
connectedProjects: string[];
|
|
49
|
+
identifier: string;
|
|
50
|
+
name: string;
|
|
51
|
+
ok: true;
|
|
52
|
+
};
|
|
53
|
+
|
|
47
54
|
export type TConnectedFetcherTarget = {
|
|
48
55
|
namespace: string;
|
|
49
56
|
controllerAccessor: string;
|
|
@@ -13,9 +13,18 @@ export type TServerHotReloadResult = {
|
|
|
13
13
|
error?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type TServerReadyConnectedProject = {
|
|
17
|
+
namespace: string;
|
|
18
|
+
identifier: string;
|
|
19
|
+
name: string;
|
|
20
|
+
urlInternal: string;
|
|
21
|
+
healthUrl: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
export type TServerReadyMessage = {
|
|
17
25
|
type: typeof serverHotReloadMessageType.ready;
|
|
18
26
|
publicUrl: string;
|
|
27
|
+
connectedProjects?: TServerReadyConnectedProject[];
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
export const isServerHotReloadRequest = (value: unknown): value is TServerHotReloadRequest =>
|
|
@@ -31,8 +40,20 @@ export const isServerHotReloadResult = (value: unknown): value is TServerHotRelo
|
|
|
31
40
|
(value as TServerHotReloadResult).type === serverHotReloadMessageType.failed) &&
|
|
32
41
|
Array.isArray((value as TServerHotReloadResult).changedFiles);
|
|
33
42
|
|
|
43
|
+
const isServerReadyConnectedProject = (value: unknown): value is TServerReadyConnectedProject =>
|
|
44
|
+
typeof value === 'object' &&
|
|
45
|
+
value !== null &&
|
|
46
|
+
typeof (value as TServerReadyConnectedProject).namespace === 'string' &&
|
|
47
|
+
typeof (value as TServerReadyConnectedProject).identifier === 'string' &&
|
|
48
|
+
typeof (value as TServerReadyConnectedProject).name === 'string' &&
|
|
49
|
+
typeof (value as TServerReadyConnectedProject).urlInternal === 'string' &&
|
|
50
|
+
typeof (value as TServerReadyConnectedProject).healthUrl === 'string';
|
|
51
|
+
|
|
34
52
|
export const isServerReadyMessage = (value: unknown): value is TServerReadyMessage =>
|
|
35
53
|
typeof value === 'object' &&
|
|
36
54
|
value !== null &&
|
|
37
55
|
(value as TServerReadyMessage).type === serverHotReloadMessageType.ready &&
|
|
38
|
-
typeof (value as TServerReadyMessage).publicUrl === 'string'
|
|
56
|
+
typeof (value as TServerReadyMessage).publicUrl === 'string' &&
|
|
57
|
+
((value as TServerReadyMessage).connectedProjects === undefined ||
|
|
58
|
+
(Array.isArray((value as TServerReadyMessage).connectedProjects) &&
|
|
59
|
+
(value as TServerReadyMessage).connectedProjects.every(isServerReadyConnectedProject)));
|
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.
|
|
4
|
+
"version": "2.1.9-2",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"preact": "^10.27.1",
|
|
70
70
|
"preact-render-to-string": "^6.6.1",
|
|
71
71
|
"prompts": "^2.4.2",
|
|
72
|
+
"react": "^19.2.0",
|
|
72
73
|
"react-apexcharts": "^2.1.0",
|
|
73
74
|
"replace-once": "^1.0.0",
|
|
74
75
|
"responsive-loader": "^3.1.2",
|
package/server/app.tsconfig.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "
|
|
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/*": ["
|
|
18
|
-
"@common/*": ["
|
|
19
|
-
"@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/
|
|
26
|
-
"react-dom": ["preact/
|
|
27
|
-
"react
|
|
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
|
-
"
|
|
34
|
+
"../types/global"
|
|
34
35
|
]
|
|
35
36
|
}
|
|
@@ -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(
|
|
@@ -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
|
|
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 = (
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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;
|