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