proteum 2.5.0 → 2.5.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 +2 -2
- package/README.md +46 -19
- package/agents/project/AGENTS.md +9 -7
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/diagnostics.md +1 -1
- package/agents/project/root/AGENTS.md +9 -7
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +11 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +14 -6
- package/cli/utils/agents.ts +1 -1
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/request/api.ts +2 -2
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +269 -0
- package/eslint.js +96 -50
- package/package.json +1 -1
- package/server/app/index.ts +28 -2
- package/server/services/router/index.ts +3 -3
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/dev-transpile-watch.test.cjs +3 -6
- package/tests/eslint-rules.test.cjs +246 -7
- package/tests/scaffold-templates.test.cjs +43 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/verify-changed.test.cjs +200 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import cp from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { type TVerificationCheckScope, type TVerificationConfig, type TVerificationSuiteConfig } from '../../common/applicationConfig';
|
|
6
|
+
import { loadVerificationConfig } from '../../common/applicationConfigLoader';
|
|
7
|
+
|
|
8
|
+
type TChangedFileMode = 'all' | 'base' | 'staged';
|
|
9
|
+
|
|
10
|
+
export type TChangedVerificationCheck = {
|
|
11
|
+
command: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
id: string;
|
|
14
|
+
matchedFiles: string[];
|
|
15
|
+
reasons: string[];
|
|
16
|
+
scope: TVerificationCheckScope;
|
|
17
|
+
source: 'builtin' | 'config';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TChangedVerificationSkippedCheck = {
|
|
21
|
+
id: string;
|
|
22
|
+
matchedFiles: string[];
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TChangedVerificationExecution = {
|
|
27
|
+
checkId: string;
|
|
28
|
+
command: string;
|
|
29
|
+
cwd: string;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
exitCode: number | null;
|
|
32
|
+
signal: NodeJS.Signals | null;
|
|
33
|
+
status: 'failed' | 'passed';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TChangedVerificationPlan = {
|
|
37
|
+
changedFiles: string[];
|
|
38
|
+
configFilepath?: string;
|
|
39
|
+
configRoot: string;
|
|
40
|
+
docsOnly: boolean;
|
|
41
|
+
gitRoot: string;
|
|
42
|
+
selectedChecks: TChangedVerificationCheck[];
|
|
43
|
+
skippedChecks: TChangedVerificationSkippedCheck[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TChangedVerificationResult = TChangedVerificationPlan & {
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
executions: TChangedVerificationExecution[];
|
|
49
|
+
result: {
|
|
50
|
+
failedChecks: number;
|
|
51
|
+
ok: boolean;
|
|
52
|
+
selectedChecks: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type TBuildChangedVerificationPlanOptions = {
|
|
57
|
+
changedFiles?: string[];
|
|
58
|
+
configSearchDir?: string;
|
|
59
|
+
cwd: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type TRunChangedVerificationOptions = TBuildChangedVerificationPlanOptions & {
|
|
63
|
+
base?: string;
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
onPlan?: (plan: TChangedVerificationPlan) => void;
|
|
66
|
+
staged?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sourceExtensions = new Set(['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']);
|
|
70
|
+
const docsOnlyExtensions = new Set(['.feature', '.md', '.mdx', '.rst', '.txt']);
|
|
71
|
+
const defaultDocsOnlyReason = 'docs-only changes do not require targeted tests unless a project rule matched.';
|
|
72
|
+
|
|
73
|
+
const dedupe = <TValue>(values: TValue[]) => [...new Set(values)];
|
|
74
|
+
const normalizePath = (value: string) => value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
75
|
+
const quoteShellValue = (value: string) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
76
|
+
const quoteFileList = (files: string[]) => files.map(quoteShellValue).join(' ');
|
|
77
|
+
|
|
78
|
+
const runGit = (cwd: string, args: string[]) => {
|
|
79
|
+
const result = cp.spawnSync('git', args, {
|
|
80
|
+
cwd,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.status !== 0) return [];
|
|
85
|
+
|
|
86
|
+
return result.stdout
|
|
87
|
+
.split(/\r?\n/)
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const resolveGitRoot = (cwd: string) => {
|
|
93
|
+
const [root] = runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
94
|
+
return root ? path.resolve(root) : path.resolve(cwd);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const fileExistsInRoot = (root: string, relativeFilepath: string) => fs.existsSync(path.join(root, relativeFilepath));
|
|
98
|
+
|
|
99
|
+
export const discoverChangedFiles = ({
|
|
100
|
+
base,
|
|
101
|
+
cwd,
|
|
102
|
+
staged,
|
|
103
|
+
}: {
|
|
104
|
+
base?: string;
|
|
105
|
+
cwd: string;
|
|
106
|
+
staged?: boolean;
|
|
107
|
+
}) => {
|
|
108
|
+
const gitRoot = resolveGitRoot(cwd);
|
|
109
|
+
const mode: TChangedFileMode = base ? 'base' : staged ? 'staged' : 'all';
|
|
110
|
+
const changedFiles =
|
|
111
|
+
mode === 'base'
|
|
112
|
+
? runGit(gitRoot, ['diff', '--name-only', '--diff-filter=ACMR', `${base}...HEAD`])
|
|
113
|
+
: mode === 'staged'
|
|
114
|
+
? runGit(gitRoot, ['diff', '--name-only', '--cached', '--diff-filter=ACMR'])
|
|
115
|
+
: [
|
|
116
|
+
...runGit(gitRoot, ['diff', '--name-only', '--diff-filter=ACMR']),
|
|
117
|
+
...runGit(gitRoot, ['diff', '--name-only', '--cached', '--diff-filter=ACMR']),
|
|
118
|
+
...runGit(gitRoot, ['ls-files', '--others', '--exclude-standard']),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
return dedupe(changedFiles.map(normalizePath)).filter((filepath) => fileExistsInRoot(gitRoot, filepath));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isTestFile = (filepath: string) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(filepath);
|
|
125
|
+
const isRelatedSourceFile = (filepath: string) => {
|
|
126
|
+
if (isTestFile(filepath)) return false;
|
|
127
|
+
if (!sourceExtensions.has(path.extname(filepath))) return false;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
filepath.startsWith('apps/') ||
|
|
131
|
+
filepath.startsWith('client/') ||
|
|
132
|
+
filepath.startsWith('cli/') ||
|
|
133
|
+
filepath.startsWith('common/') ||
|
|
134
|
+
filepath.startsWith('packages/') ||
|
|
135
|
+
filepath.startsWith('server/') ||
|
|
136
|
+
filepath.includes('/src/')
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const isDocsOnlyFile = (filepath: string) =>
|
|
141
|
+
filepath === 'AGENTS.md' ||
|
|
142
|
+
filepath === 'README.md' ||
|
|
143
|
+
filepath.startsWith('docs/') ||
|
|
144
|
+
filepath.startsWith('agents/') ||
|
|
145
|
+
docsOnlyExtensions.has(path.extname(filepath));
|
|
146
|
+
|
|
147
|
+
const normalizeGlob = (glob: string) => normalizePath(glob.trim());
|
|
148
|
+
|
|
149
|
+
const escapeRegExp = (value: string) => value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
150
|
+
|
|
151
|
+
const globToRegExp = (glob: string) => {
|
|
152
|
+
const normalized = normalizeGlob(glob);
|
|
153
|
+
let pattern = '';
|
|
154
|
+
|
|
155
|
+
for (let index = 0; index < normalized.length; ) {
|
|
156
|
+
if (normalized.slice(index, index + 3) === '**/') {
|
|
157
|
+
pattern += '(?:[^/]+/)*';
|
|
158
|
+
index += 3;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (normalized.slice(index, index + 2) === '**') {
|
|
163
|
+
pattern += '.*';
|
|
164
|
+
index += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const char = normalized[index];
|
|
169
|
+
pattern += char === '*' ? '[^/]*' : escapeRegExp(char);
|
|
170
|
+
index += 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new RegExp(`^${pattern}$`);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const matchesGlob = (filepath: string, glob: string) => globToRegExp(glob).test(normalizePath(filepath));
|
|
177
|
+
const matchFiles = (files: string[], globs: readonly string[]) =>
|
|
178
|
+
files.filter((filepath) => globs.some((glob) => matchesGlob(filepath, glob)));
|
|
179
|
+
|
|
180
|
+
const getSuiteCommand = (suite: TVerificationSuiteConfig) => (typeof suite === 'string' ? suite : suite.command);
|
|
181
|
+
const getSuiteCwd = (suite: TVerificationSuiteConfig, configRoot: string) => {
|
|
182
|
+
if (typeof suite === 'string' || !suite.cwd) return configRoot;
|
|
183
|
+
return path.isAbsolute(suite.cwd) ? suite.cwd : path.resolve(configRoot, suite.cwd);
|
|
184
|
+
};
|
|
185
|
+
const getSuiteScope = (suite: TVerificationSuiteConfig, fallback: TVerificationCheckScope = 'targeted') =>
|
|
186
|
+
typeof suite === 'string' || !suite.scope ? fallback : suite.scope;
|
|
187
|
+
|
|
188
|
+
const expandCommand = (command: string, files: string[]) => command.replace(/\{files\}/g, quoteFileList(files));
|
|
189
|
+
|
|
190
|
+
const hasFilesPlaceholder = (command: string) => command.includes('{files}');
|
|
191
|
+
|
|
192
|
+
const addCheck = ({
|
|
193
|
+
check,
|
|
194
|
+
selectedChecks,
|
|
195
|
+
}: {
|
|
196
|
+
check: TChangedVerificationCheck;
|
|
197
|
+
selectedChecks: TChangedVerificationCheck[];
|
|
198
|
+
}) => {
|
|
199
|
+
const existing = selectedChecks.find((entry) => entry.command === check.command && entry.cwd === check.cwd);
|
|
200
|
+
if (!existing) {
|
|
201
|
+
selectedChecks.push(check);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
existing.matchedFiles = dedupe([...existing.matchedFiles, ...check.matchedFiles]);
|
|
206
|
+
existing.reasons = dedupe([...existing.reasons, ...check.reasons]);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const createSuiteCheck = ({
|
|
210
|
+
configRoot,
|
|
211
|
+
files,
|
|
212
|
+
id,
|
|
213
|
+
reason,
|
|
214
|
+
scope,
|
|
215
|
+
source,
|
|
216
|
+
suite,
|
|
217
|
+
}: {
|
|
218
|
+
configRoot: string;
|
|
219
|
+
files: string[];
|
|
220
|
+
id: string;
|
|
221
|
+
reason: string;
|
|
222
|
+
scope?: TVerificationCheckScope;
|
|
223
|
+
source: 'builtin' | 'config';
|
|
224
|
+
suite: TVerificationSuiteConfig;
|
|
225
|
+
}): TChangedVerificationCheck | undefined => {
|
|
226
|
+
const command = getSuiteCommand(suite);
|
|
227
|
+
if (hasFilesPlaceholder(command) && files.length === 0) return undefined;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
command: expandCommand(command, files),
|
|
231
|
+
cwd: getSuiteCwd(suite, configRoot),
|
|
232
|
+
id,
|
|
233
|
+
matchedFiles: files,
|
|
234
|
+
reasons: [reason],
|
|
235
|
+
scope: scope || getSuiteScope(suite),
|
|
236
|
+
source,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const addConfigSuiteCheck = ({
|
|
241
|
+
config,
|
|
242
|
+
configRoot,
|
|
243
|
+
files,
|
|
244
|
+
id,
|
|
245
|
+
reason,
|
|
246
|
+
run,
|
|
247
|
+
scope,
|
|
248
|
+
selectedChecks,
|
|
249
|
+
skippedChecks,
|
|
250
|
+
}: {
|
|
251
|
+
config: TVerificationConfig;
|
|
252
|
+
configRoot: string;
|
|
253
|
+
files: string[];
|
|
254
|
+
id: string;
|
|
255
|
+
reason: string;
|
|
256
|
+
run: string;
|
|
257
|
+
scope?: TVerificationCheckScope;
|
|
258
|
+
selectedChecks: TChangedVerificationCheck[];
|
|
259
|
+
skippedChecks: TChangedVerificationSkippedCheck[];
|
|
260
|
+
}) => {
|
|
261
|
+
const suite = config.suites?.[run];
|
|
262
|
+
if (!suite) {
|
|
263
|
+
skippedChecks.push({ id: `${id}:${run}`, matchedFiles: files, reason: `Unknown verification suite "${run}".` });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const check = createSuiteCheck({
|
|
268
|
+
configRoot,
|
|
269
|
+
files,
|
|
270
|
+
id: `${id}:${run}`,
|
|
271
|
+
reason,
|
|
272
|
+
scope,
|
|
273
|
+
source: 'config',
|
|
274
|
+
suite,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!check) {
|
|
278
|
+
skippedChecks.push({ id: `${id}:${run}`, matchedFiles: files, reason: `Suite "${run}" requires matched files.` });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
addCheck({ check, selectedChecks });
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const getDocsOnlyReason = (config: TVerificationConfig) => {
|
|
286
|
+
if (config.docsOnly === false) return undefined;
|
|
287
|
+
if (typeof config.docsOnly === 'object' && config.docsOnly.reason) return config.docsOnly.reason;
|
|
288
|
+
return defaultDocsOnlyReason;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const buildChangedVerificationPlan = ({
|
|
292
|
+
changedFiles,
|
|
293
|
+
configSearchDir,
|
|
294
|
+
cwd,
|
|
295
|
+
}: TBuildChangedVerificationPlanOptions): TChangedVerificationPlan => {
|
|
296
|
+
const gitRoot = resolveGitRoot(cwd);
|
|
297
|
+
const files = dedupe((changedFiles || discoverChangedFiles({ cwd })).map(normalizePath)).filter((filepath) =>
|
|
298
|
+
fileExistsInRoot(gitRoot, filepath),
|
|
299
|
+
);
|
|
300
|
+
const { config, filepath: configFilepath, root: configRoot } = loadVerificationConfig(configSearchDir || cwd);
|
|
301
|
+
const selectedChecks: TChangedVerificationCheck[] = [];
|
|
302
|
+
const skippedChecks: TChangedVerificationSkippedCheck[] = [];
|
|
303
|
+
const docsOnly = files.length > 0 && files.every(isDocsOnlyFile);
|
|
304
|
+
|
|
305
|
+
for (const entry of config.always || []) {
|
|
306
|
+
const suite = config.suites?.[entry] || entry;
|
|
307
|
+
const check = createSuiteCheck({
|
|
308
|
+
configRoot,
|
|
309
|
+
files,
|
|
310
|
+
id: `always:${entry}`,
|
|
311
|
+
reason: 'Configured always-run verification.',
|
|
312
|
+
scope: 'static',
|
|
313
|
+
source: 'config',
|
|
314
|
+
suite,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (check) addCheck({ check, selectedChecks });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (docsOnly) {
|
|
321
|
+
const reason = getDocsOnlyReason(config);
|
|
322
|
+
if (reason) skippedChecks.push({ id: 'builtin:docs-only', matchedFiles: files, reason });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const changedTestFiles = files.filter(isTestFile);
|
|
326
|
+
if (changedTestFiles.length > 0) {
|
|
327
|
+
const check = createSuiteCheck({
|
|
328
|
+
configRoot: gitRoot,
|
|
329
|
+
files: changedTestFiles,
|
|
330
|
+
id: 'builtin:changed-tests',
|
|
331
|
+
reason: 'Changed test files should run directly.',
|
|
332
|
+
scope: 'targeted',
|
|
333
|
+
source: 'builtin',
|
|
334
|
+
suite: 'npx vitest run {files}',
|
|
335
|
+
});
|
|
336
|
+
if (check) addCheck({ check, selectedChecks });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const changedSourceFiles = docsOnly ? [] : files.filter(isRelatedSourceFile);
|
|
340
|
+
if (changedSourceFiles.length > 0) {
|
|
341
|
+
const check = createSuiteCheck({
|
|
342
|
+
configRoot: gitRoot,
|
|
343
|
+
files: changedSourceFiles,
|
|
344
|
+
id: 'builtin:related-tests',
|
|
345
|
+
reason: 'Changed source files should run related tests.',
|
|
346
|
+
scope: 'targeted',
|
|
347
|
+
source: 'builtin',
|
|
348
|
+
suite: 'npx vitest related {files}',
|
|
349
|
+
});
|
|
350
|
+
if (check) addCheck({ check, selectedChecks });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const rule of config.rules || []) {
|
|
354
|
+
const matchedFiles = matchFiles(files, rule.match);
|
|
355
|
+
if (matchedFiles.length === 0) continue;
|
|
356
|
+
|
|
357
|
+
for (const run of rule.run) {
|
|
358
|
+
addConfigSuiteCheck({
|
|
359
|
+
config,
|
|
360
|
+
configRoot,
|
|
361
|
+
files: matchedFiles,
|
|
362
|
+
id: rule.id,
|
|
363
|
+
reason: rule.reason,
|
|
364
|
+
run,
|
|
365
|
+
scope: rule.scope,
|
|
366
|
+
selectedChecks,
|
|
367
|
+
skippedChecks,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
changedFiles: files,
|
|
374
|
+
...(configFilepath ? { configFilepath } : {}),
|
|
375
|
+
configRoot,
|
|
376
|
+
docsOnly,
|
|
377
|
+
gitRoot,
|
|
378
|
+
selectedChecks,
|
|
379
|
+
skippedChecks,
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const runShellCommand = (check: TChangedVerificationCheck) =>
|
|
384
|
+
new Promise<TChangedVerificationExecution>((resolve) => {
|
|
385
|
+
const startedAt = Date.now();
|
|
386
|
+
const child = cp.spawn(check.command, [], {
|
|
387
|
+
cwd: check.cwd,
|
|
388
|
+
shell: true,
|
|
389
|
+
stdio: 'inherit',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
child.on('error', () => {
|
|
393
|
+
resolve({
|
|
394
|
+
checkId: check.id,
|
|
395
|
+
command: check.command,
|
|
396
|
+
cwd: check.cwd,
|
|
397
|
+
durationMs: Date.now() - startedAt,
|
|
398
|
+
exitCode: 1,
|
|
399
|
+
signal: null,
|
|
400
|
+
status: 'failed',
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
child.on('exit', (exitCode, signal) => {
|
|
404
|
+
resolve({
|
|
405
|
+
checkId: check.id,
|
|
406
|
+
command: check.command,
|
|
407
|
+
cwd: check.cwd,
|
|
408
|
+
durationMs: Date.now() - startedAt,
|
|
409
|
+
exitCode,
|
|
410
|
+
signal,
|
|
411
|
+
status: exitCode === 0 && signal === null ? 'passed' : 'failed',
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
export const runChangedVerification = async ({
|
|
417
|
+
base,
|
|
418
|
+
changedFiles,
|
|
419
|
+
configSearchDir,
|
|
420
|
+
cwd,
|
|
421
|
+
dryRun = false,
|
|
422
|
+
onPlan,
|
|
423
|
+
staged = false,
|
|
424
|
+
}: TRunChangedVerificationOptions): Promise<TChangedVerificationResult> => {
|
|
425
|
+
const files = changedFiles || discoverChangedFiles({ base, cwd, staged });
|
|
426
|
+
const plan = buildChangedVerificationPlan({ changedFiles: files, configSearchDir, cwd });
|
|
427
|
+
const executions: TChangedVerificationExecution[] = [];
|
|
428
|
+
|
|
429
|
+
if (!dryRun) {
|
|
430
|
+
onPlan?.(plan);
|
|
431
|
+
for (const check of plan.selectedChecks) executions.push(await runShellCommand(check));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const failedChecks = executions.filter((execution) => execution.status === 'failed').length;
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
...plan,
|
|
438
|
+
dryRun,
|
|
439
|
+
executions,
|
|
440
|
+
result: {
|
|
441
|
+
failedChecks,
|
|
442
|
+
ok: failedChecks === 0,
|
|
443
|
+
selectedChecks: plan.selectedChecks.length,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
export const renderChangedVerificationPlan = (plan: TChangedVerificationPlan) =>
|
|
449
|
+
[
|
|
450
|
+
'Changed Verification Plan',
|
|
451
|
+
`- config=${plan.configFilepath || 'none'}`,
|
|
452
|
+
`- changedFiles=${plan.changedFiles.length}`,
|
|
453
|
+
`- selectedChecks=${plan.selectedChecks.length}`,
|
|
454
|
+
`- skippedChecks=${plan.skippedChecks.length}`,
|
|
455
|
+
...plan.selectedChecks.map(
|
|
456
|
+
(check) =>
|
|
457
|
+
`- [${check.scope}] ${check.id} cwd=${path.relative(plan.gitRoot, check.cwd) || '.'} command=${check.command} reason=${check.reasons.join('; ')}`,
|
|
458
|
+
),
|
|
459
|
+
...plan.skippedChecks.map((check) => `- [skipped] ${check.id} reason=${check.reason}`),
|
|
460
|
+
].join('\n');
|
package/client/app/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ if (typeof window === 'undefined') throw new Error(`This file shouldn't be loade
|
|
|
7
7
|
|
|
8
8
|
window.dev && require('preact/debug');
|
|
9
9
|
|
|
10
|
-
// Core
|
|
11
|
-
import { CoreError } from '@common/errors';
|
|
12
10
|
import type { Layout } from '@common/router';
|
|
13
11
|
import { createDialog } from '@client/components/Dialog/Manager';
|
|
14
12
|
|
|
@@ -53,6 +51,21 @@ const isIgnorableBrowserErrorMessage = (message: string) =>
|
|
|
53
51
|
message === 'ResizeObserver loop completed with undelivered notifications.' ||
|
|
54
52
|
message === 'ResizeObserver loop limit exceeded';
|
|
55
53
|
|
|
54
|
+
export const getClientErrorMessage = (error: unknown, fallbackMessage = 'Unknown client error'): string => {
|
|
55
|
+
if (error instanceof Error && error.message) return error.message;
|
|
56
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
57
|
+
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string' && error.message)
|
|
58
|
+
return error.message;
|
|
59
|
+
|
|
60
|
+
return fallbackMessage;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const normalizeClientError = (error: unknown, fallbackMessage?: string): Error => {
|
|
64
|
+
if (error instanceof Error) return error;
|
|
65
|
+
|
|
66
|
+
return new Error(getClientErrorMessage(error, fallbackMessage));
|
|
67
|
+
};
|
|
68
|
+
|
|
56
69
|
/*----------------------------------
|
|
57
70
|
- CLASS
|
|
58
71
|
----------------------------------*/
|
|
@@ -88,8 +101,7 @@ export default abstract class Application {
|
|
|
88
101
|
public bindErrorHandlers() {
|
|
89
102
|
// Impossible de recup le stacktrace ...
|
|
90
103
|
window.addEventListener('unhandledrejection', (e) => {
|
|
91
|
-
|
|
92
|
-
this.handleError(error);
|
|
104
|
+
this.handleError(e.reason);
|
|
93
105
|
});
|
|
94
106
|
|
|
95
107
|
window.onerror = (message, file, line, col, stacktrace) => {
|
|
@@ -109,7 +121,12 @@ export default abstract class Application {
|
|
|
109
121
|
};
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
public
|
|
124
|
+
public handleError(error: unknown, fallbackMessage?: any): string | void {
|
|
125
|
+
const normalizedError = normalizeClientError(error, fallbackMessage);
|
|
126
|
+
console.error(normalizedError);
|
|
127
|
+
|
|
128
|
+
return getClientErrorMessage(error, fallbackMessage);
|
|
129
|
+
}
|
|
113
130
|
|
|
114
131
|
public abstract handleUpdate(): void;
|
|
115
132
|
|
|
@@ -194,7 +194,7 @@ export default class ClientRouter<
|
|
|
194
194
|
} = {};
|
|
195
195
|
|
|
196
196
|
public async registerRoutes() {
|
|
197
|
-
const loaders = appRoutes as TRoutesLoaders;
|
|
197
|
+
const loaders = appRoutes as unknown as TRoutesLoaders;
|
|
198
198
|
let currentRoute: TUnresolvedRoute | undefined;
|
|
199
199
|
debug && console.log(LogPrefix, `Indexing routes and finding the current route from ssr data:`, this.context);
|
|
200
200
|
|
|
@@ -109,7 +109,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
109
109
|
.then((data: TObjetDonnees) => {
|
|
110
110
|
this.set(data);
|
|
111
111
|
})
|
|
112
|
-
.catch((error:
|
|
112
|
+
.catch((error: unknown) => {
|
|
113
113
|
this.app.handleError(error);
|
|
114
114
|
});
|
|
115
115
|
}
|
|
@@ -270,7 +270,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
270
270
|
);
|
|
271
271
|
|
|
272
272
|
// API Error hook
|
|
273
|
-
this.app.handleError(e
|
|
273
|
+
this.app.handleError(e);
|
|
274
274
|
|
|
275
275
|
throw e;
|
|
276
276
|
}
|