@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.js CHANGED
@@ -189,6 +189,16 @@ function getConfiguredSharedDir(projectRoot, flags) {
189
189
  }
190
190
  return path2.join(projectRoot, "shared");
191
191
  }
192
+ function loadLintConfig(projectRoot) {
193
+ const configPath = path2.join(projectRoot, "mason-lint.config.json");
194
+ if (fs14.existsSync(configPath)) {
195
+ try {
196
+ return JSON.parse(fs14.readFileSync(configPath, "utf-8"));
197
+ } catch {
198
+ }
199
+ }
200
+ return { baseErrorPath: "src/errors/AppError.ts" };
201
+ }
192
202
 
193
203
  // src/utils/project.ts
194
204
  function findProjectRoot(startDir = process.cwd()) {
@@ -4908,6 +4918,15 @@ function classifyFile(fullPath, projectRoot) {
4908
4918
  }
4909
4919
  return { moduleType: "shared", moduleName, feature: moduleName, layer };
4910
4920
  }
4921
+ const errorMatch = rel.match(/(?:^|\/)(?:src\/)?errors(?:\/(.*))?$/);
4922
+ if (errorMatch) {
4923
+ return {
4924
+ moduleType: "error",
4925
+ moduleName: "global-errors",
4926
+ feature: "global-errors",
4927
+ layer: "domain"
4928
+ };
4929
+ }
4911
4930
  return {};
4912
4931
  }
4913
4932
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
@@ -4933,15 +4952,28 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4933
4952
  resolvedRel = specifier.replace(/^[@~]\//, "src/");
4934
4953
  } else if (specifier === "@configs" || specifier.startsWith("@configs/")) {
4935
4954
  resolvedRel = "src/" + specifier.slice(1);
4955
+ } else if (specifier.startsWith("@/errors/") || specifier.startsWith("~/errors/") || specifier === "@/errors" || specifier === "~/errors") {
4956
+ resolvedRel = specifier.replace(/^[@~]\//, "src/");
4957
+ } else if (specifier === "@errors" || specifier.startsWith("@errors/")) {
4958
+ resolvedRel = "src/" + specifier.slice(1);
4936
4959
  } else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
4937
4960
  resolvedRel = "src/" + specifier.slice(2);
4938
- } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs") {
4961
+ } else if (specifier.startsWith("features/") || specifier.startsWith("shared/") || specifier.startsWith("configs/") || specifier === "configs" || specifier.startsWith("errors/") || specifier === "errors") {
4939
4962
  resolvedRel = specifier;
4940
4963
  }
4941
4964
  if (!resolvedRel) {
4942
4965
  return { isExternal: true, isDiFramework: false };
4943
4966
  }
4944
4967
  const isGlobalConfig = resolvedRel.startsWith("src/configs/") || resolvedRel === "src/configs" || resolvedRel.startsWith("configs/") || resolvedRel === "configs";
4968
+ const isGlobalError = resolvedRel.startsWith("src/errors/") || resolvedRel === "src/errors" || resolvedRel.startsWith("errors/") || resolvedRel === "errors";
4969
+ if (isGlobalError) {
4970
+ return {
4971
+ isExternal: false,
4972
+ isDiFramework: false,
4973
+ isGlobalError: true,
4974
+ isTargetError: true
4975
+ };
4976
+ }
4945
4977
  const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
