@alint-js/config 0.7.0 → 0.7.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/dist/index.d.mts +5 -5
- package/dist/index.mjs +251 -8
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -104,7 +104,7 @@ declare const declarativeFindingSchema: import("valibot").SchemaWithPipe<readonl
|
|
|
104
104
|
readonly confidence: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").PicklistSchema<["high", "medium", "low"], undefined>, import("valibot").DescriptionAction<"high" | "medium" | "low", "Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known.">]>, undefined>;
|
|
105
105
|
readonly filePath: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file.">]>, undefined>;
|
|
106
106
|
readonly line: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").DescriptionAction<number, "Use the left-column line number from the numbered source block.">]>;
|
|
107
|
-
readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the
|
|
107
|
+
readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the problem the target must fix. Never describe the absence of a problem.">]>;
|
|
108
108
|
readonly suggestion: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Concrete remediation direction for the finding.">]>, undefined>;
|
|
109
109
|
}, undefined>, import("valibot").DescriptionAction<{
|
|
110
110
|
confidence?: "high" | "medium" | "low" | undefined;
|
|
@@ -112,13 +112,13 @@ declare const declarativeFindingSchema: import("valibot").SchemaWithPipe<readonl
|
|
|
112
112
|
line: number;
|
|
113
113
|
message: string;
|
|
114
114
|
suggestion?: string | undefined;
|
|
115
|
-
}, "One declarative rule finding.">]>;
|
|
115
|
+
}, "One declarative rule finding. Report only a change the reviewed target must make.">]>;
|
|
116
116
|
declare const declarativeFindingResponseSchema: import("valibot").SchemaWithPipe<readonly [import("valibot").ObjectSchema<{
|
|
117
117
|
readonly findings: import("valibot").SchemaWithPipe<readonly [import("valibot").ArraySchema<import("valibot").SchemaWithPipe<readonly [import("valibot").ObjectSchema<{
|
|
118
118
|
readonly confidence: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").PicklistSchema<["high", "medium", "low"], undefined>, import("valibot").DescriptionAction<"high" | "medium" | "low", "Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known.">]>, undefined>;
|
|
119
119
|
readonly filePath: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file.">]>, undefined>;
|
|
120
120
|
readonly line: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").DescriptionAction<number, "Use the left-column line number from the numbered source block.">]>;
|
|
121
|
-
readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the
|
|
121
|
+
readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the problem the target must fix. Never describe the absence of a problem.">]>;
|
|
122
122
|
readonly suggestion: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Concrete remediation direction for the finding.">]>, undefined>;
|
|
123
123
|
}, undefined>, import("valibot").DescriptionAction<{
|
|
124
124
|
confidence?: "high" | "medium" | "low" | undefined;
|
|
@@ -126,13 +126,13 @@ declare const declarativeFindingResponseSchema: import("valibot").SchemaWithPipe
|
|
|
126
126
|
line: number;
|
|
127
127
|
message: string;
|
|
128
128
|
suggestion?: string | undefined;
|
|
129
|
-
}, "One declarative rule finding.">]>, undefined>, import("valibot").DescriptionAction<{
|
|
129
|
+
}, "One declarative rule finding. Report only a change the reviewed target must make.">]>, undefined>, import("valibot").DescriptionAction<{
|
|
130
130
|
confidence?: "high" | "medium" | "low" | undefined;
|
|
131
131
|
filePath?: string | undefined;
|
|
132
132
|
line: number;
|
|
133
133
|
message: string;
|
|
134
134
|
suggestion?: string | undefined;
|
|
135
|
-
}[], "All findings for the declarative rule. Return an empty array when there are no issues.">]>;
|
|
135
|
+
}[], "All findings for the declarative rule. Return an empty array when there are no issues, and never add a finding that only states the target is already correct.">]>;
|
|
136
136
|
}, undefined>, import("valibot").DescriptionAction<{
|
|
137
137
|
findings: {
|
|
138
138
|
confidence?: "high" | "medium" | "low" | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { access, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1
2
|
import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
2
3
|
import { loadConfig } from "c12";
|
|
3
4
|
import { createJiti } from "jiti/static";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import { access, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { array, boolean, check, description, finite, integer, literal, looseObject, maxValue, minValue, nonEmpty, number, object, optional, parse, picklist, pipe, record, strictObject, string, transform, tupleWithRest, undefined_, union, unknown, variant } from "valibot";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -104,10 +104,10 @@ const declarativeFindingSchema = pipe(object({
|
|
|
104
104
|
]), description("Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known."))),
|
|
105
105
|
filePath: optional(pipe(string(), description("Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file."))),
|
|
106
106
|
line: pipe(number(), description("Use the left-column line number from the numbered source block.")),
|
|
107
|
-
message: pipe(string(), description("Human-readable diagnostic message describing the
|
|
107
|
+
message: pipe(string(), description("Human-readable diagnostic message describing the problem the target must fix. Never describe the absence of a problem.")),
|
|
108
108
|
suggestion: optional(pipe(string(), description("Concrete remediation direction for the finding.")))
|
|
109
|
-
}), description("One declarative rule finding."));
|
|
110
|
-
const declarativeFindingResponseSchema = pipe(object({ findings: pipe(array(declarativeFindingSchema), description("All findings for the declarative rule. Return an empty array when there are no issues.")) }), description("Structured declarative rule findings."));
|
|
109
|
+
}), description("One declarative rule finding. Report only a change the reviewed target must make."));
|
|
110
|
+
const declarativeFindingResponseSchema = pipe(object({ findings: pipe(array(declarativeFindingSchema), description("All findings for the declarative rule. Return an empty array when there are no issues, and never add a finding that only states the target is already correct.")) }), description("Structured declarative rule findings."));
|
|
111
111
|
function isBuiltInAgentName(value) {
|
|
112
112
|
return builtInAgentNames.includes(value);
|
|
113
113
|
}
|
|
@@ -195,6 +195,89 @@ function parseDeclarativeRuleFileInput(input, filePath) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
//#endregion
|
|
198
|
+
//#region src/plugins/declarative/cache-key.ts
|
|
199
|
+
/**
|
|
200
|
+
* Behavior version of each built-in agent.
|
|
201
|
+
*
|
|
202
|
+
* A declarative rule is built from a shared factory, so the rule source that the run engine
|
|
203
|
+
* hashes is identical for every declarative rule. Nothing in that source tells the engine what
|
|
204
|
+
* the preset actually sends to the model: the instruction, the supplemental files, and the
|
|
205
|
+
* message layout all sit in the rule definition or in this package.
|
|
206
|
+
*
|
|
207
|
+
* Bump the version of an agent when its prompt assembly, message layout, or finding reporting
|
|
208
|
+
* changes, so cached findings never outlive the behavior that produced them.
|
|
209
|
+
*/
|
|
210
|
+
const builtInAgentVersions = {
|
|
211
|
+
"basic-coding-agent": 1,
|
|
212
|
+
"basic-structured": 1
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Cache key for one declarative rule: every declared input that changes its findings.
|
|
216
|
+
*
|
|
217
|
+
* Without it, editing `instruction` in `rule.alint.toml` leaves the rule source untouched, the
|
|
218
|
+
* cache fingerprint stays equal, and the next run replays findings the new rule text would never
|
|
219
|
+
* produce. The run only reports that the entry was a cache hit, so the stale result is silent.
|
|
220
|
+
*
|
|
221
|
+
* @see {@link builtInAgentVersions}
|
|
222
|
+
*/
|
|
223
|
+
function createDeclarativeCacheKey(rule) {
|
|
224
|
+
return {
|
|
225
|
+
agent: rule.builtInAgent,
|
|
226
|
+
agentVersion: builtInAgentVersions[rule.builtInAgent],
|
|
227
|
+
excludeFiles: rule.excludeFiles,
|
|
228
|
+
includeFiles: rule.includeFiles,
|
|
229
|
+
instruction: rule.instruction
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/plugins/declarative/findings.ts
|
|
234
|
+
/**
|
|
235
|
+
* Phrases that report the absence of a problem instead of reporting nothing.
|
|
236
|
+
*
|
|
237
|
+
* A clean target makes some models answer with a finding that asserts compliance, for example
|
|
238
|
+
* "No violations found." The finding has the shape of a real one, so it reaches the reporter and
|
|
239
|
+
* renders as a diagnostic whose remediation is "change nothing", with a confidence that makes it
|
|
240
|
+
* look as trustworthy as a real hit.
|
|
241
|
+
*
|
|
242
|
+
* Every pattern requires an explicit compliance verb ("found", "needed", "compliant") so a real
|
|
243
|
+
* finding that merely starts with a negative word survives: "No guard clause before the loop" is
|
|
244
|
+
* a missing guard, not a statement that the file is clean.
|
|
245
|
+
*/
|
|
246
|
+
const compliancePatterns = [
|
|
247
|
+
/\bno\s+(?:\w+\s+){0,4}(?:violations?|issues?|problems?)\s+(?:\w+\s+){0,3}(?:found|detected|identified|present)\b/iu,
|
|
248
|
+
/\bno\s+(?:\w+\s+){0,3}(?:action|change|changes|fix|fixes|modification|modifications)\s+(?:\w+\s+){0,2}(?:needed|required|necessary)\b/iu,
|
|
249
|
+
/\bnothing\s+(?:to|needs?\s+to)\s+(?:address|change|do|fix|report)\b/iu,
|
|
250
|
+
/\b(?:is|are|looks?|appears?|seems?)\s+(?:fully\s+|otherwise\s+)?compliant\b/iu,
|
|
251
|
+
/\bcompl(?:y|ies|iant)\s+with\b/iu,
|
|
252
|
+
/\b(?:is|are)\s+(?:already\s+)?(?:correct|fine|good)\b/iu,
|
|
253
|
+
/\b(?:lgtm|looks good|looks fine)\b/iu
|
|
254
|
+
];
|
|
255
|
+
/**
|
|
256
|
+
* Compliance phrased in Simplified Chinese, which the output language option can request.
|
|
257
|
+
*
|
|
258
|
+
* "不符合规范" negates the same characters, so the pattern refuses to match right after 不 or 未.
|
|
259
|
+
*/
|
|
260
|
+
const chineseCompliancePattern = /没有(?:违规|问题)|未发现(?:问题|违规|异常)|(?<![不未])符合(?:规范|要求|约定|预期)|(?<![不未])无需(?:修改|改动)/u;
|
|
261
|
+
/**
|
|
262
|
+
* A negation anywhere in the message keeps the finding: the message may describe a violation,
|
|
263
|
+
* such as "The module is not compliant with the rule."
|
|
264
|
+
*/
|
|
265
|
+
const negationPattern = /\b(?:can(?:no|'?t)|didn'?t|doesn'?t|don'?t|isn'?t|aren'?t|never|no longer|not)\b/iu;
|
|
266
|
+
/**
|
|
267
|
+
* Whether a finding asks for a change the reviewed target can act on.
|
|
268
|
+
*
|
|
269
|
+
* Judged on the message alone: a real violation whose suggestion happens to say "no further
|
|
270
|
+
* change needed" is still a violation the reader has to see.
|
|
271
|
+
*/
|
|
272
|
+
function isActionableFinding(finding) {
|
|
273
|
+
return !isComplianceStatement(finding.message);
|
|
274
|
+
}
|
|
275
|
+
function isComplianceStatement(message) {
|
|
276
|
+
if (chineseCompliancePattern.test(message)) return true;
|
|
277
|
+
if (negationPattern.test(message)) return false;
|
|
278
|
+
return compliancePatterns.some((pattern) => pattern.test(message));
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
198
281
|
//#region src/plugins/declarative/scope.ts
|
|
199
282
|
function createReportScope(options) {
|
|
200
283
|
const includeFiles = options.includeFiles;
|
|
@@ -228,6 +311,7 @@ const maxSupplementalFiles = 12;
|
|
|
228
311
|
function createBasicStructuredRule(rule) {
|
|
229
312
|
return {
|
|
230
313
|
cache: rule.includeFiles === void 0 || rule.includeFiles.length === 0,
|
|
314
|
+
cacheKey: createDeclarativeCacheKey(rule),
|
|
231
315
|
create: (ctx) => ({
|
|
232
316
|
/**
|
|
233
317
|
* Runs one structured declarative review for a planned file.
|
|
@@ -313,6 +397,10 @@ function reportDeclarativeFindings(options) {
|
|
|
313
397
|
targetFilePath: options.targetFilePath
|
|
314
398
|
});
|
|
315
399
|
for (const finding of options.findings) {
|
|
400
|
+
if (!isActionableFinding(finding)) {
|
|
401
|
+
options.ctx.logger.debug("Ignoring declarative finding that reports no issue", { message: finding.message });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
316
404
|
const filePath = resolveFindingFilePath(options.ctx.cwd, options.targetFilePath, finding.filePath);
|
|
317
405
|
if (!scope.canReport(filePath)) {
|
|
318
406
|
options.ctx.logger.debug("Ignoring out-of-scope declarative finding", {
|
|
@@ -461,7 +549,7 @@ function createCodingAgent(model, runner = chat({
|
|
|
461
549
|
parameters: agentTool.parameters
|
|
462
550
|
}));
|
|
463
551
|
tools.push(rawTool({
|
|
464
|
-
description: "Submit all findings and finish the review. Submit an empty findings array when there are no issues.",
|
|
552
|
+
description: "Submit all findings and finish the review. Submit an empty findings array when there are no issues, instead of a finding that states the target is already correct.",
|
|
465
553
|
execute: async (input) => {
|
|
466
554
|
report = parse(declarativeFindingResponseSchema, input);
|
|
467
555
|
return report;
|
|
@@ -484,7 +572,7 @@ function createCodingAgent(model, runner = chat({
|
|
|
484
572
|
instructions: [
|
|
485
573
|
options.instruction,
|
|
486
574
|
"Use the filesystem tools to inspect the project as needed before reaching a conclusion.",
|
|
487
|
-
`When the review is complete, call ${reportFindingsToolName} exactly once with all findings. Submit an empty findings array when there are no issues.`
|
|
575
|
+
`When the review is complete, call ${reportFindingsToolName} exactly once with all findings. Submit an empty findings array when there are no issues, instead of a finding that states the target is already correct.`
|
|
488
576
|
].join("\n\n"),
|
|
489
577
|
tools,
|
|
490
578
|
turnId: "alint"
|
|
@@ -1167,6 +1255,38 @@ function toAlintConfigItems(value, options = {}) {
|
|
|
1167
1255
|
}
|
|
1168
1256
|
//#endregion
|
|
1169
1257
|
//#region src/config/load.ts
|
|
1258
|
+
/**
|
|
1259
|
+
* Config file names a nested config may use, mirroring what c12 resolves for the root.
|
|
1260
|
+
*
|
|
1261
|
+
* @see {@link loadConfig} `configFile`
|
|
1262
|
+
*/
|
|
1263
|
+
const nestedConfigFileNames = [
|
|
1264
|
+
"alint.config.cjs",
|
|
1265
|
+
"alint.config.cts",
|
|
1266
|
+
"alint.config.js",
|
|
1267
|
+
"alint.config.json",
|
|
1268
|
+
"alint.config.json5",
|
|
1269
|
+
"alint.config.jsonc",
|
|
1270
|
+
"alint.config.mjs",
|
|
1271
|
+
"alint.config.mts",
|
|
1272
|
+
"alint.config.toml",
|
|
1273
|
+
"alint.config.ts",
|
|
1274
|
+
"alint.config.yaml",
|
|
1275
|
+
"alint.config.yml"
|
|
1276
|
+
];
|
|
1277
|
+
/**
|
|
1278
|
+
* Directories that never hold a config a project means to run.
|
|
1279
|
+
*
|
|
1280
|
+
* Hidden directories are skipped wholesale so `.git`, `.cache`, and `.next` cost no lookups.
|
|
1281
|
+
*/
|
|
1282
|
+
const skippedDiscoveryDirectoryNames = /* @__PURE__ */ new Set([
|
|
1283
|
+
"build",
|
|
1284
|
+
"coverage",
|
|
1285
|
+
"dist",
|
|
1286
|
+
"node_modules",
|
|
1287
|
+
"out",
|
|
1288
|
+
"vendor"
|
|
1289
|
+
]);
|
|
1170
1290
|
async function loadAlintConfig(cwd, configFile) {
|
|
1171
1291
|
return (await loadAlintConfigWithMetadata(cwd, configFile)).config;
|
|
1172
1292
|
}
|
|
@@ -1197,10 +1317,88 @@ async function loadAlintConfigWithMetadata(cwd, configFile) {
|
|
|
1197
1317
|
async function loadStaticConfig(cwd, configFile) {
|
|
1198
1318
|
return (await loadStaticConfigSource(cwd, configFile)).config;
|
|
1199
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Rejects two config files claiming one plugin alias with different plugins.
|
|
1322
|
+
*
|
|
1323
|
+
* The lock file is keyed by alias, so the second specifier would silently win and every rule id
|
|
1324
|
+
* from the first plugin would come from the wrong plugin.
|
|
1325
|
+
*/
|
|
1326
|
+
function assertDistinctPluginAliases(sources) {
|
|
1327
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1328
|
+
for (const source of sources) for (const group of source.config.groups) for (const reference of group.plugins) {
|
|
1329
|
+
const key = getPluginSpecifierKey(reference.specifier);
|
|
1330
|
+
const previous = seen.get(reference.alias);
|
|
1331
|
+
if (previous === void 0) {
|
|
1332
|
+
seen.set(reference.alias, {
|
|
1333
|
+
configFile: source.configFile,
|
|
1334
|
+
key
|
|
1335
|
+
});
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (previous.key === key) continue;
|
|
1339
|
+
throw new Error(`Static plugin "${reference.alias}" is configured in ${formatConfigOrigin(previous.configFile)} and ${formatConfigOrigin(source.configFile)} with different specifiers ("${previous.key}" and "${key}"). Nested configs share one plugin lock file, so give each plugin a distinct alias.`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function collectNestedConfigFiles(root, directory, found) {
|
|
1343
|
+
let entries;
|
|
1344
|
+
try {
|
|
1345
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
1346
|
+
} catch {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const childDirectories = [];
|
|
1350
|
+
for (const entry of entries) {
|
|
1351
|
+
const path = join(directory, entry.name);
|
|
1352
|
+
if (entry.isDirectory()) {
|
|
1353
|
+
if (!shouldSkipDiscoveryDirectory(entry.name)) childDirectories.push(path);
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (entry.isFile() && directory !== root && nestedConfigFileNames.includes(entry.name)) found.push(path);
|
|
1357
|
+
}
|
|
1358
|
+
for (const child of childDirectories.sort()) await collectNestedConfigFiles(root, child, found);
|
|
1359
|
+
}
|
|
1360
|
+
function directoryDepth(path) {
|
|
1361
|
+
return path.split(sep).length;
|
|
1362
|
+
}
|
|
1363
|
+
async function discoverNestedConfigFiles(cwd) {
|
|
1364
|
+
const found = [];
|
|
1365
|
+
await collectNestedConfigFiles(cwd, cwd, found);
|
|
1366
|
+
return found.sort((left, right) => {
|
|
1367
|
+
const depthDifference = directoryDepth(left) - directoryDepth(right);
|
|
1368
|
+
return depthDifference === 0 ? left.localeCompare(right) : depthDifference;
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
function formatConfigOrigin(configFile) {
|
|
1372
|
+
return configFile ?? "the inline config";
|
|
1373
|
+
}
|
|
1200
1374
|
function formatPluginReferences(references) {
|
|
1201
1375
|
return references.map((reference) => `${reference.alias} (${reference.specifier.raw})`).join(", ");
|
|
1202
1376
|
}
|
|
1203
|
-
|
|
1377
|
+
/**
|
|
1378
|
+
* Loads every `alint.config.*` below `cwd`, except the root config itself.
|
|
1379
|
+
*
|
|
1380
|
+
* A package that owns its rules writes one config next to its code instead of registering a
|
|
1381
|
+
* directory plugin in the root config, scoping the plugin, and repeating that scope in every
|
|
1382
|
+
* tool that reads it. Discovery is depth-first, parents before children, so a deeper config
|
|
1383
|
+
* overrides an outer one the same way a later flat-config item does.
|
|
1384
|
+
*/
|
|
1385
|
+
async function loadNestedStaticConfigSources(cwd) {
|
|
1386
|
+
const configFiles = await discoverNestedConfigFiles(cwd);
|
|
1387
|
+
const sources = [];
|
|
1388
|
+
for (const configFile of configFiles) {
|
|
1389
|
+
const directory = dirname(configFile);
|
|
1390
|
+
const source = await loadSingleStaticConfigSource(directory, basename(configFile), { dotenv: false });
|
|
1391
|
+
if (source.configFile === void 0) continue;
|
|
1392
|
+
sources.push({
|
|
1393
|
+
config: source.config,
|
|
1394
|
+
configFile: source.configFile,
|
|
1395
|
+
relativeDirectory: toPosixPath(relative(cwd, directory))
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return sources;
|
|
1399
|
+
}
|
|
1400
|
+
/** Loads one config file in one directory. The root and every nested config go through here. */
|
|
1401
|
+
async function loadSingleStaticConfigSource(cwd, configFile, options = {}) {
|
|
1204
1402
|
const jiti = createJiti(resolve(cwd, configFile ?? "alint.config"), {
|
|
1205
1403
|
interopDefault: true,
|
|
1206
1404
|
moduleCache: false
|
|
@@ -1208,7 +1406,7 @@ async function loadStaticConfigSource(cwd, configFile) {
|
|
|
1208
1406
|
const result = await loadConfig({
|
|
1209
1407
|
configFile,
|
|
1210
1408
|
cwd,
|
|
1211
|
-
dotenv: true,
|
|
1409
|
+
dotenv: options.dotenv ?? true,
|
|
1212
1410
|
import: (id) => jiti.import(id),
|
|
1213
1411
|
name: "alint"
|
|
1214
1412
|
});
|
|
@@ -1219,6 +1417,51 @@ async function loadStaticConfigSource(cwd, configFile) {
|
|
|
1219
1417
|
configFile: resolvedConfigFile
|
|
1220
1418
|
};
|
|
1221
1419
|
}
|
|
1420
|
+
async function loadStaticConfigSource(cwd, configFile) {
|
|
1421
|
+
const root = await loadSingleStaticConfigSource(cwd, configFile);
|
|
1422
|
+
if (configFile !== void 0) return root;
|
|
1423
|
+
const nested = await loadNestedStaticConfigSources(cwd);
|
|
1424
|
+
if (nested.length === 0) return root;
|
|
1425
|
+
assertDistinctPluginAliases([{
|
|
1426
|
+
config: root.config,
|
|
1427
|
+
configFile: root.configFile
|
|
1428
|
+
}, ...nested]);
|
|
1429
|
+
return {
|
|
1430
|
+
config: { groups: [...root.config.groups, ...nested.flatMap((source) => scopeNestedGroups(source))] },
|
|
1431
|
+
configFile: root.configFile
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Scopes one nested config item to the directory that declares it.
|
|
1436
|
+
*
|
|
1437
|
+
* `basePath` makes every pattern in the item resolve against that directory, so an `ignores`
|
|
1438
|
+
* entry or a plugin config the item extends stays inside it. The two defaults cover the item
|
|
1439
|
+
* shapes that declare no patterns at all: without them a rule set written next to a package
|
|
1440
|
+
* would silently apply to the whole repository.
|
|
1441
|
+
*
|
|
1442
|
+
* Limits: an item cannot target the project root (`onTargetProject`) from a nested config,
|
|
1443
|
+
* because the project target sits outside the item's base path.
|
|
1444
|
+
*/
|
|
1445
|
+
function scopeNestedConfigItem(item, relativeDirectory) {
|
|
1446
|
+
return {
|
|
1447
|
+
...item,
|
|
1448
|
+
basePath: item.basePath === void 0 ? relativeDirectory : `${relativeDirectory}/${toPosixPath(item.basePath)}`,
|
|
1449
|
+
directories: item.directories ?? ["**"],
|
|
1450
|
+
files: item.files ?? ["**/*"]
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function scopeNestedGroups(source) {
|
|
1454
|
+
return source.config.groups.map((group) => ({
|
|
1455
|
+
...group,
|
|
1456
|
+
item: scopeNestedConfigItem(group.item, source.relativeDirectory)
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
function shouldSkipDiscoveryDirectory(name) {
|
|
1460
|
+
return name.startsWith(".") || skippedDiscoveryDirectoryNames.has(name);
|
|
1461
|
+
}
|
|
1462
|
+
function toPosixPath(path) {
|
|
1463
|
+
return path.split(sep).join("/");
|
|
1464
|
+
}
|
|
1222
1465
|
//#endregion
|
|
1223
1466
|
//#region src/config/stop-gate-write.ts
|
|
1224
1467
|
async function setStopGateConfig(options) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alint-js/config",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description": "Configuration loading and setup utilities for alint",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"tar-stream": "^3.2.0",
|
|
42
42
|
"valibot": "^1.4.2",
|
|
43
43
|
"xdg-basedir": "^5.1.0",
|
|
44
|
-
"@alint-js/core": "0.7.
|
|
45
|
-
"@alint-js/
|
|
46
|
-
"@alint-js/
|
|
44
|
+
"@alint-js/core": "0.7.2",
|
|
45
|
+
"@alint-js/utils": "0.7.2",
|
|
46
|
+
"@alint-js/tools-fs": "0.7.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/tar-stream": "^3.1.4",
|