@solid-stack/mason 1.0.31 → 1.0.33
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/bin.cjs +445 -67
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +445 -67
- package/dist/bin.js.map +1 -1
- package/dist/core/lintEngine.d.ts +11 -1
- package/dist/core/lintEngine.d.ts.map +1 -1
- package/dist/index.cjs +446 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +446 -68
- package/dist/index.js.map +1 -1
- package/dist/types/lint.d.ts +11 -3
- package/dist/types/lint.d.ts.map +1 -1
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.cjs
CHANGED
|
@@ -218,6 +218,16 @@ function getConfiguredSharedDir(projectRoot, flags) {
|
|
|
218
218
|
}
|
|
219
219
|
return path2__default.default.join(projectRoot, "shared");
|
|
220
220
|
}
|
|
221
|
+
function loadLintConfig(projectRoot) {
|
|
222
|
+
const configPath = path2__default.default.join(projectRoot, "mason-lint.config.json");
|
|
223
|
+
if (fs14__default.default.existsSync(configPath)) {
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(fs14__default.default.readFileSync(configPath, "utf-8"));
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { baseErrorPath: "src/errors/AppError.ts" };
|
|
230
|
+
}
|
|
221
231
|
|
|
222
232
|
// src/utils/project.ts
|
|
223
233
|
function findProjectRoot(startDir = process.cwd()) {
|
|
@@ -4906,22 +4916,47 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4906
4916
|
function classifyFile(fullPath, projectRoot) {
|
|
4907
4917
|
const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4908
4918
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4909
|
-
if (
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4919
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4920
|
+
const feature = featureMatch[1];
|
|
4921
|
+
const subfolder = featureMatch[2];
|
|
4922
|
+
let layer;
|
|
4923
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4924
|
+
layer = "domain";
|
|
4925
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4926
|
+
layer = "useCases";
|
|
4927
|
+
} else if (subfolder === "services") {
|
|
4928
|
+
layer = "services";
|
|
4929
|
+
} else if (subfolder === "infrastructure") {
|
|
4930
|
+
layer = "infrastructure";
|
|
4931
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4932
|
+
layer = "presentation";
|
|
4933
|
+
}
|
|
4934
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4935
|
+
}
|
|
4936
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4937
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4938
|
+
const moduleName = sharedMatch[1];
|
|
4939
|
+
const rest = sharedMatch[2] || "";
|
|
4940
|
+
let layer = "root";
|
|
4941
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4942
|
+
layer = "domain";
|
|
4943
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4944
|
+
layer = "ports";
|
|
4945
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4946
|
+
layer = "infrastructure";
|
|
4947
|
+
}
|
|
4948
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4949
|
+
}
|
|
4950
|
+
const errorMatch = rel.match(/(?:^|\/)(?:src\/)?errors(?:\/(.*))?$/);
|
|
4951
|
+
if (errorMatch) {
|
|
4952
|
+
return {
|
|
4953
|
+
moduleType: "error",
|
|
4954
|
+
moduleName: "global-errors",
|
|
4955
|
+
feature: "global-errors",
|
|
4956
|
+
layer: "domain"
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4959
|
+
return {};
|
|
4925
4960
|
}
|
|
4926
4961
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4927
4962
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4946,15 +4981,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4946
4981
|
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4947
4982
|
} else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
|
|
4948
4983
|
resolvedRel = "src/" + specifier.slice(1);
|
|
4984
|
+
} else if (specifier.startsWith("@/errors/") || specifier.startsWith("~/errors/") || specifier === "@/errors" || specifier === "~/errors") {
|
|
4985
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4986
|
+
} else if (specifier === "@errors" || specifier.startsWith("@errors/")) {
|
|
4987
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4949
4988
|
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4950
4989
|
resolvedRel = "src/" + specifier.slice(2);
|
|
4951
|
-
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
|
|
4990
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs" || specifier.startsWith("errors/") || specifier === "errors") {
|
|
4952
4991
|
resolvedRel = specifier;
|
|
4953
4992
|
}
|
|
4954
4993
|
if (!resolvedRel) {
|
|
4955
4994
|
return { isExternal: true, isDiFramework: false };
|
|
4956
4995
|
}
|
|
4957
4996
|
const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
|
|
4997
|
+
const isGlobalError = resolvedRel.startsWith("src/errors/") || resolvedRel === "src/errors" || resolvedRel.startsWith("errors/") || resolvedRel === "errors";
|
|
4998
|
+
if (isGlobalError) {
|
|
4999
|
+
return {
|
|
5000
|
+
isExternal: false,
|
|
5001
|
+
isDiFramework: false,
|
|
5002
|
+
isGlobalError: true,
|
|
5003
|
+
isTargetError: true
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
4958
5006
|
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4959
5007
|
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4960
5008
|
const targetFeature = featMatch[1];
|
|
@@ -4976,6 +5024,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4976
5024
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4977
5025
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4978
5026
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
5027
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4979
5028
|
return {
|
|
4980
5029
|
isExternal: false,
|
|
4981
5030
|
isDiFramework: false,
|
|
@@ -4984,7 +5033,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4984
5033
|
targetSubfolder,
|
|
4985
5034
|
targetFileName,
|
|
4986
5035
|
isTargetPort,
|
|
4987
|
-
isTargetRepo
|
|
5036
|
+
isTargetRepo,
|
|
5037
|
+
isTargetError
|
|
4988
5038
|
};
|
|
4989
5039
|
}
|
|
4990
5040
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -5010,17 +5060,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
5010
5060
|
function extractImportStatements(content) {
|
|
5011
5061
|
const statements = [];
|
|
5012
5062
|
const lines = content.split(/\r?\n/);
|
|
5013
|
-
const regex = /(?:import\s+(
|
|
5063
|
+
const regex = /(?:import\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|export\s+(type\s+)?(?:([\s\w*$,{}]+)\s+from\s+)?|import\s*\(|require\s*\()\s*["']([^"']+)["']/g;
|
|
5014
5064
|
let match;
|
|
5015
5065
|
while ((match = regex.exec(content)) !== null) {
|
|
5016
|
-
const
|
|
5066
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5067
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5068
|
+
const specifier = match[5];
|
|
5017
5069
|
const charIndex = match.index;
|
|
5018
5070
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5019
5071
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5072
|
+
const importedNames = [];
|
|
5073
|
+
if (importClause) {
|
|
5074
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5075
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5076
|
+
for (const w of words) {
|
|
5077
|
+
const parts = w.split(/\s+as\s+/);
|
|
5078
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5079
|
+
const alias = (parts[1] || name).trim();
|
|
5080
|
+
if (alias && alias !== "*") {
|
|
5081
|
+
importedNames.push(alias);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5020
5085
|
statements.push({
|
|
5021
5086
|
specifier,
|
|
5022
5087
|
line: lineNum,
|
|
5023
|
-
rawCode
|
|
5088
|
+
rawCode,
|
|
5089
|
+
isTypeOnly,
|
|
5090
|
+
importedNames
|
|
5024
5091
|
});
|
|
5025
5092
|
}
|
|
5026
5093
|
return statements;
|
|
@@ -5119,6 +5186,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5119
5186
|
dir: path2__default.default.join(featuresBase, e.name)
|
|
5120
5187
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5121
5188
|
}
|
|
5189
|
+
function discoverAllShared(projectRoot) {
|
|
5190
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5191
|
+
if (!fs14__default.default.existsSync(sharedBase)) return [];
|
|
5192
|
+
const entries = fs14__default.default.readdirSync(sharedBase, { withFileTypes: true });
|
|
5193
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5194
|
+
name: e.name,
|
|
5195
|
+
dir: path2__default.default.join(sharedBase, e.name)
|
|
5196
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5197
|
+
}
|
|
5122
5198
|
function extractClassMethods(content, className) {
|
|
5123
5199
|
let clean = "";
|
|
5124
5200
|
let inLineComment = false;
|
|
@@ -5467,17 +5543,56 @@ function lintCodebase(options = {}) {
|
|
|
5467
5543
|
const startTime = Date.now();
|
|
5468
5544
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5469
5545
|
const failFast = !options.complete;
|
|
5546
|
+
const lintConfig = loadLintConfig(projectRoot);
|
|
5547
|
+
const baseErrorRelative = lintConfig.baseErrorPath || "src/errors/AppError.ts";
|
|
5548
|
+
const baseErrorAbs = path2__default.default.resolve(projectRoot, baseErrorRelative);
|
|
5549
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
5550
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5551
|
+
let errorsBase = path2__default.default.join(projectRoot, "src/errors");
|
|
5552
|
+
if (!fs14__default.default.existsSync(errorsBase) && fs14__default.default.existsSync(path2__default.default.join(projectRoot, "errors"))) {
|
|
5553
|
+
errorsBase = path2__default.default.join(projectRoot, "errors");
|
|
5554
|
+
}
|
|
5555
|
+
const missingFoundations = [];
|
|
5556
|
+
if (!fs14__default.default.existsSync(featuresBase)) missingFoundations.push("features/");
|
|
5557
|
+
if (!fs14__default.default.existsSync(sharedBase)) missingFoundations.push("shared/");
|
|
5558
|
+
if (!fs14__default.default.existsSync(errorsBase)) missingFoundations.push("errors/");
|
|
5559
|
+
if (missingFoundations.length > 0) {
|
|
5560
|
+
throw new DependencyRuleError(
|
|
5561
|
+
`Project Structure Violation: Missing required foundational directories: ${missingFoundations.join(", ")}. Every project must contain 'features/', 'shared/', and 'errors/'.`,
|
|
5562
|
+
[],
|
|
5563
|
+
true
|
|
5564
|
+
);
|
|
5565
|
+
}
|
|
5566
|
+
if (!fs14__default.default.existsSync(baseErrorAbs)) {
|
|
5567
|
+
throw new DependencyRuleError(
|
|
5568
|
+
`Project Structure Violation: Base error class not found at '${baseErrorRelative}'. Create this file or update 'baseErrorPath' in mason-lint.config.json.`,
|
|
5569
|
+
[],
|
|
5570
|
+
true
|
|
5571
|
+
);
|
|
5572
|
+
}
|
|
5573
|
+
const baseErrorContent = fs14__default.default.readFileSync(baseErrorAbs, "utf-8");
|
|
5574
|
+
const baseClassMatch = baseErrorContent.match(
|
|
5575
|
+
/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/
|
|
5576
|
+
);
|
|
5577
|
+
const baseErrorClassName = baseClassMatch ? baseClassMatch[1] : "AppError";
|
|
5470
5578
|
const features = discoverAllFeatures(projectRoot);
|
|
5579
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5580
|
+
const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
|
|
5581
|
+
const allModules = [
|
|
5582
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5583
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" })),
|
|
5584
|
+
errorModule
|
|
5585
|
+
];
|
|
5471
5586
|
const graph = new FeatureDependencyGraph();
|
|
5472
5587
|
const allViolations = [];
|
|
5473
5588
|
const results = [];
|
|
5474
5589
|
const detectedCycles = [];
|
|
5475
5590
|
const addViolation = (v) => {
|
|
5476
5591
|
allViolations.push(v);
|
|
5477
|
-
const
|
|
5478
|
-
if (
|
|
5479
|
-
|
|
5480
|
-
|
|
5592
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5593
|
+
if (moduleResult) {
|
|
5594
|
+
moduleResult.violations.push(v);
|
|
5595
|
+
moduleResult.passed = false;
|
|
5481
5596
|
}
|
|
5482
5597
|
if (failFast) {
|
|
5483
5598
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5486,28 +5601,271 @@ Code: ${v.code}`;
|
|
|
5486
5601
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5487
5602
|
}
|
|
5488
5603
|
};
|
|
5489
|
-
for (const
|
|
5604
|
+
for (const mod of allModules) {
|
|
5490
5605
|
results.push({
|
|
5491
|
-
|
|
5492
|
-
|
|
5606
|
+
moduleType: mod.type,
|
|
5607
|
+
moduleName: mod.name,
|
|
5608
|
+
featureName: mod.name,
|
|
5609
|
+
featureDir: mod.dir,
|
|
5610
|
+
moduleDir: mod.dir,
|
|
5493
5611
|
scannedFilesCount: 0,
|
|
5494
5612
|
passed: true,
|
|
5495
5613
|
violations: [],
|
|
5496
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5614
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
|
|
5497
5615
|
});
|
|
5498
5616
|
}
|
|
5499
|
-
for (const
|
|
5500
|
-
const
|
|
5501
|
-
const allFiles = scanDirFiles(
|
|
5502
|
-
|
|
5617
|
+
for (const mod of allModules) {
|
|
5618
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5619
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5620
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5503
5621
|
for (const file of allFiles) {
|
|
5504
5622
|
const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5505
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5506
|
-
if (!layer) continue;
|
|
5623
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5624
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5507
5625
|
const content = fs14__default.default.readFileSync(file, "utf-8");
|
|
5508
5626
|
const statements = extractImportStatements(content);
|
|
5509
5627
|
const baseName = path2__default.default.basename(file);
|
|
5510
5628
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5629
|
+
const throwErrorRegex = /\bthrow\s+(?:new\s+)?Error\s*\(/g;
|
|
5630
|
+
let throwMatch;
|
|
5631
|
+
while ((throwMatch = throwErrorRegex.exec(content)) !== null) {
|
|
5632
|
+
const charIndex = throwMatch.index;
|
|
5633
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5634
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || throwMatch[0];
|
|
5635
|
+
if (lineCode.startsWith("//")) continue;
|
|
5636
|
+
addViolation({
|
|
5637
|
+
feature: moduleName,
|
|
5638
|
+
layer,
|
|
5639
|
+
file,
|
|
5640
|
+
relativePath: relPath,
|
|
5641
|
+
line: lineNum,
|
|
5642
|
+
category: "Plain Error Throw Violation",
|
|
5643
|
+
code: lineCode,
|
|
5644
|
+
ruleId: "E3_PLAIN_ERROR_THROW",
|
|
5645
|
+
severity: "error",
|
|
5646
|
+
advice: `Throwing plain 'Error' is prohibited. You must throw '${baseErrorClassName}' or a domain-specific error class that inherits from it.`,
|
|
5647
|
+
fixExample: {
|
|
5648
|
+
bad: lineCode,
|
|
5649
|
+
good: `throw new ${baseErrorClassName}("Something went wrong");`
|
|
5650
|
+
}
|
|
5651
|
+
});
|
|
5652
|
+
}
|
|
5653
|
+
const isErrorFile = relPath.includes("/errors/") || relPath.startsWith("errors/") || relPath.startsWith("src/errors/") || moduleType === "error" || baseName.endsWith("Error.ts") || baseName.endsWith("Errors.ts") || baseName.endsWith("Error.js") || baseName.endsWith("Errors.js");
|
|
5654
|
+
const isBaseErrorFile = path2__default.default.resolve(file) === path2__default.default.resolve(baseErrorAbs);
|
|
5655
|
+
if (isErrorFile && !isBaseErrorFile) {
|
|
5656
|
+
const classRegex = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_]+))?/g;
|
|
5657
|
+
let classMatch;
|
|
5658
|
+
while ((classMatch = classRegex.exec(content)) !== null) {
|
|
5659
|
+
const className = classMatch[1];
|
|
5660
|
+
const extendsName = classMatch[2];
|
|
5661
|
+
const charIndex = classMatch.index;
|
|
5662
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5663
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || classMatch[0];
|
|
5664
|
+
if (lineCode.startsWith("//")) continue;
|
|
5665
|
+
if (!extendsName) {
|
|
5666
|
+
addViolation({
|
|
5667
|
+
feature: moduleName,
|
|
5668
|
+
layer,
|
|
5669
|
+
file,
|
|
5670
|
+
relativePath: relPath,
|
|
5671
|
+
line: lineNum,
|
|
5672
|
+
category: "Error Inheritance Violation",
|
|
5673
|
+
code: lineCode,
|
|
5674
|
+
ruleId: "E4_MISSING_INHERITANCE",
|
|
5675
|
+
severity: "error",
|
|
5676
|
+
advice: `Error class '${className}' must extend '${baseErrorClassName}' (or another class that derives from it).`,
|
|
5677
|
+
fixExample: {
|
|
5678
|
+
bad: lineCode,
|
|
5679
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5680
|
+
}
|
|
5681
|
+
});
|
|
5682
|
+
} else if (extendsName === "Error") {
|
|
5683
|
+
addViolation({
|
|
5684
|
+
feature: moduleName,
|
|
5685
|
+
layer,
|
|
5686
|
+
file,
|
|
5687
|
+
relativePath: relPath,
|
|
5688
|
+
line: lineNum,
|
|
5689
|
+
category: "Error Inheritance Violation",
|
|
5690
|
+
code: lineCode,
|
|
5691
|
+
ruleId: "E5_NATIVE_ERROR_INHERITANCE",
|
|
5692
|
+
severity: "error",
|
|
5693
|
+
advice: `Error class '${className}' extends the native 'Error'. It must extend your base error class ('${baseErrorClassName}').`,
|
|
5694
|
+
fixExample: {
|
|
5695
|
+
bad: lineCode,
|
|
5696
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5697
|
+
}
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
if (moduleType === "error") {
|
|
5703
|
+
for (const stmt of statements) {
|
|
5704
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5705
|
+
if (target.targetFeature) {
|
|
5706
|
+
addViolation({
|
|
5707
|
+
feature: "global-errors",
|
|
5708
|
+
layer: "domain",
|
|
5709
|
+
file,
|
|
5710
|
+
relativePath: relPath,
|
|
5711
|
+
line: stmt.line,
|
|
5712
|
+
category: "Global Error Purity Violation",
|
|
5713
|
+
code: stmt.rawCode,
|
|
5714
|
+
ruleId: "E1_FEATURE_DEPENDENCY",
|
|
5715
|
+
severity: "error",
|
|
5716
|
+
advice: `Global errors must not depend on features ('${target.targetFeature}'). Global errors represent pure domain or system states.`,
|
|
5717
|
+
fixExample: {
|
|
5718
|
+
bad: stmt.rawCode,
|
|
5719
|
+
good: "// Global errors must not reference specific features. Keep error types pure."
|
|
5720
|
+
}
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
5724
|
+
addViolation({
|
|
5725
|
+
feature: "global-errors",
|
|
5726
|
+
layer: "domain",
|
|
5727
|
+
file,
|
|
5728
|
+
relativePath: relPath,
|
|
5729
|
+
line: stmt.line,
|
|
5730
|
+
category: "Global Error Purity Violation",
|
|
5731
|
+
code: stmt.rawCode,
|
|
5732
|
+
ruleId: "E2_SHARED_NON_DOMAIN",
|
|
5733
|
+
severity: "error",
|
|
5734
|
+
advice: `Global errors can only depend on shared domain elements (e.g., value objects). They cannot depend on shared ports or infrastructure.`,
|
|
5735
|
+
fixExample: {
|
|
5736
|
+
bad: stmt.rawCode,
|
|
5737
|
+
good: "// Depend only on pure domain types from shared modules."
|
|
5738
|
+
}
|
|
5739
|
+
});
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
continue;
|
|
5743
|
+
}
|
|
5744
|
+
if (moduleType === "shared") {
|
|
5745
|
+
if (layer === "domain") {
|
|
5746
|
+
const lines = content.split(/\r?\n/);
|
|
5747
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5748
|
+
const lineText = lines[i].trim();
|
|
5749
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5750
|
+
addViolation({
|
|
5751
|
+
feature: moduleName,
|
|
5752
|
+
layer,
|
|
5753
|
+
file,
|
|
5754
|
+
relativePath: relPath,
|
|
5755
|
+
line: i + 1,
|
|
5756
|
+
category: "Domain Dependence Violation",
|
|
5757
|
+
code: lineText,
|
|
5758
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5759
|
+
severity: "error",
|
|
5760
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5761
|
+
fixExample: {
|
|
5762
|
+
bad: lineText,
|
|
5763
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5764
|
+
}
|
|
5765
|
+
});
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
for (const stmt of statements) {
|
|
5770
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5771
|
+
if (target.targetFeature) {
|
|
5772
|
+
addViolation({
|
|
5773
|
+
feature: moduleName,
|
|
5774
|
+
layer,
|
|
5775
|
+
file,
|
|
5776
|
+
relativePath: relPath,
|
|
5777
|
+
line: stmt.line,
|
|
5778
|
+
category: "Shared Architecture Violation",
|
|
5779
|
+
code: stmt.rawCode,
|
|
5780
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5781
|
+
severity: "error",
|
|
5782
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5783
|
+
fixExample: {
|
|
5784
|
+
bad: stmt.rawCode,
|
|
5785
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5786
|
+
}
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5790
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5791
|
+
addViolation({
|
|
5792
|
+
feature: moduleName,
|
|
5793
|
+
layer,
|
|
5794
|
+
file,
|
|
5795
|
+
relativePath: relPath,
|
|
5796
|
+
line: stmt.line,
|
|
5797
|
+
category: "Shared Architecture Violation",
|
|
5798
|
+
code: stmt.rawCode,
|
|
5799
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5800
|
+
severity: "error",
|
|
5801
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5802
|
+
fixExample: {
|
|
5803
|
+
bad: stmt.rawCode,
|
|
5804
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5805
|
+
}
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
if (layer === "domain") {
|
|
5810
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5811
|
+
addViolation({
|
|
5812
|
+
feature: moduleName,
|
|
5813
|
+
layer,
|
|
5814
|
+
file,
|
|
5815
|
+
relativePath: relPath,
|
|
5816
|
+
line: stmt.line,
|
|
5817
|
+
category: "Domain Purity Violation",
|
|
5818
|
+
code: stmt.rawCode,
|
|
5819
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5820
|
+
severity: "error",
|
|
5821
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5822
|
+
fixExample: {
|
|
5823
|
+
bad: stmt.rawCode,
|
|
5824
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5825
|
+
}
|
|
5826
|
+
});
|
|
5827
|
+
}
|
|
5828
|
+
if (target.isDiFramework) {
|
|
5829
|
+
addViolation({
|
|
5830
|
+
feature: moduleName,
|
|
5831
|
+
layer,
|
|
5832
|
+
file,
|
|
5833
|
+
relativePath: relPath,
|
|
5834
|
+
line: stmt.line,
|
|
5835
|
+
category: "Domain Dependence Violation",
|
|
5836
|
+
code: stmt.rawCode,
|
|
5837
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5838
|
+
severity: "error",
|
|
5839
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5840
|
+
fixExample: {
|
|
5841
|
+
bad: stmt.rawCode,
|
|
5842
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5843
|
+
}
|
|
5844
|
+
});
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5848
|
+
addViolation({
|
|
5849
|
+
feature: moduleName,
|
|
5850
|
+
layer,
|
|
5851
|
+
file,
|
|
5852
|
+
relativePath: relPath,
|
|
5853
|
+
line: stmt.line,
|
|
5854
|
+
category: "Shared Architecture Violation",
|
|
5855
|
+
code: stmt.rawCode,
|
|
5856
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5857
|
+
severity: "error",
|
|
5858
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5859
|
+
fixExample: {
|
|
5860
|
+
bad: stmt.rawCode,
|
|
5861
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5862
|
+
}
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
continue;
|
|
5867
|
+
}
|
|
5868
|
+
const feat = mod;
|
|
5511
5869
|
if (layer === "useCases" && !isBarrel) {
|
|
5512
5870
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5513
5871
|
}
|
|
@@ -5576,23 +5934,33 @@ Code: ${v.code}`;
|
|
|
5576
5934
|
}
|
|
5577
5935
|
});
|
|
5578
5936
|
}
|
|
5579
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5937
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5938
|
+
for (const name of stmt.importedNames) {
|
|
5939
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5940
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5941
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5942
|
+
if (execMatch) {
|
|
5943
|
+
const charIndex = execMatch.index;
|
|
5944
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5945
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5946
|
+
addViolation({
|
|
5947
|
+
feature: feat.name,
|
|
5948
|
+
layer,
|
|
5949
|
+
file,
|
|
5950
|
+
relativePath: relPath,
|
|
5951
|
+
line: lineNum,
|
|
5952
|
+
category: "Cross-Feature Domain Violation",
|
|
5953
|
+
code: lineCode,
|
|
5954
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5955
|
+
severity: "error",
|
|
5956
|
+
advice: `Cannot instantiate domain entities/factories directly from foreign feature '${target.targetFeature}' (found instantiation of '${name}'). This bypasses its use cases and breaks Bounded Context encapsulation. Importing for types or 'instanceof' checks is allowed.`,
|
|
5957
|
+
fixExample: {
|
|
5958
|
+
bad: lineCode,
|
|
5959
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5960
|
+
}
|
|
5961
|
+
});
|
|
5594
5962
|
}
|
|
5595
|
-
}
|
|
5963
|
+
}
|
|
5596
5964
|
}
|
|
5597
5965
|
}
|
|
5598
5966
|
if (target.isGlobalConfig) {
|
|
@@ -5980,23 +6348,33 @@ Code: ${v.code}`;
|
|
|
5980
6348
|
}
|
|
5981
6349
|
});
|
|
5982
6350
|
}
|
|
5983
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
6351
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6352
|
+
for (const name of stmt.importedNames) {
|
|
6353
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6354
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6355
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6356
|
+
if (execMatch) {
|
|
6357
|
+
const charIndex = execMatch.index;
|
|
6358
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6359
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6360
|
+
addViolation({
|
|
6361
|
+
feature: feat.name,
|
|
6362
|
+
layer,
|
|
6363
|
+
file,
|
|
6364
|
+
relativePath: relPath,
|
|
6365
|
+
line: lineNum,
|
|
6366
|
+
category: "Presentation Layer Violation",
|
|
6367
|
+
code: lineCode,
|
|
6368
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6369
|
+
severity: "error",
|
|
6370
|
+
advice: `Presentation handlers must not directly instantiate domain entities (found instantiation of '${name}'). They must map requests to flat DTOs and pass them to the Use Case. Importing for types or 'instanceof' checks is allowed.`,
|
|
6371
|
+
fixExample: {
|
|
6372
|
+
bad: lineCode,
|
|
6373
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6374
|
+
}
|
|
6375
|
+
});
|
|
5998
6376
|
}
|
|
5999
|
-
}
|
|
6377
|
+
}
|
|
6000
6378
|
}
|
|
6001
6379
|
}
|
|
6002
6380
|
}
|