@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/bin.cjs CHANGED
@@ -4906,22 +4906,38 @@ function scanDirFiles(dir, projectRoot) {
4906
4906
  function classifyFile(fullPath, projectRoot) {
4907
4907
  const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
4908
4908
  const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
4909
- if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
4910
- const feature = featureMatch[1];
4911
- const subfolder = featureMatch[2];
4912
- let layer;
4913
- if (subfolder === "domain" || subfolder === "errors") {
4914
- layer = "domain";
4915
- } else if (subfolder === "useCases" || subfolder === "usecases") {
4916
- layer = "useCases";
4917
- } else if (subfolder === "services") {
4918
- layer = "services";
4919
- } else if (subfolder === "infrastructure") {
4920
- layer = "infrastructure";
4921
- } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4922
- layer = "presentation";
4923
- }
4924
- return { feature, layer };
4909
+ if (featureMatch && featureMatch[1] && featureMatch[2]) {
4910
+ const feature = featureMatch[1];
4911
+ const subfolder = featureMatch[2];
4912
+ let layer;
4913
+ if (subfolder === "domain" || subfolder === "errors") {
4914
+ layer = "domain";
4915
+ } else if (subfolder === "useCases" || subfolder === "usecases") {
4916
+ layer = "useCases";
4917
+ } else if (subfolder === "services") {
4918
+ layer = "services";
4919
+ } else if (subfolder === "infrastructure") {
4920
+ layer = "infrastructure";
4921
+ } else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
4922
+ layer = "presentation";
4923
+ }
4924
+ return { moduleType: "feature", moduleName: feature, feature, layer };
4925
+ }
4926
+ const sharedMatch = rel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
4927
+ if (sharedMatch && sharedMatch[1]) {
4928
+ const moduleName = sharedMatch[1];
4929
+ const rest = sharedMatch[2] || "";
4930
+ let layer = "root";
4931
+ if (rest.startsWith("domain/") || rest === "domain" || rest.startsWith("errors/") || rest === "errors") {
4932
+ layer = "domain";
4933
+ } else if (rest.startsWith("ports/") || rest === "ports") {
4934
+ layer = "ports";
4935
+ } else if (rest.startsWith("infrastructure/") || rest === "infrastructure") {
4936
+ layer = "infrastructure";
4937
+ }
4938
+ return { moduleType: "shared", moduleName, feature: moduleName, layer };
4939
+ }
4940
+ return {};
4925
4941
  }
4926
4942
  function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4927
4943
  if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
@@ -4976,6 +4992,7 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4976
4992
  const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
4977
4993
  const isTargetPort = /^I[A-Z]/.test(targetFileName);
4978
4994
  const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
4995
+ const isTargetError = targetSubfolder === "errors" || rest.startsWith("errors/") || rest.includes("/errors/") || targetFileName.endsWith("Error") || targetFileName.endsWith("Errors");
4979
4996
  return {
4980
4997
  isExternal: false,
4981
4998
  isDiFramework: false,
@@ -4984,7 +5001,8 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
4984
5001
  targetSubfolder,
4985
5002
  targetFileName,
4986
5003
  isTargetPort,
4987
- isTargetRepo
5004
+ isTargetRepo,
5005
+ isTargetError
4988
5006
  };
4989
5007
  }
4990
5008
  const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
@@ -5010,17 +5028,34 @@ function classifyTarget(specifier, sourceFileAbs, projectRoot) {
5010
5028
  function extractImportStatements(content) {
5011
5029
  const statements = [];
5012
5030
  const lines = content.split(/\r?\n/);
5013
- 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;
5031
+ 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;
5014
5032
  let match;
5015
5033
  while ((match = regex.exec(content)) !== null) {
5016
- const specifier = match[1];
5034
+ const isTypeOnly = Boolean(match[1] || match[3]);
5035
+ const importClause = (match[2] || match[4] || "").trim();
5036
+ const specifier = match[5];
5017
5037
  const charIndex = match.index;
5018
5038
  const lineNum = content.substring(0, charIndex).split("\n").length;
5019
5039
  const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
5040
+ const importedNames = [];
5041
+ if (importClause) {
5042
+ const cleaned = importClause.replace(/[{}]/g, "");
5043
+ const words = cleaned.split(",").map((s) => s.trim()).filter(Boolean);
5044
+ for (const w of words) {
5045
+ const parts = w.split(/\s+as\s+/);
5046
+ const name = parts[0].replace(/^type\s+/, "").trim();
5047
+ const alias = (parts[1] || name).trim();
5048
+ if (alias && alias !== "*") {
5049
+ importedNames.push(alias);
5050
+ }
5051
+ }
5052
+ }
5020
5053
  statements.push({
5021
5054
  specifier,
5022
5055
  line: lineNum,
5023
- rawCode
5056
+ rawCode,
5057
+ isTypeOnly,
5058
+ importedNames
5024
5059
  });
5025
5060
  }
5026
5061
  return statements;
@@ -5119,6 +5154,15 @@ function discoverAllFeatures(projectRoot) {
5119
5154
  dir: path2__default.default.join(featuresBase, e.name)
5120
5155
  })).sort((a, b) => a.name.localeCompare(b.name));
