@solid-stack/mason 1.0.31 → 1.0.32

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/index.js CHANGED
@@ -4886,22 +4886,38 @@ function scanDirFiles(dir, projectRoot) {
4886
4886
  function classifyFile(fullPath, projectRoot) {
4887
4887
  const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
4888
4888
  const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
4889
- if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
4890
- const feature = featureMatch[1];
4891
- const subfolder = featureMatch[2];
4892
- let layer;
4893
- if (subfolder === "domain" || subfolder === "errors") {
4894
- layer = "domain";
4895
- } else if (subfolder === "useCases" || subfolder === "usecases") {
4896
- layer = "useCases";
4897
- } else if (subfolder === "services") {
4898
- layer = "services";
4899
- } else if (subfolder === "infrastructure") {
4900
- layer = "infrastructure";
4901
- } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4902
- layer = "presentation";
4903
- }
4904
- return { feature, layer };
4889
+ if (featureMatch && featureMatch[1] && featureMatch[2]) {
4890
+ const feature = featureMatch[1];
4891
+ const subfolder = featureMatch[2];
4892
+ let layer;
4893
+ if (subfolder === "domain" || subfolder === "errors") {
4894
+ layer = "domain";
4895
+ } else if (subfolder === "useCases" || subfolder === "usecases") {
4896
+ layer = "useCases";
4897
+ } else if (subfolder === "services") {
4898
+ layer = "services";
4899
+ } else if (subfolder === "infrastructure") {
4900
+ layer = "infrastructure";
4901
+ } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4902
+ layer = "presentation";
4903
+ }
4904
+ return { moduleType: "feature", moduleName: feature, feature, layer };
4905
+ }
4906
+ const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
4907
+ if (sharedMatch && sharedMatch[1]) {
4908
+ const moduleName = sharedMatch[1];
4909
+ const rest = sharedMatch[2] || "";
4910
+ let layer = "root";
4911
+ if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
4912
+ layer = "domain";
4913
+ } else if (rest.startsWith("ports/") || rest === "ports") {
4914
+ layer = "ports";
4915
+ } else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
4916
+ layer = "infrastructure";
4917
+ }
4918
+ return { moduleType: "shared", moduleName, feature: moduleName, layer };
4919
+ }
4920
+ return {};
4905
4921
  }
4906
4922
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4907
4923
  if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
@@ -4956,6 +4972,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4956
4972
  const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
4957
4973
  const isTargetPort = /^I[A-Z]/.test(targetFileName);
4958
4974
  const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
4975
+ const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
4959
4976
  return {
4960
4977
  isExternal: false,
4961
4978
  isDiFramework: false,
@@ -4964,7 +4981,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4964
4981
  targetSubfolder,
4965
4982
  targetFileName,
4966
4983
  isTargetPort,
4967
- isTargetRepo
4984
+ isTargetRepo,
4985
+ isTargetError
4968
4986
  };
4969
4987
  }
4970
4988
  const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
@@ -4990,17 +5008,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4990
5008
  function extractImportStatements(content) {
4991
5009
  const statements = [];
4992
5010
  const lines = content.split(/\r?\n/);
4993
- 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;
5011
+ 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
5012
  let match;
4995
5013
  while ((match = regex.exec(content)) !== null) {
4996
- const specifier = match[1];
5014
+ const isTypeOnly = Boolean(match[1] || match[3]);
5015
+ const importClause = (match[2] || match[4] || "").trim();
5016
+ const specifier = match[5];
4997
5017
  const charIndex = match.index;
4998
5018
  const lineNum = content.substring(0, charIndex).split("\n").length;
4999
5019
  const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
5020
+ const importedNames = [];
5021
+ if (importClause) {
5022
+ const cleaned = importClause.replace(/[{}]/g, "");
5023
+ const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
5024
+ for (const w of words) {
5025
+ const parts = w.split(/\s+as\s+/);
5026
+ const name = parts[0].replace(/^type\s+/, "").trim();
5027
+ const alias = (parts[1] || name).trim();
5028
+ if (alias && alias !== "*") {
5029
+ importedNames.push(alias);
5030
+ }
5031
+ }
5032
+ }
5000
5033
  statements.push({
5001
5034
  specifier,
5002
5035
  line: lineNum,
5003
- rawCode
5036
+ rawCode,
5037
+ isTypeOnly,
5038
+ importedNames
5004
5039
  });
5005
5040
  }
5006
5041
  return statements;
@@ -5099,6 +5134,15 @@ function discoverAllFeatures(projectRoot) {
5099
5134
  dir: path2.join(featuresBase, e.name)
5100
5135
  })).sort((a, b) => a.name.localeCompare(b.name));
