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/AGENTS.md +16 -5
- package/README.md +5 -1
- package/agents/project/AGENTS.md +13 -2
- package/agents/project/diagnostics.md +4 -0
- package/agents/project/optimizations.md +1 -0
- package/cli/app/index.ts +7 -7
- package/cli/bin.js +0 -8
- package/cli/commands/build.ts +60 -9
- package/cli/commands/dev.ts +232 -5
- package/cli/compiler/artifacts/commands.ts +20 -5
- package/cli/compiler/client/index.ts +9 -9
- package/cli/compiler/common/bundleAnalysis.ts +56 -1
- package/cli/compiler/common/index.ts +6 -5
- package/cli/compiler/index.ts +12 -5
- package/cli/compiler/server/index.ts +7 -6
- package/cli/index.ts +43 -2
- package/cli/paths.ts +300 -10
- package/cli/presentation/commands.ts +30 -4
- package/cli/presentation/devSession.ts +24 -29
- package/cli/presentation/help.ts +4 -0
- package/cli/presentation/welcome.ts +69 -0
- package/cli/runtime/commands.ts +40 -3
- package/cli/runtime/devSessions.ts +337 -0
- 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 +1 -1
- package/server/app.tsconfig.json +10 -9
- package/server/services/router/http/index.ts +72 -10
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
export const devSessionRegistryVersion = 1 as const;
|
|
6
|
+
|
|
7
|
+
export type TDevSessionState = 'starting' | 'ready';
|
|
8
|
+
|
|
9
|
+
export type TDevSessionRecord = {
|
|
10
|
+
version: typeof devSessionRegistryVersion;
|
|
11
|
+
pid: number;
|
|
12
|
+
appRoot: string;
|
|
13
|
+
routerPort: number;
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
sessionFilePath: string;
|
|
18
|
+
state: TDevSessionState;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TDevSessionInspection = {
|
|
22
|
+
sessionFilePath: string;
|
|
23
|
+
record: TDevSessionRecord | null;
|
|
24
|
+
live: boolean;
|
|
25
|
+
stale: boolean;
|
|
26
|
+
invalid: boolean;
|
|
27
|
+
parseError: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TStopDevSessionResult = {
|
|
31
|
+
sessionFilePath: string;
|
|
32
|
+
pid: number | null;
|
|
33
|
+
routerPort: number | null;
|
|
34
|
+
publicUrl: string;
|
|
35
|
+
state: TDevSessionState | '';
|
|
36
|
+
matched: boolean;
|
|
37
|
+
stopped: boolean;
|
|
38
|
+
removed: boolean;
|
|
39
|
+
stale: boolean;
|
|
40
|
+
live: boolean;
|
|
41
|
+
invalid: boolean;
|
|
42
|
+
parseError: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const defaultRegistryDirectoryParts = ['var', 'run', 'proteum', 'dev'];
|
|
46
|
+
|
|
47
|
+
const sleep = async (durationMs: number) => await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
48
|
+
|
|
49
|
+
const isRecordShape = (value: unknown): value is TDevSessionRecord => {
|
|
50
|
+
if (!value || typeof value !== 'object') return false;
|
|
51
|
+
|
|
52
|
+
const candidate = value as Partial<TDevSessionRecord>;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
candidate.version === devSessionRegistryVersion &&
|
|
56
|
+
typeof candidate.pid === 'number' &&
|
|
57
|
+
Number.isInteger(candidate.pid) &&
|
|
58
|
+
candidate.pid > 0 &&
|
|
59
|
+
typeof candidate.appRoot === 'string' &&
|
|
60
|
+
candidate.appRoot.length > 0 &&
|
|
61
|
+
typeof candidate.routerPort === 'number' &&
|
|
62
|
+
Number.isInteger(candidate.routerPort) &&
|
|
63
|
+
candidate.routerPort > 0 &&
|
|
64
|
+
typeof candidate.publicUrl === 'string' &&
|
|
65
|
+
typeof candidate.startedAt === 'string' &&
|
|
66
|
+
typeof candidate.updatedAt === 'string' &&
|
|
67
|
+
typeof candidate.sessionFilePath === 'string' &&
|
|
68
|
+
candidate.sessionFilePath.length > 0 &&
|
|
69
|
+
(candidate.state === 'starting' || candidate.state === 'ready')
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const canSignalProcess = (pid: number, signal: NodeJS.Signals | 0) => {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, signal);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const errno = error as NodeJS.ErrnoException;
|
|
79
|
+
|
|
80
|
+
if (errno.code === 'ESRCH') return false;
|
|
81
|
+
if (errno.code === 'EPERM') return true;
|
|
82
|
+
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const isProcessAlive = (pid: number) => canSignalProcess(pid, 0);
|
|
88
|
+
|
|
89
|
+
const waitForProcessExit = async (pid: number, timeoutMs: number) => {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
if (!isProcessAlive(pid)) return true;
|
|
94
|
+
await sleep(100);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return !isProcessAlive(pid);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const getDevSessionRegistryDirectory = (appRoot: string) => path.join(appRoot, ...defaultRegistryDirectoryParts);
|
|
101
|
+
|
|
102
|
+
export const resolveDevSessionFilePath = ({
|
|
103
|
+
appRoot,
|
|
104
|
+
port,
|
|
105
|
+
sessionFilePath,
|
|
106
|
+
}: {
|
|
107
|
+
appRoot: string;
|
|
108
|
+
port: number;
|
|
109
|
+
sessionFilePath?: string;
|
|
110
|
+
}) => {
|
|
111
|
+
if (sessionFilePath && sessionFilePath.trim()) {
|
|
112
|
+
return path.isAbsolute(sessionFilePath)
|
|
113
|
+
? path.normalize(sessionFilePath)
|
|
114
|
+
: path.resolve(appRoot, sessionFilePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return path.join(getDevSessionRegistryDirectory(appRoot), `${port}.json`);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const createDevSessionRecord = ({
|
|
121
|
+
appRoot,
|
|
122
|
+
port,
|
|
123
|
+
sessionFilePath,
|
|
124
|
+
}: {
|
|
125
|
+
appRoot: string;
|
|
126
|
+
port: number;
|
|
127
|
+
sessionFilePath: string;
|
|
128
|
+
}): TDevSessionRecord => {
|
|
129
|
+
const timestamp = new Date().toISOString();
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
version: devSessionRegistryVersion,
|
|
133
|
+
pid: process.pid,
|
|
134
|
+
appRoot,
|
|
135
|
+
routerPort: port,
|
|
136
|
+
publicUrl: '',
|
|
137
|
+
startedAt: timestamp,
|
|
138
|
+
updatedAt: timestamp,
|
|
139
|
+
sessionFilePath,
|
|
140
|
+
state: 'starting',
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const writeDevSessionRecord = async (record: TDevSessionRecord) => {
|
|
145
|
+
await fs.ensureDir(path.dirname(record.sessionFilePath));
|
|
146
|
+
await fs.writeJson(record.sessionFilePath, record, { spaces: 2 });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const updateDevSessionRecord = async ({
|
|
150
|
+
sessionFilePath,
|
|
151
|
+
patch,
|
|
152
|
+
}: {
|
|
153
|
+
sessionFilePath: string;
|
|
154
|
+
patch: Partial<Omit<TDevSessionRecord, 'version' | 'pid' | 'appRoot' | 'routerPort' | 'startedAt' | 'sessionFilePath'>>;
|
|
155
|
+
}) => {
|
|
156
|
+
const inspection = await inspectDevSessionFile(sessionFilePath);
|
|
157
|
+
if (!inspection || !inspection.record) return;
|
|
158
|
+
|
|
159
|
+
await writeDevSessionRecord({
|
|
160
|
+
...inspection.record,
|
|
161
|
+
...patch,
|
|
162
|
+
updatedAt: new Date().toISOString(),
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const removeDevSessionRecord = async (sessionFilePath: string) => {
|
|
167
|
+
await fs.remove(sessionFilePath);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const removeDevSessionRecordSync = (sessionFilePath: string) => {
|
|
171
|
+
try {
|
|
172
|
+
fs.removeSync(sessionFilePath);
|
|
173
|
+
} catch {
|
|
174
|
+
// Best-effort cleanup during process exit.
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const inspectDevSessionFile = async (sessionFilePath: string): Promise<TDevSessionInspection | null> => {
|
|
179
|
+
if (!(await fs.pathExists(sessionFilePath))) return null;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const rawValue = await fs.readJson(sessionFilePath);
|
|
183
|
+
if (!isRecordShape(rawValue)) {
|
|
184
|
+
return {
|
|
185
|
+
sessionFilePath,
|
|
186
|
+
record: null,
|
|
187
|
+
live: false,
|
|
188
|
+
stale: true,
|
|
189
|
+
invalid: true,
|
|
190
|
+
parseError: 'Session file contents do not match the Proteum dev session schema.',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const record = rawValue;
|
|
195
|
+
const live = isProcessAlive(record.pid);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
sessionFilePath,
|
|
199
|
+
record,
|
|
200
|
+
live,
|
|
201
|
+
stale: !live,
|
|
202
|
+
invalid: false,
|
|
203
|
+
parseError: '',
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
sessionFilePath,
|
|
208
|
+
record: null,
|
|
209
|
+
live: false,
|
|
210
|
+
stale: true,
|
|
211
|
+
invalid: true,
|
|
212
|
+
parseError: error instanceof Error ? error.message : String(error),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const listDevSessionFiles = async ({
|
|
218
|
+
appRoot,
|
|
219
|
+
sessionFilePath,
|
|
220
|
+
}: {
|
|
221
|
+
appRoot: string;
|
|
222
|
+
sessionFilePath?: string;
|
|
223
|
+
}) => {
|
|
224
|
+
if (sessionFilePath && sessionFilePath.trim())
|
|
225
|
+
return [resolveDevSessionFilePath({ appRoot, port: 1, sessionFilePath })];
|
|
226
|
+
|
|
227
|
+
const registryDirectory = getDevSessionRegistryDirectory(appRoot);
|
|
228
|
+
if (!(await fs.pathExists(registryDirectory))) return [];
|
|
229
|
+
|
|
230
|
+
const entries = await fs.readdir(registryDirectory);
|
|
231
|
+
|
|
232
|
+
return entries
|
|
233
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
234
|
+
.sort((left, right) => left.localeCompare(right))
|
|
235
|
+
.map((entry) => path.join(registryDirectory, entry));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const listDevSessionInspections = async ({
|
|
239
|
+
appRoot,
|
|
240
|
+
sessionFilePath,
|
|
241
|
+
}: {
|
|
242
|
+
appRoot: string;
|
|
243
|
+
sessionFilePath?: string;
|
|
244
|
+
}) => {
|
|
245
|
+
const sessionFilePaths = await listDevSessionFiles({ appRoot, sessionFilePath });
|
|
246
|
+
const inspections = await Promise.all(sessionFilePaths.map((entryPath) => inspectDevSessionFile(entryPath)));
|
|
247
|
+
|
|
248
|
+
return inspections.filter((inspection): inspection is TDevSessionInspection => inspection !== null);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const stopDevSessionFile = async (sessionFilePath: string): Promise<TStopDevSessionResult> => {
|
|
252
|
+
const inspection = await inspectDevSessionFile(sessionFilePath);
|
|
253
|
+
|
|
254
|
+
if (!inspection) {
|
|
255
|
+
return {
|
|
256
|
+
sessionFilePath,
|
|
257
|
+
pid: null,
|
|
258
|
+
routerPort: null,
|
|
259
|
+
publicUrl: '',
|
|
260
|
+
state: '',
|
|
261
|
+
matched: false,
|
|
262
|
+
stopped: false,
|
|
263
|
+
removed: false,
|
|
264
|
+
stale: false,
|
|
265
|
+
live: false,
|
|
266
|
+
invalid: false,
|
|
267
|
+
parseError: '',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!inspection.record) {
|
|
272
|
+
await removeDevSessionRecord(sessionFilePath);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
sessionFilePath,
|
|
276
|
+
pid: null,
|
|
277
|
+
routerPort: null,
|
|
278
|
+
publicUrl: '',
|
|
279
|
+
state: '',
|
|
280
|
+
matched: true,
|
|
281
|
+
stopped: true,
|
|
282
|
+
removed: true,
|
|
283
|
+
stale: true,
|
|
284
|
+
live: false,
|
|
285
|
+
invalid: true,
|
|
286
|
+
parseError: inspection.parseError,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { record } = inspection;
|
|
291
|
+
|
|
292
|
+
if (!inspection.live) {
|
|
293
|
+
await removeDevSessionRecord(sessionFilePath);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
sessionFilePath,
|
|
297
|
+
pid: record.pid,
|
|
298
|
+
routerPort: record.routerPort,
|
|
299
|
+
publicUrl: record.publicUrl,
|
|
300
|
+
state: record.state,
|
|
301
|
+
matched: true,
|
|
302
|
+
stopped: true,
|
|
303
|
+
removed: true,
|
|
304
|
+
stale: true,
|
|
305
|
+
live: false,
|
|
306
|
+
invalid: false,
|
|
307
|
+
parseError: '',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (canSignalProcess(record.pid, 'SIGTERM')) {
|
|
312
|
+
const exitedAfterTerm = await waitForProcessExit(record.pid, 5000);
|
|
313
|
+
if (!exitedAfterTerm && canSignalProcess(record.pid, 'SIGKILL')) {
|
|
314
|
+
await waitForProcessExit(record.pid, 2000);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const live = isProcessAlive(record.pid);
|
|
319
|
+
if (!live) {
|
|
320
|
+
await removeDevSessionRecord(sessionFilePath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
sessionFilePath,
|
|
325
|
+
pid: record.pid,
|
|
326
|
+
routerPort: record.routerPort,
|
|
327
|
+
publicUrl: record.publicUrl,
|
|
328
|
+
state: record.state,
|
|
329
|
+
matched: true,
|
|
330
|
+
stopped: !live,
|
|
331
|
+
removed: !live,
|
|
332
|
+
stale: !live,
|
|
333
|
+
live,
|
|
334
|
+
invalid: false,
|
|
335
|
+
parseError: '',
|
|
336
|
+
};
|
|
337
|
+
};
|
package/cli/scaffold/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createServiceConfigTemplate,
|
|
27
27
|
createServiceTemplate,
|
|
28
28
|
} from './templates';
|
|
29
|
+
import type { TTsconfigTemplatePaths } from './templates';
|
|
29
30
|
import type { TScaffoldFilePlan, TScaffoldInitConfig, TScaffoldKind, TScaffoldResult } from './types';
|
|
30
31
|
|
|
31
32
|
type TCreatePlan = {
|
|
@@ -602,7 +603,29 @@ const assertInitTarget = ({ appRoot }: { appRoot: string }) => {
|
|
|
602
603
|
);
|
|
603
604
|
};
|
|
604
605
|
|
|
605
|
-
const
|
|
606
|
+
const createAppTsconfigTemplatePaths = ({
|
|
607
|
+
appRoot,
|
|
608
|
+
side,
|
|
609
|
+
}: {
|
|
610
|
+
appRoot: string;
|
|
611
|
+
side: 'client' | 'server';
|
|
612
|
+
}): TTsconfigTemplatePaths => {
|
|
613
|
+
const tsconfigFilepath = path.join(appRoot, side, 'tsconfig.json');
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
frameworkTsconfig: cli.paths.relativeFrameworkPathForAppRoot(appRoot, tsconfigFilepath, 'tsconfig.common.json'),
|
|
617
|
+
frameworkClient: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'client', '*'),
|
|
618
|
+
frameworkCommon: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'common', '*'),
|
|
619
|
+
frameworkServer: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'server', '*'),
|
|
620
|
+
frameworkTypesGlobal: cli.paths.relativeFrameworkPathForAppRoot(appRoot, tsconfigFilepath, 'types', 'global'),
|
|
621
|
+
preactCompat: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'compat'),
|
|
622
|
+
preactCompatClient: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'compat', 'client'),
|
|
623
|
+
preactTestUtils: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'test-utils'),
|
|
624
|
+
preactJsxRuntime: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'jsx-runtime'),
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const createInitFilePlans = ({ appRoot, config }: { appRoot: string; config: TScaffoldInitConfig }): TScaffoldFilePlan[] => [
|
|
606
629
|
{
|
|
607
630
|
relativePath: 'package.json',
|
|
608
631
|
content: createPackageJsonTemplate({
|
|
@@ -643,11 +666,11 @@ const createInitFilePlans = (config: TScaffoldInitConfig): TScaffoldFilePlan[] =
|
|
|
643
666
|
},
|
|
644
667
|
{
|
|
645
668
|
relativePath: path.join('client', 'tsconfig.json'),
|
|
646
|
-
content: createClientTsconfigTemplate(),
|
|
669
|
+
content: createClientTsconfigTemplate(createAppTsconfigTemplatePaths({ appRoot, side: 'client' })),
|
|
647
670
|
},
|
|
648
671
|
{
|
|
649
672
|
relativePath: path.join('server', 'tsconfig.json'),
|
|
650
|
-
content: createServerTsconfigTemplate(),
|
|
673
|
+
content: createServerTsconfigTemplate(createAppTsconfigTemplatePaths({ appRoot, side: 'server' })),
|
|
651
674
|
},
|
|
652
675
|
{
|
|
653
676
|
relativePath: path.join('server', 'config', 'app.ts'),
|
|
@@ -675,7 +698,7 @@ export const runInitScaffold = async () => {
|
|
|
675
698
|
assertInitTarget({ appRoot });
|
|
676
699
|
|
|
677
700
|
const result = createEmptyResult({ dryRun: isDryRun() });
|
|
678
|
-
const filePlans = createInitFilePlans(config);
|
|
701
|
+
const filePlans = createInitFilePlans({ appRoot, config });
|
|
679
702
|
|
|
680
703
|
maybeWriteFilePlans({ rootDir: appRoot, filePlans, result });
|
|
681
704
|
|
|
@@ -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)));
|