@noctcore/eslint-plugin-architecture 0.1.0 โ 0.2.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 +8 -2
- package/dist/index.cjs +444 -28
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +444 -28
- package/docs/rules/barrel-purity.md +57 -0
- package/docs/rules/colocated-test-required.md +48 -0
- package/docs/rules/filename-matches-export.md +56 -0
- package/docs/rules/max-import-depth.md +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,13 +39,19 @@ export default [
|
|
|
39
39
|
|
|
40
40
|
Every rule anchors on a configurable directory segment (default `components`) rather than an absolute
|
|
41
41
|
path, so it behaves the same whether ESLint runs from the repo root or per-package, on POSIX or
|
|
42
|
-
Windows.
|
|
43
|
-
against real file paths, not virtual sources.
|
|
42
|
+
Windows. Several rules inspect files on disk (sibling sets, barrel siblings, colocated tests), so run
|
|
43
|
+
ESLint against real file paths, not virtual sources.
|
|
44
44
|
|
|
45
45
|
## Rules
|
|
46
46
|
|
|
47
|
+
๐ง = autofixable ยท ๐ก = provides an editor suggestion.
|
|
48
|
+
|
|
47
49
|
| Rule | Description | ๐ง |
|
|
48
50
|
| --- | --- | --- |
|
|
51
|
+
| [`barrel-purity`](./docs/rules/barrel-purity.md) | A barrel (`index.ts` / `index.tsx`) must contain only re-exports โ no local declarations, side effects, or default-exported values. | |
|
|
52
|
+
| [`colocated-test-required`](./docs/rules/colocated-test-required.md) | A source file matching an `include` glob must have a colocated `*.test.*` / `*.spec.*` sibling on disk. Off until configured. | |
|
|
49
53
|
| [`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. | |
|
|
54
|
+
| [`filename-matches-export`](./docs/rules/filename-matches-export.md) | A file's basename must match its primary export (default export, or the sole named export). | ๐ก |
|
|
50
55
|
| [`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. | |
|
|
56
|
+
| [`max-import-depth`](./docs/rules/max-import-depth.md) | A relative import may not climb more than `max` parent levels (default 3); autofixed to a path alias when one is configured. | ๐ง |
|
|
51
57
|
| [`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
CHANGED
|
@@ -38,13 +38,21 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
38
|
|
|
39
39
|
// src/configs/recommended.ts
|
|
40
40
|
var recommended = {
|
|
41
|
+
"noctcore-architecture/barrel-purity": "error",
|
|
42
|
+
// Ships OFF: this rule does nothing until you name the source globs that must
|
|
43
|
+
// be tested (there is no universal "everything needs a test" default). Enable
|
|
44
|
+
// it with your own `include`, e.g.
|
|
45
|
+
// 'noctcore-architecture/colocated-test-required': ['error', { include: ['**/use*.ts', '**/*.service.ts'] }]
|
|
46
|
+
"noctcore-architecture/colocated-test-required": "off",
|
|
41
47
|
"noctcore-architecture/component-folder-structure": "error",
|
|
48
|
+
"noctcore-architecture/filename-matches-export": "error",
|
|
42
49
|
"noctcore-architecture/index-must-reexport-default": "error",
|
|
50
|
+
"noctcore-architecture/max-import-depth": "error",
|
|
43
51
|
"noctcore-architecture/no-cross-feature-imports": "error"
|
|
44
52
|
};
|
|
45
53
|
|
|
46
|
-
// src/rules/
|
|
47
|
-
var
|
|
54
|
+
// src/rules/barrel-purity.ts
|
|
55
|
+
var import_utils = require("@typescript-eslint/utils");
|
|
48
56
|
|
|
49
57
|
// src/createRule.ts
|
|
50
58
|
var import_eslint_utils = require("@noctcore/eslint-utils");
|
|
@@ -136,8 +144,154 @@ function readDirSafe(dir) {
|
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
// src/rules/barrel-purity.ts
|
|
148
|
+
var RULE_NAME = "barrel-purity";
|
|
149
|
+
var DEFAULT_ALLOW = [];
|
|
150
|
+
var BARREL_BASENAME = /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
151
|
+
var optionSchema = {
|
|
152
|
+
type: "object",
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
allow: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function impurityOf(stmt) {
|
|
159
|
+
switch (stmt.type) {
|
|
160
|
+
case import_utils.AST_NODE_TYPES.ImportDeclaration:
|
|
161
|
+
return stmt.specifiers.length === 0 ? "a side-effect import" : null;
|
|
162
|
+
case import_utils.AST_NODE_TYPES.ExportAllDeclaration:
|
|
163
|
+
return null;
|
|
164
|
+
case import_utils.AST_NODE_TYPES.ExportNamedDeclaration:
|
|
165
|
+
if (stmt.source !== null) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (stmt.declaration === null) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return "a local declaration";
|
|
172
|
+
case import_utils.AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
173
|
+
return stmt.declaration.type === import_utils.AST_NODE_TYPES.Identifier ? null : "a default-exported value";
|
|
174
|
+
case import_utils.AST_NODE_TYPES.ExpressionStatement:
|
|
175
|
+
return "a side-effect statement";
|
|
176
|
+
default:
|
|
177
|
+
return "non-re-export code";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var barrelPurityRule = createRule({
|
|
181
|
+
name: RULE_NAME,
|
|
182
|
+
meta: {
|
|
183
|
+
type: "problem",
|
|
184
|
+
docs: {
|
|
185
|
+
description: "A barrel (`index.ts` / `index.tsx`) must contain only re-exports \u2014 never local declarations, side effects, or default-exported values."
|
|
186
|
+
},
|
|
187
|
+
schema: [optionSchema],
|
|
188
|
+
messages: {
|
|
189
|
+
impureBarrel: "A barrel must contain only re-exports; found {{kind}}. Move it into a sibling module and re-export it from here."
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
defaultOptions: [{ allow: [] }],
|
|
193
|
+
create(context, [options]) {
|
|
194
|
+
const allow = options.allow ?? DEFAULT_ALLOW;
|
|
195
|
+
const filename = context.filename;
|
|
196
|
+
if (!BARREL_BASENAME.test(getBasename(filename)) || isIgnoredPath(filename, allow)) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
Program(node) {
|
|
201
|
+
for (const stmt of node.body) {
|
|
202
|
+
const kind = impurityOf(stmt);
|
|
203
|
+
if (kind !== null) {
|
|
204
|
+
context.report({ node: stmt, messageId: "impureBarrel", data: { kind } });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/rules/colocated-test-required.ts
|
|
213
|
+
var import_node_fs2 = require("fs");
|
|
214
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
215
|
+
var RULE_NAME2 = "colocated-test-required";
|
|
216
|
+
var DEFAULT_INCLUDE = [];
|
|
217
|
+
var DEFAULT_IGNORE = [];
|
|
218
|
+
var TEST_SIBLING = /\.(test|spec)\.[^.]+$/;
|
|
219
|
+
var optionSchema2 = {
|
|
220
|
+
type: "object",
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
properties: {
|
|
223
|
+
include: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
224
|
+
ignore: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var dirCache = /* @__PURE__ */ new Map();
|
|
228
|
+
function readDirCached(dir) {
|
|
229
|
+
const cached = dirCache.get(dir);
|
|
230
|
+
if (cached !== void 0) {
|
|
231
|
+
return cached;
|
|
232
|
+
}
|
|
233
|
+
let entries;
|
|
234
|
+
try {
|
|
235
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
236
|
+
} catch {
|
|
237
|
+
entries = [];
|
|
238
|
+
}
|
|
239
|
+
dirCache.set(dir, entries);
|
|
240
|
+
return entries;
|
|
241
|
+
}
|
|
242
|
+
function stemOf(basename) {
|
|
243
|
+
const ext = import_node_path2.default.extname(basename);
|
|
244
|
+
return ext === "" ? basename : basename.slice(0, -ext.length);
|
|
245
|
+
}
|
|
246
|
+
function hasColocatedTest(dir, stem) {
|
|
247
|
+
const prefix = `${stem}.`;
|
|
248
|
+
return readDirCached(dir).some(
|
|
249
|
+
(entry) => entry.startsWith(prefix) && TEST_SIBLING.test(entry)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
var colocatedTestRequiredRule = createRule({
|
|
253
|
+
name: RULE_NAME2,
|
|
254
|
+
meta: {
|
|
255
|
+
type: "problem",
|
|
256
|
+
docs: {
|
|
257
|
+
description: "A source file matching an `include` glob must have a colocated `*.test.*` / `*.spec.*` sibling on disk. Off until `include` is configured."
|
|
258
|
+
},
|
|
259
|
+
schema: [optionSchema2],
|
|
260
|
+
messages: {
|
|
261
|
+
missingTest: "Source file `{{basename}}` has no colocated test. Add a sibling `{{stem}}.test.*` (or `.spec.*`) next to it."
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
defaultOptions: [{ include: [], ignore: [] }],
|
|
265
|
+
create(context, [options]) {
|
|
266
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
267
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
268
|
+
const filename = context.filename;
|
|
269
|
+
if (include.length === 0 || !isIgnoredPath(filename, include)) {
|
|
270
|
+
return {};
|
|
271
|
+
}
|
|
272
|
+
const basename = getBasename(filename);
|
|
273
|
+
if (TEST_SIBLING.test(basename) || isIgnoredPath(filename, ignore)) {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
const stem = stemOf(basename);
|
|
277
|
+
const dir = import_node_path2.default.dirname(filename);
|
|
278
|
+
return {
|
|
279
|
+
Program(node) {
|
|
280
|
+
if (!hasColocatedTest(dir, stem)) {
|
|
281
|
+
context.report({
|
|
282
|
+
node,
|
|
283
|
+
messageId: "missingTest",
|
|
284
|
+
data: { basename, stem }
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
139
292
|
// src/rules/component-folder-structure.ts
|
|
140
|
-
var
|
|
293
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
294
|
+
var RULE_NAME3 = "component-folder-structure";
|
|
141
295
|
var DEFAULT_COMPONENT_ROOT = "components";
|
|
142
296
|
var DEFAULT_IGNORE_PATHS = ["**/ui/**"];
|
|
143
297
|
var DEFAULT_REQUIRED_SIBLINGS = [
|
|
@@ -150,7 +304,7 @@ var DEFAULT_REQUIRED_SIBLINGS = [
|
|
|
150
304
|
function resolveSibling(template, name) {
|
|
151
305
|
return template.startsWith(".") ? `${name}${template}` : template;
|
|
152
306
|
}
|
|
153
|
-
var
|
|
307
|
+
var optionSchema3 = {
|
|
154
308
|
type: "object",
|
|
155
309
|
additionalProperties: false,
|
|
156
310
|
properties: {
|
|
@@ -160,13 +314,13 @@ var optionSchema = {
|
|
|
160
314
|
}
|
|
161
315
|
};
|
|
162
316
|
var componentFolderStructureRule = createRule({
|
|
163
|
-
name:
|
|
317
|
+
name: RULE_NAME3,
|
|
164
318
|
meta: {
|
|
165
319
|
type: "problem",
|
|
166
320
|
docs: {
|
|
167
321
|
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
322
|
},
|
|
169
|
-
schema: [
|
|
323
|
+
schema: [optionSchema3],
|
|
170
324
|
messages: {
|
|
171
325
|
missingSiblings: "Component `{{name}}` is missing sibling file(s): {{missing}}. Every component folder must carry its hooks, types, stories, test, and index barrel."
|
|
172
326
|
}
|
|
@@ -190,7 +344,7 @@ var componentFolderStructureRule = createRule({
|
|
|
190
344
|
return {};
|
|
191
345
|
}
|
|
192
346
|
const name = getComponentName(filename);
|
|
193
|
-
const dir =
|
|
347
|
+
const dir = import_node_path3.default.dirname(filename);
|
|
194
348
|
const required = siblingTemplates.map((template) => resolveSibling(template, name));
|
|
195
349
|
const present = readDirSafe(dir);
|
|
196
350
|
const missing = required.filter((sibling) => !present.has(sibling));
|
|
@@ -208,12 +362,164 @@ var componentFolderStructureRule = createRule({
|
|
|
208
362
|
}
|
|
209
363
|
});
|
|
210
364
|
|
|
365
|
+
// src/rules/filename-matches-export.ts
|
|
366
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
367
|
+
var import_utils5 = require("@typescript-eslint/utils");
|
|
368
|
+
var RULE_NAME4 = "filename-matches-export";
|
|
369
|
+
var DEFAULT_IGNORE2 = [];
|
|
370
|
+
var VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
371
|
+
var optionSchema4 = {
|
|
372
|
+
type: "object",
|
|
373
|
+
additionalProperties: false,
|
|
374
|
+
properties: {
|
|
375
|
+
ignore: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function stemOf2(filename) {
|
|
379
|
+
const basename = getBasename(filename);
|
|
380
|
+
const ext = import_node_path4.default.extname(basename);
|
|
381
|
+
return ext === "" ? basename : basename.slice(0, -ext.length);
|
|
382
|
+
}
|
|
383
|
+
function normalize(value) {
|
|
384
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
385
|
+
}
|
|
386
|
+
function namedExportId(decl) {
|
|
387
|
+
if (decl.declaration === null) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const d = decl.declaration;
|
|
391
|
+
switch (d.type) {
|
|
392
|
+
case import_utils5.AST_NODE_TYPES.FunctionDeclaration:
|
|
393
|
+
case import_utils5.AST_NODE_TYPES.ClassDeclaration:
|
|
394
|
+
return d.id;
|
|
395
|
+
case import_utils5.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
396
|
+
case import_utils5.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
397
|
+
case import_utils5.AST_NODE_TYPES.TSEnumDeclaration:
|
|
398
|
+
return d.id;
|
|
399
|
+
case import_utils5.AST_NODE_TYPES.VariableDeclaration: {
|
|
400
|
+
const only = d.declarations.length === 1 ? d.declarations[0] : void 0;
|
|
401
|
+
return only !== void 0 && only.id.type === import_utils5.AST_NODE_TYPES.Identifier ? only.id : null;
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function defaultExportId(decl) {
|
|
408
|
+
const d = decl.declaration;
|
|
409
|
+
if ((d.type === import_utils5.AST_NODE_TYPES.FunctionDeclaration || d.type === import_utils5.AST_NODE_TYPES.ClassDeclaration) && d.id !== null) {
|
|
410
|
+
return d.id;
|
|
411
|
+
}
|
|
412
|
+
return d.type === import_utils5.AST_NODE_TYPES.Identifier ? d : null;
|
|
413
|
+
}
|
|
414
|
+
function resolvePrimary(body) {
|
|
415
|
+
let hasDefault = false;
|
|
416
|
+
let defaultId = null;
|
|
417
|
+
const named = [];
|
|
418
|
+
for (const stmt of body) {
|
|
419
|
+
if (stmt.type === import_utils5.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
420
|
+
hasDefault = true;
|
|
421
|
+
defaultId = defaultExportId(stmt);
|
|
422
|
+
} else if (stmt.type === import_utils5.AST_NODE_TYPES.ExportNamedDeclaration && stmt.source === null) {
|
|
423
|
+
const id = namedExportId(stmt);
|
|
424
|
+
if (id !== null) {
|
|
425
|
+
named.push(id);
|
|
426
|
+
} else if (stmt.declaration === null) {
|
|
427
|
+
for (const spec of stmt.specifiers) {
|
|
428
|
+
if (spec.exported.type === import_utils5.AST_NODE_TYPES.Identifier) {
|
|
429
|
+
named.push(spec.exported);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (hasDefault) {
|
|
436
|
+
return defaultId === null ? null : { name: defaultId.name, node: defaultId };
|
|
437
|
+
}
|
|
438
|
+
const only = named.length === 1 ? named[0] : void 0;
|
|
439
|
+
return only !== void 0 ? { name: only.name, node: only } : null;
|
|
440
|
+
}
|
|
441
|
+
function moduleVariable(sourceCode, name) {
|
|
442
|
+
const globalScope = sourceCode.scopeManager?.globalScope ?? null;
|
|
443
|
+
if (globalScope === null) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const moduleScope = globalScope.childScopes[0] ?? globalScope;
|
|
447
|
+
return moduleScope.variables.find((v) => v.name === name) ?? null;
|
|
448
|
+
}
|
|
449
|
+
var IMPORT_DEF_NODES = /* @__PURE__ */ new Set([
|
|
450
|
+
import_utils5.AST_NODE_TYPES.ImportSpecifier,
|
|
451
|
+
import_utils5.AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
452
|
+
import_utils5.AST_NODE_TYPES.ImportNamespaceSpecifier
|
|
453
|
+
]);
|
|
454
|
+
function isImportBinding(variable) {
|
|
455
|
+
return variable.defs.some((def) => IMPORT_DEF_NODES.has(def.node.type));
|
|
456
|
+
}
|
|
457
|
+
var filenameMatchesExportRule = createRule({
|
|
458
|
+
name: RULE_NAME4,
|
|
459
|
+
meta: {
|
|
460
|
+
type: "suggestion",
|
|
461
|
+
hasSuggestions: true,
|
|
462
|
+
docs: {
|
|
463
|
+
description: "A file's basename must match its primary export (a default export, or the sole named export)."
|
|
464
|
+
},
|
|
465
|
+
schema: [optionSchema4],
|
|
466
|
+
messages: {
|
|
467
|
+
filenameMismatch: "File `{{basename}}` exports `{{name}}` as its primary export \u2014 the basename should match it (rename the file to `{{expected}}`, or the export).",
|
|
468
|
+
renameExport: "Rename the export to `{{expected}}` to match the filename."
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
defaultOptions: [{ ignore: [] }],
|
|
472
|
+
create(context, [options]) {
|
|
473
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE2;
|
|
474
|
+
const filename = context.filename;
|
|
475
|
+
const stem = stemOf2(filename);
|
|
476
|
+
if (stem === "index" || isIgnoredPath(filename, ignore)) {
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
Program(node) {
|
|
481
|
+
const primary = resolvePrimary(node.body);
|
|
482
|
+
if (primary === null || normalize(primary.name) === normalize(stem)) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const basename = getBasename(filename);
|
|
486
|
+
const canRename = VALID_IDENTIFIER.test(stem) && stem !== primary.name;
|
|
487
|
+
context.report({
|
|
488
|
+
node: primary.node,
|
|
489
|
+
messageId: "filenameMismatch",
|
|
490
|
+
data: { basename, name: primary.name, expected: stem },
|
|
491
|
+
suggest: canRename ? [
|
|
492
|
+
{
|
|
493
|
+
messageId: "renameExport",
|
|
494
|
+
data: { expected: stem },
|
|
495
|
+
fix: (fixer) => {
|
|
496
|
+
const variable = moduleVariable(context.sourceCode, primary.name);
|
|
497
|
+
if (variable !== null && !isImportBinding(variable)) {
|
|
498
|
+
const targets = /* @__PURE__ */ new Map();
|
|
499
|
+
for (const id of variable.identifiers) {
|
|
500
|
+
targets.set(id.range[0], id);
|
|
501
|
+
}
|
|
502
|
+
for (const ref of variable.references) {
|
|
503
|
+
targets.set(ref.identifier.range[0], ref.identifier);
|
|
504
|
+
}
|
|
505
|
+
return [...targets.values()].map((id) => fixer.replaceText(id, stem));
|
|
506
|
+
}
|
|
507
|
+
return [fixer.replaceText(primary.node, stem)];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
] : void 0
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
211
517
|
// src/rules/index-must-reexport-default.ts
|
|
212
|
-
var
|
|
213
|
-
var
|
|
214
|
-
var
|
|
518
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
519
|
+
var import_utils7 = require("@typescript-eslint/utils");
|
|
520
|
+
var RULE_NAME5 = "index-must-reexport-default";
|
|
215
521
|
var DEFAULT_IGNORE_PATHS2 = [];
|
|
216
|
-
var
|
|
522
|
+
var optionSchema5 = {
|
|
217
523
|
type: "object",
|
|
218
524
|
additionalProperties: false,
|
|
219
525
|
properties: {
|
|
@@ -225,17 +531,17 @@ function reexportsDefault(node) {
|
|
|
225
531
|
return false;
|
|
226
532
|
}
|
|
227
533
|
return node.specifiers.some(
|
|
228
|
-
(specifier) => specifier.local.type ===
|
|
534
|
+
(specifier) => specifier.local.type === import_utils7.AST_NODE_TYPES.Identifier && specifier.local.name === "default"
|
|
229
535
|
);
|
|
230
536
|
}
|
|
231
537
|
var indexMustReexportDefaultRule = createRule({
|
|
232
|
-
name:
|
|
538
|
+
name: RULE_NAME5,
|
|
233
539
|
meta: {
|
|
234
540
|
type: "problem",
|
|
235
541
|
docs: {
|
|
236
542
|
description: "A component folder's `index.ts` must re-export the component default (`export { default as <Name> } from './<Name>'`)."
|
|
237
543
|
},
|
|
238
|
-
schema: [
|
|
544
|
+
schema: [optionSchema5],
|
|
239
545
|
messages: {
|
|
240
546
|
missingDefaultReexport: "`index.ts` must re-export the {{name}} default: `export { default as {{name}} } from './{{name}}'`."
|
|
241
547
|
}
|
|
@@ -247,8 +553,8 @@ var indexMustReexportDefaultRule = createRule({
|
|
|
247
553
|
if (getBasename(filename) !== "index.ts" || isIgnoredPath(filename, ignorePaths)) {
|
|
248
554
|
return {};
|
|
249
555
|
}
|
|
250
|
-
const dir =
|
|
251
|
-
const folderName =
|
|
556
|
+
const dir = import_node_path5.default.dirname(filename);
|
|
557
|
+
const folderName = import_node_path5.default.basename(dir);
|
|
252
558
|
if (!isPascalCase(folderName) || !siblingExists(dir, `${folderName}.tsx`)) {
|
|
253
559
|
return {};
|
|
254
560
|
}
|
|
@@ -272,14 +578,120 @@ var indexMustReexportDefaultRule = createRule({
|
|
|
272
578
|
}
|
|
273
579
|
});
|
|
274
580
|
|
|
581
|
+
// src/rules/max-import-depth.ts
|
|
582
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
583
|
+
var import_utils9 = require("@typescript-eslint/utils");
|
|
584
|
+
var RULE_NAME6 = "max-import-depth";
|
|
585
|
+
var DEFAULT_MAX = 3;
|
|
586
|
+
var optionSchema6 = {
|
|
587
|
+
type: "object",
|
|
588
|
+
additionalProperties: false,
|
|
589
|
+
properties: {
|
|
590
|
+
max: { type: "integer", minimum: 0 },
|
|
591
|
+
alias: {
|
|
592
|
+
type: "object",
|
|
593
|
+
additionalProperties: { type: "string" }
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
function climbDepth(source) {
|
|
598
|
+
if (!source.startsWith(".")) {
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
let depth = 0;
|
|
602
|
+
for (const segment of source.split("/")) {
|
|
603
|
+
if (segment === "..") {
|
|
604
|
+
depth += 1;
|
|
605
|
+
} else if (segment === ".") {
|
|
606
|
+
continue;
|
|
607
|
+
} else {
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return depth;
|
|
612
|
+
}
|
|
613
|
+
function aliasRewrite(source, currentFile, alias) {
|
|
614
|
+
const resolved = toPosix(import_node_path6.default.resolve(import_node_path6.default.dirname(currentFile), source));
|
|
615
|
+
for (const [anchor, prefix] of Object.entries(alias)) {
|
|
616
|
+
const marker = `/${anchor}/`;
|
|
617
|
+
const idx = resolved.lastIndexOf(marker);
|
|
618
|
+
if (idx === -1) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const rest = resolved.slice(idx + marker.length);
|
|
622
|
+
if (rest.length === 0) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
return `${prefix}/${rest}`;
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
var maxImportDepthRule = createRule({
|
|
630
|
+
name: RULE_NAME6,
|
|
631
|
+
meta: {
|
|
632
|
+
type: "suggestion",
|
|
633
|
+
fixable: "code",
|
|
634
|
+
docs: {
|
|
635
|
+
description: "A relative import may not climb more than `max` parent levels (default 3). Autofixed to a path alias when one is configured."
|
|
636
|
+
},
|
|
637
|
+
schema: [optionSchema6],
|
|
638
|
+
messages: {
|
|
639
|
+
tooDeep: "Relative import `{{source}}` climbs {{depth}} levels \u2014 over the limit of {{max}}. Use a path alias instead of reaching this far up the tree."
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
defaultOptions: [{ max: DEFAULT_MAX, alias: {} }],
|
|
643
|
+
create(context, [options]) {
|
|
644
|
+
const max = options.max ?? DEFAULT_MAX;
|
|
645
|
+
const alias = options.alias ?? {};
|
|
646
|
+
const filename = context.filename;
|
|
647
|
+
function check(sourceNode) {
|
|
648
|
+
if (sourceNode === null || sourceNode === void 0 || typeof sourceNode.value !== "string") {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const source = sourceNode.value;
|
|
652
|
+
const depth = climbDepth(source);
|
|
653
|
+
if (depth <= max) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const rewrite = aliasRewrite(source, filename, alias);
|
|
657
|
+
context.report({
|
|
658
|
+
node: sourceNode,
|
|
659
|
+
messageId: "tooDeep",
|
|
660
|
+
data: { source, depth, max },
|
|
661
|
+
fix: rewrite === null ? void 0 : (fixer) => {
|
|
662
|
+
const quote = sourceNode.raw.charAt(0);
|
|
663
|
+
return fixer.replaceText(sourceNode, `${quote}${rewrite}${quote}`);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
function literalSource(node) {
|
|
668
|
+
return node !== null && node !== void 0 && node.type === import_utils9.AST_NODE_TYPES.Literal ? node : null;
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
ImportDeclaration(node) {
|
|
672
|
+
check(node.source);
|
|
673
|
+
},
|
|
674
|
+
ImportExpression(node) {
|
|
675
|
+
check(literalSource(node.source));
|
|
676
|
+
},
|
|
677
|
+
ExportNamedDeclaration(node) {
|
|
678
|
+
check(literalSource(node.source));
|
|
679
|
+
},
|
|
680
|
+
ExportAllDeclaration(node) {
|
|
681
|
+
check(literalSource(node.source));
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
275
687
|
// src/rules/no-cross-feature-imports.ts
|
|
276
|
-
var
|
|
277
|
-
var
|
|
278
|
-
var
|
|
688
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
689
|
+
var import_utils11 = require("@typescript-eslint/utils");
|
|
690
|
+
var RULE_NAME7 = "no-cross-feature-imports";
|
|
279
691
|
var DEFAULT_FEATURE_ROOT = "components";
|
|
280
692
|
var DEFAULT_ALIAS = "@/components";
|
|
281
693
|
var DEFAULT_SHARED_FEATURES = ["ui"];
|
|
282
|
-
var
|
|
694
|
+
var optionSchema7 = {
|
|
283
695
|
type: "object",
|
|
284
696
|
additionalProperties: false,
|
|
285
697
|
properties: {
|
|
@@ -301,19 +713,19 @@ function resolveTargetFeature(source, currentFile, aliasRe, featureRoot) {
|
|
|
301
713
|
return aliasMatch[1] ?? null;
|
|
302
714
|
}
|
|
303
715
|
if (source.startsWith(".")) {
|
|
304
|
-
const resolved =
|
|
716
|
+
const resolved = import_node_path7.default.resolve(import_node_path7.default.dirname(currentFile), source);
|
|
305
717
|
return getFeatureName(resolved, featureRoot);
|
|
306
718
|
}
|
|
307
719
|
return null;
|
|
308
720
|
}
|
|
309
721
|
var noCrossFeatureImportsRule = createRule({
|
|
310
|
-
name:
|
|
722
|
+
name: RULE_NAME7,
|
|
311
723
|
meta: {
|
|
312
724
|
type: "problem",
|
|
313
725
|
docs: {
|
|
314
726
|
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
727
|
},
|
|
316
|
-
schema: [
|
|
728
|
+
schema: [optionSchema7],
|
|
317
729
|
messages: {
|
|
318
730
|
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
731
|
}
|
|
@@ -356,25 +768,25 @@ var noCrossFeatureImportsRule = createRule({
|
|
|
356
768
|
}
|
|
357
769
|
return {
|
|
358
770
|
ImportDeclaration(node) {
|
|
359
|
-
if (node.source.type ===
|
|
771
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
360
772
|
checkSource(node.source, node.importKind === "type");
|
|
361
773
|
}
|
|
362
774
|
},
|
|
363
775
|
// Dynamic `import()` is runtime by nature โ never type-only.
|
|
364
776
|
ImportExpression(node) {
|
|
365
|
-
if (node.source.type ===
|
|
777
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
366
778
|
checkSource(node.source, false);
|
|
367
779
|
}
|
|
368
780
|
},
|
|
369
781
|
// `export { x } from 'โฆ'` re-export laundering.
|
|
370
782
|
ExportNamedDeclaration(node) {
|
|
371
|
-
if (node.source !== null && node.source.type ===
|
|
783
|
+
if (node.source !== null && node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
372
784
|
checkSource(node.source, node.exportKind === "type");
|
|
373
785
|
}
|
|
374
786
|
},
|
|
375
787
|
// `export * from 'โฆ'` re-export laundering.
|
|
376
788
|
ExportAllDeclaration(node) {
|
|
377
|
-
if (node.source.type ===
|
|
789
|
+
if (node.source.type === import_utils11.AST_NODE_TYPES.Literal) {
|
|
378
790
|
checkSource(node.source, node.exportKind === "type");
|
|
379
791
|
}
|
|
380
792
|
}
|
|
@@ -384,14 +796,18 @@ var noCrossFeatureImportsRule = createRule({
|
|
|
384
796
|
|
|
385
797
|
// src/rules/index.ts
|
|
386
798
|
var rules = {
|
|
799
|
+
"barrel-purity": barrelPurityRule,
|
|
800
|
+
"colocated-test-required": colocatedTestRequiredRule,
|
|
387
801
|
"component-folder-structure": componentFolderStructureRule,
|
|
802
|
+
"filename-matches-export": filenameMatchesExportRule,
|
|
388
803
|
"index-must-reexport-default": indexMustReexportDefaultRule,
|
|
804
|
+
"max-import-depth": maxImportDepthRule,
|
|
389
805
|
"no-cross-feature-imports": noCrossFeatureImportsRule
|
|
390
806
|
};
|
|
391
807
|
|
|
392
808
|
// src/index.ts
|
|
393
809
|
var NAMESPACE = "noctcore-architecture";
|
|
394
|
-
var VERSION = "0.
|
|
810
|
+
var VERSION = "0.2.0";
|
|
395
811
|
var plugin = {
|
|
396
812
|
meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
|
|
397
813
|
rules,
|