5121
5156
  }
5157
+ function discoverAllShared(projectRoot) {
5158
+ const sharedBase = resolveSharedDir(projectRoot);
5159
+ if (!fs14__default.default.existsSync(sharedBase)) return [];
5160
+ const entries = fs14__default.default.readdirSync(sharedBase, { withFileTypes: true });
5161
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
5162
+ name: e.name,
5163
+ dir: path2__default.default.join(sharedBase, e.name)
5164
+ })).sort((a, b) => a.name.localeCompare(b.name));
5165
+ }
5122
5166
  function extractClassMethods(content, className) {
5123
5167
  let clean = "";
5124
5168
  let inLineComment = false;
@@ -5468,16 +5512,21 @@ function lintCodebase(options = {}) {
5468
5512
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
5469
5513
  const failFast = !options.complete;
5470
5514
  const features = discoverAllFeatures(projectRoot);
5515
+ const sharedModules = discoverAllShared(projectRoot);
5516
+ const allModules = [
5517
+ ...features.map((f) => ({ ...f, type: "feature" })),
5518
+ ...sharedModules.map((s) => ({ ...s, type: "shared" }))
5519
+ ];
5471
5520
  const graph = new FeatureDependencyGraph();
5472
5521
  const allViolations = [];
5473
5522
  const results = [];
5474
5523
  const detectedCycles = [];
5475
5524
  const addViolation = (v) => {
5476
5525
  allViolations.push(v);
5477
- const featureResult = results.find((r) => r.featureName === v.feature);
5478
- if (featureResult) {
5479
- featureResult.violations.push(v);
5480
- featureResult.passed = false;
5526
+ const moduleResult = results.find((r) => r.featureName === v.feature);
5527
+ if (moduleResult) {
5528
+ moduleResult.violations.push(v);
5529
+ moduleResult.passed = false;
5481
5530
  }
5482
5531
  if (failFast) {
5483
5532
  const errorMsg = `Dependency rule violation detected in ${v.relativePath}:${v.line}:
@@ -5486,28 +5535,156 @@ Code: ${v.code}`;
5486
5535
  throw new DependencyRuleError(errorMsg, [v], true);
5487
5536
  }
5488
5537
  };
5489
- for (const feat of features) {
5538
+ for (const mod of allModules) {
5490
5539
  results.push({
5491
- featureName: feat.name,
5492
- featureDir: feat.dir,
5540
+ moduleType: mod.type,
5541
+ moduleName: mod.name,
5542
+ featureName: mod.name,
5543
+ featureDir: mod.dir,
5544
+ moduleDir: mod.dir,
5493
5545
  scannedFilesCount: 0,
5494
5546
  passed: true,
5495
5547
  violations: [],
5496
- layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
5548
+ layersChecked: mod.type === "feature" ? ["domain", "useCases", "services", "infrastructure", "presentation"] : ["domain", "ports", "infrastructure", "root"]
5497
5549
  });
5498
5550
  }
5499
- for (const feat of features) {
5500
- const featResult = results.find((r) => r.featureName === feat.name);
5501
- const allFiles = scanDirFiles(feat.dir, projectRoot);
5502
- featResult.scannedFilesCount = allFiles.length;
5551
+ for (const mod of allModules) {
5552
+ const modResult = results.find((r) => r.featureName === mod.name);
5553
+ const allFiles = scanDirFiles(mod.dir, projectRoot);
5554
+ modResult.scannedFilesCount = allFiles.length;
5503
5555
  for (const file of allFiles) {
5504
5556
  const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
5505
- const { layer } = classifyFile(file, projectRoot);
5506
- if (!layer) continue;
5557
+ const { moduleType, moduleName, layer } = classifyFile(file, projectRoot);
5558
+ if (!layer || !moduleType || !moduleName) continue;
5507
5559
  const content = fs14__default.default.readFileSync(file, "utf-8");
5508
5560
  const statements = extractImportStatements(content);
5509
5561
  const baseName = path2__default.default.basename(file);
5510
5562
  const isBarrel = baseName === "index.ts" || baseName === "index.js";
5563
+ if (moduleType === "shared") {
5564
+ if (layer === "domain") {
5565
+ const lines = content.split(/\r?\n/);
5566
+ for (let i = 0; i < lines.length; i++) {
5567
+ const lineText = lines[i].trim();
5568
+ if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
5569
+ addViolation({
5570
+ feature: moduleName,
5571
+ layer,
5572
+ file,
5573
+ relativePath: relPath,
5574
+ line: i + 1,
5575
+ category: "Domain Dependence Violation",
5576
+ code: lineText,
5577
+ ruleId: "D2_DI_DEPENDENCE",
5578
+ severity: "error",
5579
+ advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
5580
+ fixExample: {
5581
+ bad: lineText,
5582
+ good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
5583
+ }
5584
+ });
5585
+ }
5586
+ }
5587
+ }
5588
+ for (const stmt of statements) {
5589
+ const target = classifyTarget(stmt.specifier, file, projectRoot);
5590
+ if (target.targetFeature) {
5591
+ addViolation({
5592
+ feature: moduleName,
5593
+ layer,
5594
+ file,
5595
+ relativePath: relPath,
5596
+ line: stmt.line,
5597
+ category: "Shared Architecture Violation",
5598
+ code: stmt.rawCode,
5599
+ ruleId: "SF1_FEATURE_DEPENDENCY",
5600
+ severity: "error",
5601
+ advice: `Shared modules ('${moduleName}') are foundational and must NEVER import from feature modules ('${target.targetFeature}').`,
5602
+ fixExample: {
5603
+ bad: stmt.rawCode,
5604
+ good: "// Move shared logic/types to shared/ or communicate via domain events/callbacks."
5605
+ }
5606
+ });
5607
+ }
5608
+ if (target.targetSharedModule && target.targetSharedModule !== moduleName) {
5609
+ if (target.isSharedInfra || target.isSharedPorts) {
5610
+ addViolation({
5611
+ feature: moduleName,
5612
+ layer,
5613
+ file,
5614
+ relativePath: relPath,
5615
+ line: stmt.line,
5616
+ category: "Shared Architecture Violation",
5617
+ code: stmt.rawCode,
5618
+ ruleId: "SS1_SHARED_ENCAPSULATION",
5619
+ severity: "error",
5620
+ advice: `Shared module '${moduleName}' cannot import internal details (ports/infra) of shared module '${target.targetSharedModule}'. Depend only on its root classes or domain types.`,
5621
+ fixExample: {
5622
+ bad: stmt.rawCode,
5623
+ good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
5624
+ }
5625
+ });
5626
+ }
5627
+ }
5628
+ if (layer === "domain") {
5629
+ if (target.targetSharedModule === moduleName && (target.isSharedInfra || target.isSharedPorts || target.isSharedRoot)) {
5630
+ addViolation({
5631
+ feature: moduleName,
5632
+ layer,
5633
+ file,
5634
+ relativePath: relPath,
5635
+ line: stmt.line,
5636
+ category: "Domain Purity Violation",
5637
+ code: stmt.rawCode,
5638
+ ruleId: "SD1_OUTER_IMPORT",
5639
+ severity: "error",
5640
+ advice: `Shared domain layer must not import from its own ports, infrastructure, or root classes.`,
5641
+ fixExample: {
5642
+ bad: stmt.rawCode,
5643
+ good: "// Model pure business types and value objects directly within domain/."
5644
+ }
5645
+ });
5646
+ }
5647
+ if (target.isDiFramework) {
5648
+ addViolation({
5649
+ feature: moduleName,
5650
+ layer,
5651
+ file,
5652
+ relativePath: relPath,
5653
+ line: stmt.line,
5654
+ category: "Domain Dependence Violation",
5655
+ code: stmt.rawCode,
5656
+ ruleId: "D2_DI_DEPENDENCE",
5657
+ severity: "error",
5658
+ advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
5659
+ fixExample: {
5660
+ bad: stmt.rawCode,
5661
+ good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
5662
+ }
5663
+ });
5664
+ }
5665
+ }
5666
+ if (layer === "ports" && target.targetSharedModule === moduleName && target.isSharedInfra) {
5667
+ addViolation({
5668
+ feature: moduleName,
5669
+ layer,
5670
+ file,
5671
+ relativePath: relPath,
5672
+ line: stmt.line,
5673
+ category: "Shared Architecture Violation",
5674
+ code: stmt.rawCode,
5675
+ ruleId: "SP1_INFRA_IN_PORT",
5676
+ severity: "error",
5677
+ advice: `Shared ports cannot depend on infrastructure. Infrastructure implements ports, not the other way around.`,
5678
+ fixExample: {
5679
+ bad: stmt.rawCode,
5680
+ good: "// Ports define abstract interfaces without concrete infrastructure dependencies."
5681
+ }
5682
+ });
5683
+ }
5684
+ }
5685
+ continue;
5686
+ }
5687
+ const feat = mod;
5511
5688
  if (layer === "useCases" && !isBarrel) {
5512
5689
  checkUseCaseShape(feat.name, file, relPath, content, addViolation);
5513
5690
  }
@@ -5576,23 +5753,33 @@ Code: ${v.code}`;
5576
5753
  }
5577
5754
  });
5578
5755
  }
5579
- if (target.targetLayer === "domain" && !target.isTargetPort) {
5580
- addViolation({
5581
- feature: feat.name,
5582
- layer,
5583
- file,
5584
- relativePath: relPath,
5585
- line: stmt.line,
5586
- category: "Cross-Feature Domain Violation",
5587
- code: stmt.rawCode,
5588
- ruleId: "F1_CROSS_FEATURE_DOMAIN",
5589
- severity: "error",
5590
- 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.`,
5591
- fixExample: {
5592
- bad: stmt.rawCode,
5593
- 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).`
5756
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetError && !stmt.isTypeOnly) {
5757
+ for (const name of stmt.importedNames) {
5758
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
5759
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
5760
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
5761
+ if (execMatch) {
5762
+ const charIndex = execMatch.index;
5763
+ const lineNum = content.substring(0, charIndex).split("\n").length;
5764
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
5765
+ addViolation({
5766
+ feature: feat.name,
5767
+ layer,
5768
+ file,
5769
+ relativePath: relPath,
5770
+ line: lineNum,
5771
+ category: "Cross-Feature Domain Violation",
5772
+ code: lineCode,
5773
+ ruleId: "F1_CROSS_FEATURE_DOMAIN",
5774
+ severity: "error",
5775
+ 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.`,
5776
+ fixExample: {
5777
+ bad: lineCode,
5778
+ good: `// Define a Port in ${feat.name}/domain/ and implement the cross-feature communication in ${feat.name}/infrastructure/.`
5779
+ }
5780
+ });
5594
5781
  }
5595
- });
5782
+ }
5596
5783
  }
