@solid-stack/mason 1.0.32 → 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 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()) {
@@ -4937,6 +4947,15 @@ function classifyFile(fullPath, projectRoot) {
4937
4947
  }
4938
4948
  return { moduleType: "shared", moduleName, feature: moduleName, layer };
4939
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
+ }
4940
4959
  return {};
4941
4960
  }
4942
4961
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
@@ -4962,15 +4981,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4962
4981
  resolvedRel = specifier.replace(/^[@~]\//, "src/");
4963
4982
  } else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
4964
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);
4965
4988
  } else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
4966
4989
  resolvedRel = "src/" + specifier.slice(2);
4967
- } 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") {
4968
4991
  resolvedRel = specifier;
4969
4992
  }
4970
4993
  if (!resolvedRel) {
4971
4994
  return { isExternal: true, isDiFramework: false };
4972
4995
  }
4973
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
+ }
4974
5006
  const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
4975
5007
  if (featMatch && featMatch[1] && featMatch[2]) {
4976
5008
  const targetFeature = featMatch[1];
@@ -5511,11 +5543,45 @@ function lintCodebase(options = {}) {
5511
5543
  const startTime = Date.now();
5512
5544
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5513
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";
5514
5578
  const features = discoverAllFeatures(projectRoot);
5515
5579
  const sharedModules = discoverAllShared(projectRoot);
5580
+ const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
5516
5581
  const allModules = [
5517
5582
  ...features.map((f) => ({ ...f, type: "feature" })),
5518
- ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5583
+ ...sharedModules.map((s) => ({ ...s, type: "shared" })),
5584
+ errorModule
5519
5585
  ];
5520
5586
  const graph = new FeatureDependencyGraph();
5521
5587
  const allViolations = [];
@@ -5545,7 +5611,7 @@ Code: ${v.code}`;
5545
5611
  scannedFilesCount: 0,
5546
5612
  passed: true,
5547
5613
  violations: [],
5548
- layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5614
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
5549
5615
  });
5550
5616
  }
5551
5617
  for (const mod of allModules) {
@@ -5560,6 +5626,121 @@ Code: ${v.code}`;
5560
5626
  const statements = extractImportStatements(content);
5561
5627
  const baseName = path2__default.default.basename(file);
5562
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
+ }
5563
5744
  if (moduleType === "shared") {
5564
5745
  if (layer === "domain") {
5565
5746
  const lines = content.split(/\r?\n/);