@solid-stack/mason 1.0.23 → 1.0.24

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
@@ -4410,6 +4410,350 @@ function discoverAllFeatures(projectRoot) {
4410
4410
  dir: path2.join(featuresBase, e.name)
4411
4411
  })).sort((a, b) => a.name.localeCompare(b.name));
4412
4412
  }
4413
+ function extractClassMethods(content, className) {
4414
+ let clean = "";
4415
+ let inLineComment = false;
4416
+ let inBlockComment = false;
4417
+ let inString = null;
4418
+ let escaped = false;
4419
+ for (let i = 0; i < content.length; i++) {
4420
+ const c = content[i];
4421
+ const next = content[i + 1];
4422
+ if (inLineComment) {
4423
+ if (c === "\n") {
4424
+ inLineComment = false;
4425
+ clean += "\n";
4426
+ } else {
4427
+ clean += " ";
4428
+ }
4429
+ continue;
4430
+ }
4431
+ if (inBlockComment) {
4432
+ if (c === "*" && next === "/") {
4433
+ inBlockComment = false;
4434
+ clean += " ";
4435
+ i++;
4436
+ } else {
4437
+ clean += c === "\n" ? "\n" : " ";
4438
+ }
4439
+ continue;
4440
+ }
4441
+ if (inString) {
4442
+ clean += c === "\n" ? "\n" : " ";
4443
+ if (escaped) {
4444
+ escaped = false;
4445
+ } else if (c === "\\") {
4446
+ escaped = true;
4447
+ } else if (c === inString) {
4448
+ inString = null;
4449
+ }
4450
+ continue;
4451
+ }
4452
+ if (c === "/" && next === "/") {
4453
+ inLineComment = true;
4454
+ clean += " ";
4455
+ i++;
4456
+ continue;
4457
+ }
4458
+ if (c === "/" && next === "*") {
4459
+ inBlockComment = true;
4460
+ clean += " ";
4461
+ i++;
4462
+ continue;
4463
+ }
4464
+ if (c === '"' || c === "'" || c === "`") {
4465
+ inString = c;
4466
+ clean += " ";
4467
+ continue;
4468
+ }
4469
+ clean += c;
4470
+ }
4471
+ const classDeclRegex = new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b[^{]*\\{`);
4472
+ const match = classDeclRegex.exec(clean);
4473
+ if (!match) return null;
4474
+ const classBodyStart = match.index + match[0].length - 1;
4475
+ const methods = [];
4476
+ let depth = 1;
4477
+ let parenDepth = 0;
4478
+ let atMemberStart = true;
4479
+ const methodRegex = /^\s*(?:@[a-zA-Z0-9_$]+(?:\([^)]*\))?\s+)*(?:(public|private|protected|static)\s+)*(?:(async)\s+)?(?:(get|set)\s+)?(#?[a-zA-Z0-9_$]+)\s*(?:<[^>]+>)?\s*\(/;
4480
+ const arrowRegex = /^\s*(?:(public|private|protected|static)\s+)?(#?[a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\(/;
4481
+ for (let i = classBodyStart + 1; i < clean.length && depth > 0; i++) {
4482
+ const c = clean[i];
4483
+ if (c === "{") {
4484
+ depth++;
4485
+ atMemberStart = false;
4486
+ continue;
4487
+ }
4488
+ if (c === "}") {
4489
+ depth--;
4490
+ if (depth === 1) {
4491
+ atMemberStart = true;
4492
+ }
4493
+ continue;
4494
+ }
4495
+ if (depth === 1) {
4496
+ if (c === ";") {
4497
+ atMemberStart = true;
4498
+ parenDepth = 0;
4499
+ continue;
4500
+ }
4501
+ if (c === "(") {
4502
+ parenDepth++;
4503
+ } else if (c === ")") {
4504
+ parenDepth = Math.max(0, parenDepth - 1);
4505
+ }
4506
+ if (atMemberStart && parenDepth === 0) {
4507
+ if (/\s/.test(c)) continue;
4508
+ const slice = clean.slice(i);
4509
+ const methodMatch = slice.match(methodRegex);
4510
+ if (methodMatch) {
4511
+ const fullMatch = methodMatch[0];
4512
+ const beforeParen = fullMatch.slice(0, fullMatch.lastIndexOf("(")).trim();
4513
+ const tokens = beforeParen.split(/\s+/).filter(Boolean);
4514
+ const rawName = tokens[tokens.length - 1] || "";
4515
+ const methodName = rawName.replace(/<.*$/, "");
4516
+ const controlKeywords = ["if", "for", "while", "switch", "catch", "with"];
4517
+ if (!controlKeywords.includes(methodName)) {
4518
+ const isPrivate = tokens.includes("private") || tokens.includes("protected") || methodName.startsWith("#");
4519
+ const isAsync = tokens.includes("async");
4520
+ const isPublic = !isPrivate;
4521
+ const lineNum = content.slice(0, i).split("\n").length;
4522
+ const lineCode = content.split("\n")[lineNum - 1]?.trim() || "";
4523
+ methods.push({
4524
+ name: methodName,
4525
+ isAsync,
4526
+ isPublic,
4527
+ line: lineNum,
4528
+ code: lineCode
4529
+ });
4530
+ i += fullMatch.length - 1;
4531
+ parenDepth = 1;
4532
+ atMemberStart = false;
4533
+ continue;
4534
+ }
4535
+ }
4536
+ const arrowMatch = slice.match(arrowRegex);
4537
+ if (arrowMatch) {
4538
+ const fullMatch = arrowMatch[0];
4539
+ const beforeEquals = fullMatch.split("=")[0].trim();
4540
+ const tokens = beforeEquals.split(/\s+/).filter(Boolean);
4541
+ const rawName = tokens[tokens.length - 1] || "";
4542
+ const isPrivate = tokens.includes("private") || tokens.includes("protected") || rawName.startsWith("#");
4543
+ const isAsync = fullMatch.includes("async");
4544
+ const isPublic = !isPrivate;
4545
+ const lineNum = content.slice(0, i).split("\n").length;
4546
+ const lineCode = content.split("\n")[lineNum - 1]?.trim() || "";
4547
+ methods.push({
4548
+ name: rawName,
4549
+ isAsync,
4550
+ isPublic,
4551
+ line: lineNum,
4552
+ code: lineCode
4553
+ });
4554
+ i += fullMatch.length - 1;
4555
+ parenDepth = 1;
4556
+ atMemberStart = false;
4557
+ continue;
4558
+ }
4559
+ atMemberStart = false;
4560
+ }
4561
+ }
4562
+ }
4563
+ return methods;
4564
+ }
4565
+ function checkUseCaseShape(featureName, filePath, relativePath, content, addViolation) {
4566
+ const fileName = path2.basename(filePath);
4567
+ const lines = content.split(/\r?\n/);
4568
+ const classMatch = content.match(/(?:export\s+(?:default\s+)?)?class\s+([A-Za-z0-9_]+)/);
4569
+ if (!classMatch) {
4570
+ addViolation({
4571
+ feature: featureName,
4572
+ layer: "useCases",
4573
+ file: filePath,
4574
+ relativePath,
4575
+ line: 1,
4576
+ category: "Use Case Shape Violation",
4577
+ code: lines[0]?.trim() || "",
4578
+ ruleId: "U_SHAPE_CLASS",
4579
+ severity: "error",
4580
+ advice: `Use case file '${fileName}' must define and export a class.`,
4581
+ fixExample: {
4582
+ bad: "// Missing class definition",
4583
+ good: `export class ${fileName.replace(/\.(ts|js)$/, "")} { ... }`
4584
+ }
4585
+ });
4586
+ return;
4587
+ }
4588
+ const className = classMatch[1];
4589
+ let classLine = 1;
4590
+ for (let i = 0; i < lines.length; i++) {
4591
+ if (new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b`).test(lines[i])) {
4592
+ classLine = i + 1;
4593
+ break;
4594
+ }
4595
+ }
4596
+ if (!/@MakeInjectable\b/.test(content)) {
4597
+ addViolation({
4598
+ feature: featureName,
4599
+ layer: "useCases",
4600
+ file: filePath,
4601
+ relativePath,
4602
+ line: classLine,
4603
+ category: "Use Case Shape Violation",
4604
+ code: lines[classLine - 1]?.trim() || `class ${className}`,
4605
+ ruleId: "U_SHAPE_DECORATOR",
4606
+ severity: "error",
4607
+ advice: `Use case class '${className}' in '${fileName}' must be decorated with @MakeInjectable.`,
4608
+ fixExample: {
4609
+ bad: lines[classLine - 1]?.trim() || `class ${className}`,
4610
+ good: `@MakeInjectable
4611
+ export class ${className} { ... }`
4612
+ }
4613
+ });
4614
+ }
4615
+ const hasAsyncExecute = /(?:public\s+)?async\s+execute\s*(?:<[^>]+>)?\s*\(/.test(content);
4616
+ if (!hasAsyncExecute) {
4617
+ const hasSyncExecute = /(?:public\s+)?execute\s*(?:<[^>]+>)?\s*\(/.test(content);
4618
+ addViolation({
4619
+ feature: featureName,
4620
+ layer: "useCases",
4621
+ file: filePath,
4622
+ relativePath,
4623
+ line: classLine,
4624
+ category: "Use Case Shape Violation",
4625
+ code: lines[classLine - 1]?.trim() || `class ${className}`,
4626
+ ruleId: "U_SHAPE_EXECUTE",
4627
+ severity: "error",
4628
+ advice: hasSyncExecute ? `Use case class '${className}' in '${fileName}' must have an 'async execute' method (found synchronous 'execute').` : `Use case class '${className}' in '${fileName}' must implement an 'async execute(...)' method.`,
4629
+ fixExample: {
4630
+ bad: hasSyncExecute ? "execute(props: ...) { ... }" : `class ${className} { ... }`,
4631
+ good: "async execute(props: ...): Promise<...> { ... }"
4632
+ }
4633
+ });
4634
+ }
4635
+ const classMethods = extractClassMethods(content, className);
4636
+ if (classMethods) {
4637
+ const extraPublicMethods = classMethods.filter(
4638
+ (m) => m.isPublic && m.name !== "constructor" && m.name !== "execute"
4639
+ );
4640
+ for (const m of extraPublicMethods) {
4641
+ addViolation({
4642
+ feature: featureName,
4643
+ layer: "useCases",
4644
+ file: filePath,
4645
+ relativePath,
4646
+ line: m.line,
4647
+ category: "Use Case Shape Violation",
4648
+ code: m.code || `public ${m.name}(...)`,
4649
+ ruleId: "U_SHAPE_ONLY_EXECUTE",
4650
+ severity: "error",
4651
+ advice: `Use case class '${className}' in '${fileName}' has disallowed public method '${m.name}'. The only allowed public methods are constructor and async execute. Extract extra methods into their own use cases, or make them private.`,
4652
+ fixExample: {
4653
+ bad: m.code || `${m.name}(...) { ... }`,
4654
+ good: `// Extract '${m.name}' into its own dedicated Use Case, or make it private if it is internal helper logic:
4655
+ private ${m.name}(...) { ... }`
4656
+ }
4657
+ });
4658
+ }
4659
+ }
4660
+ }
4661
+ function checkServiceShape(featureName, filePath, relativePath, content, addViolation) {
4662
+ const fileName = path2.basename(filePath);
4663
+ const lines = content.split(/\r?\n/);
4664
+ const classMatch = content.match(/(?:export\s+(?:default\s+)?)?class\s+([A-Za-z0-9_]+)/);
4665
+ if (!classMatch) {
4666
+ addViolation({
4667
+ feature: featureName,
4668
+ layer: "services",
4669
+ file: filePath,
4670
+ relativePath,
4671
+ line: 1,
4672
+ category: "Service Shape Violation",
4673
+ code: lines[0]?.trim() || "",
4674
+ ruleId: "S_SHAPE_CLASS",
4675
+ severity: "error",
4676
+ advice: `Service file '${fileName}' must define and export a class.`,
4677
+ fixExample: {
4678
+ bad: "// Missing class definition",
4679
+ good: `export class ${fileName.replace(/\.(ts|js)$/, "")} { ... }`
4680
+ }
4681
+ });
4682
+ return;
4683
+ }
4684
+ const className = classMatch[1];
4685
+ let classLine = 1;
4686
+ for (let i = 0; i < lines.length; i++) {
4687
+ if (new RegExp(`(?:export\\s+(?:default\\s+)?)?class\\s+${className}\\b`).test(lines[i])) {
4688
+ classLine = i + 1;
4689
+ break;
4690
+ }
4691
+ }
4692
+ if (!/@MakeInjectable\b/.test(content)) {
4693
+ addViolation({
4694
+ feature: featureName,
4695
+ layer: "services",
4696
+ file: filePath,
4697
+ relativePath,
4698
+ line: classLine,
4699
+ category: "Service Shape Violation",
4700
+ code: lines[classLine - 1]?.trim() || `class ${className}`,
4701
+ ruleId: "S_SHAPE_DECORATOR",
4702
+ severity: "error",
4703
+ advice: `Service class '${className}' in '${fileName}' must be decorated with @MakeInjectable.`,
4704
+ fixExample: {
4705
+ bad: lines[classLine - 1]?.trim() || `class ${className}`,
4706
+ good: `@MakeInjectable
4707
+ export class ${className} { ... }`
4708
+ }
4709
+ });
4710
+ }
4711
+ const hasAsyncExecute = /(?:public\s+)?async\s+execute\s*(?:<[^>]+>)?\s*\(/.test(content);
4712
+ if (!hasAsyncExecute) {
4713
+ const hasSyncExecute = /(?:public\s+)?execute\s*(?:<[^>]+>)?\s*\(/.test(content);
4714
+ addViolation({
4715
+ feature: featureName,
4716
+ layer: "services",
4717
+ file: filePath,
4718
+ relativePath,
4719
+ line: classLine,
4720
+ category: "Service Shape Violation",
4721
+ code: lines[classLine - 1]?.trim() || `class ${className}`,
4722
+ ruleId: "S_SHAPE_EXECUTE",
4723
+ severity: "error",
4724
+ advice: hasSyncExecute ? `Service class '${className}' in '${fileName}' must have an 'async execute' method (found synchronous 'execute').` : `Service class '${className}' in '${fileName}' must implement an 'async execute(...)' method.`,
4725
+ fixExample: {
4726
+ bad: hasSyncExecute ? "execute(props: ...) { ... }" : `class ${className} { ... }`,
4727
+ good: "async execute(props: ...): Promise<...> { ... }"
4728
+ }
4729
+ });
4730
+ }
4731
+ const classMethods = extractClassMethods(content, className);
4732
+ if (classMethods) {
4733
+ const extraPublicMethods = classMethods.filter(
4734
+ (m) => m.isPublic && m.name !== "constructor" && m.name !== "execute"
4735
+ );
4736
+ for (const m of extraPublicMethods) {
4737
+ addViolation({
4738
+ feature: featureName,
4739
+ layer: "services",
4740
+ file: filePath,
4741
+ relativePath,
4742
+ line: m.line,
4743
+ category: "Service Shape Violation",
4744
+ code: m.code || `public ${m.name}(...)`,
4745
+ ruleId: "S_SHAPE_ONLY_EXECUTE",
4746
+ severity: "error",
4747
+ advice: `Service class '${className}' in '${fileName}' has disallowed public method '${m.name}'. The only allowed public methods are constructor and async execute. If a service has multiple public methods, those should be on their own services instead of being cramped in a single service.`,
4748
+ fixExample: {
4749
+ bad: m.code || `${m.name}(...) { ... }`,
4750
+ good: `// Extract '${m.name}' into its own dedicated Service, or make it private if it is internal helper logic:
4751
+ private ${m.name}(...) { ... }`
4752
+ }
4753
+ });
4754
+ }
4755
+ }
4756
+ }
4413
4757
  function lintCodebase(options = {}) {
4414
4758
  const startTime = Date.now();
4415
4759
  const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
@@ -4453,6 +4797,14 @@ Code: ${v.code}`;
4453
4797
  if (!layer) continue;
4454
4798
  const content = fs4.readFileSync(file, "utf-8");
4455
4799
  const statements = extractImportStatements(content);
4800
+ const baseName = path2.basename(file);
4801
+ const isBarrel = baseName === "index.ts" || baseName === "index.js";
4802
+ if (layer === "useCases" && !isBarrel) {
4803
+ checkUseCaseShape(feat.name, file, relPath, content, addViolation);
4804
+ }
4805
+ if (layer === "services" && !isBarrel) {
4806
+ checkServiceShape(feat.name, file, relPath, content, addViolation);
4807
+ }
4456
4808
  if (layer === "domain") {
4457
4809
  const lines = content.split(/\r?\n/);
4458
4810
  for (let i = 0; i < lines.length; i++) {
@@ -4863,6 +5215,24 @@ Code: ${v.code}`;
4863
5215
  }
4864
5216
  });
4865
5217
  }
5218
+ if (target.targetLayer === "services") {
5219
+ addViolation({
5220
+ feature: feat.name,
5221
+ layer,
5222
+ file,
5223
+ relativePath: relPath,
5224
+ line: stmt.line,
5225
+ category: "Presentation Layer Violation",
5226
+ code: stmt.rawCode,
5227
+ ruleId: "P4_DIRECT_SERVICE",
5228
+ severity: "error",
5229
+ advice: "Presentation handlers must not depend on services directly. They must use use cases, and use cases will then depend on the service.",
5230
+ fixExample: {
5231
+ bad: stmt.rawCode,
5232
+ good: "// Inject a Use Case via static deps instead of a Service."
5233
+ }
5234
+ });
5235
+ }
4866
5236
  }
4867
5237
  }
4868
5238
  }