5597
5784
  }
5598
5785
  if (target.isGlobalConfig) {
@@ -5980,23 +6167,33 @@ Code: ${v.code}`;
5980
6167
  }
5981
6168
  });
5982
6169
  }
5983
- if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo) {
5984
- addViolation({
5985
- feature: feat.name,
5986
- layer,
5987
- file,
5988
- relativePath: relPath,
5989
- line: stmt.line,
5990
- category: "Presentation Layer Violation",
5991
- code: stmt.rawCode,
5992
- ruleId: "P5_DIRECT_DOMAIN_ENTITY",
5993
- severity: "error",
5994
- 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.",
5995
- fixExample: {
5996
- bad: stmt.rawCode,
5997
- good: "// Construct a DTO and pass it into the injected Use Case's execute() method."
6170
+ if (target.targetLayer === "domain" && !target.isTargetPort && !target.isTargetRepo && !target.isTargetError && !stmt.isTypeOnly) {
6171
+ for (const name of stmt.importedNames) {
6172
+ const createRegex = new RegExp(`\\b${name}\\.create\\s*\\(`, "g");
6173
+ const newRegex = new RegExp(`\\bnew\\s+${name}\\b`, "g");
6174
+ const execMatch = createRegex.exec(content) || newRegex.exec(content);
6175
+ if (execMatch) {
6176
+ const charIndex = execMatch.index;
6177
+ const lineNum = content.substring(0, charIndex).split("\n").length;
6178
+ const lineCode = content.split(/\r?\n/)[lineNum - 1]?.trim() || stmt.rawCode;
6179
+ addViolation({
6180
+ feature: feat.name,
6181
+ layer,
6182
+ file,
6183
+ relativePath: relPath,
6184
+ line: lineNum,
6185
+ category: "Presentation Layer Violation",
6186
+ code: lineCode,
6187
+ ruleId: "P5_DIRECT_DOMAIN_ENTITY",
6188
+ severity: "error",
6189
+ 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.`,
6190
+ fixExample: {
6191
+ bad: lineCode,
6192
+ good: "// Construct a plain DTO object and pass it into the injected Use Case's execute() method."
6193
+ }
6194
+ });
5998
6195
  }
5999
- });
6196
+ }
6000
6197
  }
6001
6198
  }
6002
6199
  }