5101
5136
  }
5137
+ function discoverAllShared(projectRoot) {
5138
+ const sharedBase = resolveSharedDir(projectRoot);
5139
+ if (!fs4.existsSync(sharedBase)) return [];
5140
+ const entries = fs4.readdirSync(sharedBase, { withFileTypes: true });
5141
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
5142
+ name: e.name,
5143
+ dir: path2.join(sharedBase, e.name)
5144
+ })).sort((a, b) => a.name.localeCompare(b.name));
5145
+ }
5102
5146
  function extractClassMethods(content, className) {
5103
5147
  let clean = "";
5104
5148
  let inLineComment = false;
@@ -5448,16 +5492,21 @@ function lintCodebase(options = {}) {
5448
5492
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5449
5493
  const failFast = !options.complete;
5450
5494
  const features = discoverAllFeatures(projectRoot);
5495
+ const sharedModules = discoverAllShared(projectRoot);
5496
+ const allModules = [
5497
+ ...features.map((f) => ({ ...f, type: "feature" })),
5498
+ ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5499
+ ];
5451
5500
  const graph = new FeatureDependencyGraph();
5452
5501
  const allViolations = [];
5453
5502
  const results = [];
5454
5503
  const detectedCycles = [];
5455
5504
  const addViolation = (v) => {
5456
5505
  allViolations.push(v);
5457
- const featureResult = results.find((r) => r.featureName === v.feature);
5458
- if (featureResult) {
5459
- featureResult.violations.push(v);
5460
- featureResult.passed = false;
5506
+ const moduleResult = results.find((r) => r.featureName === v.feature);
5507
+ if (moduleResult) {
5508
+ moduleResult.violations.push(v);
5509
+ moduleResult.passed = false;
5461
5510
  }
5462
5511
  if (failFast) {
5463
5512
  const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
@@ -5466,28 +5515,156 @@ Code: ${v.code}`;
5466
5515
  throw new DependencyRuleError(errorMsg, [v], true);
5467
5516
  }
5468
5517
  };
5469
- for (const feat of features) {
5518
+ for (const mod of allModules) {
5470
5519
  results.push({
5471
- featureName: feat.name,
5472
- featureDir: feat.dir,
5520
+ moduleType: mod.type,
5521
+ moduleName: mod.name,
5522
+ featureName: mod.name,
5523
+ featureDir: mod.dir,
5524
+ moduleDir: mod.dir,
5473
5525
  scannedFilesCount: 0,
5474
5526
  passed: true,
5475
5527
  violations: [],
5476
- layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
5528
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5477
5529
  });
5478
5530
  }
5479
- for (const feat of features) {
5480
- const featResult = results.find((r) => r.featureName === feat.name);
5481
- const allFiles = scanDirFiles(feat.dir, projectRoot);
5482
- featResult.scannedFilesCount = allFiles.length;
5531
+ for (const mod of allModules) {
5532
+ const modResult = results.find((r) => r.featureName === mod.name);
5533
+ const allFiles = scanDirFiles(mod.dir, projectRoot);
5534
+ modResult.scannedFilesCount = allFiles.length;
5483
5535
  for (const file of allFiles) {
5484
5536
  const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
5485
- const { layer } = classifyFile(file, projectRoot);
5486
- if (!layer) continue;
5537
+ const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
5538
+ if (!layer || !moduleType || !moduleName) continue;
5487
5539
  const content = fs4.readFileSync(file, "utf-8");
5488
5540
  const statements = extractImportStatements(content);
5489
5541
  const baseName = path2.basename(file);
5490
5542
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5543
+ if (moduleType === "shared") {
5544
+ if (layer === "domain") {
5545
+ const lines = content.split(/\r?\n/);
5546
+ for (let i = 0; i < lines.length; i++) {
5547
+ const lineText = lines[i].trim();
5548
+ if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
5549
+ addViolation({
5550
+ feature: moduleName,
5551
+ layer,
5552
+ file,
5553
+ relativePath: relPath,
5554
+ line: i + 1,
5555
+ category: "Domain Dependence Violation",
5556
+ code: lineText,
5557
+ ruleId: "D2_DI_DEPENDENCE",
5558
+ severity: "error",
5559
+ advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
5560
+ fixExample: {
5561
+ bad: lineText,
5562
+ good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
5563
+ }
5564
+ });
5565
+ }
5566
+ }
5567
+ }
5568
+ for (const stmt of statements) {
5569
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5570
+ if (target.targetFeature) {
5571
+ addViolation({
5572
+ feature: moduleName,
5573
+ layer,
5574
+ file,
5575
+ relativePath: relPath,
5576
+ line: stmt.line,
5577
+ category: "Shared Architecture Violation",
5578
+ code: stmt.rawCode,
5579
+ ruleId: "SF1_FEATURE_DEPENDENCY",
5580
+ severity: "error",
5581
+ advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
5582
+ fixExample: {
5583
+ bad: stmt.rawCode,
5584
+ good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
5585
+ }
5586
+ });
5587
+ }
5588
+ if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
5589
+ if (target.isSharedInfra || target.isSharedPorts) {
5590
+ addViolation({
5591
+ feature: moduleName,
5592
+ layer,
5593
+ file,
5594
+ relativePath: relPath,
5595
+ line: stmt.line,
5596
+ category: "Shared Architecture Violation",
5597
+ code: stmt.rawCode,
5598
+ ruleId: "SS1_SHARED_ENCAPSULATION",
5599
+ severity: "error",
5600
+ advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
5601
+ fixExample: {
5602
+ bad: stmt.rawCode,
5603
+ good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
5604
+ }
5605
+ });
5606
+ }
5607
+ }
5608
+ if (layer === "domain") {
5609
+ if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
5610
+ addViolation({
5611
+ feature: moduleName,
5612
+ layer,
5613
+ file,
5614
+ relativePath: relPath,
5615
+ line: stmt.line,
5616
+ category: "Domain Purity Violation",
5617
+ code: stmt.rawCode,
5618
+ ruleId: "SD1_OUTER_IMPORT",
5619
+ severity: "error",
5620
+ advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
5621
+ fixExample: {
5622
+ bad: stmt.rawCode,
5623
+ good: "// Model pure business types and value objects directly within domain/."
5624
+ }
5625
+ });
5626
+ }
5627
+ if (target.isDiFramework) {
5628
+ addViolation({
5629
+ feature: moduleName,
5630
+ layer,
5631
+ file,
5632
+ relativePath: relPath,
5633
+ line: stmt.line,
5634
+ category: "Domain Dependence Violation",
5635
+ code: stmt.rawCode,
5636
+ ruleId: "D2_DI_DEPENDENCE",
5637
+ severity: "error",
5638
+ advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
5639
+ fixExample: {
5640
+ bad: stmt.rawCode,
5641
+ good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
5642
+ }
5643
+ });
5644
+ }
5645
+ }
5646
+ if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
5647
+ addViolation({
5648
+ feature: moduleName,
5649
+ layer,
5650
+ file,
5651
+ relativePath: relPath,
5652
+ line: stmt.line,
5653
+ category: "Shared Architecture Violation",
5654
+ code: stmt.rawCode,
5655
+ ruleId: "SP1_INFRA_IN_PORT",
5656
+ severity: "error",
5657
+ advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
5658
+ fixExample: {
5659
+ bad: stmt.rawCode,
5660
+ good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
5661
+ }
5662
+ });
5663
+ }
5664
+ }
5665
+ continue;
5666
+ }
5667
+ const feat = mod;
5491
5668
  if (layer === "useCases" && !isBarrel) {
5492
5669
  checkUseCaseShape(feat.name, file, relPath, content, addViolation);
5493
5670
  }
@@ -5556,23 +5733,33 @@ Code: ${v.code}`;
5556
5733
  }
5557
5734
  });
5558
5735
  }
5559
- if (target.targetLayer === "domain" && !target.isTargetPort) {
5560
- addViolation({
5561
- feature: feat.name,
5562
- layer,
5563
- file,
5564
- relativePath: relPath,
5565
- line: stmt.line,
5566
- category: "Cross-Feature Domain Violation",
5567
- code: stmt.rawCode,
5568
- ruleId: "F1_CROSS_FEATURE_DOMAIN",
5569
- severity: "error",
5570
- advice: `Cannot import domain entities/factories directly from foreign feature '${target.targetFeature}'. This bypasses its use cases and breaks Bounded Context encapsulation. Communicate via ports instead.`,
5571
- fixExample: {
5572
- bad: stmt.rawCode,
5573
- good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/ (which can then call ${target.targetFeature}'s Use Case).`
5736
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
5737
+ for (const name of stmt.importedNames) {
5738
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
5739
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
5740
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
5741
+ if (execMatch) {
5742
+ const charIndex = execMatch.index;
5743
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5744
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
5745
+ addViolation({
5746
+ feature: feat.name,
5747
+ layer,
5748
+ file,
5749
+ relativePath: relPath,
5750
+ line: lineNum,
5751
+ category: "Cross-Feature Domain Violation",
5752
+ code: lineCode,
5753
+ ruleId: "F1_CROSS_FEATURE_DOMAIN",
5754
+ severity: "error",
5755
+ 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.`,
5756
+ fixExample: {
5757
+ bad: lineCode,
5758
+ good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
5759
+ }
5760
+ });
5574
5761
  }
5575
- });
5762
+ }
5576
5763
  }
5577
5764
  }
5578
5765
  if (target.isGlobalConfig) {
@@ -5960,23 +6147,33 @@ Code: ${v.code}`;
5960
6147
  }
5961
6148
  });
5962
6149
  }
5963
- if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
5964
- addViolation({
5965
- feature: feat.name,
5966
- layer,
5967
- file,
5968
- relativePath: relPath,
5969
- line: stmt.line,
5970
- category: "Presentation Layer Violation",
5971
- code: stmt.rawCode,
5972
- ruleId: "P5_DIRECT_DOMAIN_ENTITY",
5973
- severity: "error",
5974
- advice: "Presentation handlers must not directly instantiate or import domain entities/factories. They must map requests to flat DTOs and pass them to the Use Case.",
5975
- fixExample: {
5976
- bad: stmt.rawCode,
5977
- good: "// Construct a DTO and pass it into the injected Use Case's execute() method."
6150
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
6151
+ for (const name of stmt.importedNames) {
6152
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
6153
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
6154
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
6155
+ if (execMatch) {
6156
+ const charIndex = execMatch.index;
6157
+ const lineNum = content.substring(0, charIndex).split("\n").length;
6158
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
6159
+ addViolation({
6160
+ feature: feat.name,
6161
+ layer,
6162
+ file,
6163
+ relativePath: relPath,
6164
+ line: lineNum,
6165
+ category: "Presentation Layer Violation",
6166
+ code: lineCode,
6167
+ ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6168
+ severity: "error",
6169
+ 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.`,
6170
+ fixExample: {
6171
+ bad: lineCode,
6172
+ good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
6173
+ }
6174
+ });
5978
6175
  }
5979
- });
6176
+ }
5980
6177
  }
5981
6178
  }
5982
6179
  }