@noctcore/eslint-plugin-architecture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @noctcore/eslint-plugin-architecture
2
+
3
+ Framework-agnostic folder-per-component and feature-boundary architecture rules. Flat-config only,
4
+ ESLint 9+.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ bun add -D @noctcore/eslint-plugin-architecture # or npm i -D / pnpm add -D
10
+ ```
11
+
12
+ ## Use
13
+
14
+ ```js
15
+ // eslint.config.js
16
+ import architecture from '@noctcore/eslint-plugin-architecture';
17
+
18
+ export default [
19
+ architecture.configs.recommended,
20
+ ];
21
+ ```
22
+
23
+ Or wire rules individually:
24
+
25
+ ```js
26
+ import architecture from '@noctcore/eslint-plugin-architecture';
27
+
28
+ export default [
29
+ {
30
+ plugins: { 'noctcore-architecture': architecture },
31
+ rules: {
32
+ 'noctcore-architecture/component-folder-structure': ['error', { componentRoot: 'components' }],
33
+ 'noctcore-architecture/no-cross-feature-imports': ['error', { alias: '@/components' }],
34
+ 'noctcore-architecture/index-must-reexport-default': 'error',
35
+ },
36
+ },
37
+ ];
38
+ ```
39
+
40
+ Every rule anchors on a configurable directory segment (default `components`) rather than an absolute
41
+ path, so it behaves the same whether ESLint runs from the repo root or per-package, on POSIX or
42
+ Windows. Two of the three rules inspect files on disk (sibling sets, barrel siblings), so run ESLint
43
+ against real file paths, not virtual sources.
44
+
45
+ ## Rules
46
+
47
+ | Rule | Description | 🔧 |
48
+ | --- | --- | --- |
49
+ | [`component-folder-structure`](./docs/rules/component-folder-structure.md) | A component entry file must ship its full sibling set (hooks, types, story, test, barrel) on disk. | |
50
+ | [`index-must-reexport-default`](./docs/rules/index-must-reexport-default.md) | A component folder's `index.ts` must re-export the sibling default named after the folder. | |
51
+ | [`no-cross-feature-imports`](./docs/rules/no-cross-feature-imports.md) | A file in one feature may not import runtime code from another feature. | |
package/dist/index.cjs ADDED
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ configs: () => configs,
34
+ default: () => index_default,
35
+ rules: () => rules
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/configs/recommended.ts
40
+ var recommended = {
41
+ "noctcore-architecture/component-folder-structure": "error",
42
+ "noctcore-architecture/index-must-reexport-default": "error",
43
+ "noctcore-architecture/no-cross-feature-imports": "error"
44
+ };
45
+
46
+ // src/rules/component-folder-structure.ts
47
+ var import_node_path2 = __toESM(require("path"), 1);
48
+
49
+ // src/createRule.ts
50
+ var import_eslint_utils = require("@noctcore/eslint-utils");
51
+ var createRule = (0, import_eslint_utils.makeCreateRule)("architecture");
52
+
53
+ // src/utils.ts
54
+ var import_node_fs = require("fs");
55
+ var import_node_path = __toESM(require("path"), 1);
56
+ function toPosix(filename) {
57
+ return filename.split(import_node_path.default.sep).join("/").split("\\").join("/");
58
+ }
59
+ function getBasename(filename) {
60
+ return import_node_path.default.basename(filename);
61
+ }
62
+ function isPascalCase(segment) {
63
+ return /^[A-Z][A-Za-z0-9]*$/.test(segment);
64
+ }
65
+ function isComponentFileName(filename) {
66
+ return /^[A-Z][A-Za-z0-9]*\.tsx$/.test(getBasename(filename));
67
+ }
68
+ function getComponentName(filename) {
69
+ return getBasename(filename).replace(/\.tsx$/, "");
70
+ }
71
+ function isComponentEntryFile(filename) {
72
+ if (!isComponentFileName(filename)) {
73
+ return false;
74
+ }
75
+ return getComponentName(filename) === import_node_path.default.basename(import_node_path.default.dirname(filename));
76
+ }
77
+ function segmentsAfterRoot(filename, root) {
78
+ const marker = `/${root}/`;
79
+ const posix = toPosix(filename);
80
+ const idx = posix.lastIndexOf(marker);
81
+ if (idx === -1) {
82
+ return null;
83
+ }
84
+ return posix.slice(idx + marker.length).split("/").filter((segment) => segment.length > 0);
85
+ }
86
+ function getFeatureName(filename, root) {
87
+ const segments = segmentsAfterRoot(filename, root);
88
+ if (segments === null || segments.length === 0) {
89
+ return null;
90
+ }
91
+ const feature = segments[0];
92
+ return feature !== void 0 && feature.length > 0 ? feature : null;
93
+ }
94
+ function escapeRegExpLiteral(value) {
95
+ return value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
96
+ }
97
+ function globToRegExp(glob) {
98
+ let out = "";
99
+ for (let i = 0; i < glob.length; i += 1) {
100
+ const char = glob[i];
101
+ if (char === "*") {
102
+ if (glob[i + 1] === "*") {
103
+ if (glob[i + 2] === "/") {
104
+ out += "(?:[^/]*/)*";
105
+ i += 2;
106
+ } else {
107
+ out += ".*";
108
+ i += 1;
109
+ }
110
+ } else {
111
+ out += "[^/]*";
112
+ }
113
+ } else if (char === "?") {
114
+ out += "[^/]";
115
+ } else if (char !== void 0) {
116
+ out += escapeRegExpLiteral(char);
117
+ }
118
+ }
119
+ return new RegExp(`^${out}$`);
120
+ }
121
+ function isIgnoredPath(filename, ignorePaths) {
122
+ if (ignorePaths.length === 0) {
123
+ return false;
124
+ }
125
+ const posix = toPosix(filename);
126
+ return ignorePaths.some((glob) => globToRegExp(glob).test(posix));
127
+ }
128
+ function siblingExists(dir, sibling) {
129
+ return (0, import_node_fs.existsSync)(import_node_path.default.join(dir, sibling));
130
+ }
131
+ function readDirSafe(dir) {
132
+ try {
133
+ return new Set((0, import_node_fs.readdirSync)(dir));
134
+ } catch {
135
+ return /* @__PURE__ */ new Set();
136
+ }
137
+ }
138
+
139
+ // src/rules/component-folder-structure.ts
140
+ var RULE_NAME = "component-folder-structure";
141
+ var DEFAULT_COMPONENT_ROOT = "components";
142
+ var DEFAULT_IGNORE_PATHS = ["**/ui/**"];
143
+ var DEFAULT_REQUIRED_SIBLINGS = [
144
+ ".hooks.ts",
145
+ ".types.ts",
146
+ ".stories.tsx",
147
+ ".test.tsx",
148
+ "index.ts"
149
+ ];
150
+ function resolveSibling(template, name) {
151
+ return template.startsWith(".") ? `${name}${template}` : template;
152
+ }
153
+ var optionSchema = {
154
+ type: "object",
155
+ additionalProperties: false,
156
+ properties: {
157
+ componentRoot: { type: "string" },
158
+ requiredSiblings: { type: "array", items: { type: "string" }, uniqueItems: true },
159
+ ignorePaths: { type: "array", items: { type: "string" }, uniqueItems: true }
160
+ }
161
+ };
162
+ var componentFolderStructureRule = createRule({
163
+ name: RULE_NAME,
164
+ meta: {
165
+ type: "problem",
166
+ docs: {
167
+ description: "A component `<Name>/<Name>.tsx` under `<componentRoot>/<feature>/...` must have its sibling set (`.hooks.ts`, `.types.ts`, `.stories.tsx`, `.test.tsx`, `index.ts`) present on disk."
168
+ },
169
+ schema: [optionSchema],
170
+ messages: {
171
+ missingSiblings: "Component `{{name}}` is missing sibling file(s): {{missing}}. Every component folder must carry its hooks, types, stories, test, and index barrel."
172
+ }
173
+ },
174
+ defaultOptions: [
175
+ {
176
+ componentRoot: DEFAULT_COMPONENT_ROOT,
177
+ requiredSiblings: [...DEFAULT_REQUIRED_SIBLINGS],
178
+ ignorePaths: [...DEFAULT_IGNORE_PATHS]
179
+ }
180
+ ],
181
+ create(context, [options]) {
182
+ const componentRoot = options.componentRoot ?? DEFAULT_COMPONENT_ROOT;
183
+ const ignorePaths = options.ignorePaths ?? DEFAULT_IGNORE_PATHS;
184
+ const siblingTemplates = options.requiredSiblings ?? DEFAULT_REQUIRED_SIBLINGS;
185
+ const filename = context.filename;
186
+ if (!isComponentEntryFile(filename) || isIgnoredPath(filename, ignorePaths)) {
187
+ return {};
188
+ }
189
+ if (getFeatureName(filename, componentRoot) === null) {
190
+ return {};
191
+ }
192
+ const name = getComponentName(filename);
193
+ const dir = import_node_path2.default.dirname(filename);
194
+ const required = siblingTemplates.map((template) => resolveSibling(template, name));
195
+ const present = readDirSafe(dir);
196
+ const missing = required.filter((sibling) => !present.has(sibling));
197
+ return {
198
+ Program(node) {
199
+ if (missing.length > 0) {
200
+ context.report({
201
+ node,
202
+ messageId: "missingSiblings",
203
+ data: { name, missing: missing.join(", ") }
204
+ });
205
+ }
206
+ }
207
+ };
208
+ }
209
+ });
210
+
211
+ // src/rules/index-must-reexport-default.ts
212
+ var import_node_path3 = __toESM(require("path"), 1);
213
+ var import_utils2 = require("@typescript-eslint/utils");
214
+ var RULE_NAME2 = "index-must-reexport-default";
215
+ var DEFAULT_IGNORE_PATHS2 = [];
216
+ var optionSchema2 = {
217
+ type: "object",
218
+ additionalProperties: false,
219
+ properties: {
220
+ ignorePaths: { type: "array", items: { type: "string" }, uniqueItems: true }
221
+ }
222
+ };
223
+ function reexportsDefault(node) {
224
+ if (node.source === null) {
225
+ return false;
226
+ }
227
+ return node.specifiers.some(
228
+ (specifier) => specifier.local.type === import_utils2.AST_NODE_TYPES.Identifier && specifier.local.name === "default"
229
+ );
230
+ }
231
+ var indexMustReexportDefaultRule = createRule({
232
+ name: RULE_NAME2,
233
+ meta: {
234
+ type: "problem",
235
+ docs: {
236
+ description: "A component folder's `index.ts` must re-export the component default (`export { default as <Name> } from './<Name>'`)."
237
+ },
238
+ schema: [optionSchema2],
239
+ messages: {
240
+ missingDefaultReexport: "`index.ts` must re-export the {{name}} default: `export { default as {{name}} } from './{{name}}'`."
241
+ }
242
+ },
243
+ defaultOptions: [{ ignorePaths: [] }],
244
+ create(context, [options]) {
245
+ const ignorePaths = options.ignorePaths ?? DEFAULT_IGNORE_PATHS2;
246
+ const filename = context.filename;
247
+ if (getBasename(filename) !== "index.ts" || isIgnoredPath(filename, ignorePaths)) {
248
+ return {};
249
+ }
250
+ const dir = import_node_path3.default.dirname(filename);
251
+ const folderName = import_node_path3.default.basename(dir);
252
+ if (!isPascalCase(folderName) || !siblingExists(dir, `${folderName}.tsx`)) {
253
+ return {};
254
+ }
255
+ let hasDefaultReexport = false;
256
+ return {
257
+ ExportNamedDeclaration(node) {
258
+ if (reexportsDefault(node)) {
259
+ hasDefaultReexport = true;
260
+ }
261
+ },
262
+ "Program:exit"(node) {
263
+ if (!hasDefaultReexport) {
264
+ context.report({
265
+ node,
266
+ messageId: "missingDefaultReexport",
267
+ data: { name: folderName }
268
+ });
269
+ }
270
+ }
271
+ };
272
+ }
273
+ });
274
+
275
+ // src/rules/no-cross-feature-imports.ts
276
+ var import_node_path4 = __toESM(require("path"), 1);
277
+ var import_utils4 = require("@typescript-eslint/utils");
278
+ var RULE_NAME3 = "no-cross-feature-imports";
279
+ var DEFAULT_FEATURE_ROOT = "components";
280
+ var DEFAULT_ALIAS = "@/components";
281
+ var DEFAULT_SHARED_FEATURES = ["ui"];
282
+ var optionSchema3 = {
283
+ type: "object",
284
+ additionalProperties: false,
285
+ properties: {
286
+ featureRoot: { type: "string" },
287
+ alias: { type: "string" },
288
+ sharedFeatures: { type: "array", items: { type: "string" }, uniqueItems: true },
289
+ allowTypeImports: { type: "boolean" }
290
+ }
291
+ };
292
+ function escapeRegExpLiteral2(value) {
293
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
294
+ }
295
+ function aliasFeatureMatcher(alias) {
296
+ return new RegExp(`^${escapeRegExpLiteral2(alias)}/([^/]+)`);
297
+ }
298
+ function resolveTargetFeature(source, currentFile, aliasRe, featureRoot) {
299
+ const aliasMatch = aliasRe.exec(source);
300
+ if (aliasMatch) {
301
+ return aliasMatch[1] ?? null;
302
+ }
303
+ if (source.startsWith(".")) {
304
+ const resolved = import_node_path4.default.resolve(import_node_path4.default.dirname(currentFile), source);
305
+ return getFeatureName(resolved, featureRoot);
306
+ }
307
+ return null;
308
+ }
309
+ var noCrossFeatureImportsRule = createRule({
310
+ name: RULE_NAME3,
311
+ meta: {
312
+ type: "problem",
313
+ docs: {
314
+ description: "A file in one feature may not import runtime code from another feature. Move shared code to a shared module or a shared feature."
315
+ },
316
+ schema: [optionSchema3],
317
+ messages: {
318
+ crossFeatureImport: "Cross-feature import: `{{current}}` may not import runtime code from `{{root}}/{{target}}`. Move shared code to a shared module or a shared feature (e.g. `{{root}}/ui`)."
319
+ }
320
+ },
321
+ defaultOptions: [
322
+ {
323
+ featureRoot: DEFAULT_FEATURE_ROOT,
324
+ alias: DEFAULT_ALIAS,
325
+ sharedFeatures: [...DEFAULT_SHARED_FEATURES],
326
+ allowTypeImports: true
327
+ }
328
+ ],
329
+ create(context, [options]) {
330
+ const featureRoot = options.featureRoot ?? DEFAULT_FEATURE_ROOT;
331
+ const alias = options.alias ?? DEFAULT_ALIAS;
332
+ const sharedFeatures = options.sharedFeatures ?? DEFAULT_SHARED_FEATURES;
333
+ const allowTypeImports = options.allowTypeImports ?? true;
334
+ const aliasRe = aliasFeatureMatcher(alias);
335
+ const current = getFeatureName(context.filename, featureRoot);
336
+ if (current === null) {
337
+ return {};
338
+ }
339
+ function checkSource(sourceNode, typeOnly) {
340
+ if (allowTypeImports && typeOnly) {
341
+ return;
342
+ }
343
+ const source = sourceNode.value;
344
+ if (typeof source !== "string") {
345
+ return;
346
+ }
347
+ const target = resolveTargetFeature(source, context.filename, aliasRe, featureRoot);
348
+ if (target === null || target === current || sharedFeatures.includes(target)) {
349
+ return;
350
+ }
351
+ context.report({
352
+ node: sourceNode,
353
+ messageId: "crossFeatureImport",
354
+ data: { current, target, root: featureRoot }
355
+ });
356
+ }
357
+ return {
358
+ ImportDeclaration(node) {
359
+ if (node.source.type === import_utils4.AST_NODE_TYPES.Literal) {
360
+ checkSource(node.source, node.importKind === "type");
361
+ }
362
+ },
363
+ // Dynamic `import()` is runtime by nature — never type-only.
364
+ ImportExpression(node) {
365
+ if (node.source.type === import_utils4.AST_NODE_TYPES.Literal) {
366
+ checkSource(node.source, false);
367
+ }
368
+ },
369
+ // `export { x } from '…'` re-export laundering.
370
+ ExportNamedDeclaration(node) {
371
+ if (node.source !== null && node.source.type === import_utils4.AST_NODE_TYPES.Literal) {
372
+ checkSource(node.source, node.exportKind === "type");
373
+ }
374
+ },
375
+ // `export * from '…'` re-export laundering.
376
+ ExportAllDeclaration(node) {
377
+ if (node.source.type === import_utils4.AST_NODE_TYPES.Literal) {
378
+ checkSource(node.source, node.exportKind === "type");
379
+ }
380
+ }
381
+ };
382
+ }
383
+ });
384
+
385
+ // src/rules/index.ts
386
+ var rules = {
387
+ "component-folder-structure": componentFolderStructureRule,
388
+ "index-must-reexport-default": indexMustReexportDefaultRule,
389
+ "no-cross-feature-imports": noCrossFeatureImportsRule
390
+ };
391
+
392
+ // src/index.ts
393
+ var NAMESPACE = "noctcore-architecture";
394
+ var VERSION = "0.1.0";
395
+ var plugin = {
396
+ meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
397
+ rules,
398
+ configs: {}
399
+ };
400
+ plugin.configs.recommended = {
401
+ plugins: { [NAMESPACE]: plugin },
402
+ rules: recommended
403
+ };
404
+ var configs = plugin.configs;
405
+ var index_default = plugin;
406
+ // Annotate the CommonJS export names for ESM import in node:
407
+ 0 && (module.exports = {
408
+ configs,
409
+ rules
410
+ });
@@ -0,0 +1,54 @@
1
+ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
2
+
3
+ interface NoCrossFeatureImportsOptions {
4
+ readonly featureRoot?: string;
5
+ readonly alias?: string;
6
+ readonly sharedFeatures?: readonly string[];
7
+ readonly allowTypeImports?: boolean;
8
+ }
9
+
10
+ interface IndexMustReexportDefaultOptions {
11
+ readonly ignorePaths?: readonly string[];
12
+ }
13
+
14
+ interface ComponentFolderStructureOptions {
15
+ readonly componentRoot?: string;
16
+ readonly requiredSiblings?: readonly string[];
17
+ readonly ignorePaths?: readonly string[];
18
+ }
19
+
20
+ /** Every rule this plugin exposes, keyed by its (unprefixed) rule id. */
21
+ declare const rules: {
22
+ 'component-folder-structure': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSiblings", [ComponentFolderStructureOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
23
+ name: string;
24
+ };
25
+ 'index-must-reexport-default': _typescript_eslint_utils_ts_eslint.RuleModule<"missingDefaultReexport", [IndexMustReexportDefaultOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
26
+ name: string;
27
+ };
28
+ 'no-cross-feature-imports': _typescript_eslint_utils_ts_eslint.RuleModule<"crossFeatureImport", [NoCrossFeatureImportsOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
29
+ name: string;
30
+ };
31
+ };
32
+
33
+ declare const plugin: {
34
+ meta: {
35
+ name: string;
36
+ version: string;
37
+ };
38
+ rules: {
39
+ 'component-folder-structure': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSiblings", [ComponentFolderStructureOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
40
+ name: string;
41
+ };
42
+ 'index-must-reexport-default': _typescript_eslint_utils_ts_eslint.RuleModule<"missingDefaultReexport", [IndexMustReexportDefaultOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
43
+ name: string;
44
+ };
45
+ 'no-cross-feature-imports': _typescript_eslint_utils_ts_eslint.RuleModule<"crossFeatureImport", [NoCrossFeatureImportsOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
46
+ name: string;
47
+ };
48
+ };
49
+ configs: Record<string, unknown>;
50
+ };
51
+
52
+ declare const configs: Record<string, unknown>;
53
+
54
+ export { configs, plugin as default, rules };
@@ -0,0 +1,54 @@
1
+ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
2
+
3
+ interface NoCrossFeatureImportsOptions {
4
+ readonly featureRoot?: string;
5
+ readonly alias?: string;
6
+ readonly sharedFeatures?: readonly string[];
7
+ readonly allowTypeImports?: boolean;
8
+ }
9
+
10
+ interface IndexMustReexportDefaultOptions {
11
+ readonly ignorePaths?: readonly string[];
12
+ }
13
+
14
+ interface ComponentFolderStructureOptions {
15
+ readonly componentRoot?: string;
16
+ readonly requiredSiblings?: readonly string[];
17
+ readonly ignorePaths?: readonly string[];
18
+ }
19
+
20
+ /** Every rule this plugin exposes, keyed by its (unprefixed) rule id. */
21
+ declare const rules: {
22
+ 'component-folder-structure': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSiblings", [ComponentFolderStructureOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
23
+ name: string;
24
+ };
25
+ 'index-must-reexport-default': _typescript_eslint_utils_ts_eslint.RuleModule<"missingDefaultReexport", [IndexMustReexportDefaultOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
26
+ name: string;
27
+ };
28
+ 'no-cross-feature-imports': _typescript_eslint_utils_ts_eslint.RuleModule<"crossFeatureImport", [NoCrossFeatureImportsOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
29
+ name: string;
30
+ };
31
+ };
32
+
33
+ declare const plugin: {
34
+ meta: {
35
+ name: string;
36
+ version: string;
37
+ };
38
+ rules: {
39
+ 'component-folder-structure': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSiblings", [ComponentFolderStructureOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
40
+ name: string;
41
+ };
42
+ 'index-must-reexport-default': _typescript_eslint_utils_ts_eslint.RuleModule<"missingDefaultReexport", [IndexMustReexportDefaultOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
43
+ name: string;
44
+ };
45
+ 'no-cross-feature-imports': _typescript_eslint_utils_ts_eslint.RuleModule<"crossFeatureImport", [NoCrossFeatureImportsOptions], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
46
+ name: string;
47
+ };
48
+ };
49
+ configs: Record<string, unknown>;
50
+ };
51
+
52
+ declare const configs: Record<string, unknown>;
53
+
54
+ export { configs, plugin as default, rules };
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ // src/configs/recommended.ts
2
+ var recommended = {
3
+ "noctcore-architecture/component-folder-structure": "error",
4
+ "noctcore-architecture/index-must-reexport-default": "error",
5
+ "noctcore-architecture/no-cross-feature-imports": "error"
6
+ };
7
+
8
+ // src/rules/component-folder-structure.ts
9
+ import path2 from "path";
10
+
11
+ // src/createRule.ts
12
+ import { makeCreateRule } from "@noctcore/eslint-utils";
13
+ var createRule = makeCreateRule("architecture");
14
+
15
+ // src/utils.ts
16
+ import { existsSync, readdirSync } from "fs";
17
+ import path from "path";
18
+ function toPosix(filename) {
19
+ return filename.split(path.sep).join("/").split("\\").join("/");
20
+ }
21
+ function getBasename(filename) {
22
+ return path.basename(filename);
23
+ }
24
+ function isPascalCase(segment) {
25
+ return /^[A-Z][A-Za-z0-9]*$/.test(segment);
26
+ }
27
+ function isComponentFileName(filename) {
28
+ return /^[A-Z][A-Za-z0-9]*\.tsx$/.test(getBasename(filename));
29
+ }
30
+ function getComponentName(filename) {
31
+ return getBasename(filename).replace(/\.tsx$/, "");
32
+ }
33
+ function isComponentEntryFile(filename) {
34
+ if (!isComponentFileName(filename)) {
35
+ return false;
36
+ }
37
+ return getComponentName(filename) === path.basename(path.dirname(filename));
38
+ }
39
+ function segmentsAfterRoot(filename, root) {
40
+ const marker = `/${root}/`;
41
+ const posix = toPosix(filename);
42
+ const idx = posix.lastIndexOf(marker);
43
+ if (idx === -1) {
44
+ return null;
45
+ }
46
+ return posix.slice(idx + marker.length).split("/").filter((segment) => segment.length > 0);
47
+ }
48
+ function getFeatureName(filename, root) {
49
+ const segments = segmentsAfterRoot(filename, root);
50
+ if (segments === null || segments.length === 0) {
51
+ return null;
52
+ }
53
+ const feature = segments[0];
54
+ return feature !== void 0 && feature.length > 0 ? feature : null;
55
+ }
56
+ function escapeRegExpLiteral(value) {
57
+ return value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
58
+ }
59
+ function globToRegExp(glob) {
60
+ let out = "";
61
+ for (let i = 0; i < glob.length; i += 1) {
62
+ const char = glob[i];
63
+ if (char === "*") {
64
+ if (glob[i + 1] === "*") {
65
+ if (glob[i + 2] === "/") {
66
+ out += "(?:[^/]*/)*";
67
+ i += 2;
68
+ } else {
69
+ out += ".*";
70
+ i += 1;
71
+ }
72
+ } else {
73
+ out += "[^/]*";
74
+ }
75
+ } else if (char === "?") {
76
+ out += "[^/]";
77
+ } else if (char !== void 0) {
78
+ out += escapeRegExpLiteral(char);
79
+ }
80
+ }
81
+ return new RegExp(`^${out}$`);
82
+ }
83
+ function isIgnoredPath(filename, ignorePaths) {
84
+ if (ignorePaths.length === 0) {
85
+ return false;
86
+ }
87
+ const posix = toPosix(filename);
88
+ return ignorePaths.some((glob) => globToRegExp(glob).test(posix));
89
+ }
90
+ function siblingExists(dir, sibling) {
91
+ return existsSync(path.join(dir, sibling));
92
+ }
93
+ function readDirSafe(dir) {
94
+ try {
95
+ return new Set(readdirSync(dir));
96
+ } catch {
97
+ return /* @__PURE__ */ new Set();
98
+ }
99
+ }
100
+
101
+ // src/rules/component-folder-structure.ts
102
+ var RULE_NAME = "component-folder-structure";
103
+ var DEFAULT_COMPONENT_ROOT = "components";
104
+ var DEFAULT_IGNORE_PATHS = ["**/ui/**"];
105
+ var DEFAULT_REQUIRED_SIBLINGS = [
106
+ ".hooks.ts",
107
+ ".types.ts",
108
+ ".stories.tsx",
109
+ ".test.tsx",
110
+ "index.ts"
111
+ ];
112
+ function resolveSibling(template, name) {
113
+ return template.startsWith(".") ? `${name}${template}` : template;
114
+ }
115
+ var optionSchema = {
116
+ type: "object",
117
+ additionalProperties: false,
118
+ properties: {
119
+ componentRoot: { type: "string" },
120
+ requiredSiblings: { type: "array", items: { type: "string" }, uniqueItems: true },
121
+ ignorePaths: { type: "array", items: { type: "string" }, uniqueItems: true }
122
+ }
123
+ };
124
+ var componentFolderStructureRule = createRule({
125
+ name: RULE_NAME,
126
+ meta: {
127
+ type: "problem",
128
+ docs: {
129
+ description: "A component `<Name>/<Name>.tsx` under `<componentRoot>/<feature>/...` must have its sibling set (`.hooks.ts`, `.types.ts`, `.stories.tsx`, `.test.tsx`, `index.ts`) present on disk."
130
+ },
131
+ schema: [optionSchema],
132
+ messages: {
133
+ missingSiblings: "Component `{{name}}` is missing sibling file(s): {{missing}}. Every component folder must carry its hooks, types, stories, test, and index barrel."
134
+ }
135
+ },
136
+ defaultOptions: [
137
+ {
138
+ componentRoot: DEFAULT_COMPONENT_ROOT,
139
+ requiredSiblings: [...DEFAULT_REQUIRED_SIBLINGS],
140
+ ignorePaths: [...DEFAULT_IGNORE_PATHS]
141
+ }
142
+ ],
143
+ create(context, [options]) {
144
+ const componentRoot = options.componentRoot ?? DEFAULT_COMPONENT_ROOT;
145
+ const ignorePaths = options.ignorePaths ?? DEFAULT_IGNORE_PATHS;
146
+ const siblingTemplates = options.requiredSiblings ?? DEFAULT_REQUIRED_SIBLINGS;
147
+ const filename = context.filename;
148
+ if (!isComponentEntryFile(filename) || isIgnoredPath(filename, ignorePaths)) {
149
+ return {};
150
+ }
151
+ if (getFeatureName(filename, componentRoot) === null) {
152
+ return {};
153
+ }
154
+ const name = getComponentName(filename);
155
+ const dir = path2.dirname(filename);
156
+ const required = siblingTemplates.map((template) => resolveSibling(template, name));
157
+ const present = readDirSafe(dir);
158
+ const missing = required.filter((sibling) => !present.has(sibling));
159
+ return {
160
+ Program(node) {
161
+ if (missing.length > 0) {
162
+ context.report({
163
+ node,
164
+ messageId: "missingSiblings",
165
+ data: { name, missing: missing.join(", ") }
166
+ });
167
+ }
168
+ }
169
+ };
170
+ }
171
+ });
172
+
173
+ // src/rules/index-must-reexport-default.ts
174
+ import path3 from "path";
175
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
176
+ var RULE_NAME2 = "index-must-reexport-default";
177
+ var DEFAULT_IGNORE_PATHS2 = [];
178
+ var optionSchema2 = {
179
+ type: "object",
180
+ additionalProperties: false,
181
+ properties: {
182
+ ignorePaths: { type: "array", items: { type: "string" }, uniqueItems: true }
183
+ }
184
+ };
185
+ function reexportsDefault(node) {
186
+ if (node.source === null) {
187
+ return false;
188
+ }
189
+ return node.specifiers.some(
190
+ (specifier) => specifier.local.type === AST_NODE_TYPES.Identifier && specifier.local.name === "default"
191
+ );
192
+ }
193
+ var indexMustReexportDefaultRule = createRule({
194
+ name: RULE_NAME2,
195
+ meta: {
196
+ type: "problem",
197
+ docs: {
198
+ description: "A component folder's `index.ts` must re-export the component default (`export { default as <Name> } from './<Name>'`)."
199
+ },
200
+ schema: [optionSchema2],
201
+ messages: {
202
+ missingDefaultReexport: "`index.ts` must re-export the {{name}} default: `export { default as {{name}} } from './{{name}}'`."
203
+ }
204
+ },
205
+ defaultOptions: [{ ignorePaths: [] }],
206
+ create(context, [options]) {
207
+ const ignorePaths = options.ignorePaths ?? DEFAULT_IGNORE_PATHS2;
208
+ const filename = context.filename;
209
+ if (getBasename(filename) !== "index.ts" || isIgnoredPath(filename, ignorePaths)) {
210
+ return {};
211
+ }
212
+ const dir = path3.dirname(filename);
213
+ const folderName = path3.basename(dir);
214
+ if (!isPascalCase(folderName) || !siblingExists(dir, `${folderName}.tsx`)) {
215
+ return {};
216
+ }
217
+ let hasDefaultReexport = false;
218
+ return {
219
+ ExportNamedDeclaration(node) {
220
+ if (reexportsDefault(node)) {
221
+ hasDefaultReexport = true;
222
+ }
223
+ },
224
+ "Program:exit"(node) {
225
+ if (!hasDefaultReexport) {
226
+ context.report({
227
+ node,
228
+ messageId: "missingDefaultReexport",
229
+ data: { name: folderName }
230
+ });
231
+ }
232
+ }
233
+ };
234
+ }
235
+ });
236
+
237
+ // src/rules/no-cross-feature-imports.ts
238
+ import path4 from "path";
239
+ import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
240
+ var RULE_NAME3 = "no-cross-feature-imports";
241
+ var DEFAULT_FEATURE_ROOT = "components";
242
+ var DEFAULT_ALIAS = "@/components";
243
+ var DEFAULT_SHARED_FEATURES = ["ui"];
244
+ var optionSchema3 = {
245
+ type: "object",
246
+ additionalProperties: false,
247
+ properties: {
248
+ featureRoot: { type: "string" },
249
+ alias: { type: "string" },
250
+ sharedFeatures: { type: "array", items: { type: "string" }, uniqueItems: true },
251
+ allowTypeImports: { type: "boolean" }
252
+ }
253
+ };
254
+ function escapeRegExpLiteral2(value) {
255
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
256
+ }
257
+ function aliasFeatureMatcher(alias) {
258
+ return new RegExp(`^${escapeRegExpLiteral2(alias)}/([^/]+)`);
259
+ }
260
+ function resolveTargetFeature(source, currentFile, aliasRe, featureRoot) {
261
+ const aliasMatch = aliasRe.exec(source);
262
+ if (aliasMatch) {
263
+ return aliasMatch[1] ?? null;
264
+ }
265
+ if (source.startsWith(".")) {
266
+ const resolved = path4.resolve(path4.dirname(currentFile), source);
267
+ return getFeatureName(resolved, featureRoot);
268
+ }
269
+ return null;
270
+ }
271
+ var noCrossFeatureImportsRule = createRule({
272
+ name: RULE_NAME3,
273
+ meta: {
274
+ type: "problem",
275
+ docs: {
276
+ description: "A file in one feature may not import runtime code from another feature. Move shared code to a shared module or a shared feature."
277
+ },
278
+ schema: [optionSchema3],
279
+ messages: {
280
+ crossFeatureImport: "Cross-feature import: `{{current}}` may not import runtime code from `{{root}}/{{target}}`. Move shared code to a shared module or a shared feature (e.g. `{{root}}/ui`)."
281
+ }
282
+ },
283
+ defaultOptions: [
284
+ {
285
+ featureRoot: DEFAULT_FEATURE_ROOT,
286
+ alias: DEFAULT_ALIAS,
287
+ sharedFeatures: [...DEFAULT_SHARED_FEATURES],
288
+ allowTypeImports: true
289
+ }
290
+ ],
291
+ create(context, [options]) {
292
+ const featureRoot = options.featureRoot ?? DEFAULT_FEATURE_ROOT;
293
+ const alias = options.alias ?? DEFAULT_ALIAS;
294
+ const sharedFeatures = options.sharedFeatures ?? DEFAULT_SHARED_FEATURES;
295
+ const allowTypeImports = options.allowTypeImports ?? true;
296
+ const aliasRe = aliasFeatureMatcher(alias);
297
+ const current = getFeatureName(context.filename, featureRoot);
298
+ if (current === null) {
299
+ return {};
300
+ }
301
+ function checkSource(sourceNode, typeOnly) {
302
+ if (allowTypeImports && typeOnly) {
303
+ return;
304
+ }
305
+ const source = sourceNode.value;
306
+ if (typeof source !== "string") {
307
+ return;
308
+ }
309
+ const target = resolveTargetFeature(source, context.filename, aliasRe, featureRoot);
310
+ if (target === null || target === current || sharedFeatures.includes(target)) {
311
+ return;
312
+ }
313
+ context.report({
314
+ node: sourceNode,
315
+ messageId: "crossFeatureImport",
316
+ data: { current, target, root: featureRoot }
317
+ });
318
+ }
319
+ return {
320
+ ImportDeclaration(node) {
321
+ if (node.source.type === AST_NODE_TYPES2.Literal) {
322
+ checkSource(node.source, node.importKind === "type");
323
+ }
324
+ },
325
+ // Dynamic `import()` is runtime by nature — never type-only.
326
+ ImportExpression(node) {
327
+ if (node.source.type === AST_NODE_TYPES2.Literal) {
328
+ checkSource(node.source, false);
329
+ }
330
+ },
331
+ // `export { x } from '…'` re-export laundering.
332
+ ExportNamedDeclaration(node) {
333
+ if (node.source !== null && node.source.type === AST_NODE_TYPES2.Literal) {
334
+ checkSource(node.source, node.exportKind === "type");
335
+ }
336
+ },
337
+ // `export * from '…'` re-export laundering.
338
+ ExportAllDeclaration(node) {
339
+ if (node.source.type === AST_NODE_TYPES2.Literal) {
340
+ checkSource(node.source, node.exportKind === "type");
341
+ }
342
+ }
343
+ };
344
+ }
345
+ });
346
+
347
+ // src/rules/index.ts
348
+ var rules = {
349
+ "component-folder-structure": componentFolderStructureRule,
350
+ "index-must-reexport-default": indexMustReexportDefaultRule,
351
+ "no-cross-feature-imports": noCrossFeatureImportsRule
352
+ };
353
+
354
+ // src/index.ts
355
+ var NAMESPACE = "noctcore-architecture";
356
+ var VERSION = "0.1.0";
357
+ var plugin = {
358
+ meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
359
+ rules,
360
+ configs: {}
361
+ };
362
+ plugin.configs.recommended = {
363
+ plugins: { [NAMESPACE]: plugin },
364
+ rules: recommended
365
+ };
366
+ var configs = plugin.configs;
367
+ var index_default = plugin;
368
+ export {
369
+ configs,
370
+ index_default as default,
371
+ rules
372
+ };
@@ -0,0 +1,50 @@
1
+ # `noctcore-architecture/component-folder-structure`
2
+
3
+ > A component entry file must ship its full sibling set (hooks, types, story, test, barrel) on disk.
4
+
5
+ ## Why
6
+
7
+ In a folder-per-component layout, a component is a folder — not a lone `.tsx`. When the logic
8
+ (`.hooks.ts`), the types (`.types.ts`), the story (`.stories.tsx`), the test (`.test.tsx`), and the
9
+ `index.ts` barrel always travel with the component, refactors stay local and nothing is quietly
10
+ untested or undocumented. This rule enforces that colocation by construction.
11
+
12
+ ## What it flags
13
+
14
+ For every **component entry file** — a PascalCase `.tsx` whose basename equals its parent folder
15
+ (`TaskCard/TaskCard.tsx`) — that lives under the configured `componentRoot`, the rule reads the
16
+ component's directory and reports any sibling from the required set that is missing on disk.
17
+
18
+ ```
19
+ components/board/TaskCard/
20
+ TaskCard.tsx ← entry file (checked)
21
+ TaskCard.hooks.ts ┐
22
+ TaskCard.types.ts │ required siblings
23
+ TaskCard.stories.tsx │ (missing → reported)
24
+ TaskCard.test.tsx │
25
+ index.ts ┘
26
+ ```
27
+
28
+ Files that are not entry files (`task-card.tsx`, `Group/Widget.tsx`), files outside the
29
+ `componentRoot`, and files matched by `ignorePaths` are never checked.
30
+
31
+ ## Options
32
+
33
+ | Option | Type | Default | Meaning |
34
+ | --- | --- | --- | --- |
35
+ | `componentRoot` | `string` | `'components'` | The directory segment the layout is anchored on. |
36
+ | `requiredSiblings` | `string[]` | `['.hooks.ts', '.types.ts', '.stories.tsx', '.test.tsx', 'index.ts']` | The sibling set. An entry starting with `.` is a name-relative suffix (`.hooks.ts` → `<Name>.hooks.ts`); any other entry is a literal filename (`index.ts`). |
37
+ | `ignorePaths` | `string[]` | `['**/ui/**']` | Globs (supporting `**`, `*`, `?`) of paths to skip. |
38
+
39
+ ```js
40
+ 'noctcore-architecture/component-folder-structure': ['error', {
41
+ componentRoot: 'components',
42
+ requiredSiblings: ['.hooks.ts', '.types.ts', '.stories.tsx', '.test.tsx', 'index.ts'],
43
+ ignorePaths: ['**/ui/**'],
44
+ }]
45
+ ```
46
+
47
+ ## When not to use it
48
+
49
+ If your components are single files rather than folders, or you do not colocate stories/tests with
50
+ components, disable this rule or trim `requiredSiblings` to just the pieces you do colocate.
@@ -0,0 +1,44 @@
1
+ # `noctcore-architecture/index-must-reexport-default`
2
+
3
+ > A component folder's `index.ts` must re-export the sibling default named after the folder.
4
+
5
+ ## Why
6
+
7
+ When a component folder's `index.ts` re-exports the component default, consumers import the folder
8
+ (`import Card from '@/components/Card'`) rather than reaching for the inner file
9
+ (`.../Card/Card`). The barrel becomes the folder's single public entry point, and the internal file
10
+ layout stays free to change.
11
+
12
+ ## What it flags
13
+
14
+ The rule only activates for an `index.ts` that sits next to a `<Folder>.tsx` of the same PascalCase
15
+ name on disk (`Card/Card.tsx` beside `Card/index.ts`). For those barrels, it reports when the file
16
+ never re-exports the sibling's default export.
17
+
18
+ ```ts
19
+ // Card/index.ts
20
+
21
+ export { default as Card } from './Card'; // ✓
22
+ export { default } from './Card'; // ✓
23
+ export * from './Card.types'; // ✗ (on its own — no default re-export)
24
+ ```
25
+
26
+ Non-component `index.ts` files — those whose folder is not PascalCase, or that have no
27
+ `<Folder>.tsx` sibling on disk — are left untouched.
28
+
29
+ ## Options
30
+
31
+ | Option | Type | Default | Meaning |
32
+ | --- | --- | --- | --- |
33
+ | `ignorePaths` | `string[]` | `[]` | Globs (supporting `**`, `*`, `?`) of paths to skip. |
34
+
35
+ ```js
36
+ 'noctcore-architecture/index-must-reexport-default': ['error', {
37
+ ignorePaths: ['**/ui/**'],
38
+ }]
39
+ ```
40
+
41
+ ## When not to use it
42
+
43
+ If you do not use barrel files, or you export components as named (not default) exports, this rule
44
+ does not apply.
@@ -0,0 +1,57 @@
1
+ # `noctcore-architecture/no-cross-feature-imports`
2
+
3
+ > A file in one feature may not import runtime code from another feature.
4
+
5
+ ## Why
6
+
7
+ Features are meant to be decoupled: a change inside feature `board` should never be able to ripple
8
+ into feature `projects`. When one feature reaches directly into another's internals, that boundary
9
+ erodes and the two become one tangled unit. Shared code belongs outside the feature root (a `lib`,
10
+ `hooks`, or similar module) or in a designated **shared feature** (default `ui`) that everything is
11
+ allowed to import.
12
+
13
+ ## What it flags
14
+
15
+ For a file that lives under `<featureRoot>/<feature>/…`, the rule reports any import whose target
16
+ resolves to a **different** feature under the same root, on every source-carrying construct:
17
+
18
+ - static `import … from '…'`
19
+ - dynamic `import('…')`
20
+ - `export … from '…'` and `export * from '…'` re-export laundering
21
+
22
+ Both the alias form (`<alias>/<feature>/…`) and relative paths that climb into another feature
23
+ (`../../projects/…`) are detected. Type-only imports are allowed by default (flip `allowTypeImports`
24
+ to forbid them). Imports of the current feature, of a shared feature, or of non-feature modules are
25
+ always fine.
26
+
27
+ ```tsx
28
+ // in components/board/Board/Board.tsx
29
+
30
+ import { Button } from '@/components/ui/Button'; // ✓ shared feature
31
+ import { cn } from '@/lib/utils'; // ✓ non-feature module
32
+ import { TaskCard } from '../TaskCard/TaskCard'; // ✓ same feature
33
+ import { ProjectCard } from '@/components/projects/…'; // ✗ cross-feature
34
+ ```
35
+
36
+ ## Options
37
+
38
+ | Option | Type | Default | Meaning |
39
+ | --- | --- | --- | --- |
40
+ | `featureRoot` | `string` | `'components'` | The directory segment whose immediate children are features. |
41
+ | `alias` | `string` | `'@/components'` | The import-alias prefix that maps onto `featureRoot`. |
42
+ | `sharedFeatures` | `string[]` | `['ui']` | Features every other feature is allowed to import. |
43
+ | `allowTypeImports` | `boolean` | `true` | When `true`, `import type` / `export type` cross-feature imports are permitted. |
44
+
45
+ ```js
46
+ 'noctcore-architecture/no-cross-feature-imports': ['error', {
47
+ featureRoot: 'components',
48
+ alias: '@/components',
49
+ sharedFeatures: ['ui'],
50
+ allowTypeImports: true,
51
+ }]
52
+ ```
53
+
54
+ ## When not to use it
55
+
56
+ If your app is not organized as sibling features under a single root, or you allow features to
57
+ depend on each other freely, this rule does not apply.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@noctcore/eslint-plugin-architecture",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic folder-per-component and feature-boundary architecture ESLint rules.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "docs",
21
+ "README.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "keywords": [
25
+ "eslint",
26
+ "eslintplugin",
27
+ "eslint-plugin",
28
+ "architecture",
29
+ "noctcore"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/noctcore/eslint-plugins.git",
38
+ "directory": "packages/eslint-plugin-architecture"
39
+ },
40
+ "homepage": "https://github.com/noctcore/eslint-plugins/tree/main/packages/eslint-plugin-architecture",
41
+ "bugs": "https://github.com/noctcore/eslint-plugins/issues",
42
+ "scripts": {
43
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@noctcore/eslint-utils": "^0.1.0",
49
+ "@typescript-eslint/utils": "^8.61.1"
50
+ },
51
+ "peerDependencies": {
52
+ "eslint": ">=9.0.0",
53
+ "typescript": ">=5.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@noctcore/eslint-test-utils": "workspace:*",
57
+ "@types/node": "^22.0.0",
58
+ "@typescript-eslint/parser": "^8.61.1",
59
+ "@typescript-eslint/rule-tester": "^8.61.1",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.6.0",
62
+ "vitest": "^3"
63
+ }
64
+ }