4946
4978
  if (featMatch && featMatch[1] && featMatch[2]) {
4947
4979
  const targetFeature = featMatch[1];
@@ -5482,11 +5514,45 @@ function lintCodebase(options = {}) {
5482
5514
  const startTime = Date.now();
5483
5515
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5484
5516
  const failFast = !options.complete;
5517
+ const lintConfig = loadLintConfig(projectRoot);
5518
+ const baseErrorRelative = lintConfig.baseErrorPath || "src/errors/AppError.ts";
5519
+ const baseErrorAbs = path2.resolve(projectRoot, baseErrorRelative);
5520
+ const featuresBase = resolveFeaturesDir(projectRoot);
5521
+ const sharedBase = resolveSharedDir(projectRoot);
5522
+ let errorsBase = path2.join(projectRoot, "src/errors");
5523
+ if (!fs14.existsSync(errorsBase) && fs14.existsSync(path2.join(projectRoot, "errors"))) {
5524
+ errorsBase = path2.join(projectRoot, "errors");
5525
+ }
5526
+ const missingFoundations = [];
5527
+ if (!fs14.existsSync(featuresBase)) missingFoundations.push("features/");
5528
+ if (!fs14.existsSync(sharedBase)) missingFoundations.push("shared/");
5529
+ if (!fs14.existsSync(errorsBase)) missingFoundations.push("errors/");
5530
+ if (missingFoundations.length > 0) {
5531
+ throw new DependencyRuleError(
5532
+ `Project Structure Violation: Missing required foundational directories: ${missingFoundations.join(", ")}. Every project must contain 'features/', 'shared/', and 'errors/'.`,
5533
+ [],
5534
+ true
5535
+ );
5536
+ }
5537
+ if (!fs14.existsSync(baseErrorAbs)) {
5538
+ throw new DependencyRuleError(
5539
+ `Project Structure Violation: Base error class not found at '${baseErrorRelative}'. Create this file or update 'baseErrorPath' in mason-lint.config.json.`,
5540
+ [],
5541
+ true
5542
+ );
5543
+ }
5544
+ const baseErrorContent = fs14.readFileSync(baseErrorAbs, "utf-8");
5545
+ const baseClassMatch = baseErrorContent.match(
5546
+ /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/
5547
+ );
5548
+ const baseErrorClassName = baseClassMatch ? baseClassMatch[1] : "AppError";
5485
5549
  const features = discoverAllFeatures(projectRoot);
5486
5550
  const sharedModules = discoverAllShared(projectRoot);
5551
+ const errorModule = { name: "global-errors", dir: errorsBase, type: "error" };
5487
5552
  const allModules = [
5488
5553
  ...features.map((f) => ({ ...f, type: "feature" })),
5489
- ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5554
+ ...sharedModules.map((s) => ({ ...s, type: "shared" })),
5555
+ errorModule
5490
5556
  ];
5491
5557
  const graph = new FeatureDependencyGraph();
5492
5558
  const allViolations = [];
@@ -5516,7 +5582,7 @@ Code: ${v.code}`;
5516
5582
  scannedFilesCount: 0,
5517
5583
  passed: true,
5518
5584
  violations: [],
5519
- layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5585
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : mod.type === "shared" ? ["domain", "ports", "infrastructure", "root"] : ["domain"]
5520
5586
  });
5521
5587
  }
5522
5588
  for (const mod of allModules) {
@@ -5531,6 +5597,121 @@ Code: ${v.code}`;
5531
5597
  const statements = extractImportStatements(content);
5532
5598
  const baseName = path2.basename(file);
5533
5599
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5600
+ const throwErrorRegex = /\bthrow\s+(?:new\s+)?Error\s*\(/g;
5601
+ let throwMatch;
5602
+ while ((throwMatch = throwErrorRegex.exec(content)) !== null) {
5603
+ const charIndex = throwMatch.index;
5604
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5605
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || throwMatch[0];
5606
+ if (lineCode.startsWith("//")) continue;
5607
+ addViolation({
5608
+ feature: moduleName,
5609
+ layer,
5610
+ file,
5611
+ relativePath: relPath,
5612
+ line: lineNum,
5613
+ category: "Plain Error Throw Violation",
5614
+ code: lineCode,
5615
+ ruleId: "E3_PLAIN_ERROR_THROW",
5616
+ severity: "error",
5617
+ advice: `Throwing plain 'Error' is prohibited. You must throw '${baseErrorClassName}' or a domain-specific error class that inherits from it.`,
5618
+ fixExample: {
5619
+ bad: lineCode,
5620
+ good: `throw new ${baseErrorClassName}("Something went wrong");`
5621
+ }
5622
+ });
5623
+ }
5624
+ 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");
5625
+ const isBaseErrorFile = path2.resolve(file) === path2.resolve(baseErrorAbs);
5626
+ if (isErrorFile && !isBaseErrorFile) {
5627
+ const classRegex = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_]+))?/g;
5628
+ let classMatch;
5629
+ while ((classMatch = classRegex.exec(content)) !== null) {
5630
+ const className = classMatch[1];
5631
+ const extendsName = classMatch[2];
5632
+ const charIndex = classMatch.index;
5633
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5634
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || classMatch[0];
5635
+ if (lineCode.startsWith("//")) continue;
5636
+ if (!extendsName) {
5637
+ addViolation({
5638
+ feature: moduleName,
5639
+ layer,
5640
+ file,
5641
+ relativePath: relPath,
5642
+ line: lineNum,
5643
+ category: "Error Inheritance Violation",
5644
+ code: lineCode,
5645
+ ruleId: "E4_MISSING_INHERITANCE",
5646
+ severity: "error",
5647
+ advice: `Error class '${className}' must extend '${baseErrorClassName}' (or another class that derives from it).`,
5648
+ fixExample: {
5649
+ bad: lineCode,
5650
+ good: `export class ${className} extends ${baseErrorClassName} { ... }`
5651
+ }
5652
+ });
5653
+ } else if (extendsName === "Error") {
5654
+ addViolation({
5655
+ feature: moduleName,
5656
+ layer,
5657
+ file,
5658
+ relativePath: relPath,
5659
+ line: lineNum,
5660
+ category: "Error Inheritance Violation",
5661
+ code: lineCode,
5662
+ ruleId: "E5_NATIVE_ERROR_INHERITANCE",
5663
+ severity: "error",
5664
+ advice: `Error class '${className}' extends the native 'Error'. It must extend your base error class ('${baseErrorClassName}').`,
5665
+ fixExample: {
5666
+ bad: lineCode,
5667
+ good: `export class ${className} extends ${baseErrorClassName} { ... }`
5668
+ }
5669
+ });
5670
+ }
5671
+ }
5672
+ }
5673
+ if (moduleType === "error") {
5674
+ for (const stmt of statements) {
5675
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5676
+ if (target.targetFeature) {
5677
+ addViolation({
5678
+ feature: "global-errors",
5679
+ layer: "domain",
5680
+ file,
5681
+ relativePath: relPath,
5682
+ line: stmt.line,
5683
+ category: "Global Error Purity Violation",
5684
+ code: stmt.rawCode,
5685
+ ruleId: "E1_FEATURE_DEPENDENCY",
5686
+ severity: "error",
5687
+ advice: `Global errors must not depend on features ('${target.targetFeature}'). Global errors represent pure domain or system states.`,
5688
+ fixExample: {
5689
+ bad: stmt.rawCode,
5690
+ good: "// Global errors must not reference specific features. Keep error types pure."
5691
+ }
5692
+ });
5693
+ }
5694
+ if (target.targetSharedModule && !target.isSharedDomain) {
5695
+ addViolation({
5696
+ feature: "global-errors",
5697
+ layer: "domain",
5698
+ file,
5699
+ relativePath: relPath,
5700
+ line: stmt.line,
5701
+ category: "Global Error Purity Violation",
5702
+ code: stmt.rawCode,
5703
+ ruleId: "E2_SHARED_NON_DOMAIN",
5704
+ severity: "error",
5705
+ advice: `Global errors can only depend on shared domain elements (e.g., value objects). They cannot depend on shared ports or infrastructure.`,
5706
+ fixExample: {
5707
+ bad: stmt.rawCode,
5708
+ good: "// Depend only on pure domain types from shared modules."
5709
+ }
5710
+ });
5711
+ }
5712
+ }
5713
+ continue;
5714
+ }
5534
5715
  if (moduleType === "shared") {
5535
5716
  if (layer === "domain") {
5536
5717
  const lines = content.split(/\r?\n/);