eslint-plugin-ai-rules 0.2.1 → 0.3.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/README.md +1144 -174
- package/dist/index.cjs +1640 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +1636 -2
- package/dist/index.js.map +1 -1
- package/package.json +25 -11
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,10 @@ __export(index_exports, {
|
|
|
35
35
|
getSortedContent: () => getSortedContent,
|
|
36
36
|
noMultilineImports: () => noMultilineImports,
|
|
37
37
|
plugin: () => plugin,
|
|
38
|
+
presets: () => presets,
|
|
39
|
+
projectSetup: () => projectSetup,
|
|
40
|
+
projectStructure: () => projectStructure,
|
|
41
|
+
projects: () => projects,
|
|
38
42
|
recommended: () => recommended,
|
|
39
43
|
sortImports: () => sortImports
|
|
40
44
|
});
|
|
@@ -154,6 +158,352 @@ var noMultilineImports = {
|
|
|
154
158
|
}
|
|
155
159
|
};
|
|
156
160
|
|
|
161
|
+
// src/rules/ProjectStructure.ts
|
|
162
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
163
|
+
|
|
164
|
+
// src/utils/matchNameRule.ts
|
|
165
|
+
var CASE_PATTERNS = {
|
|
166
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
167
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
168
|
+
"kebab-case": /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/,
|
|
169
|
+
snake_case: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/
|
|
170
|
+
};
|
|
171
|
+
var NAME_CASES = Object.keys(CASE_PATTERNS);
|
|
172
|
+
function isNameCase(value) {
|
|
173
|
+
return value in CASE_PATTERNS;
|
|
174
|
+
}
|
|
175
|
+
function matchesNameCase(value, caseName) {
|
|
176
|
+
return CASE_PATTERNS[caseName].test(value);
|
|
177
|
+
}
|
|
178
|
+
function matchesNameRule(value, rule) {
|
|
179
|
+
if (rule.case && !matchesNameCase(value, rule.case)) return false;
|
|
180
|
+
if (rule.prefix && !value.startsWith(rule.prefix)) return false;
|
|
181
|
+
if (rule.suffix && !value.endsWith(rule.suffix)) return false;
|
|
182
|
+
if (rule.pattern) {
|
|
183
|
+
try {
|
|
184
|
+
if (!new RegExp(rule.pattern).test(value)) return false;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function describeNameRule(rule) {
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (rule.case) parts.push(rule.case);
|
|
194
|
+
if (rule.prefix) parts.push(`prefix "${rule.prefix}"`);
|
|
195
|
+
if (rule.suffix) parts.push(`suffix "${rule.suffix}"`);
|
|
196
|
+
if (rule.pattern) parts.push(`pattern /${rule.pattern}/`);
|
|
197
|
+
return parts.length > 0 ? parts.join(", ") : "any name";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/utils/matchPathPattern.ts
|
|
201
|
+
function expandBraces(pattern) {
|
|
202
|
+
const match = pattern.match(/\{([^{}]+)\}/);
|
|
203
|
+
if (!match) return [pattern];
|
|
204
|
+
const [fullMatch, body] = match;
|
|
205
|
+
const alternatives = body.split(",");
|
|
206
|
+
return alternatives.flatMap(
|
|
207
|
+
(alternative) => expandBraces(pattern.replace(fullMatch, () => alternative))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function globToRegExp(pattern) {
|
|
211
|
+
let source = "^";
|
|
212
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
213
|
+
const char = pattern[index];
|
|
214
|
+
if (char === "*") {
|
|
215
|
+
if (pattern[index + 1] === "*") {
|
|
216
|
+
if (pattern[index + 2] === "/") {
|
|
217
|
+
source += "(?:.*/)?";
|
|
218
|
+
index += 2;
|
|
219
|
+
} else {
|
|
220
|
+
source += ".*";
|
|
221
|
+
index += 1;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
source += "[^/]*";
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (char === "?") {
|
|
229
|
+
source += "[^/]";
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if ("\\^$+?.()|{}[]".includes(char)) {
|
|
233
|
+
source += `\\${char}`;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
source += char;
|
|
237
|
+
}
|
|
238
|
+
source += "$";
|
|
239
|
+
return new RegExp(source);
|
|
240
|
+
}
|
|
241
|
+
function normalizePathPattern(value) {
|
|
242
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
243
|
+
}
|
|
244
|
+
function matchPathPattern(value, pattern) {
|
|
245
|
+
const normalizedValue = normalizePathPattern(value);
|
|
246
|
+
return expandBraces(normalizePathPattern(pattern)).some(
|
|
247
|
+
(expandedPattern) => globToRegExp(expandedPattern).test(normalizedValue)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/utils/matchFileRule.ts
|
|
252
|
+
function getMatchingExtension(filename, extensions) {
|
|
253
|
+
return [...extensions].sort((left, right) => right.length - left.length).find((extension) => filename.endsWith(extension));
|
|
254
|
+
}
|
|
255
|
+
function matchesFileRule(filename, rule) {
|
|
256
|
+
if (typeof rule === "string") return matchPathPattern(filename, rule);
|
|
257
|
+
const extension = getMatchingExtension(filename, rule.extensions);
|
|
258
|
+
if (!extension) return false;
|
|
259
|
+
const baseName = filename.slice(0, -extension.length);
|
|
260
|
+
return matchesNameRule(baseName, rule);
|
|
261
|
+
}
|
|
262
|
+
function fileRuleMatchesExtension(filename, rule) {
|
|
263
|
+
if (typeof rule === "string") return false;
|
|
264
|
+
return getMatchingExtension(filename, rule.extensions) !== void 0;
|
|
265
|
+
}
|
|
266
|
+
function describeFileRule(rule) {
|
|
267
|
+
if (typeof rule === "string") return rule;
|
|
268
|
+
const extensions = rule.extensions.join(", ");
|
|
269
|
+
return `${describeNameRule(rule)}, extension ${extensions}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/rules/ProjectStructure.ts
|
|
273
|
+
var DYNAMIC_FOLDER_PATTERN = /^\[.+\]$/;
|
|
274
|
+
function getFileRules(node) {
|
|
275
|
+
return [...node.files ?? [], ...node.additionalFiles ?? []];
|
|
276
|
+
}
|
|
277
|
+
function describeAllowedFolders(node) {
|
|
278
|
+
const entries = Object.entries(node.folders ?? {});
|
|
279
|
+
if (entries.length === 0) return "none";
|
|
280
|
+
return entries.map(([folderName, child]) => {
|
|
281
|
+
if (!DYNAMIC_FOLDER_PATTERN.test(folderName)) return folderName;
|
|
282
|
+
if (!child.name) return folderName;
|
|
283
|
+
return `${folderName} (${describeNameRule(child.name)})`;
|
|
284
|
+
}).join(", ");
|
|
285
|
+
}
|
|
286
|
+
function describeAllowedFiles(node) {
|
|
287
|
+
const rules = getFileRules(node);
|
|
288
|
+
if (rules.length === 0) return "none";
|
|
289
|
+
return rules.map(describeFileRule).join(", ");
|
|
290
|
+
}
|
|
291
|
+
function matchFolder(node, segment) {
|
|
292
|
+
let dynamicFolder;
|
|
293
|
+
for (const [folderName, child] of Object.entries(node.folders ?? {})) {
|
|
294
|
+
if (folderName === segment) return { node: child };
|
|
295
|
+
if (!dynamicFolder && DYNAMIC_FOLDER_PATTERN.test(folderName)) {
|
|
296
|
+
dynamicFolder = { name: folderName, child };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!dynamicFolder) return {};
|
|
300
|
+
if (!dynamicFolder.child.name) return { node: dynamicFolder.child };
|
|
301
|
+
if (matchesNameRule(segment, dynamicFolder.child.name)) {
|
|
302
|
+
return { node: dynamicFolder.child };
|
|
303
|
+
}
|
|
304
|
+
return { mismatch: describeNameRule(dynamicFolder.child.name) };
|
|
305
|
+
}
|
|
306
|
+
function findViolation(relativePath, structure) {
|
|
307
|
+
const segments = relativePath.split("/").filter(Boolean);
|
|
308
|
+
let current = structure;
|
|
309
|
+
const consumed = [];
|
|
310
|
+
for (let index = 0; index < segments.length; index++) {
|
|
311
|
+
const segment = segments[index];
|
|
312
|
+
const isFile = index === segments.length - 1;
|
|
313
|
+
const parent = consumed.length > 0 ? consumed.join("/") : ".";
|
|
314
|
+
if (isFile) {
|
|
315
|
+
const rules = getFileRules(current);
|
|
316
|
+
if (rules.some((rule) => matchesFileRule(segment, rule))) return null;
|
|
317
|
+
const matchingExtensionRules = rules.filter(
|
|
318
|
+
(rule) => fileRuleMatchesExtension(segment, rule)
|
|
319
|
+
);
|
|
320
|
+
if (matchingExtensionRules.length > 0) {
|
|
321
|
+
return {
|
|
322
|
+
messageId: "fileNameMismatch",
|
|
323
|
+
data: {
|
|
324
|
+
file: segment,
|
|
325
|
+
parent,
|
|
326
|
+
expected: matchingExtensionRules.map(describeFileRule).join(", ")
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
messageId: "fileNotAllowed",
|
|
332
|
+
data: {
|
|
333
|
+
file: segment,
|
|
334
|
+
parent,
|
|
335
|
+
allowed: describeAllowedFiles(current)
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const folderMatch = matchFolder(current, segment);
|
|
340
|
+
if (folderMatch.node) {
|
|
341
|
+
if (folderMatch.node.ignore) return null;
|
|
342
|
+
current = folderMatch.node;
|
|
343
|
+
consumed.push(segment);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (folderMatch.mismatch) {
|
|
347
|
+
return {
|
|
348
|
+
messageId: "folderNameMismatch",
|
|
349
|
+
data: {
|
|
350
|
+
folder: segment,
|
|
351
|
+
parent,
|
|
352
|
+
expected: folderMatch.mismatch
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
messageId: "folderNotAllowed",
|
|
358
|
+
data: {
|
|
359
|
+
folder: segment,
|
|
360
|
+
parent,
|
|
361
|
+
allowed: describeAllowedFolders(current)
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function findProject(projects2, fileAbsolutePath, cwd) {
|
|
368
|
+
let matchedProject;
|
|
369
|
+
let matchedRootLength = -1;
|
|
370
|
+
for (const project of projects2) {
|
|
371
|
+
const rootAbsolutePath = import_node_path2.default.resolve(cwd, project.rootFolder);
|
|
372
|
+
const relativePath = import_node_path2.default.relative(rootAbsolutePath, fileAbsolutePath);
|
|
373
|
+
if (relativePath.length === 0 || relativePath.startsWith("..") || import_node_path2.default.isAbsolute(relativePath)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (rootAbsolutePath.length > matchedRootLength) {
|
|
377
|
+
matchedProject = project;
|
|
378
|
+
matchedRootLength = rootAbsolutePath.length;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return matchedProject;
|
|
382
|
+
}
|
|
383
|
+
function getPhysicalFilename(context) {
|
|
384
|
+
return context.getPhysicalFilename?.() ?? context.physicalFilename ?? context.filename ?? "";
|
|
385
|
+
}
|
|
386
|
+
var projectStructure = {
|
|
387
|
+
meta: {
|
|
388
|
+
type: "problem",
|
|
389
|
+
docs: {
|
|
390
|
+
description: "Enforce the configured project folder and file structure"
|
|
391
|
+
},
|
|
392
|
+
schema: [],
|
|
393
|
+
messages: {
|
|
394
|
+
folderNotAllowed: 'Folder "{{folder}}" is not allowed inside "{{parent}}". Allowed folders: {{allowed}}.',
|
|
395
|
+
folderNameMismatch: 'Folder "{{folder}}" inside "{{parent}}" must match {{expected}}.',
|
|
396
|
+
fileNotAllowed: 'File "{{file}}" is not allowed inside "{{parent}}". Allowed files: {{allowed}}.',
|
|
397
|
+
fileNameMismatch: 'File "{{file}}" inside "{{parent}}" must match {{expected}}.'
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
create(context) {
|
|
401
|
+
const settings = context.settings["ai-rules"];
|
|
402
|
+
const projects2 = settings?.projects ?? (settings?.project ? [settings.project] : []);
|
|
403
|
+
if (projects2.length === 0) return {};
|
|
404
|
+
return {
|
|
405
|
+
Program(node) {
|
|
406
|
+
const physicalFilename = getPhysicalFilename(context);
|
|
407
|
+
if (!physicalFilename || physicalFilename === "<input>" || physicalFilename === "<text>") {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const cwd = process.cwd();
|
|
411
|
+
const fileAbsolutePath = import_node_path2.default.resolve(cwd, physicalFilename);
|
|
412
|
+
const project = findProject(projects2, fileAbsolutePath, cwd);
|
|
413
|
+
if (!project || !project.structure) return;
|
|
414
|
+
const rootAbsolutePath = import_node_path2.default.resolve(cwd, project.rootFolder);
|
|
415
|
+
const relativePath = import_node_path2.default.relative(rootAbsolutePath, fileAbsolutePath).split(import_node_path2.default.sep).join("/");
|
|
416
|
+
if (relativePath.length === 0 || project.structure.ignore || (project.structure.ignoredPaths ?? []).some(
|
|
417
|
+
(pattern) => matchPathPattern(relativePath, pattern)
|
|
418
|
+
)) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const violation = findViolation(relativePath, project.structure);
|
|
422
|
+
if (!violation) return;
|
|
423
|
+
switch (violation.messageId) {
|
|
424
|
+
case "folderNotAllowed":
|
|
425
|
+
context.report({
|
|
426
|
+
node,
|
|
427
|
+
messageId: "folderNotAllowed",
|
|
428
|
+
data: violation.data
|
|
429
|
+
});
|
|
430
|
+
break;
|
|
431
|
+
case "folderNameMismatch":
|
|
432
|
+
context.report({
|
|
433
|
+
node,
|
|
434
|
+
messageId: "folderNameMismatch",
|
|
435
|
+
data: violation.data
|
|
436
|
+
});
|
|
437
|
+
break;
|
|
438
|
+
case "fileNotAllowed":
|
|
439
|
+
context.report({
|
|
440
|
+
node,
|
|
441
|
+
messageId: "fileNotAllowed",
|
|
442
|
+
data: violation.data
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
case "fileNameMismatch":
|
|
446
|
+
context.report({
|
|
447
|
+
node,
|
|
448
|
+
messageId: "fileNameMismatch",
|
|
449
|
+
data: violation.data
|
|
450
|
+
});
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// src/structures/hasStructureRules.ts
|
|
459
|
+
function hasStructureRules(structure) {
|
|
460
|
+
return structure.ignore !== void 0 || structure.name !== void 0 || structure.files !== void 0 || structure.additionalFiles !== void 0 || structure.folders !== void 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/rules/ProjectSetup.ts
|
|
464
|
+
var hasWarned = false;
|
|
465
|
+
function isConfiguredProject(project) {
|
|
466
|
+
if (project.relations) return true;
|
|
467
|
+
if (project.preset === "frontend" || project.preset === "backend" || project.preset === "other") {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return Boolean(project.structure && hasStructureRules(project.structure));
|
|
471
|
+
}
|
|
472
|
+
function warnOnce() {
|
|
473
|
+
if (hasWarned) return;
|
|
474
|
+
if (process.env.AI_RULES_SILENT === "1" || process.env.AI_RULES_SILENT === "true") {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
hasWarned = true;
|
|
478
|
+
console.warn(
|
|
479
|
+
[
|
|
480
|
+
"[eslint-plugin-ai-rules] Project structure validation is not configured.",
|
|
481
|
+
"Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).",
|
|
482
|
+
"If this project is not a frontend/backend, use preset: 'other' to disable this warning."
|
|
483
|
+
].join("\n")
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
var projectSetup = {
|
|
487
|
+
meta: {
|
|
488
|
+
type: "suggestion",
|
|
489
|
+
docs: {
|
|
490
|
+
description: "Warn when the project structure validation is not configured"
|
|
491
|
+
},
|
|
492
|
+
schema: [],
|
|
493
|
+
messages: {}
|
|
494
|
+
},
|
|
495
|
+
create(context) {
|
|
496
|
+
return {
|
|
497
|
+
Program() {
|
|
498
|
+
const settings = context.settings["ai-rules"];
|
|
499
|
+
const projects2 = settings?.projects ?? (settings?.project ? [settings.project] : []);
|
|
500
|
+
if (projects2.some(isConfiguredProject)) return;
|
|
501
|
+
warnOnce();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
157
507
|
// src/rules/SortImports.ts
|
|
158
508
|
var sortImports = {
|
|
159
509
|
meta: {
|
|
@@ -282,11 +632,13 @@ function getSortedContent(content) {
|
|
|
282
632
|
var plugin = {
|
|
283
633
|
meta: {
|
|
284
634
|
name: "eslint-plugin-ai-rules",
|
|
285
|
-
version: "0.
|
|
635
|
+
version: "0.3.0"
|
|
286
636
|
},
|
|
287
637
|
rules: {
|
|
288
638
|
"filename-pascal-case": filenamePascalCase,
|
|
289
639
|
"no-multiline-imports": noMultilineImports,
|
|
640
|
+
"project-setup": projectSetup,
|
|
641
|
+
"project-structure": projectStructure,
|
|
290
642
|
"sort-imports": sortImports
|
|
291
643
|
}
|
|
292
644
|
};
|
|
@@ -311,6 +663,7 @@ var recommended = [
|
|
|
311
663
|
"ai-rules/sort-imports": "error",
|
|
312
664
|
"ai-rules/no-multiline-imports": "error",
|
|
313
665
|
"ai-rules/filename-pascal-case": "error",
|
|
666
|
+
"ai-rules/project-setup": "warn",
|
|
314
667
|
"max-len": "off",
|
|
315
668
|
"import-x/no-duplicates": "error",
|
|
316
669
|
"import-x/no-default-export": "off",
|
|
@@ -332,11 +685,1292 @@ var recommended = [
|
|
|
332
685
|
}
|
|
333
686
|
];
|
|
334
687
|
|
|
688
|
+
// src/configs/projects.ts
|
|
689
|
+
var import_eslint_plugin_boundaries = __toESM(require("eslint-plugin-boundaries"), 1);
|
|
690
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
691
|
+
|
|
692
|
+
// src/structures/buildRelationsConfig.ts
|
|
693
|
+
var import_node_fs = require("fs");
|
|
694
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
695
|
+
function layerPatterns(paths) {
|
|
696
|
+
return Array.isArray(paths) ? paths : [paths];
|
|
697
|
+
}
|
|
698
|
+
function buildRelationsSettings(relations, rootAbsolutePath) {
|
|
699
|
+
const resolver = {
|
|
700
|
+
node: {
|
|
701
|
+
extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts", ".mjs", ".cjs"]
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
if ((0, import_node_fs.existsSync)(import_node_path3.default.join(rootAbsolutePath, "tsconfig.json"))) {
|
|
705
|
+
resolver.typescript = {
|
|
706
|
+
alwaysTryTypes: true,
|
|
707
|
+
project: rootAbsolutePath
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
"boundaries/root-path": rootAbsolutePath,
|
|
712
|
+
"boundaries/elements": Object.entries(relations.layers).map(
|
|
713
|
+
([type, pattern]) => ({
|
|
714
|
+
type,
|
|
715
|
+
pattern: layerPatterns(pattern),
|
|
716
|
+
partialMatch: false,
|
|
717
|
+
...relations.scope?.layers.includes(type) ? { capture: [relations.scope.capture] } : {}
|
|
718
|
+
})
|
|
719
|
+
),
|
|
720
|
+
"import/resolver": resolver
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function fromSelector(from) {
|
|
724
|
+
return {
|
|
725
|
+
element: {
|
|
726
|
+
type: from
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function targetSelector(type) {
|
|
731
|
+
return {
|
|
732
|
+
to: {
|
|
733
|
+
element: {
|
|
734
|
+
type
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function scopedTargetSelector(type, scope) {
|
|
740
|
+
return {
|
|
741
|
+
to: {
|
|
742
|
+
element: {
|
|
743
|
+
type,
|
|
744
|
+
captured: {
|
|
745
|
+
[scope.capture]: `{{ from.element.captured.${scope.capture} }}`
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function buildDisallowPolicy(rule, targetLayer) {
|
|
752
|
+
return {
|
|
753
|
+
from: fromSelector(rule.from),
|
|
754
|
+
disallow: targetSelector(targetLayer),
|
|
755
|
+
...rule.message ? { message: rule.message } : {}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function buildScopedAllowPolicy(rule, targetLayer, scope) {
|
|
759
|
+
return {
|
|
760
|
+
from: fromSelector(rule.from),
|
|
761
|
+
allow: scopedTargetSelector(targetLayer, scope)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function buildTypeAllowPolicy(from) {
|
|
765
|
+
return {
|
|
766
|
+
from: fromSelector(from),
|
|
767
|
+
allow: {
|
|
768
|
+
dependency: {
|
|
769
|
+
kind: "type"
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function buildRelationsRuleOptions(relations) {
|
|
775
|
+
const layerNames = Object.keys(relations.layers);
|
|
776
|
+
const scope = relations.scope;
|
|
777
|
+
const scopeLayers = new Set(scope?.layers ?? []);
|
|
778
|
+
const policies = [];
|
|
779
|
+
for (const rule of relations.rules) {
|
|
780
|
+
const fromIsScoped = Boolean(
|
|
781
|
+
scope && scopeLayers.has(rule.from)
|
|
782
|
+
);
|
|
783
|
+
if (rule.allow) {
|
|
784
|
+
const allowedLayers = new Set(rule.allow);
|
|
785
|
+
for (const layerName of layerNames) {
|
|
786
|
+
if (layerName === rule.from) continue;
|
|
787
|
+
if (!allowedLayers.has(layerName)) {
|
|
788
|
+
policies.push(buildDisallowPolicy(rule, layerName));
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (fromIsScoped && scope && scopeLayers.has(layerName)) {
|
|
792
|
+
policies.push(buildDisallowPolicy(rule, layerName));
|
|
793
|
+
policies.push(buildScopedAllowPolicy(rule, layerName, scope));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} else if (rule.deny) {
|
|
797
|
+
for (const layerName of rule.deny) {
|
|
798
|
+
if (layerName === rule.from) continue;
|
|
799
|
+
policies.push(buildDisallowPolicy(rule, layerName));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const allowSameLayer = rule.allowSameLayer ?? true;
|
|
803
|
+
if (!allowSameLayer) {
|
|
804
|
+
policies.push(buildDisallowPolicy(rule, rule.from));
|
|
805
|
+
} else if (fromIsScoped && scope) {
|
|
806
|
+
policies.push(buildDisallowPolicy(rule, rule.from));
|
|
807
|
+
policies.push(buildScopedAllowPolicy(rule, rule.from, scope));
|
|
808
|
+
}
|
|
809
|
+
const allowTypeImports = rule.allowTypeImports ?? relations.allowTypeImports ?? false;
|
|
810
|
+
if (allowTypeImports) {
|
|
811
|
+
policies.push(buildTypeAllowPolicy(rule.from));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const checkInternals = relations.rules.some(
|
|
815
|
+
(rule) => rule.allowSameLayer === false
|
|
816
|
+
);
|
|
817
|
+
return {
|
|
818
|
+
default: "allow",
|
|
819
|
+
checkAllOrigins: false,
|
|
820
|
+
checkUnknownLocals: false,
|
|
821
|
+
checkInternals,
|
|
822
|
+
policies
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/structures/validateRelations.ts
|
|
827
|
+
function isRecord(value) {
|
|
828
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
829
|
+
}
|
|
830
|
+
function assertString(value, path5) {
|
|
831
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
832
|
+
throw new Error(`Invalid relations: "${path5}" must be a non-empty string.`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function validatePaths(value, path5) {
|
|
836
|
+
const paths = Array.isArray(value) ? value : [value];
|
|
837
|
+
if (paths.length === 0) {
|
|
838
|
+
throw new Error(`Invalid relations: "${path5}" must contain at least one path.`);
|
|
839
|
+
}
|
|
840
|
+
paths.forEach((entry, index) => {
|
|
841
|
+
assertString(entry, Array.isArray(value) ? `${path5}[${index}]` : path5);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function validateLayerReferences(value, path5, layerNames, validateReferences) {
|
|
845
|
+
if (!Array.isArray(value)) {
|
|
846
|
+
throw new Error(`Invalid relations: "${path5}" must be an array.`);
|
|
847
|
+
}
|
|
848
|
+
value.forEach((layerName, index) => {
|
|
849
|
+
assertString(layerName, `${path5}[${index}]`);
|
|
850
|
+
if (validateReferences && !layerNames.has(layerName)) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Invalid relations: "${path5}[${index}]" references unknown layer "${layerName}".`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
function validateRelations(value, options = {}) {
|
|
858
|
+
const validateReferences = options.validateReferences ?? true;
|
|
859
|
+
if (!isRecord(value)) {
|
|
860
|
+
throw new Error("Invalid relations: relations must be an object.");
|
|
861
|
+
}
|
|
862
|
+
if (!isRecord(value.layers)) {
|
|
863
|
+
throw new Error('Invalid relations: "relations.layers" must be an object.');
|
|
864
|
+
}
|
|
865
|
+
const layers = value.layers;
|
|
866
|
+
const layerNames = new Set(Object.keys(layers));
|
|
867
|
+
if (layerNames.size === 0) {
|
|
868
|
+
throw new Error('Invalid relations: "relations.layers" cannot be empty.');
|
|
869
|
+
}
|
|
870
|
+
for (const [layerName, paths] of Object.entries(layers)) {
|
|
871
|
+
assertString(layerName, "relations.layers key");
|
|
872
|
+
validatePaths(paths, `relations.layers.${layerName}`);
|
|
873
|
+
}
|
|
874
|
+
if (value.allowTypeImports !== void 0 && typeof value.allowTypeImports !== "boolean") {
|
|
875
|
+
throw new Error('Invalid relations: "relations.allowTypeImports" must be a boolean.');
|
|
876
|
+
}
|
|
877
|
+
if (!Array.isArray(value.rules)) {
|
|
878
|
+
throw new Error('Invalid relations: "relations.rules" must be an array.');
|
|
879
|
+
}
|
|
880
|
+
const fromLayers = /* @__PURE__ */ new Set();
|
|
881
|
+
value.rules.forEach((rule, index) => {
|
|
882
|
+
const rulePath = `relations.rules[${index}]`;
|
|
883
|
+
if (!isRecord(rule)) {
|
|
884
|
+
throw new Error(`Invalid relations: "${rulePath}" must be an object.`);
|
|
885
|
+
}
|
|
886
|
+
assertString(rule.from, `${rulePath}.from`);
|
|
887
|
+
if (validateReferences && !layerNames.has(rule.from)) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Invalid relations: "${rulePath}.from" references unknown layer "${rule.from}".`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (fromLayers.has(rule.from)) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Invalid relations: duplicate rule for layer "${rule.from}" at "${rulePath}".`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
fromLayers.add(rule.from);
|
|
898
|
+
const hasAllow = rule.allow !== void 0;
|
|
899
|
+
const hasDeny = rule.deny !== void 0;
|
|
900
|
+
if (hasAllow === hasDeny) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`Invalid relations: "${rulePath}" must define exactly one of "allow" or "deny".`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (hasAllow) validateLayerReferences(rule.allow, `${rulePath}.allow`, layerNames, validateReferences);
|
|
906
|
+
if (hasDeny) validateLayerReferences(rule.deny, `${rulePath}.deny`, layerNames, validateReferences);
|
|
907
|
+
const layerReferences = hasAllow ? rule.allow : rule.deny;
|
|
908
|
+
if (validateReferences && layerReferences.includes(rule.from)) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Invalid relations: "${rulePath}" cannot reference its own layer; use "allowSameLayer" instead.`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (rule.allowSameLayer !== void 0 && typeof rule.allowSameLayer !== "boolean") {
|
|
914
|
+
throw new Error(`Invalid relations: "${rulePath}.allowSameLayer" must be a boolean.`);
|
|
915
|
+
}
|
|
916
|
+
if (rule.allowTypeImports !== void 0 && typeof rule.allowTypeImports !== "boolean") {
|
|
917
|
+
throw new Error(`Invalid relations: "${rulePath}.allowTypeImports" must be a boolean.`);
|
|
918
|
+
}
|
|
919
|
+
if (rule.message !== void 0) assertString(rule.message, `${rulePath}.message`);
|
|
920
|
+
});
|
|
921
|
+
if (value.scope !== void 0) {
|
|
922
|
+
if (!isRecord(value.scope)) {
|
|
923
|
+
throw new Error('Invalid relations: "relations.scope" must be an object.');
|
|
924
|
+
}
|
|
925
|
+
assertString(value.scope.capture, "relations.scope.capture");
|
|
926
|
+
if (!Array.isArray(value.scope.layers) || value.scope.layers.length === 0) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
'Invalid relations: "relations.scope.layers" must be a non-empty array.'
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const scopeLayers = /* @__PURE__ */ new Set();
|
|
932
|
+
value.scope.layers.forEach((layerName, index) => {
|
|
933
|
+
assertString(layerName, `relations.scope.layers[${index}]`);
|
|
934
|
+
if (validateReferences && !layerNames.has(layerName)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Invalid relations: "relations.scope.layers[${index}]" references unknown layer "${layerName}".`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (scopeLayers.has(layerName)) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`Invalid relations: duplicate scope layer "${layerName}".`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
scopeLayers.add(layerName);
|
|
945
|
+
if (!validateReferences) return;
|
|
946
|
+
const layerPaths = Array.isArray(layers[layerName]) ? layers[layerName] : [layers[layerName]];
|
|
947
|
+
if (!layerPaths.every((layerPath) => layerPath.includes("*"))) {
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Invalid relations: scope layer "${layerName}" must use a pattern with a wildcard.`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/structures/validateStructure.ts
|
|
957
|
+
var DYNAMIC_FOLDER_PATTERN2 = /^\[.+\]$/;
|
|
958
|
+
function isRecord2(value) {
|
|
959
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
960
|
+
}
|
|
961
|
+
function assertString2(value, path5) {
|
|
962
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
963
|
+
throw new Error(`Invalid structure: "${path5}" must be a non-empty string.`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function validateRegex(value, path5) {
|
|
967
|
+
assertString2(value, path5);
|
|
968
|
+
try {
|
|
969
|
+
new RegExp(value);
|
|
970
|
+
} catch {
|
|
971
|
+
throw new Error(`Invalid structure: "${path5}" must be a valid regular expression.`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function validateNameRule(value, path5) {
|
|
975
|
+
if (!isRecord2(value)) {
|
|
976
|
+
throw new Error(`Invalid structure: "${path5}" must be an object.`);
|
|
977
|
+
}
|
|
978
|
+
if (value.case !== void 0) {
|
|
979
|
+
if (typeof value.case !== "string" || !isNameCase(value.case)) {
|
|
980
|
+
throw new Error(
|
|
981
|
+
`Invalid structure: "${path5}.case" must be one of: PascalCase, camelCase, kebab-case, snake_case.`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (value.prefix !== void 0) assertString2(value.prefix, `${path5}.prefix`);
|
|
986
|
+
if (value.suffix !== void 0) assertString2(value.suffix, `${path5}.suffix`);
|
|
987
|
+
if (value.pattern !== void 0) validateRegex(value.pattern, `${path5}.pattern`);
|
|
988
|
+
}
|
|
989
|
+
function validateFileRule(value, path5) {
|
|
990
|
+
if (typeof value === "string") {
|
|
991
|
+
assertString2(value, path5);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (!isRecord2(value)) {
|
|
995
|
+
throw new Error(`Invalid structure: "${path5}" must be a string or an object.`);
|
|
996
|
+
}
|
|
997
|
+
if (!Array.isArray(value.extensions) || value.extensions.length === 0) {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`Invalid structure: "${path5}.extensions" must be a non-empty array of strings.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
value.extensions.forEach((extension, index) => {
|
|
1003
|
+
if (typeof extension !== "string" || !extension.startsWith(".")) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`Invalid structure: "${path5}.extensions[${index}]" must start with a dot.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
if (value.case !== void 0) {
|
|
1010
|
+
if (typeof value.case !== "string" || !isNameCase(value.case)) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`Invalid structure: "${path5}.case" must be one of: PascalCase, camelCase, kebab-case, snake_case.`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (value.prefix !== void 0) assertString2(value.prefix, `${path5}.prefix`);
|
|
1017
|
+
if (value.suffix !== void 0) assertString2(value.suffix, `${path5}.suffix`);
|
|
1018
|
+
if (value.pattern !== void 0) validateRegex(value.pattern, `${path5}.pattern`);
|
|
1019
|
+
}
|
|
1020
|
+
function validateFileRules(value, path5) {
|
|
1021
|
+
if (!Array.isArray(value)) {
|
|
1022
|
+
throw new Error(`Invalid structure: "${path5}" must be an array.`);
|
|
1023
|
+
}
|
|
1024
|
+
value.forEach((rule, index) => validateFileRule(rule, `${path5}[${index}]`));
|
|
1025
|
+
}
|
|
1026
|
+
function validateNode(value, path5, isRoot = false) {
|
|
1027
|
+
if (!isRecord2(value)) {
|
|
1028
|
+
throw new Error(`Invalid structure: "${path5}" must be an object.`);
|
|
1029
|
+
}
|
|
1030
|
+
if (value.name !== void 0) {
|
|
1031
|
+
if (isRoot) {
|
|
1032
|
+
throw new Error(`Invalid structure: "${path5}.name" is only allowed for dynamic folders.`);
|
|
1033
|
+
}
|
|
1034
|
+
validateNameRule(value.name, `${path5}.name`);
|
|
1035
|
+
}
|
|
1036
|
+
if (value.ignore !== void 0 && typeof value.ignore !== "boolean") {
|
|
1037
|
+
throw new Error(`Invalid structure: "${path5}.ignore" must be a boolean.`);
|
|
1038
|
+
}
|
|
1039
|
+
if (value.files !== void 0) validateFileRules(value.files, `${path5}.files`);
|
|
1040
|
+
if (value.additionalFiles !== void 0) {
|
|
1041
|
+
validateFileRules(value.additionalFiles, `${path5}.additionalFiles`);
|
|
1042
|
+
}
|
|
1043
|
+
if (value.folders !== void 0) {
|
|
1044
|
+
if (!isRecord2(value.folders)) {
|
|
1045
|
+
throw new Error(`Invalid structure: "${path5}.folders" must be an object.`);
|
|
1046
|
+
}
|
|
1047
|
+
let dynamicFolders = 0;
|
|
1048
|
+
for (const [folderName, child] of Object.entries(value.folders)) {
|
|
1049
|
+
if (folderName.trim().length === 0) {
|
|
1050
|
+
throw new Error(`Invalid structure: "${path5}.folders" cannot have empty keys.`);
|
|
1051
|
+
}
|
|
1052
|
+
const isDynamicFolder = DYNAMIC_FOLDER_PATTERN2.test(folderName);
|
|
1053
|
+
if (isDynamicFolder) {
|
|
1054
|
+
dynamicFolders += 1;
|
|
1055
|
+
} else if (isRecord2(child) && child.name !== void 0) {
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`Invalid structure: "${path5}.folders.${folderName}.name" is only allowed for dynamic folders.`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
validateNode(child, `${path5}.folders.${folderName}`);
|
|
1061
|
+
}
|
|
1062
|
+
if (dynamicFolders > 1) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Invalid structure: "${path5}.folders" can have only one dynamic folder per level.`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function validateStructure(value) {
|
|
1070
|
+
validateNode(value, "structure", true);
|
|
1071
|
+
if (value.ignoredPaths !== void 0) {
|
|
1072
|
+
if (!Array.isArray(value.ignoredPaths)) {
|
|
1073
|
+
throw new Error('Invalid structure: "structure.ignoredPaths" must be an array.');
|
|
1074
|
+
}
|
|
1075
|
+
value.ignoredPaths.forEach((pattern, index) => {
|
|
1076
|
+
assertString2(pattern, `structure.ignoredPaths[${index}]`);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/structures/mergeRelations.ts
|
|
1082
|
+
function cloneLayerPaths(paths) {
|
|
1083
|
+
return Array.isArray(paths) ? [...paths] : paths;
|
|
1084
|
+
}
|
|
1085
|
+
function cloneRule(rule) {
|
|
1086
|
+
return {
|
|
1087
|
+
...rule,
|
|
1088
|
+
...rule.allow ? { allow: [...rule.allow] } : {},
|
|
1089
|
+
...rule.deny ? { deny: [...rule.deny] } : {}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function cloneScope(scope) {
|
|
1093
|
+
return {
|
|
1094
|
+
...scope,
|
|
1095
|
+
layers: [...scope.layers]
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function mergeRelations(base, override) {
|
|
1099
|
+
if (!base && !override) return void 0;
|
|
1100
|
+
const layers = {
|
|
1101
|
+
...base?.layers ?? {},
|
|
1102
|
+
...override?.layers ?? {}
|
|
1103
|
+
};
|
|
1104
|
+
const normalizedLayers = Object.fromEntries(
|
|
1105
|
+
Object.entries(layers).map(([layerName, paths]) => [
|
|
1106
|
+
layerName,
|
|
1107
|
+
cloneLayerPaths(paths)
|
|
1108
|
+
])
|
|
1109
|
+
);
|
|
1110
|
+
const rules = (base?.rules ?? []).map(cloneRule);
|
|
1111
|
+
for (const overrideRule of override?.rules ?? []) {
|
|
1112
|
+
const index = rules.findIndex((rule) => rule.from === overrideRule.from);
|
|
1113
|
+
if (index >= 0) {
|
|
1114
|
+
rules[index] = cloneRule(overrideRule);
|
|
1115
|
+
} else {
|
|
1116
|
+
rules.push(cloneRule(overrideRule));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const scope = override?.scope ? cloneScope(override.scope) : base?.scope ? cloneScope(base.scope) : void 0;
|
|
1120
|
+
const allowTypeImports = override?.allowTypeImports ?? base?.allowTypeImports;
|
|
1121
|
+
return {
|
|
1122
|
+
layers: normalizedLayers,
|
|
1123
|
+
rules,
|
|
1124
|
+
...allowTypeImports !== void 0 ? { allowTypeImports } : {},
|
|
1125
|
+
...scope ? { scope } : {}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/structures/mergeStructure.ts
|
|
1130
|
+
function cloneFileRule(rule) {
|
|
1131
|
+
if (typeof rule === "string") return rule;
|
|
1132
|
+
return {
|
|
1133
|
+
...rule,
|
|
1134
|
+
extensions: [...rule.extensions]
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function mergeNameRule(base, override) {
|
|
1138
|
+
if (!base && !override) return void 0;
|
|
1139
|
+
return {
|
|
1140
|
+
...base ?? {},
|
|
1141
|
+
...override ?? {}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function combineFiles(node) {
|
|
1145
|
+
return [...node.files ?? [], ...node.additionalFiles ?? []];
|
|
1146
|
+
}
|
|
1147
|
+
function mergeFolders(base, override) {
|
|
1148
|
+
if (!base && !override) return void 0;
|
|
1149
|
+
const result = {};
|
|
1150
|
+
const folderNames = /* @__PURE__ */ new Set([
|
|
1151
|
+
...Object.keys(base ?? {}),
|
|
1152
|
+
...Object.keys(override ?? {})
|
|
1153
|
+
]);
|
|
1154
|
+
for (const folderName of folderNames) {
|
|
1155
|
+
result[folderName] = mergeNode(
|
|
1156
|
+
base?.[folderName] ?? {},
|
|
1157
|
+
override?.[folderName] ?? {}
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|
|
1162
|
+
function mergeNode(base, override) {
|
|
1163
|
+
const node = {};
|
|
1164
|
+
const baseHasFileConfig = base.files !== void 0 || base.additionalFiles !== void 0;
|
|
1165
|
+
const overrideHasFileConfig = override.files !== void 0 || override.additionalFiles !== void 0;
|
|
1166
|
+
if (overrideHasFileConfig) {
|
|
1167
|
+
const selectedFiles = override.files !== void 0 ? override.files : combineFiles(base);
|
|
1168
|
+
node.files = [
|
|
1169
|
+
...selectedFiles,
|
|
1170
|
+
...override.additionalFiles ?? []
|
|
1171
|
+
].map(cloneFileRule);
|
|
1172
|
+
} else if (baseHasFileConfig) {
|
|
1173
|
+
node.files = combineFiles(base).map(cloneFileRule);
|
|
1174
|
+
}
|
|
1175
|
+
const name = mergeNameRule(base.name, override.name);
|
|
1176
|
+
if (name) node.name = name;
|
|
1177
|
+
if (override.ignore !== void 0) {
|
|
1178
|
+
node.ignore = override.ignore;
|
|
1179
|
+
} else if (base.ignore !== void 0) {
|
|
1180
|
+
node.ignore = base.ignore;
|
|
1181
|
+
}
|
|
1182
|
+
const folders = mergeFolders(base.folders, override.folders);
|
|
1183
|
+
if (folders) node.folders = folders;
|
|
1184
|
+
return node;
|
|
1185
|
+
}
|
|
1186
|
+
function mergeStructure(base, override) {
|
|
1187
|
+
const merged = mergeNode(base ?? {}, override ?? {});
|
|
1188
|
+
const ignoredPaths = Array.from(/* @__PURE__ */ new Set([
|
|
1189
|
+
...base?.ignoredPaths ?? [],
|
|
1190
|
+
...override?.ignoredPaths ?? []
|
|
1191
|
+
]));
|
|
1192
|
+
if (ignoredPaths.length > 0) {
|
|
1193
|
+
merged.ignoredPaths = ignoredPaths;
|
|
1194
|
+
}
|
|
1195
|
+
return merged;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/structures/presets/helpers.ts
|
|
1199
|
+
var pascalTs = {
|
|
1200
|
+
case: "PascalCase",
|
|
1201
|
+
extensions: [".ts"]
|
|
1202
|
+
};
|
|
1203
|
+
var pascalTsx = {
|
|
1204
|
+
case: "PascalCase",
|
|
1205
|
+
extensions: [".tsx"]
|
|
1206
|
+
};
|
|
1207
|
+
var pascalTsOrTsx = {
|
|
1208
|
+
case: "PascalCase",
|
|
1209
|
+
extensions: [".ts", ".tsx"]
|
|
1210
|
+
};
|
|
1211
|
+
var commonIgnoredPaths = [
|
|
1212
|
+
"node_modules/**",
|
|
1213
|
+
"dist/**",
|
|
1214
|
+
"build/**",
|
|
1215
|
+
"coverage/**"
|
|
1216
|
+
];
|
|
1217
|
+
|
|
1218
|
+
// src/structures/presets/frontend.ts
|
|
1219
|
+
var frontendRelations = {
|
|
1220
|
+
layers: {
|
|
1221
|
+
pageComponent: ["src/pages/*/components", "src/pages/*/tabs"],
|
|
1222
|
+
pageHook: "src/pages/*/hooks",
|
|
1223
|
+
pageService: "src/pages/*/services",
|
|
1224
|
+
pageContext: "src/pages/*/contexts",
|
|
1225
|
+
pageHelper: "src/pages/*/helpers",
|
|
1226
|
+
page: "src/pages/*",
|
|
1227
|
+
components: "src/components",
|
|
1228
|
+
hooks: "src/hooks",
|
|
1229
|
+
contexts: "src/contexts",
|
|
1230
|
+
generated: "src/services/generated",
|
|
1231
|
+
services: "src/services",
|
|
1232
|
+
utils: "src/utils",
|
|
1233
|
+
helpers: "src/helpers",
|
|
1234
|
+
constants: "src/constants",
|
|
1235
|
+
env: "src/env"
|
|
1236
|
+
},
|
|
1237
|
+
scope: {
|
|
1238
|
+
capture: "page",
|
|
1239
|
+
layers: [
|
|
1240
|
+
"pageComponent",
|
|
1241
|
+
"pageHook",
|
|
1242
|
+
"pageService",
|
|
1243
|
+
"pageContext",
|
|
1244
|
+
"pageHelper",
|
|
1245
|
+
"page"
|
|
1246
|
+
]
|
|
1247
|
+
},
|
|
1248
|
+
rules: [
|
|
1249
|
+
{
|
|
1250
|
+
from: "components",
|
|
1251
|
+
allow: [
|
|
1252
|
+
"hooks",
|
|
1253
|
+
"contexts",
|
|
1254
|
+
"services",
|
|
1255
|
+
"generated",
|
|
1256
|
+
"utils",
|
|
1257
|
+
"helpers",
|
|
1258
|
+
"constants",
|
|
1259
|
+
"env"
|
|
1260
|
+
],
|
|
1261
|
+
allowSameLayer: true
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
from: "hooks",
|
|
1265
|
+
allow: [
|
|
1266
|
+
"contexts",
|
|
1267
|
+
"services",
|
|
1268
|
+
"generated",
|
|
1269
|
+
"utils",
|
|
1270
|
+
"helpers",
|
|
1271
|
+
"constants",
|
|
1272
|
+
"env"
|
|
1273
|
+
],
|
|
1274
|
+
allowSameLayer: true
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
from: "contexts",
|
|
1278
|
+
allow: [
|
|
1279
|
+
"services",
|
|
1280
|
+
"generated",
|
|
1281
|
+
"utils",
|
|
1282
|
+
"helpers",
|
|
1283
|
+
"constants",
|
|
1284
|
+
"env"
|
|
1285
|
+
],
|
|
1286
|
+
allowSameLayer: false
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
from: "services",
|
|
1290
|
+
allow: ["generated", "utils", "helpers", "constants", "env"],
|
|
1291
|
+
allowSameLayer: false
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
from: "generated",
|
|
1295
|
+
allow: [],
|
|
1296
|
+
allowSameLayer: true
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
from: "utils",
|
|
1300
|
+
allow: ["helpers", "constants", "env"],
|
|
1301
|
+
allowSameLayer: false
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
from: "helpers",
|
|
1305
|
+
allow: ["utils", "constants", "env"],
|
|
1306
|
+
allowSameLayer: false
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
from: "constants",
|
|
1310
|
+
allow: [],
|
|
1311
|
+
allowSameLayer: false
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
from: "env",
|
|
1315
|
+
allow: [],
|
|
1316
|
+
allowSameLayer: false
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
from: "page",
|
|
1320
|
+
allow: [
|
|
1321
|
+
"components",
|
|
1322
|
+
"hooks",
|
|
1323
|
+
"contexts",
|
|
1324
|
+
"services",
|
|
1325
|
+
"generated",
|
|
1326
|
+
"utils",
|
|
1327
|
+
"helpers",
|
|
1328
|
+
"constants",
|
|
1329
|
+
"env",
|
|
1330
|
+
"pageComponent",
|
|
1331
|
+
"pageHook",
|
|
1332
|
+
"pageService",
|
|
1333
|
+
"pageContext",
|
|
1334
|
+
"pageHelper"
|
|
1335
|
+
],
|
|
1336
|
+
allowSameLayer: true
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
from: "pageComponent",
|
|
1340
|
+
allow: [
|
|
1341
|
+
"components",
|
|
1342
|
+
"hooks",
|
|
1343
|
+
"contexts",
|
|
1344
|
+
"services",
|
|
1345
|
+
"generated",
|
|
1346
|
+
"utils",
|
|
1347
|
+
"helpers",
|
|
1348
|
+
"constants",
|
|
1349
|
+
"env",
|
|
1350
|
+
"page",
|
|
1351
|
+
"pageHook",
|
|
1352
|
+
"pageService",
|
|
1353
|
+
"pageContext",
|
|
1354
|
+
"pageHelper"
|
|
1355
|
+
],
|
|
1356
|
+
allowSameLayer: true
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
from: "pageHook",
|
|
1360
|
+
allow: [
|
|
1361
|
+
"hooks",
|
|
1362
|
+
"contexts",
|
|
1363
|
+
"services",
|
|
1364
|
+
"generated",
|
|
1365
|
+
"utils",
|
|
1366
|
+
"helpers",
|
|
1367
|
+
"constants",
|
|
1368
|
+
"env",
|
|
1369
|
+
"pageService",
|
|
1370
|
+
"pageContext",
|
|
1371
|
+
"pageHelper"
|
|
1372
|
+
],
|
|
1373
|
+
allowSameLayer: true
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
from: "pageService",
|
|
1377
|
+
allow: [
|
|
1378
|
+
"services",
|
|
1379
|
+
"generated",
|
|
1380
|
+
"utils",
|
|
1381
|
+
"helpers",
|
|
1382
|
+
"constants",
|
|
1383
|
+
"env",
|
|
1384
|
+
"pageHelper"
|
|
1385
|
+
],
|
|
1386
|
+
allowSameLayer: false
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
from: "pageContext",
|
|
1390
|
+
allow: [
|
|
1391
|
+
"contexts",
|
|
1392
|
+
"services",
|
|
1393
|
+
"generated",
|
|
1394
|
+
"utils",
|
|
1395
|
+
"helpers",
|
|
1396
|
+
"constants",
|
|
1397
|
+
"env",
|
|
1398
|
+
"pageService",
|
|
1399
|
+
"pageHelper"
|
|
1400
|
+
],
|
|
1401
|
+
allowSameLayer: false
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
from: "pageHelper",
|
|
1405
|
+
allow: ["helpers", "utils", "constants", "env"],
|
|
1406
|
+
allowSameLayer: false
|
|
1407
|
+
}
|
|
1408
|
+
]
|
|
1409
|
+
};
|
|
1410
|
+
var frontendPreset = {
|
|
1411
|
+
structure: {
|
|
1412
|
+
files: [
|
|
1413
|
+
"package.json",
|
|
1414
|
+
"package-lock.json",
|
|
1415
|
+
"yarn.lock",
|
|
1416
|
+
"pnpm-lock.yaml",
|
|
1417
|
+
"bun.lockb",
|
|
1418
|
+
"*.config.{js,mjs,cjs,ts}",
|
|
1419
|
+
"tsconfig*.json",
|
|
1420
|
+
"*.tsbuildinfo",
|
|
1421
|
+
".env",
|
|
1422
|
+
".env.*",
|
|
1423
|
+
".gitignore",
|
|
1424
|
+
".dockerignore",
|
|
1425
|
+
".editorconfig",
|
|
1426
|
+
"Dockerfile",
|
|
1427
|
+
"entrypoint.sh",
|
|
1428
|
+
"nginx.conf",
|
|
1429
|
+
"index.html",
|
|
1430
|
+
"AGENTS.md",
|
|
1431
|
+
"README.md",
|
|
1432
|
+
"LICENSE"
|
|
1433
|
+
],
|
|
1434
|
+
folders: {
|
|
1435
|
+
src: {
|
|
1436
|
+
files: [
|
|
1437
|
+
"App.tsx",
|
|
1438
|
+
"AppRoutes.tsx",
|
|
1439
|
+
"main.tsx",
|
|
1440
|
+
"index.css",
|
|
1441
|
+
"vite-env.d.ts"
|
|
1442
|
+
],
|
|
1443
|
+
folders: {
|
|
1444
|
+
components: {
|
|
1445
|
+
files: [pascalTsx],
|
|
1446
|
+
folders: {
|
|
1447
|
+
ui: {
|
|
1448
|
+
files: [pascalTsOrTsx]
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
constants: {
|
|
1453
|
+
files: [pascalTs]
|
|
1454
|
+
},
|
|
1455
|
+
contexts: {
|
|
1456
|
+
files: [
|
|
1457
|
+
{
|
|
1458
|
+
case: "PascalCase",
|
|
1459
|
+
extensions: [".tsx"]
|
|
1460
|
+
}
|
|
1461
|
+
]
|
|
1462
|
+
},
|
|
1463
|
+
env: {
|
|
1464
|
+
files: ["index.ts"]
|
|
1465
|
+
},
|
|
1466
|
+
hooks: {
|
|
1467
|
+
files: [pascalTs]
|
|
1468
|
+
},
|
|
1469
|
+
helpers: {
|
|
1470
|
+
files: [pascalTs]
|
|
1471
|
+
},
|
|
1472
|
+
pages: {
|
|
1473
|
+
files: [pascalTsx],
|
|
1474
|
+
folders: {
|
|
1475
|
+
"[feature]": {
|
|
1476
|
+
name: { case: "kebab-case" },
|
|
1477
|
+
files: [pascalTsx, "helpers.ts", "schema.ts", "types.ts"],
|
|
1478
|
+
folders: {
|
|
1479
|
+
components: {
|
|
1480
|
+
files: [pascalTsx]
|
|
1481
|
+
},
|
|
1482
|
+
hooks: {
|
|
1483
|
+
files: [pascalTs]
|
|
1484
|
+
},
|
|
1485
|
+
services: {
|
|
1486
|
+
files: [pascalTs]
|
|
1487
|
+
},
|
|
1488
|
+
contexts: {
|
|
1489
|
+
files: [
|
|
1490
|
+
{
|
|
1491
|
+
case: "PascalCase",
|
|
1492
|
+
extensions: [".tsx"]
|
|
1493
|
+
}
|
|
1494
|
+
]
|
|
1495
|
+
},
|
|
1496
|
+
helpers: {
|
|
1497
|
+
files: [pascalTs]
|
|
1498
|
+
},
|
|
1499
|
+
tabs: {
|
|
1500
|
+
folders: {
|
|
1501
|
+
"[tab]": {
|
|
1502
|
+
name: { case: "kebab-case" },
|
|
1503
|
+
files: [pascalTsx],
|
|
1504
|
+
folders: {
|
|
1505
|
+
"[section]": {
|
|
1506
|
+
name: { case: "kebab-case" },
|
|
1507
|
+
files: [
|
|
1508
|
+
pascalTs,
|
|
1509
|
+
pascalTsx,
|
|
1510
|
+
"collisionDetection.ts",
|
|
1511
|
+
"contextualFilters.ts"
|
|
1512
|
+
],
|
|
1513
|
+
folders: {
|
|
1514
|
+
components: {
|
|
1515
|
+
files: [pascalTsx]
|
|
1516
|
+
},
|
|
1517
|
+
contents: {
|
|
1518
|
+
files: [pascalTsx],
|
|
1519
|
+
folders: {
|
|
1520
|
+
components: {
|
|
1521
|
+
files: [pascalTsx]
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
services: {
|
|
1536
|
+
folders: {
|
|
1537
|
+
generated: { ignore: true },
|
|
1538
|
+
utils: {
|
|
1539
|
+
files: [pascalTs]
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
utils: {
|
|
1544
|
+
files: [pascalTs]
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
e2e: {
|
|
1549
|
+
files: [".env.e2e", ".env.e2e.example"],
|
|
1550
|
+
folders: {
|
|
1551
|
+
".auth": { ignore: true },
|
|
1552
|
+
fixtures: {
|
|
1553
|
+
files: ["*.png", "*.jpg", "*.jpeg", "*.svg", "*.webp"]
|
|
1554
|
+
},
|
|
1555
|
+
pages: {
|
|
1556
|
+
files: [pascalTs]
|
|
1557
|
+
},
|
|
1558
|
+
specs: {
|
|
1559
|
+
files: ["*.spec.ts"]
|
|
1560
|
+
},
|
|
1561
|
+
support: {
|
|
1562
|
+
files: [pascalTs],
|
|
1563
|
+
folders: {
|
|
1564
|
+
api: {
|
|
1565
|
+
files: [pascalTs]
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
scripts: {
|
|
1572
|
+
files: [pascalTs]
|
|
1573
|
+
},
|
|
1574
|
+
public: { ignore: true },
|
|
1575
|
+
".github": { ignore: true },
|
|
1576
|
+
".vscode": { ignore: true }
|
|
1577
|
+
},
|
|
1578
|
+
ignoredPaths: [
|
|
1579
|
+
...commonIgnoredPaths,
|
|
1580
|
+
"playwright-report/**",
|
|
1581
|
+
"test-results/**",
|
|
1582
|
+
"src/services/generated/**",
|
|
1583
|
+
"scripts/types/**"
|
|
1584
|
+
]
|
|
1585
|
+
},
|
|
1586
|
+
relations: frontendRelations
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/structures/presets/backend.ts
|
|
1590
|
+
var backendRelations = {
|
|
1591
|
+
layers: {
|
|
1592
|
+
controller: "src/http/controllers",
|
|
1593
|
+
http: "src/http/hooks",
|
|
1594
|
+
useCase: "src/use-case",
|
|
1595
|
+
repository: "src/repositories",
|
|
1596
|
+
generated: "src/services/generated",
|
|
1597
|
+
serviceModule: "src/services/*",
|
|
1598
|
+
service: "src/services",
|
|
1599
|
+
databaseSchema: "src/database/schema",
|
|
1600
|
+
database: "src/database",
|
|
1601
|
+
helper: "src/helpers",
|
|
1602
|
+
env: "src/env",
|
|
1603
|
+
types: ["src/@types", "src/http/types"]
|
|
1604
|
+
},
|
|
1605
|
+
scope: {
|
|
1606
|
+
capture: "service",
|
|
1607
|
+
layers: ["serviceModule"]
|
|
1608
|
+
},
|
|
1609
|
+
rules: [
|
|
1610
|
+
{
|
|
1611
|
+
from: "controller",
|
|
1612
|
+
allow: ["useCase", "http", "generated", "helper", "env"],
|
|
1613
|
+
allowSameLayer: false
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
from: "http",
|
|
1617
|
+
allow: ["useCase", "helper", "generated"],
|
|
1618
|
+
allowSameLayer: false
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
from: "useCase",
|
|
1622
|
+
allow: ["repository", "service", "serviceModule", "generated"],
|
|
1623
|
+
allowSameLayer: true
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
from: "repository",
|
|
1627
|
+
allow: ["database", "databaseSchema"],
|
|
1628
|
+
allowSameLayer: false
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
from: "service",
|
|
1632
|
+
allow: ["generated", "env"],
|
|
1633
|
+
allowSameLayer: false
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
from: "generated",
|
|
1637
|
+
allow: [],
|
|
1638
|
+
allowSameLayer: true
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
from: "serviceModule",
|
|
1642
|
+
allow: ["generated", "env"],
|
|
1643
|
+
allowSameLayer: true
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
from: "database",
|
|
1647
|
+
allow: ["databaseSchema", "env"],
|
|
1648
|
+
allowSameLayer: false
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
from: "databaseSchema",
|
|
1652
|
+
allow: [],
|
|
1653
|
+
allowSameLayer: true
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
from: "helper",
|
|
1657
|
+
allow: [],
|
|
1658
|
+
allowSameLayer: false
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
from: "env",
|
|
1662
|
+
allow: [],
|
|
1663
|
+
allowSameLayer: false
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
from: "types",
|
|
1667
|
+
allow: [],
|
|
1668
|
+
allowSameLayer: false
|
|
1669
|
+
}
|
|
1670
|
+
]
|
|
1671
|
+
};
|
|
1672
|
+
var backendPreset = {
|
|
1673
|
+
structure: {
|
|
1674
|
+
files: [
|
|
1675
|
+
"package.json",
|
|
1676
|
+
"package-lock.json",
|
|
1677
|
+
"yarn.lock",
|
|
1678
|
+
"pnpm-lock.yaml",
|
|
1679
|
+
"bun.lockb",
|
|
1680
|
+
"*.config.{js,mjs,cjs,ts}",
|
|
1681
|
+
"tsconfig*.json",
|
|
1682
|
+
"*.tsbuildinfo",
|
|
1683
|
+
".env",
|
|
1684
|
+
".env.*",
|
|
1685
|
+
".gitignore",
|
|
1686
|
+
".dockerignore",
|
|
1687
|
+
".editorconfig",
|
|
1688
|
+
"Dockerfile",
|
|
1689
|
+
"AGENTS.md",
|
|
1690
|
+
"README.md",
|
|
1691
|
+
"LICENSE"
|
|
1692
|
+
],
|
|
1693
|
+
folders: {
|
|
1694
|
+
src: {
|
|
1695
|
+
files: ["index.ts", "Server.ts", "Schemas.ts"],
|
|
1696
|
+
folders: {
|
|
1697
|
+
database: {
|
|
1698
|
+
files: ["Connection.ts"],
|
|
1699
|
+
folders: {
|
|
1700
|
+
schema: {
|
|
1701
|
+
files: ["index.ts", pascalTs]
|
|
1702
|
+
},
|
|
1703
|
+
seeds: {
|
|
1704
|
+
files: ["index.ts"]
|
|
1705
|
+
},
|
|
1706
|
+
migrations: { ignore: true }
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1709
|
+
repositories: {
|
|
1710
|
+
files: ["Setup.ts", pascalTs]
|
|
1711
|
+
},
|
|
1712
|
+
env: {
|
|
1713
|
+
files: ["index.ts"]
|
|
1714
|
+
},
|
|
1715
|
+
helpers: {
|
|
1716
|
+
files: [pascalTs]
|
|
1717
|
+
},
|
|
1718
|
+
"@types": {
|
|
1719
|
+
files: ["*.d.ts"]
|
|
1720
|
+
},
|
|
1721
|
+
http: {
|
|
1722
|
+
files: ["Routes.ts"],
|
|
1723
|
+
folders: {
|
|
1724
|
+
controllers: {
|
|
1725
|
+
files: ["Health.ts"],
|
|
1726
|
+
folders: {
|
|
1727
|
+
"[feature]": {
|
|
1728
|
+
name: { case: "kebab-case" },
|
|
1729
|
+
files: [pascalTs],
|
|
1730
|
+
folders: {
|
|
1731
|
+
"[resource]": {
|
|
1732
|
+
name: { case: "kebab-case" },
|
|
1733
|
+
files: [pascalTs]
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
hooks: {
|
|
1740
|
+
files: [pascalTs]
|
|
1741
|
+
},
|
|
1742
|
+
types: {
|
|
1743
|
+
files: ["*.d.ts"]
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
services: {
|
|
1748
|
+
files: [pascalTs],
|
|
1749
|
+
folders: {
|
|
1750
|
+
generated: { ignore: true },
|
|
1751
|
+
"[service]": {
|
|
1752
|
+
name: { case: "kebab-case" },
|
|
1753
|
+
files: ["index.ts", pascalTs]
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
"use-case": {
|
|
1758
|
+
files: ["Errors.ts"],
|
|
1759
|
+
folders: {
|
|
1760
|
+
"[feature]": {
|
|
1761
|
+
name: { case: "kebab-case" },
|
|
1762
|
+
files: [pascalTs],
|
|
1763
|
+
folders: {
|
|
1764
|
+
"[resource]": {
|
|
1765
|
+
name: { case: "kebab-case" },
|
|
1766
|
+
files: [pascalTs]
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
docs: { ignore: true },
|
|
1775
|
+
scripts: {
|
|
1776
|
+
files: [pascalTs]
|
|
1777
|
+
},
|
|
1778
|
+
test: {
|
|
1779
|
+
files: [
|
|
1780
|
+
{ case: "PascalCase", extensions: [".test.ts"] },
|
|
1781
|
+
"GlobalSetup.ts",
|
|
1782
|
+
"Setup.ts",
|
|
1783
|
+
"*.d.ts"
|
|
1784
|
+
],
|
|
1785
|
+
folders: {
|
|
1786
|
+
Helpers: {
|
|
1787
|
+
files: [pascalTs]
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
".github": { ignore: true },
|
|
1792
|
+
".vscode": { ignore: true }
|
|
1793
|
+
},
|
|
1794
|
+
ignoredPaths: [
|
|
1795
|
+
...commonIgnoredPaths,
|
|
1796
|
+
"uploads/**",
|
|
1797
|
+
"src/services/generated/**"
|
|
1798
|
+
]
|
|
1799
|
+
},
|
|
1800
|
+
relations: backendRelations
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
// src/structures/presets/other.ts
|
|
1804
|
+
var otherPreset = {
|
|
1805
|
+
structure: {}
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
// src/structures/presets/index.ts
|
|
1809
|
+
var presets = {
|
|
1810
|
+
frontend: frontendPreset,
|
|
1811
|
+
backend: backendPreset,
|
|
1812
|
+
other: otherPreset
|
|
1813
|
+
};
|
|
1814
|
+
function getPreset(preset) {
|
|
1815
|
+
const value = presets[preset];
|
|
1816
|
+
if (!value) {
|
|
1817
|
+
throw new Error(
|
|
1818
|
+
`Unknown preset "${preset}". Expected one of: ${Object.keys(presets).join(", ")}.`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
return value;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/configs/projects.ts
|
|
1825
|
+
var PROJECT_PRESETS = ["frontend", "backend", "other"];
|
|
1826
|
+
function normalizeRootFolder(rootFolder) {
|
|
1827
|
+
const absolutePath = import_node_path4.default.resolve(process.cwd(), rootFolder.trim());
|
|
1828
|
+
const relativePath = import_node_path4.default.relative(process.cwd(), absolutePath);
|
|
1829
|
+
if (relativePath.length === 0) return ".";
|
|
1830
|
+
if (relativePath.startsWith("..") || import_node_path4.default.isAbsolute(relativePath)) {
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`Invalid project: rootFolder "${rootFolder}" must be inside the current working directory.`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
return relativePath.split(import_node_path4.default.sep).join("/");
|
|
1836
|
+
}
|
|
1837
|
+
function isInside(parent, child) {
|
|
1838
|
+
if (parent === child) return false;
|
|
1839
|
+
if (parent === ".") return true;
|
|
1840
|
+
return child.startsWith(`${parent}/`);
|
|
1841
|
+
}
|
|
1842
|
+
function findNestedRoots(rootFolder, definitions) {
|
|
1843
|
+
return definitions.map((definition) => definition.rootFolder).filter((candidate) => isInside(rootFolder, candidate));
|
|
1844
|
+
}
|
|
1845
|
+
function validateDefinition(definition, index) {
|
|
1846
|
+
if (!definition || typeof definition !== "object") {
|
|
1847
|
+
throw new Error(`Invalid project at index ${index}: definition must be an object.`);
|
|
1848
|
+
}
|
|
1849
|
+
if (typeof definition.rootFolder !== "string" || definition.rootFolder.trim().length === 0) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`Invalid project at index ${index}: rootFolder must be a non-empty string.`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
if (definition.preset !== void 0 && !PROJECT_PRESETS.includes(definition.preset)) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
`Invalid project at index ${index}: preset must be one of: ${PROJECT_PRESETS.join(", ")}.`
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
if (definition.structure !== void 0) {
|
|
1860
|
+
try {
|
|
1861
|
+
validateStructure(definition.structure);
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1864
|
+
throw new Error(`Invalid project at index ${index}: ${message}`, {
|
|
1865
|
+
cause: error
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (definition.relations !== void 0) {
|
|
1870
|
+
try {
|
|
1871
|
+
validateRelations(definition.relations, { validateReferences: false });
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1874
|
+
throw new Error(`Invalid project at index ${index}: ${message}`, {
|
|
1875
|
+
cause: error
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function resolveStructure(definition, preset) {
|
|
1881
|
+
return mergeStructure(preset?.structure, definition.structure);
|
|
1882
|
+
}
|
|
1883
|
+
function projects(definitions) {
|
|
1884
|
+
if (!Array.isArray(definitions) || definitions.length === 0) {
|
|
1885
|
+
throw new Error("projects() requires a non-empty array of project definitions.");
|
|
1886
|
+
}
|
|
1887
|
+
const normalizedDefinitions = definitions.map((definition, index) => {
|
|
1888
|
+
validateDefinition(definition, index);
|
|
1889
|
+
return {
|
|
1890
|
+
...definition,
|
|
1891
|
+
rootFolder: normalizeRootFolder(definition.rootFolder)
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
1895
|
+
for (const definition of normalizedDefinitions) {
|
|
1896
|
+
if (seenRoots.has(definition.rootFolder)) {
|
|
1897
|
+
throw new Error(
|
|
1898
|
+
`Invalid projects: rootFolder "${definition.rootFolder}" is duplicated.`
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
seenRoots.add(definition.rootFolder);
|
|
1902
|
+
}
|
|
1903
|
+
return normalizedDefinitions.map((definition, index) => {
|
|
1904
|
+
const preset = definition.preset ? getPreset(definition.preset) : void 0;
|
|
1905
|
+
const structure = resolveStructure(definition, preset);
|
|
1906
|
+
const structureEnabled = hasStructureRules(structure);
|
|
1907
|
+
const relations = mergeRelations(preset?.relations, definition.relations);
|
|
1908
|
+
if (relations) {
|
|
1909
|
+
try {
|
|
1910
|
+
validateRelations(relations);
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1913
|
+
throw new Error(`Invalid project at index ${index}: ${message}`, {
|
|
1914
|
+
cause: error
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
const rootAbsolutePath = import_node_path4.default.resolve(process.cwd(), definition.rootFolder);
|
|
1919
|
+
const baseConfig = recommended[0];
|
|
1920
|
+
const rootPrefix = definition.rootFolder === "." ? "" : `${definition.rootFolder}/`;
|
|
1921
|
+
const nestedRoots = findNestedRoots(
|
|
1922
|
+
definition.rootFolder,
|
|
1923
|
+
normalizedDefinitions
|
|
1924
|
+
);
|
|
1925
|
+
const ignoredPaths = Array.from(/* @__PURE__ */ new Set([
|
|
1926
|
+
...(structure.ignoredPaths ?? []).map(
|
|
1927
|
+
(pattern) => `${rootPrefix}${pattern}`
|
|
1928
|
+
),
|
|
1929
|
+
...nestedRoots.map((nestedRoot) => `${nestedRoot}/**`)
|
|
1930
|
+
]));
|
|
1931
|
+
const rules = {
|
|
1932
|
+
...baseConfig.rules,
|
|
1933
|
+
...preset?.rules ?? {},
|
|
1934
|
+
"ai-rules/project-structure": structureEnabled ? "error" : "off",
|
|
1935
|
+
"ai-rules/filename-pascal-case": structureEnabled ? "off" : baseConfig.rules?.["ai-rules/filename-pascal-case"] ?? "error",
|
|
1936
|
+
...relations ? {
|
|
1937
|
+
"boundaries/dependencies": [
|
|
1938
|
+
"error",
|
|
1939
|
+
buildRelationsRuleOptions(relations)
|
|
1940
|
+
]
|
|
1941
|
+
} : {}
|
|
1942
|
+
};
|
|
1943
|
+
return {
|
|
1944
|
+
...baseConfig,
|
|
1945
|
+
plugins: {
|
|
1946
|
+
...baseConfig.plugins,
|
|
1947
|
+
...relations ? { boundaries: import_eslint_plugin_boundaries.default } : {}
|
|
1948
|
+
},
|
|
1949
|
+
files: [`${rootPrefix}**/*.{js,cjs,mjs,jsx,ts,tsx}`],
|
|
1950
|
+
...ignoredPaths.length > 0 ? { ignores: ignoredPaths } : {},
|
|
1951
|
+
settings: {
|
|
1952
|
+
...baseConfig.settings,
|
|
1953
|
+
...relations ? buildRelationsSettings(relations, rootAbsolutePath) : {},
|
|
1954
|
+
"ai-rules": {
|
|
1955
|
+
project: {
|
|
1956
|
+
rootFolder: definition.rootFolder,
|
|
1957
|
+
preset: definition.preset,
|
|
1958
|
+
structure,
|
|
1959
|
+
relations
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
rules
|
|
1964
|
+
};
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
|
|
335
1968
|
// src/index.ts
|
|
336
1969
|
var aiGuardPlugin = {
|
|
337
1970
|
...plugin,
|
|
338
1971
|
configs: {
|
|
339
|
-
recommended
|
|
1972
|
+
recommended,
|
|
1973
|
+
projects
|
|
340
1974
|
}
|
|
341
1975
|
};
|
|
342
1976
|
var index_default = aiGuardPlugin;
|
|
@@ -346,6 +1980,10 @@ var index_default = aiGuardPlugin;
|
|
|
346
1980
|
getSortedContent,
|
|
347
1981
|
noMultilineImports,
|
|
348
1982
|
plugin,
|
|
1983
|
+
presets,
|
|
1984
|
+
projectSetup,
|
|
1985
|
+
projectStructure,
|
|
1986
|
+
projects,
|
|
349
1987
|
recommended,
|
|
350
1988
|
sortImports
|
|
351
1989
|
});
|