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