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