@gotgenes/pi-autoformat 0.1.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.
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/release-please.yml +22 -0
- package/.markdownlint-cli2.yaml +3 -0
- package/.pi/extensions/pi-autoformat/config.json +28 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +71 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/biome.json +17 -0
- package/docs/configuration.md +177 -0
- package/docs/plans/0001-initial-implementation-plan.md +402 -0
- package/package.json +32 -0
- package/prek.toml +24 -0
- package/release-please-config.json +22 -0
- package/schemas/pi-autoformat.schema.json +87 -0
- package/src/config-loader.ts +520 -0
- package/src/extension.ts +374 -0
- package/src/formatter-config.ts +80 -0
- package/src/formatter-executor.ts +68 -0
- package/src/formatter-registry.ts +61 -0
- package/src/index.ts +42 -0
- package/src/prompt-autoformatter.ts +58 -0
- package/src/touched-files-queue.ts +46 -0
- package/test/config-loader.test.ts +199 -0
- package/test/extension.test.ts +364 -0
- package/test/formatter-config.test.ts +64 -0
- package/test/formatter-executor.test.ts +82 -0
- package/test/formatter-registry.test.ts +75 -0
- package/test/prompt-autoformatter.test.ts +93 -0
- package/test/smoke.test.ts +9 -0
- package/test/touched-files-queue.test.ts +46 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type AutoformatConfig,
|
|
7
|
+
createFormatterConfig,
|
|
8
|
+
type FormatMode,
|
|
9
|
+
type UserFormatterConfig,
|
|
10
|
+
} from "./formatter-config.js";
|
|
11
|
+
import type { FormatterDefinition } from "./formatter-registry.js";
|
|
12
|
+
|
|
13
|
+
export const AUTOFORMAT_EXTENSION_ID = "pi-autoformat";
|
|
14
|
+
export const AUTOFORMAT_CONFIG_FILE_NAME = "config.json";
|
|
15
|
+
|
|
16
|
+
export type ConfigValidationIssue = {
|
|
17
|
+
path: string;
|
|
18
|
+
message: string;
|
|
19
|
+
sourcePath?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ValidateConfigResult = {
|
|
23
|
+
config: UserFormatterConfig;
|
|
24
|
+
issues: ConfigValidationIssue[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type LoadConfigResult = {
|
|
28
|
+
config: AutoformatConfig;
|
|
29
|
+
globalConfigPath: string;
|
|
30
|
+
projectConfigPath: string;
|
|
31
|
+
issues: ConfigValidationIssue[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function defaultAgentDir(): string {
|
|
35
|
+
return join(homedir(), ".pi", "agent");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pushIssue(
|
|
43
|
+
issues: ConfigValidationIssue[],
|
|
44
|
+
path: string,
|
|
45
|
+
message: string,
|
|
46
|
+
sourcePath?: string,
|
|
47
|
+
): void {
|
|
48
|
+
issues.push({ path, message, sourcePath });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateFormatMode(
|
|
52
|
+
value: unknown,
|
|
53
|
+
issues: ConfigValidationIssue[],
|
|
54
|
+
sourcePath?: string,
|
|
55
|
+
): FormatMode | undefined {
|
|
56
|
+
if (value === "tool" || value === "prompt" || value === "session") {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pushIssue(
|
|
61
|
+
issues,
|
|
62
|
+
"formatMode",
|
|
63
|
+
'Expected one of "tool", "prompt", or "session".',
|
|
64
|
+
sourcePath,
|
|
65
|
+
);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateCommandTimeoutMs(
|
|
70
|
+
value: unknown,
|
|
71
|
+
issues: ConfigValidationIssue[],
|
|
72
|
+
sourcePath?: string,
|
|
73
|
+
): number | undefined {
|
|
74
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pushIssue(
|
|
79
|
+
issues,
|
|
80
|
+
"commandTimeoutMs",
|
|
81
|
+
"Expected a positive integer.",
|
|
82
|
+
sourcePath,
|
|
83
|
+
);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateBooleanField(
|
|
88
|
+
fieldPath: string,
|
|
89
|
+
value: unknown,
|
|
90
|
+
issues: ConfigValidationIssue[],
|
|
91
|
+
sourcePath?: string,
|
|
92
|
+
): boolean | undefined {
|
|
93
|
+
if (typeof value === "boolean") {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pushIssue(issues, fieldPath, "Expected a boolean.", sourcePath);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateStringArray(
|
|
102
|
+
fieldPath: string,
|
|
103
|
+
value: unknown,
|
|
104
|
+
issues: ConfigValidationIssue[],
|
|
105
|
+
sourcePath?: string,
|
|
106
|
+
): string[] | undefined {
|
|
107
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
108
|
+
pushIssue(
|
|
109
|
+
issues,
|
|
110
|
+
fieldPath,
|
|
111
|
+
"Expected a non-empty array of strings.",
|
|
112
|
+
sourcePath,
|
|
113
|
+
);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalized: string[] = [];
|
|
118
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
119
|
+
const entry = value[index];
|
|
120
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
121
|
+
pushIssue(
|
|
122
|
+
issues,
|
|
123
|
+
`${fieldPath}[${index}]`,
|
|
124
|
+
"Expected a non-empty string.",
|
|
125
|
+
sourcePath,
|
|
126
|
+
);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
normalized.push(entry);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return normalized;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateExtensionArray(
|
|
136
|
+
fieldPath: string,
|
|
137
|
+
value: unknown,
|
|
138
|
+
issues: ConfigValidationIssue[],
|
|
139
|
+
sourcePath?: string,
|
|
140
|
+
): string[] | undefined {
|
|
141
|
+
const extensions = validateStringArray(fieldPath, value, issues, sourcePath);
|
|
142
|
+
if (!extensions) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const normalized: string[] = [];
|
|
147
|
+
for (let index = 0; index < extensions.length; index += 1) {
|
|
148
|
+
const extension = extensions[index];
|
|
149
|
+
if (!extension.startsWith(".")) {
|
|
150
|
+
pushIssue(
|
|
151
|
+
issues,
|
|
152
|
+
`${fieldPath}[${index}]`,
|
|
153
|
+
'Expected a file extension beginning with ".".',
|
|
154
|
+
sourcePath,
|
|
155
|
+
);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lowercased = extension.toLowerCase();
|
|
160
|
+
if (!normalized.includes(lowercased)) {
|
|
161
|
+
normalized.push(lowercased);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateEnvironment(
|
|
169
|
+
fieldPath: string,
|
|
170
|
+
value: unknown,
|
|
171
|
+
issues: ConfigValidationIssue[],
|
|
172
|
+
sourcePath?: string,
|
|
173
|
+
): Record<string, string> | undefined {
|
|
174
|
+
if (!isRecord(value)) {
|
|
175
|
+
pushIssue(
|
|
176
|
+
issues,
|
|
177
|
+
fieldPath,
|
|
178
|
+
"Expected an object with string values.",
|
|
179
|
+
sourcePath,
|
|
180
|
+
);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const environment: Record<string, string> = {};
|
|
185
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
186
|
+
if (typeof entry !== "string") {
|
|
187
|
+
pushIssue(
|
|
188
|
+
issues,
|
|
189
|
+
`${fieldPath}.${key}`,
|
|
190
|
+
"Expected a string value.",
|
|
191
|
+
sourcePath,
|
|
192
|
+
);
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
environment[key] = entry;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return environment;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateFormatterDefinition(
|
|
202
|
+
formatterName: string,
|
|
203
|
+
value: unknown,
|
|
204
|
+
issues: ConfigValidationIssue[],
|
|
205
|
+
sourcePath?: string,
|
|
206
|
+
): FormatterDefinition | undefined {
|
|
207
|
+
const fieldPath = `formatters.${formatterName}`;
|
|
208
|
+
if (!isRecord(value)) {
|
|
209
|
+
pushIssue(issues, fieldPath, "Expected an object.", sourcePath);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const definition: Partial<FormatterDefinition> = {};
|
|
214
|
+
|
|
215
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
216
|
+
if (key === "command") {
|
|
217
|
+
definition.command = validateStringArray(
|
|
218
|
+
`${fieldPath}.command`,
|
|
219
|
+
entry,
|
|
220
|
+
issues,
|
|
221
|
+
sourcePath,
|
|
222
|
+
);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (key === "extensions") {
|
|
227
|
+
definition.extensions = validateExtensionArray(
|
|
228
|
+
`${fieldPath}.extensions`,
|
|
229
|
+
entry,
|
|
230
|
+
issues,
|
|
231
|
+
sourcePath,
|
|
232
|
+
);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (key === "environment") {
|
|
237
|
+
definition.environment = validateEnvironment(
|
|
238
|
+
`${fieldPath}.environment`,
|
|
239
|
+
entry,
|
|
240
|
+
issues,
|
|
241
|
+
sourcePath,
|
|
242
|
+
);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (key === "disabled") {
|
|
247
|
+
definition.disabled = validateBooleanField(
|
|
248
|
+
`${fieldPath}.disabled`,
|
|
249
|
+
entry,
|
|
250
|
+
issues,
|
|
251
|
+
sourcePath,
|
|
252
|
+
);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
pushIssue(
|
|
257
|
+
issues,
|
|
258
|
+
`${fieldPath}.${key}`,
|
|
259
|
+
"Unknown formatter property.",
|
|
260
|
+
sourcePath,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!definition.command || !definition.extensions) {
|
|
265
|
+
if (!definition.command) {
|
|
266
|
+
pushIssue(
|
|
267
|
+
issues,
|
|
268
|
+
`${fieldPath}.command`,
|
|
269
|
+
"Missing required property.",
|
|
270
|
+
sourcePath,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (!definition.extensions) {
|
|
274
|
+
pushIssue(
|
|
275
|
+
issues,
|
|
276
|
+
`${fieldPath}.extensions`,
|
|
277
|
+
"Missing required property.",
|
|
278
|
+
sourcePath,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
command: definition.command,
|
|
286
|
+
extensions: definition.extensions,
|
|
287
|
+
environment: definition.environment,
|
|
288
|
+
disabled: definition.disabled,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateFormatters(
|
|
293
|
+
value: unknown,
|
|
294
|
+
issues: ConfigValidationIssue[],
|
|
295
|
+
sourcePath?: string,
|
|
296
|
+
): Record<string, FormatterDefinition> | undefined {
|
|
297
|
+
if (!isRecord(value)) {
|
|
298
|
+
pushIssue(issues, "formatters", "Expected an object.", sourcePath);
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const formatters: Record<string, FormatterDefinition> = {};
|
|
303
|
+
for (const [formatterName, formatterValue] of Object.entries(value)) {
|
|
304
|
+
const definition = validateFormatterDefinition(
|
|
305
|
+
formatterName,
|
|
306
|
+
formatterValue,
|
|
307
|
+
issues,
|
|
308
|
+
sourcePath,
|
|
309
|
+
);
|
|
310
|
+
if (definition) {
|
|
311
|
+
formatters[formatterName] = definition;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return formatters;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateChains(
|
|
319
|
+
value: unknown,
|
|
320
|
+
issues: ConfigValidationIssue[],
|
|
321
|
+
sourcePath?: string,
|
|
322
|
+
): Record<string, string[]> | undefined {
|
|
323
|
+
if (!isRecord(value)) {
|
|
324
|
+
pushIssue(issues, "chains", "Expected an object.", sourcePath);
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const chains: Record<string, string[]> = {};
|
|
329
|
+
for (const [extension, chainValue] of Object.entries(value)) {
|
|
330
|
+
if (!extension.startsWith(".")) {
|
|
331
|
+
pushIssue(
|
|
332
|
+
issues,
|
|
333
|
+
`chains.${extension}`,
|
|
334
|
+
'Expected a file extension key beginning with ".".',
|
|
335
|
+
sourcePath,
|
|
336
|
+
);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const chain = validateStringArray(
|
|
341
|
+
`chains.${extension}`,
|
|
342
|
+
chainValue,
|
|
343
|
+
issues,
|
|
344
|
+
sourcePath,
|
|
345
|
+
);
|
|
346
|
+
if (chain) {
|
|
347
|
+
chains[extension.toLowerCase()] = chain;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return chains;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function validateConfigObject(
|
|
355
|
+
value: unknown,
|
|
356
|
+
sourcePath?: string,
|
|
357
|
+
): ValidateConfigResult {
|
|
358
|
+
const issues: ConfigValidationIssue[] = [];
|
|
359
|
+
const config: UserFormatterConfig = {};
|
|
360
|
+
|
|
361
|
+
if (!isRecord(value)) {
|
|
362
|
+
pushIssue(issues, "$", "Expected a JSON object.", sourcePath);
|
|
363
|
+
return { config, issues };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
367
|
+
if (key === "$schema") {
|
|
368
|
+
if (typeof entry !== "string") {
|
|
369
|
+
pushIssue(issues, "$schema", "Expected a string.", sourcePath);
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (key === "formatMode") {
|
|
375
|
+
const formatMode = validateFormatMode(entry, issues, sourcePath);
|
|
376
|
+
if (formatMode) {
|
|
377
|
+
config.formatMode = formatMode;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (key === "commandTimeoutMs") {
|
|
383
|
+
const commandTimeoutMs = validateCommandTimeoutMs(
|
|
384
|
+
entry,
|
|
385
|
+
issues,
|
|
386
|
+
sourcePath,
|
|
387
|
+
);
|
|
388
|
+
if (commandTimeoutMs !== undefined) {
|
|
389
|
+
config.commandTimeoutMs = commandTimeoutMs;
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (key === "hideSummariesInTui") {
|
|
395
|
+
const hideSummariesInTui = validateBooleanField(
|
|
396
|
+
"hideSummariesInTui",
|
|
397
|
+
entry,
|
|
398
|
+
issues,
|
|
399
|
+
sourcePath,
|
|
400
|
+
);
|
|
401
|
+
if (hideSummariesInTui !== undefined) {
|
|
402
|
+
config.hideSummariesInTui = hideSummariesInTui;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (key === "formatters") {
|
|
408
|
+
const formatters = validateFormatters(entry, issues, sourcePath);
|
|
409
|
+
if (formatters) {
|
|
410
|
+
config.formatters = formatters;
|
|
411
|
+
}
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (key === "chains") {
|
|
416
|
+
const chains = validateChains(entry, issues, sourcePath);
|
|
417
|
+
if (chains) {
|
|
418
|
+
config.chains = chains;
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
pushIssue(issues, key, "Unknown top-level property.", sourcePath);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return { config, issues };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function validateUserFormatterConfig(
|
|
430
|
+
value: unknown,
|
|
431
|
+
sourcePath?: string,
|
|
432
|
+
): ValidateConfigResult {
|
|
433
|
+
return validateConfigObject(value, sourcePath);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readJsonFile(filePath: string): unknown | undefined {
|
|
437
|
+
if (!existsSync(filePath)) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as unknown;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function mergeUserConfigs(
|
|
445
|
+
base: UserFormatterConfig,
|
|
446
|
+
overrides: UserFormatterConfig,
|
|
447
|
+
): UserFormatterConfig {
|
|
448
|
+
return {
|
|
449
|
+
formatMode: overrides.formatMode ?? base.formatMode,
|
|
450
|
+
commandTimeoutMs: overrides.commandTimeoutMs ?? base.commandTimeoutMs,
|
|
451
|
+
hideSummariesInTui: overrides.hideSummariesInTui ?? base.hideSummariesInTui,
|
|
452
|
+
formatters: {
|
|
453
|
+
...base.formatters,
|
|
454
|
+
...overrides.formatters,
|
|
455
|
+
},
|
|
456
|
+
chains: {
|
|
457
|
+
...base.chains,
|
|
458
|
+
...overrides.chains,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function getGlobalConfigPath(agentDir = defaultAgentDir()): string {
|
|
464
|
+
return join(
|
|
465
|
+
agentDir,
|
|
466
|
+
"extensions",
|
|
467
|
+
AUTOFORMAT_EXTENSION_ID,
|
|
468
|
+
AUTOFORMAT_CONFIG_FILE_NAME,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function getProjectConfigPath(cwd: string): string {
|
|
473
|
+
return join(
|
|
474
|
+
cwd,
|
|
475
|
+
".pi",
|
|
476
|
+
"extensions",
|
|
477
|
+
AUTOFORMAT_EXTENSION_ID,
|
|
478
|
+
AUTOFORMAT_CONFIG_FILE_NAME,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function loadAutoformatConfig(options?: {
|
|
483
|
+
cwd?: string;
|
|
484
|
+
agentDir?: string;
|
|
485
|
+
}): LoadConfigResult {
|
|
486
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
487
|
+
const agentDir = options?.agentDir ?? defaultAgentDir();
|
|
488
|
+
const globalConfigPath = getGlobalConfigPath(agentDir);
|
|
489
|
+
const projectConfigPath = getProjectConfigPath(cwd);
|
|
490
|
+
const issues: ConfigValidationIssue[] = [];
|
|
491
|
+
|
|
492
|
+
let mergedUserConfig: UserFormatterConfig = {};
|
|
493
|
+
|
|
494
|
+
for (const configPath of [globalConfigPath, projectConfigPath]) {
|
|
495
|
+
const rawConfig = (() => {
|
|
496
|
+
try {
|
|
497
|
+
return readJsonFile(configPath);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
500
|
+
pushIssue(issues, "$", `Failed to read config: ${message}`, configPath);
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
})();
|
|
504
|
+
|
|
505
|
+
if (rawConfig === undefined) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const validated = validateUserFormatterConfig(rawConfig, configPath);
|
|
510
|
+
issues.push(...validated.issues);
|
|
511
|
+
mergedUserConfig = mergeUserConfigs(mergedUserConfig, validated.config);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
config: createFormatterConfig(mergedUserConfig),
|
|
516
|
+
globalConfigPath,
|
|
517
|
+
projectConfigPath,
|
|
518
|
+
issues,
|
|
519
|
+
};
|
|
520
|
+
}
|