@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.js
CHANGED
|
@@ -190,6 +190,16 @@ function getConfiguredSharedDir(projectRoot, flags) {
|
|
|
190
190
|
}
|
|
191
191
|
return path2.join(projectRoot, "shared");
|
|
192
192
|
}
|
|
193
|
+
function loadLintConfig(projectRoot) {
|
|
194
|
+
const configPath = path2.join(projectRoot, "mason-lint.config.json");
|
|
195
|
+
if (fs4.existsSync(configPath)) {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { baseErrorPath: "src/errors/AppError.ts" };
|
|
202
|
+
}
|
|
193
203
|
|
|
194
204
|
// src/utils/project.ts
|
|
195
205
|
function findProjectRoot(startDir = process.cwd()) {
|
|
@@ -4886,22 +4896,47 @@ function scanDirFiles(dir, projectRoot) {
|
|
|
4886
4896
|
function classifyFile(fullPath, projectRoot) {
|
|
4887
4897
|
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4888
4898
|
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4889
|
-
if (
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4899
|
+
if (featureMatch && featureMatch[1] && featureMatch[2]) {
|
|
4900
|
+
const feature = featureMatch[1];
|
|
4901
|
+
const subfolder = featureMatch[2];
|
|
4902
|
+
let layer;
|
|
4903
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4904
|
+
layer = "domain";
|
|
4905
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4906
|
+
layer = "useCases";
|
|
4907
|
+
} else if (subfolder === "services") {
|
|
4908
|
+
layer = "services";
|
|
4909
|
+
} else if (subfolder === "infrastructure") {
|
|
4910
|
+
layer = "infrastructure";
|
|
4911
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4912
|
+
layer = "presentation";
|
|
4913
|
+
}
|
|
4914
|
+
return { moduleType: "feature", moduleName: feature, feature, layer };
|
|
4915
|
+
}
|
|
4916
|
+
const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4917
|
+
if (sharedMatch && sharedMatch[1]) {
|
|
4918
|
+
const moduleName = sharedMatch[1];
|
|
4919
|
+
const rest = sharedMatch[2] || "";
|
|
4920
|
+
let layer = "root";
|
|
4921
|
+
if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
|
|
4922
|
+
layer = "domain";
|
|
4923
|
+
} else if (rest.startsWith("ports/") || rest === "ports") {
|
|
4924
|
+
layer = "ports";
|
|
4925
|
+
} else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
|
|
4926
|
+
layer = "infrastructure";
|
|
4927
|
+
}
|
|
4928
|
+
return { moduleType: "shared", moduleName, feature: moduleName, layer };
|
|
4929
|
+
}
|
|
4930
|
+
const errorMatch = rel.match(/(?:^|\/)(?:src\/)?errors(?:\/(.*))?$/);
|
|
4931
|
+
if (errorMatch) {
|
|
4932
|
+
return {
|
|
4933
|
+
moduleType: "error",
|
|
4934
|
+
moduleName: "global-errors",
|
|
4935
|
+
feature: "global-errors",
|
|
4936
|
+
layer: "domain"
|
|
4937
|
+
};
|
|
4938
|
+
}
|
|
4939
|
+
return {};
|
|
4905
4940
|
}
|
|
4906
4941
|
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4907
4942
|
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
@@ -4926,15 +4961,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4926
4961
|
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4927
4962
|
} else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
|
|
4928
4963
|
resolvedRel = "src/" + specifier.slice(1);
|
|
4964
|
+
} else if (specifier.startsWith("@/errors/") || specifier.startsWith("~/errors/") || specifier === "@/errors" || specifier === "~/errors") {
|
|
4965
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4966
|
+
} else if (specifier === "@errors" || specifier.startsWith("@errors/")) {
|
|
4967
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4929
4968
|
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4930
4969
|
resolvedRel = "src/" + specifier.slice(2);
|
|
4931
|
-
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
|
|
4970
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs" || specifier.startsWith("errors/") || specifier === "errors") {
|
|
4932
4971
|
resolvedRel = specifier;
|
|
4933
4972
|
}
|
|
4934
4973
|
if (!resolvedRel) {
|
|
4935
4974
|
return { isExternal: true, isDiFramework: false };
|
|
4936
4975
|
}
|
|
4937
4976
|
const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
|
|
4977
|
+
const isGlobalError = resolvedRel.startsWith("src/errors/") || resolvedRel === "src/errors" || resolvedRel.startsWith("errors/") || resolvedRel === "errors";
|
|
4978
|
+
if (isGlobalError) {
|
|
4979
|
+
return {
|
|
4980
|
+
isExternal: false,
|
|
4981
|
+
isDiFramework: false,
|
|
4982
|
+
isGlobalError: true,
|
|
4983
|
+
isTargetError: true
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4938
4986
|
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4939
4987
|
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4940
4988
|
const targetFeature = featMatch[1];
|
|
@@ -4956,6 +5004,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4956
5004
|
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4957
5005
|
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4958
5006
|
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
5007
|
+
const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
|
|
4959
5008
|
return {
|
|
4960
5009
|
isExternal: false,
|
|
4961
5010
|
isDiFramework: false,
|
|
@@ -4964,7 +5013,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4964
5013
|
targetSubfolder,
|
|
4965
5014
|
targetFileName,
|
|
4966
5015
|
isTargetPort,
|
|
4967
|
-
isTargetRepo
|
|
5016
|
+
isTargetRepo,
|
|
5017
|
+
isTargetError
|
|
4968
5018
|
};
|
|
4969
5019
|
}
|
|
4970
5020
|
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
@@ -4990,17 +5040,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
|
4990
5040
|
function extractImportStatements(content) {
|
|
4991
5041
|
const statements = [];
|
|
4992
5042
|
const lines = content.split(/\r?\n/);
|
|
4993
|
-
const regex = /(?:import\s+(
|
|
5043
|
+
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;
|
|
4994
5044
|
let match;
|
|
4995
5045
|
while ((match = regex.exec(content)) !== null) {
|
|
4996
|
-
const
|
|
5046
|
+
const isTypeOnly = Boolean(match[1] || match[3]);
|
|
5047
|
+
const importClause = (match[2] || match[4] || "").trim();
|
|
5048
|
+
const specifier = match[5];
|
|
4997
5049
|
const charIndex = match.index;
|
|
4998
5050
|
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4999
5051
|
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
5052
|
+
const importedNames = [];
|
|
5053
|
+
if (importClause) {
|
|
5054
|
+
const cleaned = importClause.replace(/[{}]/g, "");
|
|
5055
|
+
const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5056
|
+
for (const w of words) {
|
|
5057
|
+
const parts = w.split(/\s+as\s+/);
|
|
5058
|
+
const name = parts[0].replace(/^type\s+/, "").trim();
|
|
5059
|
+
const alias = (parts[1] || name).trim();
|
|
5060
|
+
if (alias && alias !== "*") {
|
|
5061
|
+
importedNames.push(alias);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5000
5065
|
statements.push({
|
|
5001
5066
|
specifier,
|
|
5002
5067
|
line: lineNum,
|
|
5003
|
-
rawCode
|
|
5068
|
+
rawCode,
|
|
5069
|
+
isTypeOnly,
|
|
5070
|
+
importedNames
|
|
5004
5071
|
});
|
|
5005
5072
|
}
|
|
5006
5073
|
return statements;
|
|
@@ -5099,6 +5166,15 @@ function discoverAllFeatures(projectRoot) {
|
|
|
5099
5166
|
dir: path2.join(featuresBase, e.name)
|
|
5100
5167
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5101
5168
|
}
|
|
5169
|
+
function discoverAllShared(projectRoot) {
|
|
5170
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5171
|
+
if (!fs4.existsSync(sharedBase)) return [];
|
|
5172
|
+
const entries = fs4.readdirSync(sharedBase, { withFileTypes: true });
|
|
5173
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
5174
|
+
name: e.name,
|
|
5175
|
+
dir: path2.join(sharedBase, e.name)
|
|
5176
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5177
|
+
}
|
|
5102
5178
|
function extractClassMethods(content, className) {
|
|
5103
5179
|
let clean = "";
|
|
5104
5180
|
let inLineComment = false;
|
|
@@ -5447,17 +5523,56 @@ function lintCodebase(options = {}) {
|
|
|
5447
5523
|
const startTime = Date.now();
|
|
5448
5524
|
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
5449
5525
|
const failFast = !options.complete;
|
|
5526
|
+
const lintConfig = loadLintConfig(projectRoot);
|
|
5527
|
+
const baseErrorRelative = lintConfig.baseErrorPath || "src/errors/AppError.ts";
|
|
5528
|
+
const baseErrorAbs = path2.resolve(projectRoot, baseErrorRelative);
|
|
5529
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
5530
|
+
const sharedBase = resolveSharedDir(projectRoot);
|
|
5531
|
+
let errorsBase = path2.join(projectRoot, "src/errors");
|
|
5532
|
+
if (!fs4.existsSync(errorsBase) && fs4.existsSync(path2.join(projectRoot, "errors"))) {
|
|
5533
|
+
errorsBase = path2.join(projectRoot, "errors");
|
|
5534
|
+
}
|
|
5535
|
+
const missingFoundations = [];
|
|
5536
|
+
if (!fs4.existsSync(featuresBase)) missingFoundations.push("features/");
|
|
5537
|
+
if (!fs4.existsSync(sharedBase)) missingFoundations.push("shared/");
|
|
5538
|
+
if (!fs4.existsSync(errorsBase)) missingFoundations.push("errors/");
|
|
5539
|
+
if (missingFoundations.length > 0) {
|
|
5540
|
+
throw new DependencyRuleError(
|
|
5541
|
+
`Project Structure Violation: Missing required foundational directories: ${missingFoundations.join(", ")}. Every project must contain 'features/', 'shared/', and 'errors/'.`,
|
|
5542
|
+
[],
|
|
5543
|
+
true
|
|
5544
|
+
);
|
|
5545
|
+
}
|
|
5546
|
+
if (!fs4.existsSync(baseErrorAbs)) {
|
|
5547
|
+
throw new DependencyRuleError(
|
|
5548
|
+
`Project Structure Violation: Base error class not found at '${baseErrorRelative}'. Create this file or update 'baseErrorPath' in mason-lint.config.json.`,
|
|
5549
|
+
[],
|
|
5550
|
+
true
|
|
5551
|
+
);
|
|
5552
|
+
}
|
|
5553
|
+
const baseErrorContent = fs4.readFileSync(baseErrorAbs, "utf-8");
|
|
5554
|
+
const baseClassMatch = baseErrorContent.match(
|
|
5555
|
+
/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/
|
|
5556
|
+
);
|
|
5557
|
+
const baseErrorClassName = baseClassMatch ? baseClassMatch[1] : "AppError";
|
|
5450
5558
|
const features = discoverAllFeatures(projectRoot);
|
|
5559
|
+
const sharedModules = discoverAllShared(projectRoot);
|
|
5560
|
+
const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
|
|
5561
|
+
const allModules = [
|
|
5562
|
+
...features.map((f) => ({ ...f, type: "feature" })),
|
|
5563
|
+
...sharedModules.map((s) => ({ ...s, type: "shared" })),
|
|
5564
|
+
errorModule
|
|
5565
|
+
];
|
|
5451
5566
|
const graph = new FeatureDependencyGraph();
|
|
5452
5567
|
const allViolations = [];
|
|
5453
5568
|
const results = [];
|
|
5454
5569
|
const detectedCycles = [];
|
|
5455
5570
|
const addViolation = (v) => {
|
|
5456
5571
|
allViolations.push(v);
|
|
5457
|
-
const
|
|
5458
|
-
if (
|
|
5459
|
-
|
|
5460
|
-
|
|
5572
|
+
const moduleResult = results.find((r) => r.featureName === v.feature);
|
|
5573
|
+
if (moduleResult) {
|
|
5574
|
+
moduleResult.violations.push(v);
|
|
5575
|
+
moduleResult.passed = false;
|
|
5461
5576
|
}
|
|
5462
5577
|
if (failFast) {
|
|
5463
5578
|
const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
|
|
@@ -5466,28 +5581,271 @@ Code: ${v.code}`;
|
|
|
5466
5581
|
throw new DependencyRuleError(errorMsg, [v], true);
|
|
5467
5582
|
}
|
|
5468
5583
|
};
|
|
5469
|
-
for (const
|
|
5584
|
+
for (const mod of allModules) {
|
|
5470
5585
|
results.push({
|
|
5471
|
-
|
|
5472
|
-
|
|
5586
|
+
moduleType: mod.type,
|
|
5587
|
+
moduleName: mod.name,
|
|
5588
|
+
featureName: mod.name,
|
|
5589
|
+
featureDir: mod.dir,
|
|
5590
|
+
moduleDir: mod.dir,
|
|
5473
5591
|
scannedFilesCount: 0,
|
|
5474
5592
|
passed: true,
|
|
5475
5593
|
violations: [],
|
|
5476
|
-
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
5594
|
+
layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
|
|
5477
5595
|
});
|
|
5478
5596
|
}
|
|
5479
|
-
for (const
|
|
5480
|
-
const
|
|
5481
|
-
const allFiles = scanDirFiles(
|
|
5482
|
-
|
|
5597
|
+
for (const mod of allModules) {
|
|
5598
|
+
const modResult = results.find((r) => r.featureName === mod.name);
|
|
5599
|
+
const allFiles = scanDirFiles(mod.dir, projectRoot);
|
|
5600
|
+
modResult.scannedFilesCount = allFiles.length;
|
|
5483
5601
|
for (const file of allFiles) {
|
|
5484
5602
|
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
5485
|
-
const { layer } = classifyFile(file, projectRoot);
|
|
5486
|
-
if (!layer) continue;
|
|
5603
|
+
const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
|
|
5604
|
+
if (!layer || !moduleType || !moduleName) continue;
|
|
5487
5605
|
const content = fs4.readFileSync(file, "utf-8");
|
|
5488
5606
|
const statements = extractImportStatements(content);
|
|
5489
5607
|
const baseName = path2.basename(file);
|
|
5490
5608
|
const isBarrel = baseName === "index.ts" || baseName === "index.js";
|
|
5609
|
+
const throwErrorRegex = /\bthrow\s+(?:new\s+)?Error\s*\(/g;
|
|
5610
|
+
let throwMatch;
|
|
5611
|
+
while ((throwMatch = throwErrorRegex.exec(content)) !== null) {
|
|
5612
|
+
const charIndex = throwMatch.index;
|
|
5613
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5614
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || throwMatch[0];
|
|
5615
|
+
if (lineCode.startsWith("//")) continue;
|
|
5616
|
+
addViolation({
|
|
5617
|
+
feature: moduleName,
|
|
5618
|
+
layer,
|
|
5619
|
+
file,
|
|
5620
|
+
relativePath: relPath,
|
|
5621
|
+
line: lineNum,
|
|
5622
|
+
category: "Plain Error Throw Violation",
|
|
5623
|
+
code: lineCode,
|
|
5624
|
+
ruleId: "E3_PLAIN_ERROR_THROW",
|
|
5625
|
+
severity: "error",
|
|
5626
|
+
advice: `Throwing plain 'Error' is prohibited. You must throw '${baseErrorClassName}' or a domain-specific error class that inherits from it.`,
|
|
5627
|
+
fixExample: {
|
|
5628
|
+
bad: lineCode,
|
|
5629
|
+
good: `throw new ${baseErrorClassName}("Something went wrong");`
|
|
5630
|
+
}
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
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");
|
|
5634
|
+
const isBaseErrorFile = path2.resolve(file) === path2.resolve(baseErrorAbs);
|
|
5635
|
+
if (isErrorFile && !isBaseErrorFile) {
|
|
5636
|
+
const classRegex = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_]+))?/g;
|
|
5637
|
+
let classMatch;
|
|
5638
|
+
while ((classMatch = classRegex.exec(content)) !== null) {
|
|
5639
|
+
const className = classMatch[1];
|
|
5640
|
+
const extendsName = classMatch[2];
|
|
5641
|
+
const charIndex = classMatch.index;
|
|
5642
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5643
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || classMatch[0];
|
|
5644
|
+
if (lineCode.startsWith("//")) continue;
|
|
5645
|
+
if (!extendsName) {
|
|
5646
|
+
addViolation({
|
|
5647
|
+
feature: moduleName,
|
|
5648
|
+
layer,
|
|
5649
|
+
file,
|
|
5650
|
+
relativePath: relPath,
|
|
5651
|
+
line: lineNum,
|
|
5652
|
+
category: "Error Inheritance Violation",
|
|
5653
|
+
code: lineCode,
|
|
5654
|
+
ruleId: "E4_MISSING_INHERITANCE",
|
|
5655
|
+
severity: "error",
|
|
5656
|
+
advice: `Error class '${className}' must extend '${baseErrorClassName}' (or another class that derives from it).`,
|
|
5657
|
+
fixExample: {
|
|
5658
|
+
bad: lineCode,
|
|
5659
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5660
|
+
}
|
|
5661
|
+
});
|
|
5662
|
+
} else if (extendsName === "Error") {
|
|
5663
|
+
addViolation({
|
|
5664
|
+
feature: moduleName,
|
|
5665
|
+
layer,
|
|
5666
|
+
file,
|
|
5667
|
+
relativePath: relPath,
|
|
5668
|
+
line: lineNum,
|
|
5669
|
+
category: "Error Inheritance Violation",
|
|
5670
|
+
code: lineCode,
|
|
5671
|
+
ruleId: "E5_NATIVE_ERROR_INHERITANCE",
|
|
5672
|
+
severity: "error",
|
|
5673
|
+
advice: `Error class '${className}' extends the native 'Error'. It must extend your base error class ('${baseErrorClassName}').`,
|
|
5674
|
+
fixExample: {
|
|
5675
|
+
bad: lineCode,
|
|
5676
|
+
good: `export class ${className} extends ${baseErrorClassName} { ... }`
|
|
5677
|
+
}
|
|
5678
|
+
});
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
if (moduleType === "error") {
|
|
5683
|
+
for (const stmt of statements) {
|
|
5684
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5685
|
+
if (target.targetFeature) {
|
|
5686
|
+
addViolation({
|
|
5687
|
+
feature: "global-errors",
|
|
5688
|
+
layer: "domain",
|
|
5689
|
+
file,
|
|
5690
|
+
relativePath: relPath,
|
|
5691
|
+
line: stmt.line,
|
|
5692
|
+
category: "Global Error Purity Violation",
|
|
5693
|
+
code: stmt.rawCode,
|
|
5694
|
+
ruleId: "E1_FEATURE_DEPENDENCY",
|
|
5695
|
+
severity: "error",
|
|
5696
|
+
advice: `Global errors must not depend on features ('${target.targetFeature}'). Global errors represent pure domain or system states.`,
|
|
5697
|
+
fixExample: {
|
|
5698
|
+
bad: stmt.rawCode,
|
|
5699
|
+
good: "// Global errors must not reference specific features. Keep error types pure."
|
|
5700
|
+
}
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
5704
|
+
addViolation({
|
|
5705
|
+
feature: "global-errors",
|
|
5706
|
+
layer: "domain",
|
|
5707
|
+
file,
|
|
5708
|
+
relativePath: relPath,
|
|
5709
|
+
line: stmt.line,
|
|
5710
|
+
category: "Global Error Purity Violation",
|
|
5711
|
+
code: stmt.rawCode,
|
|
5712
|
+
ruleId: "E2_SHARED_NON_DOMAIN",
|
|
5713
|
+
severity: "error",
|
|
5714
|
+
advice: `Global errors can only depend on shared domain elements (e.g., value objects). They cannot depend on shared ports or infrastructure.`,
|
|
5715
|
+
fixExample: {
|
|
5716
|
+
bad: stmt.rawCode,
|
|
5717
|
+
good: "// Depend only on pure domain types from shared modules."
|
|
5718
|
+
}
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
continue;
|
|
5723
|
+
}
|
|
5724
|
+
if (moduleType === "shared") {
|
|
5725
|
+
if (layer === "domain") {
|
|
5726
|
+
const lines = content.split(/\r?\n/);
|
|
5727
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5728
|
+
const lineText = lines[i].trim();
|
|
5729
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
5730
|
+
addViolation({
|
|
5731
|
+
feature: moduleName,
|
|
5732
|
+
layer,
|
|
5733
|
+
file,
|
|
5734
|
+
relativePath: relPath,
|
|
5735
|
+
line: i + 1,
|
|
5736
|
+
category: "Domain Dependence Violation",
|
|
5737
|
+
code: lineText,
|
|
5738
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5739
|
+
severity: "error",
|
|
5740
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
5741
|
+
fixExample: {
|
|
5742
|
+
bad: lineText,
|
|
5743
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
5744
|
+
}
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
for (const stmt of statements) {
|
|
5750
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
5751
|
+
if (target.targetFeature) {
|
|
5752
|
+
addViolation({
|
|
5753
|
+
feature: moduleName,
|
|
5754
|
+
layer,
|
|
5755
|
+
file,
|
|
5756
|
+
relativePath: relPath,
|
|
5757
|
+
line: stmt.line,
|
|
5758
|
+
category: "Shared Architecture Violation",
|
|
5759
|
+
code: stmt.rawCode,
|
|
5760
|
+
ruleId: "SF1_FEATURE_DEPENDENCY",
|
|
5761
|
+
severity: "error",
|
|
5762
|
+
advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
|
|
5763
|
+
fixExample: {
|
|
5764
|
+
bad: stmt.rawCode,
|
|
5765
|
+
good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
|
|
5766
|
+
}
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
|
|
5770
|
+
if (target.isSharedInfra || target.isSharedPorts) {
|
|
5771
|
+
addViolation({
|
|
5772
|
+
feature: moduleName,
|
|
5773
|
+
layer,
|
|
5774
|
+
file,
|
|
5775
|
+
relativePath: relPath,
|
|
5776
|
+
line: stmt.line,
|
|
5777
|
+
category: "Shared Architecture Violation",
|
|
5778
|
+
code: stmt.rawCode,
|
|
5779
|
+
ruleId: "SS1_SHARED_ENCAPSULATION",
|
|
5780
|
+
severity: "error",
|
|
5781
|
+
advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
|
|
5782
|
+
fixExample: {
|
|
5783
|
+
bad: stmt.rawCode,
|
|
5784
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
5785
|
+
}
|
|
5786
|
+
});
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
if (layer === "domain") {
|
|
5790
|
+
if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
|
|
5791
|
+
addViolation({
|
|
5792
|
+
feature: moduleName,
|
|
5793
|
+
layer,
|
|
5794
|
+
file,
|
|
5795
|
+
relativePath: relPath,
|
|
5796
|
+
line: stmt.line,
|
|
5797
|
+
category: "Domain Purity Violation",
|
|
5798
|
+
code: stmt.rawCode,
|
|
5799
|
+
ruleId: "SD1_OUTER_IMPORT",
|
|
5800
|
+
severity: "error",
|
|
5801
|
+
advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
|
|
5802
|
+
fixExample: {
|
|
5803
|
+
bad: stmt.rawCode,
|
|
5804
|
+
good: "// Model pure business types and value objects directly within domain/."
|
|
5805
|
+
}
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5808
|
+
if (target.isDiFramework) {
|
|
5809
|
+
addViolation({
|
|
5810
|
+
feature: moduleName,
|
|
5811
|
+
layer,
|
|
5812
|
+
file,
|
|
5813
|
+
relativePath: relPath,
|
|
5814
|
+
line: stmt.line,
|
|
5815
|
+
category: "Domain Dependence Violation",
|
|
5816
|
+
code: stmt.rawCode,
|
|
5817
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
5818
|
+
severity: "error",
|
|
5819
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
5820
|
+
fixExample: {
|
|
5821
|
+
bad: stmt.rawCode,
|
|
5822
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
5823
|
+
}
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
|
|
5828
|
+
addViolation({
|
|
5829
|
+
feature: moduleName,
|
|
5830
|
+
layer,
|
|
5831
|
+
file,
|
|
5832
|
+
relativePath: relPath,
|
|
5833
|
+
line: stmt.line,
|
|
5834
|
+
category: "Shared Architecture Violation",
|
|
5835
|
+
code: stmt.rawCode,
|
|
5836
|
+
ruleId: "SP1_INFRA_IN_PORT",
|
|
5837
|
+
severity: "error",
|
|
5838
|
+
advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
|
|
5839
|
+
fixExample: {
|
|
5840
|
+
bad: stmt.rawCode,
|
|
5841
|
+
good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
|
|
5842
|
+
}
|
|
5843
|
+
});
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
continue;
|
|
5847
|
+
}
|
|
5848
|
+
const feat = mod;
|
|
5491
5849
|
if (layer === "useCases" && !isBarrel) {
|
|
5492
5850
|
checkUseCaseShape(feat.name, file, relPath, content, addViolation);
|
|
5493
5851
|
}
|
|
@@ -5556,23 +5914,33 @@ Code: ${v.code}`;
|
|
|
5556
5914
|
}
|
|
5557
5915
|
});
|
|
5558
5916
|
}
|
|
5559
|
-
if (target.targetLayer === "domain" && !target.isTargetPort) {
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5917
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
|
|
5918
|
+
for (const name of stmt.importedNames) {
|
|
5919
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
5920
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
5921
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
5922
|
+
if (execMatch) {
|
|
5923
|
+
const charIndex = execMatch.index;
|
|
5924
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
5925
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
5926
|
+
addViolation({
|
|
5927
|
+
feature: feat.name,
|
|
5928
|
+
layer,
|
|
5929
|
+
file,
|
|
5930
|
+
relativePath: relPath,
|
|
5931
|
+
line: lineNum,
|
|
5932
|
+
category: "Cross-Feature Domain Violation",
|
|
5933
|
+
code: lineCode,
|
|
5934
|
+
ruleId: "F1_CROSS_FEATURE_DOMAIN",
|
|
5935
|
+
severity: "error",
|
|
5936
|
+
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.`,
|
|
5937
|
+
fixExample: {
|
|
5938
|
+
bad: lineCode,
|
|
5939
|
+
good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
|
|
5940
|
+
}
|
|
5941
|
+
});
|
|
5574
5942
|
}
|
|
5575
|
-
}
|
|
5943
|
+
}
|
|
5576
5944
|
}
|
|
5577
5945
|
}
|
|
5578
5946
|
if (target.isGlobalConfig) {
|
|
@@ -5960,23 +6328,33 @@ Code: ${v.code}`;
|
|
|
5960
6328
|
}
|
|
5961
6329
|
});
|
|
5962
6330
|
}
|
|
5963
|
-
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6331
|
+
if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
|
|
6332
|
+
for (const name of stmt.importedNames) {
|
|
6333
|
+
const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
|
|
6334
|
+
const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
|
|
6335
|
+
const execMatch = createRegex.exec(content) || newRegex.exec(content);
|
|
6336
|
+
if (execMatch) {
|
|
6337
|
+
const charIndex = execMatch.index;
|
|
6338
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
6339
|
+
const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
|
|
6340
|
+
addViolation({
|
|
6341
|
+
feature: feat.name,
|
|
6342
|
+
layer,
|
|
6343
|
+
file,
|
|
6344
|
+
relativePath: relPath,
|
|
6345
|
+
line: lineNum,
|
|
6346
|
+
category: "Presentation Layer Violation",
|
|
6347
|
+
code: lineCode,
|
|
6348
|
+
ruleId: "P5_DIRECT_DOMAIN_ENTITY",
|
|
6349
|
+
severity: "error",
|
|
6350
|
+
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.`,
|
|
6351
|
+
fixExample: {
|
|
6352
|
+
bad: lineCode,
|
|
6353
|
+
good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
|
|
6354
|
+
}
|
|
6355
|
+
});
|
|
5978
6356
|
}
|
|
5979
|
-
}
|
|
6357
|
+
}
|
|
5980
6358
|
}
|
|
5981
6359
|
}
|
|
5982
6360
|
}
|
|
@@ -6365,6 +6743,6 @@ async function runCli(argv = process.argv) {
|
|
|
6365
6743
|
cli.parse(argv, { run: true });
|
|
6366
6744
|
}
|
|
6367
6745
|
|
|
6368
|
-
export { DEFAULT_REGISTRY_BASE, DependencyRuleError, EnvEditor, FeatureDependencyGraph, PromptCancelledError, computeDirectoryHash, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, discoverEnvironments, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateShared, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getAllFiles, getConfiguredFeaturesDir, getConfiguredSharedDir, getEffectiveMode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleEnv, handleImport, handleInit, handleLint, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, lintCodebase, listDomainEntities, listDomainInterfaces, listExistingFeatures, listExistingShared, listUseCases, loadInterfaceHierarchy, loadMasonConfig, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveEnvFilename, resolveFeaturesDir, resolveSharedDir, runCli, runEnvAdd, runEnvAudit, runEnvCommand, runEnvDelete, runEnvExamplesCommand, runEnvInit, runEnvInitExamples, runEnvMenu, runEnvUpdate, runSharedCommand, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
6746
|
+
export { DEFAULT_REGISTRY_BASE, DependencyRuleError, EnvEditor, FeatureDependencyGraph, PromptCancelledError, computeDirectoryHash, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, discoverEnvironments, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateShared, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getAllFiles, getConfiguredFeaturesDir, getConfiguredSharedDir, getEffectiveMode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleEnv, handleImport, handleInit, handleLint, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, lintCodebase, listDomainEntities, listDomainInterfaces, listExistingFeatures, listExistingShared, listUseCases, loadInterfaceHierarchy, loadLintConfig, loadMasonConfig, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveEnvFilename, resolveFeaturesDir, resolveSharedDir, runCli, runEnvAdd, runEnvAudit, runEnvCommand, runEnvDelete, runEnvExamplesCommand, runEnvInit, runEnvInitExamples, runEnvMenu, runEnvUpdate, runSharedCommand, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
6369
6747
|
//# sourceMappingURL=index.js.map
|
|
6370
6748
|
//# sourceMappingURL=index.js.map
|