@solid-stack/mason 1.0.21 → 1.0.22
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 +957 -48
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +956 -47
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/commands/lint.d.ts +7 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/core/lintEngine.d.ts +55 -0
- package/dist/core/lintEngine.d.ts.map +1 -0
- package/dist/index.cjs +961 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +957 -48
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lint.d.ts +60 -0
- package/dist/types/lint.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from 'cac';
|
|
3
3
|
import * as p14 from '@clack/prompts';
|
|
4
|
-
import
|
|
4
|
+
import pc15 from 'picocolors';
|
|
5
5
|
import fs9 from 'fs';
|
|
6
6
|
import path2 from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
@@ -47,17 +47,17 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
47
47
|
function printGeneratedFiles(files, isDryRun = false) {
|
|
48
48
|
console.log("");
|
|
49
49
|
if (isDryRun) {
|
|
50
|
-
p14.log.warn(
|
|
50
|
+
p14.log.warn(pc15.yellow(pc15.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
51
51
|
}
|
|
52
52
|
for (const f of files) {
|
|
53
53
|
if (f.action === "created") {
|
|
54
|
-
p14.log.success(`${
|
|
54
|
+
p14.log.success(`${pc15.bold(f.relativePath)} ${pc15.dim("(created)")}`);
|
|
55
55
|
} else if (f.action === "overwritten") {
|
|
56
|
-
p14.log.warn(`${
|
|
56
|
+
p14.log.warn(`${pc15.bold(f.relativePath)} ${pc15.dim("(overwritten)")}`);
|
|
57
57
|
} else if (f.action === "skipped") {
|
|
58
|
-
p14.log.info(`${
|
|
58
|
+
p14.log.info(`${pc15.dim(f.relativePath)} ${pc15.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
59
59
|
} else {
|
|
60
|
-
p14.log.info(`${
|
|
60
|
+
p14.log.info(`${pc15.bold(f.relativePath)} ${pc15.dim("(preview)")}`);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -1744,13 +1744,13 @@ function handleCancel(result) {
|
|
|
1744
1744
|
}
|
|
1745
1745
|
function printBanner() {
|
|
1746
1746
|
console.log("");
|
|
1747
|
-
p14.intro(
|
|
1747
|
+
p14.intro(pc15.bgCyan(pc15.black(" @solid-stack/mason ")));
|
|
1748
1748
|
}
|
|
1749
1749
|
async function promptProperties(options = {}) {
|
|
1750
1750
|
const properties = options.defaults ? [...options.defaults] : [];
|
|
1751
1751
|
if (properties.length > 0) {
|
|
1752
1752
|
const summary = properties.map((p16) => `${p16.name}: ${p16.type}${p16.optional ? "?" : ""}`).join(", ");
|
|
1753
|
-
p14.log.info(
|
|
1753
|
+
p14.log.info(pc15.dim(`Pre-configured fields: ${summary}`));
|
|
1754
1754
|
const addMore = handleCancel(
|
|
1755
1755
|
await p14.confirm({
|
|
1756
1756
|
message: "Would you like to add custom fields/properties?",
|
|
@@ -1761,7 +1761,7 @@ async function promptProperties(options = {}) {
|
|
|
1761
1761
|
return properties;
|
|
1762
1762
|
}
|
|
1763
1763
|
}
|
|
1764
|
-
p14.log.info(
|
|
1764
|
+
p14.log.info(pc15.cyan("Enter properties (leave name blank when finished):"));
|
|
1765
1765
|
while (true) {
|
|
1766
1766
|
const name = handleCancel(
|
|
1767
1767
|
await p14.text({
|
|
@@ -1809,7 +1809,7 @@ async function promptProperties(options = {}) {
|
|
|
1809
1809
|
optional
|
|
1810
1810
|
});
|
|
1811
1811
|
p14.log.success(
|
|
1812
|
-
|
|
1812
|
+
pc15.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
1813
1813
|
);
|
|
1814
1814
|
}
|
|
1815
1815
|
return properties;
|
|
@@ -2069,7 +2069,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2069
2069
|
}
|
|
2070
2070
|
}
|
|
2071
2071
|
const s = p14.spinner();
|
|
2072
|
-
s.start(`Generating feature '${
|
|
2072
|
+
s.start(`Generating feature '${pc15.cyan(normalizedName)}'...`);
|
|
2073
2073
|
const options = {
|
|
2074
2074
|
name: normalizedName,
|
|
2075
2075
|
projectRoot,
|
|
@@ -2116,7 +2116,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2116
2116
|
s.stop("Feature scaffolded successfully!");
|
|
2117
2117
|
printGeneratedFiles(files, options.dryRun);
|
|
2118
2118
|
p14.outro(
|
|
2119
|
-
|
|
2119
|
+
pc15.green(
|
|
2120
2120
|
`\u2713 Feature '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2121
2121
|
)
|
|
2122
2122
|
);
|
|
@@ -2272,7 +2272,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2272
2272
|
);
|
|
2273
2273
|
}
|
|
2274
2274
|
const s = p14.spinner();
|
|
2275
|
-
s.start(`Generating shared module '${
|
|
2275
|
+
s.start(`Generating shared module '${pc15.cyan(normalizedName)}'...`);
|
|
2276
2276
|
const files = generateShared({
|
|
2277
2277
|
name: normalizedName,
|
|
2278
2278
|
description,
|
|
@@ -2286,7 +2286,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2286
2286
|
s.stop("Shared module generated successfully!");
|
|
2287
2287
|
printGeneratedFiles(files, Boolean(flags.dryRun));
|
|
2288
2288
|
p14.outro(
|
|
2289
|
-
|
|
2289
|
+
pc15.green(
|
|
2290
2290
|
`\u2713 Shared module '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2291
2291
|
)
|
|
2292
2292
|
);
|
|
@@ -2414,7 +2414,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2414
2414
|
}
|
|
2415
2415
|
}
|
|
2416
2416
|
const s = p14.spinner();
|
|
2417
|
-
s.start(`Generating use case '${
|
|
2417
|
+
s.start(`Generating use case '${pc15.cyan(ucName)}'...`);
|
|
2418
2418
|
const options = {
|
|
2419
2419
|
name: ucName,
|
|
2420
2420
|
feature,
|
|
@@ -2446,7 +2446,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2446
2446
|
}
|
|
2447
2447
|
s.stop("Use case generated successfully!");
|
|
2448
2448
|
printGeneratedFiles(files, options.dryRun);
|
|
2449
|
-
p14.outro(
|
|
2449
|
+
p14.outro(pc15.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2450
2450
|
}
|
|
2451
2451
|
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2452
2452
|
const projectRoot = findProjectRoot();
|
|
@@ -2517,7 +2517,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2517
2517
|
});
|
|
2518
2518
|
}
|
|
2519
2519
|
const s = p14.spinner();
|
|
2520
|
-
s.start(`Generating domain model '${
|
|
2520
|
+
s.start(`Generating domain model '${pc15.cyan(entityName)}'...`);
|
|
2521
2521
|
const options = {
|
|
2522
2522
|
name: entityName,
|
|
2523
2523
|
feature,
|
|
@@ -2530,7 +2530,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2530
2530
|
const file = generateDomain(options);
|
|
2531
2531
|
s.stop("Domain model generated successfully!");
|
|
2532
2532
|
printGeneratedFiles([file], options.dryRun);
|
|
2533
|
-
p14.outro(
|
|
2533
|
+
p14.outro(pc15.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2534
2534
|
}
|
|
2535
2535
|
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2536
2536
|
const projectRoot = findProjectRoot();
|
|
@@ -2699,7 +2699,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2699
2699
|
registerInProvider = !flags.noProvider;
|
|
2700
2700
|
}
|
|
2701
2701
|
const s = p14.spinner();
|
|
2702
|
-
s.start(`Generating infrastructure class '${
|
|
2702
|
+
s.start(`Generating infrastructure class '${pc15.cyan(className)}'...`);
|
|
2703
2703
|
const files = [];
|
|
2704
2704
|
const options = {
|
|
2705
2705
|
name: className,
|
|
@@ -2734,7 +2734,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2734
2734
|
}
|
|
2735
2735
|
s.stop("Infrastructure class generated successfully!");
|
|
2736
2736
|
printGeneratedFiles(files, options.dryRun);
|
|
2737
|
-
p14.outro(
|
|
2737
|
+
p14.outro(pc15.green(`Infrastructure class '${className}' created!`));
|
|
2738
2738
|
}
|
|
2739
2739
|
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
2740
2740
|
const projectRoot = findProjectRoot();
|
|
@@ -2847,7 +2847,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2847
2847
|
}
|
|
2848
2848
|
}
|
|
2849
2849
|
const s = p14.spinner();
|
|
2850
|
-
s.start(`Generating interface '${
|
|
2850
|
+
s.start(`Generating interface '${pc15.cyan(ifaceName)}'...`);
|
|
2851
2851
|
const files = [];
|
|
2852
2852
|
const options = {
|
|
2853
2853
|
name: ifaceName,
|
|
@@ -2896,7 +2896,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2896
2896
|
}
|
|
2897
2897
|
s.stop("Interface generated successfully!");
|
|
2898
2898
|
printGeneratedFiles(files, options.dryRun);
|
|
2899
|
-
p14.outro(
|
|
2899
|
+
p14.outro(pc15.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
2900
2900
|
}
|
|
2901
2901
|
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
2902
2902
|
const projectRoot = findProjectRoot();
|
|
@@ -3013,7 +3013,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3013
3013
|
}
|
|
3014
3014
|
}
|
|
3015
3015
|
const s = p14.spinner();
|
|
3016
|
-
s.start(`Generating HTTP handler '${
|
|
3016
|
+
s.start(`Generating HTTP handler '${pc15.cyan(className)}'...`);
|
|
3017
3017
|
const options = {
|
|
3018
3018
|
name: className,
|
|
3019
3019
|
feature,
|
|
@@ -3031,7 +3031,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3031
3031
|
const files = generateHttpHandler(options);
|
|
3032
3032
|
s.stop("HTTP handler generated successfully!");
|
|
3033
3033
|
printGeneratedFiles(files, options.dryRun);
|
|
3034
|
-
p14.outro(
|
|
3034
|
+
p14.outro(pc15.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
3035
3035
|
}
|
|
3036
3036
|
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
3037
3037
|
const formattedDeps = dependencies.map((dep) => ({
|
|
@@ -3187,7 +3187,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3187
3187
|
);
|
|
3188
3188
|
}
|
|
3189
3189
|
const s = p14.spinner();
|
|
3190
|
-
s.start(`Generating injectable '${
|
|
3190
|
+
s.start(`Generating injectable '${pc15.cyan(className)}'...`);
|
|
3191
3191
|
const options = {
|
|
3192
3192
|
name: className,
|
|
3193
3193
|
feature,
|
|
@@ -3201,7 +3201,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3201
3201
|
const files = generateInjectable(options);
|
|
3202
3202
|
s.stop("Injectable class generated successfully!");
|
|
3203
3203
|
printGeneratedFiles(files, options.dryRun);
|
|
3204
|
-
p14.outro(
|
|
3204
|
+
p14.outro(pc15.green(`Injectable '${className}' created successfully!`));
|
|
3205
3205
|
}
|
|
3206
3206
|
async function runInteractiveMenu(flags = {}) {
|
|
3207
3207
|
printBanner();
|
|
@@ -3315,7 +3315,7 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3315
3315
|
break;
|
|
3316
3316
|
default:
|
|
3317
3317
|
p14.log.error(
|
|
3318
|
-
|
|
3318
|
+
pc15.red(
|
|
3319
3319
|
`Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3320
3320
|
)
|
|
3321
3321
|
);
|
|
@@ -3551,7 +3551,7 @@ function installNpmPackages(projectRoot, packages, pm) {
|
|
|
3551
3551
|
installCmd = `npm install ${packages.join(" ")}`;
|
|
3552
3552
|
break;
|
|
3553
3553
|
}
|
|
3554
|
-
p14.log.info(`Installing missing dependencies with ${manager}: ${
|
|
3554
|
+
p14.log.info(`Installing missing dependencies with ${manager}: ${pc15.cyan(packages.join(", "))}`);
|
|
3555
3555
|
execSync(installCmd, {
|
|
3556
3556
|
cwd: projectRoot,
|
|
3557
3557
|
stdio: "inherit"
|
|
@@ -3707,19 +3707,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3707
3707
|
);
|
|
3708
3708
|
}
|
|
3709
3709
|
if (type !== "shared" && type !== "feature") {
|
|
3710
|
-
p14.log.error(
|
|
3710
|
+
p14.log.error(pc15.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3711
3711
|
process.exit(1);
|
|
3712
3712
|
}
|
|
3713
3713
|
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3714
3714
|
const s = p14.spinner();
|
|
3715
|
-
s.start(`Connecting to registry at ${
|
|
3715
|
+
s.start(`Connecting to registry at ${pc15.dim(registryBase)}...`);
|
|
3716
3716
|
let index;
|
|
3717
3717
|
try {
|
|
3718
3718
|
index = await fetchRegistryIndex(registryBase);
|
|
3719
3719
|
s.stop("Registry index loaded.");
|
|
3720
3720
|
} catch (err) {
|
|
3721
|
-
s.stop(
|
|
3722
|
-
p14.log.error(
|
|
3721
|
+
s.stop(pc15.red("Failed to load registry index."));
|
|
3722
|
+
p14.log.error(pc15.red(err.message || String(err)));
|
|
3723
3723
|
process.exit(1);
|
|
3724
3724
|
}
|
|
3725
3725
|
const availableList = type === "shared" ? index.shared : index.features;
|
|
@@ -3743,9 +3743,9 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3743
3743
|
}
|
|
3744
3744
|
if (!candidateNames.includes(name)) {
|
|
3745
3745
|
const closest = findClosestMatch(name, candidateNames);
|
|
3746
|
-
p14.log.error(
|
|
3746
|
+
p14.log.error(pc15.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3747
3747
|
if (closest) {
|
|
3748
|
-
p14.log.info(
|
|
3748
|
+
p14.log.info(pc15.yellow(`Did you mean '${pc15.cyan(closest)}'?`));
|
|
3749
3749
|
}
|
|
3750
3750
|
process.exit(1);
|
|
3751
3751
|
}
|
|
@@ -3757,7 +3757,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3757
3757
|
try {
|
|
3758
3758
|
manifest = await fetchModuleManifest(registryBase, modType, modName);
|
|
3759
3759
|
} catch (err) {
|
|
3760
|
-
p14.log.error(
|
|
3760
|
+
p14.log.error(pc15.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
|
|
3761
3761
|
process.exit(1);
|
|
3762
3762
|
}
|
|
3763
3763
|
nodes.set(key, {
|
|
@@ -3797,7 +3797,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3797
3797
|
buildTopologicalOrder(rootKey);
|
|
3798
3798
|
const resolutions = /* @__PURE__ */ new Map();
|
|
3799
3799
|
const globalAliasMap = /* @__PURE__ */ new Map();
|
|
3800
|
-
p14.log.step(`Importing ${type}: ${
|
|
3800
|
+
p14.log.step(`Importing ${type}: ${pc15.cyan(name)}...`);
|
|
3801
3801
|
for (const key of orderedKeys) {
|
|
3802
3802
|
const node = nodes.get(key);
|
|
3803
3803
|
const { type: modType, name: registryName, manifest } = node;
|
|
@@ -3830,7 +3830,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3830
3830
|
if (programmaticPointTo) {
|
|
3831
3831
|
if (!checkModuleExists(projectRoot, modType, programmaticPointTo)) {
|
|
3832
3832
|
p14.log.error(
|
|
3833
|
-
|
|
3833
|
+
pc15.red(
|
|
3834
3834
|
`Module '${programmaticPointTo}' specified in --point-to does not exist in ${folder}.`
|
|
3835
3835
|
)
|
|
3836
3836
|
);
|
|
@@ -3866,7 +3866,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3866
3866
|
}
|
|
3867
3867
|
const existingDir = getExistingModuleDir(projectRoot, modType, registryName);
|
|
3868
3868
|
if (!existingDir) {
|
|
3869
|
-
p14.log.info(` \u21B3 Resolving required ${modType} dependency: ${
|
|
3869
|
+
p14.log.info(` \u21B3 Resolving required ${modType} dependency: ${pc15.cyan(registryName)}`);
|
|
3870
3870
|
resolutions.set(key, {
|
|
3871
3871
|
type: modType,
|
|
3872
3872
|
registryName,
|
|
@@ -4097,7 +4097,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4097
4097
|
const isOverwrite = Boolean(flags.overwrite || res.action === "overwrite");
|
|
4098
4098
|
if (key !== rootKey) {
|
|
4099
4099
|
p14.log.step(
|
|
4100
|
-
`Importing ${res.type}: ${
|
|
4100
|
+
`Importing ${res.type}: ${pc15.cyan(res.targetName)}${res.targetName !== res.registryName ? pc15.dim(` (from ${res.registryName})`) : ""}...`
|
|
4101
4101
|
);
|
|
4102
4102
|
}
|
|
4103
4103
|
if (res.action === "overwrite" && !flags.dryRun) {
|
|
@@ -4116,7 +4116,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4116
4116
|
const localFilePath = path2.join(targetDir, file);
|
|
4117
4117
|
if (fs9.existsSync(localFilePath) && !isOverwrite) {
|
|
4118
4118
|
p14.log.warn(
|
|
4119
|
-
` \u21B3 Skipping existing file: ${
|
|
4119
|
+
` \u21B3 Skipping existing file: ${pc15.dim(path2.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
|
|
4120
4120
|
);
|
|
4121
4121
|
continue;
|
|
4122
4122
|
}
|
|
@@ -4128,13 +4128,13 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4128
4128
|
}
|
|
4129
4129
|
const displayPath = path2.relative(projectRoot, localFilePath);
|
|
4130
4130
|
totalWrittenFiles.push(displayPath);
|
|
4131
|
-
p14.log.message(` ${
|
|
4131
|
+
p14.log.message(` ${pc15.green("+")} ${displayPath}`);
|
|
4132
4132
|
}
|
|
4133
4133
|
}
|
|
4134
4134
|
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
4135
4135
|
if (missingNpm.length > 0) {
|
|
4136
4136
|
if (flags.skipInstall || flags.dryRun) {
|
|
4137
|
-
p14.log.info(`Required npm dependencies to install: ${
|
|
4137
|
+
p14.log.info(`Required npm dependencies to install: ${pc15.cyan(missingNpm.join(", "))}`);
|
|
4138
4138
|
} else {
|
|
4139
4139
|
try {
|
|
4140
4140
|
installNpmPackages(projectRoot, missingNpm);
|
|
@@ -4148,18 +4148,901 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4148
4148
|
}
|
|
4149
4149
|
}
|
|
4150
4150
|
if (flags.dryRun) {
|
|
4151
|
-
p14.log.info(
|
|
4151
|
+
p14.log.info(pc15.yellow("Dry run complete: No files were written to disk."));
|
|
4152
4152
|
} else {
|
|
4153
4153
|
const rootRes = resolutions.get(rootKey);
|
|
4154
4154
|
const baseDir = getModuleBaseDir(projectRoot, type);
|
|
4155
4155
|
const displayTarget = path2.relative(projectRoot, path2.join(baseDir, rootRes.targetName));
|
|
4156
4156
|
p14.log.success(
|
|
4157
|
-
|
|
4157
|
+
pc15.green(
|
|
4158
4158
|
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
|
|
4159
4159
|
)
|
|
4160
4160
|
);
|
|
4161
4161
|
}
|
|
4162
4162
|
}
|
|
4163
|
+
var DependencyRuleError = class extends Error {
|
|
4164
|
+
violations;
|
|
4165
|
+
isFailFast;
|
|
4166
|
+
constructor(message, violations, isFailFast = false) {
|
|
4167
|
+
super(message);
|
|
4168
|
+
this.name = "DependencyRuleError";
|
|
4169
|
+
this.violations = violations;
|
|
4170
|
+
this.isFailFast = isFailFast;
|
|
4171
|
+
}
|
|
4172
|
+
};
|
|
4173
|
+
function isTestFile2(filePath, projectRoot) {
|
|
4174
|
+
const norm = (projectRoot ? path2.relative(projectRoot, filePath) : filePath).replace(/\\/g, "/");
|
|
4175
|
+
const base = path2.basename(norm);
|
|
4176
|
+
return base.endsWith(".test.ts") || base.endsWith(".test.js") || base.endsWith(".spec.ts") || base.endsWith(".spec.js") || base.endsWith(".d.ts") || norm.includes("/__tests__/") || norm.startsWith("__tests__/") || /(?:^|\/)tests?\//.test(norm);
|
|
4177
|
+
}
|
|
4178
|
+
function scanDirFiles(dir, projectRoot) {
|
|
4179
|
+
if (!fs9.existsSync(dir)) return [];
|
|
4180
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
4181
|
+
const files = [];
|
|
4182
|
+
for (const entry of entries) {
|
|
4183
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
4184
|
+
continue;
|
|
4185
|
+
}
|
|
4186
|
+
const fullPath = path2.join(dir, entry.name);
|
|
4187
|
+
if (entry.isDirectory()) {
|
|
4188
|
+
files.push(...scanDirFiles(fullPath, projectRoot));
|
|
4189
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !isTestFile2(fullPath, projectRoot)) {
|
|
4190
|
+
files.push(fullPath);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return files;
|
|
4194
|
+
}
|
|
4195
|
+
function classifyFile(fullPath, projectRoot) {
|
|
4196
|
+
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4197
|
+
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4198
|
+
if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
|
|
4199
|
+
const feature = featureMatch[1];
|
|
4200
|
+
const subfolder = featureMatch[2];
|
|
4201
|
+
let layer;
|
|
4202
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4203
|
+
layer = "domain";
|
|
4204
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4205
|
+
layer = "useCases";
|
|
4206
|
+
} else if (subfolder === "services") {
|
|
4207
|
+
layer = "services";
|
|
4208
|
+
} else if (subfolder === "infrastructure") {
|
|
4209
|
+
layer = "infrastructure";
|
|
4210
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4211
|
+
layer = "presentation";
|
|
4212
|
+
}
|
|
4213
|
+
return { feature, layer };
|
|
4214
|
+
}
|
|
4215
|
+
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4216
|
+
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
4217
|
+
return { isExternal: true, isDiFramework: true };
|
|
4218
|
+
}
|
|
4219
|
+
if (specifier.startsWith("node:") || ["crypto", "fs", "path", "http", "https", "url", "os", "child_process", "stream", "express", "cors", "dotenv"].includes(specifier)) {
|
|
4220
|
+
return { isExternal: true, isDiFramework: false };
|
|
4221
|
+
}
|
|
4222
|
+
let resolvedRel = null;
|
|
4223
|
+
if (specifier.startsWith(".")) {
|
|
4224
|
+
const abs = path2.resolve(path2.dirname(sourceFileAbs), specifier);
|
|
4225
|
+
resolvedRel = path2.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
4226
|
+
} else if (specifier.startsWith("@/features/") || specifier.startsWith("~/features/")) {
|
|
4227
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4228
|
+
} else if (specifier.startsWith("@features/")) {
|
|
4229
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4230
|
+
} else if (specifier.startsWith("@/shared/") || specifier.startsWith("~/shared/")) {
|
|
4231
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4232
|
+
} else if (specifier.startsWith("@shared/")) {
|
|
4233
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4234
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4235
|
+
resolvedRel = "src/" + specifier.slice(2);
|
|
4236
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4237
|
+
resolvedRel = specifier;
|
|
4238
|
+
}
|
|
4239
|
+
if (!resolvedRel) {
|
|
4240
|
+
return { isExternal: true, isDiFramework: false };
|
|
4241
|
+
}
|
|
4242
|
+
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4243
|
+
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4244
|
+
const targetFeature = featMatch[1];
|
|
4245
|
+
const targetSubfolder = featMatch[2];
|
|
4246
|
+
const rest = featMatch[3] || "";
|
|
4247
|
+
let targetLayer;
|
|
4248
|
+
if (targetSubfolder === "domain" || targetSubfolder === "errors") {
|
|
4249
|
+
targetLayer = "domain";
|
|
4250
|
+
} else if (targetSubfolder === "useCases" || targetSubfolder === "usecases") {
|
|
4251
|
+
targetLayer = "useCases";
|
|
4252
|
+
} else if (targetSubfolder === "services") {
|
|
4253
|
+
targetLayer = "services";
|
|
4254
|
+
} else if (targetSubfolder === "infrastructure") {
|
|
4255
|
+
targetLayer = "infrastructure";
|
|
4256
|
+
} else if (targetSubfolder === "pxExpress" || targetSubfolder === "presentation" || targetSubfolder === "handlers" || targetSubfolder.startsWith("px")) {
|
|
4257
|
+
targetLayer = "presentation";
|
|
4258
|
+
}
|
|
4259
|
+
const basePart = rest ? path2.basename(rest) : targetSubfolder;
|
|
4260
|
+
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4261
|
+
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4262
|
+
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4263
|
+
return {
|
|
4264
|
+
isExternal: false,
|
|
4265
|
+
isDiFramework: false,
|
|
4266
|
+
targetFeature,
|
|
4267
|
+
targetLayer,
|
|
4268
|
+
targetSubfolder,
|
|
4269
|
+
targetFileName,
|
|
4270
|
+
isTargetPort,
|
|
4271
|
+
isTargetRepo
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4275
|
+
if (sharedMatch) {
|
|
4276
|
+
const targetSharedModule = sharedMatch[1];
|
|
4277
|
+
const sharedRest = sharedMatch[2] || "";
|
|
4278
|
+
const isSharedRoot = !sharedRest.includes("/");
|
|
4279
|
+
const isSharedPorts = sharedRest.startsWith("ports/") || sharedRest === "ports";
|
|
4280
|
+
const isSharedInfra = sharedRest.startsWith("infrastructure/") || sharedRest === "infrastructure";
|
|
4281
|
+
const isSharedDomain = sharedRest.startsWith("domain/") || sharedRest === "domain";
|
|
4282
|
+
return {
|
|
4283
|
+
isExternal: false,
|
|
4284
|
+
isDiFramework: false,
|
|
4285
|
+
targetSharedModule,
|
|
4286
|
+
isSharedRoot,
|
|
4287
|
+
isSharedPorts,
|
|
4288
|
+
isSharedInfra,
|
|
4289
|
+
isSharedDomain
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
return { isExternal: true, isDiFramework: false };
|
|
4293
|
+
}
|
|
4294
|
+
function extractImportStatements(content) {
|
|
4295
|
+
const statements = [];
|
|
4296
|
+
const lines = content.split(/\r?\n/);
|
|
4297
|
+
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;
|
|
4298
|
+
let match;
|
|
4299
|
+
while ((match = regex.exec(content)) !== null) {
|
|
4300
|
+
const specifier = match[1];
|
|
4301
|
+
const charIndex = match.index;
|
|
4302
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4303
|
+
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
4304
|
+
statements.push({
|
|
4305
|
+
specifier,
|
|
4306
|
+
line: lineNum,
|
|
4307
|
+
rawCode
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
return statements;
|
|
4311
|
+
}
|
|
4312
|
+
var FeatureDependencyGraph = class {
|
|
4313
|
+
edges = /* @__PURE__ */ new Map();
|
|
4314
|
+
addEdge(evidence) {
|
|
4315
|
+
const { fromFeature, toFeature } = evidence;
|
|
4316
|
+
if (fromFeature === toFeature) {
|
|
4317
|
+
return { createsCycle: false };
|
|
4318
|
+
}
|
|
4319
|
+
if (!this.edges.has(fromFeature)) {
|
|
4320
|
+
this.edges.set(fromFeature, /* @__PURE__ */ new Map());
|
|
4321
|
+
}
|
|
4322
|
+
this.edges.get(fromFeature).set(toFeature, evidence);
|
|
4323
|
+
const reverse = this.edges.get(toFeature)?.get(fromFeature);
|
|
4324
|
+
if (reverse) {
|
|
4325
|
+
return { createsCycle: true, directPartner: reverse, cycleChain: [fromFeature, toFeature, fromFeature] };
|
|
4326
|
+
}
|
|
4327
|
+
const cycle = this.findCycleFrom(fromFeature);
|
|
4328
|
+
if (cycle && cycle.length > 0) {
|
|
4329
|
+
return { createsCycle: true, cycleChain: cycle };
|
|
4330
|
+
}
|
|
4331
|
+
return { createsCycle: false };
|
|
4332
|
+
}
|
|
4333
|
+
getEdges() {
|
|
4334
|
+
const list = [];
|
|
4335
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4336
|
+
for (const [to, ev] of toMap.entries()) {
|
|
4337
|
+
list.push({ from, to, evidence: ev });
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
return list;
|
|
4341
|
+
}
|
|
4342
|
+
findDirectCycles() {
|
|
4343
|
+
const cycles = [];
|
|
4344
|
+
const checkedPairs = /* @__PURE__ */ new Set();
|
|
4345
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4346
|
+
for (const [to, evAtoB] of toMap.entries()) {
|
|
4347
|
+
const pairKey = [from, to].sort().join("<->");
|
|
4348
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
4349
|
+
const evBtoA = this.edges.get(to)?.get(from);
|
|
4350
|
+
if (evBtoA) {
|
|
4351
|
+
checkedPairs.add(pairKey);
|
|
4352
|
+
cycles.push({
|
|
4353
|
+
featureA: from,
|
|
4354
|
+
featureB: to,
|
|
4355
|
+
cycle: [from, to, from],
|
|
4356
|
+
evidenceAtoB: evAtoB,
|
|
4357
|
+
evidenceBtoA: evBtoA
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
return cycles;
|
|
4363
|
+
}
|
|
4364
|
+
findCycleFrom(start) {
|
|
4365
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4366
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
4367
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4368
|
+
const dfs = (curr) => {
|
|
4369
|
+
visited.add(curr);
|
|
4370
|
+
inStack.add(curr);
|
|
4371
|
+
const neighbors = this.edges.get(curr);
|
|
4372
|
+
if (neighbors) {
|
|
4373
|
+
for (const next of neighbors.keys()) {
|
|
4374
|
+
if (!visited.has(next)) {
|
|
4375
|
+
parent.set(next, curr);
|
|
4376
|
+
const cycle = dfs(next);
|
|
4377
|
+
if (cycle) return cycle;
|
|
4378
|
+
} else if (inStack.has(next)) {
|
|
4379
|
+
const cycle = [next];
|
|
4380
|
+
let p16 = curr;
|
|
4381
|
+
while (p16 !== next) {
|
|
4382
|
+
cycle.push(p16);
|
|
4383
|
+
p16 = parent.get(p16) || next;
|
|
4384
|
+
}
|
|
4385
|
+
cycle.push(next);
|
|
4386
|
+
cycle.reverse();
|
|
4387
|
+
return cycle;
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
inStack.delete(curr);
|
|
4392
|
+
return null;
|
|
4393
|
+
};
|
|
4394
|
+
return dfs(start);
|
|
4395
|
+
}
|
|
4396
|
+
};
|
|
4397
|
+
function discoverAllFeatures(projectRoot) {
|
|
4398
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
4399
|
+
if (!fs9.existsSync(featuresBase)) return [];
|
|
4400
|
+
const entries = fs9.readdirSync(featuresBase, { withFileTypes: true });
|
|
4401
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
4402
|
+
name: e.name,
|
|
4403
|
+
dir: path2.join(featuresBase, e.name)
|
|
4404
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4405
|
+
}
|
|
4406
|
+
function lintCodebase(options = {}) {
|
|
4407
|
+
const startTime = Date.now();
|
|
4408
|
+
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
4409
|
+
const failFast = !options.complete;
|
|
4410
|
+
const features = discoverAllFeatures(projectRoot);
|
|
4411
|
+
const graph = new FeatureDependencyGraph();
|
|
4412
|
+
const allViolations = [];
|
|
4413
|
+
const results = [];
|
|
4414
|
+
const detectedCycles = [];
|
|
4415
|
+
const addViolation = (v) => {
|
|
4416
|
+
allViolations.push(v);
|
|
4417
|
+
const featureResult = results.find((r) => r.featureName === v.feature);
|
|
4418
|
+
if (featureResult) {
|
|
4419
|
+
featureResult.violations.push(v);
|
|
4420
|
+
featureResult.passed = false;
|
|
4421
|
+
}
|
|
4422
|
+
if (failFast) {
|
|
4423
|
+
const errorMsg = `[FAIL FAST] Dependency rule violation detected:
|
|
4424
|
+
Feature: ${v.feature}
|
|
4425
|
+
Category: ${v.category}
|
|
4426
|
+
Rule: ${v.ruleId}
|
|
4427
|
+
Location: ${v.relativePath}:${v.line}
|
|
4428
|
+
Code: ${v.code}
|
|
4429
|
+
Explanation: ${v.advice}`;
|
|
4430
|
+
throw new DependencyRuleError(errorMsg, [v], true);
|
|
4431
|
+
}
|
|
4432
|
+
};
|
|
4433
|
+
for (const feat of features) {
|
|
4434
|
+
results.push({
|
|
4435
|
+
featureName: feat.name,
|
|
4436
|
+
featureDir: feat.dir,
|
|
4437
|
+
scannedFilesCount: 0,
|
|
4438
|
+
passed: true,
|
|
4439
|
+
violations: [],
|
|
4440
|
+
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
for (const feat of features) {
|
|
4444
|
+
const featResult = results.find((r) => r.featureName === feat.name);
|
|
4445
|
+
const allFiles = scanDirFiles(feat.dir, projectRoot);
|
|
4446
|
+
featResult.scannedFilesCount = allFiles.length;
|
|
4447
|
+
for (const file of allFiles) {
|
|
4448
|
+
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
4449
|
+
const { layer } = classifyFile(file, projectRoot);
|
|
4450
|
+
if (!layer) continue;
|
|
4451
|
+
const content = fs9.readFileSync(file, "utf-8");
|
|
4452
|
+
const statements = extractImportStatements(content);
|
|
4453
|
+
if (layer === "domain") {
|
|
4454
|
+
const lines = content.split(/\r?\n/);
|
|
4455
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4456
|
+
const lineText = lines[i].trim();
|
|
4457
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
4458
|
+
addViolation({
|
|
4459
|
+
feature: feat.name,
|
|
4460
|
+
layer,
|
|
4461
|
+
file,
|
|
4462
|
+
relativePath: relPath,
|
|
4463
|
+
line: i + 1,
|
|
4464
|
+
category: "Domain Dependence Violation",
|
|
4465
|
+
code: lineText,
|
|
4466
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4467
|
+
severity: "error",
|
|
4468
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
4469
|
+
fixExample: {
|
|
4470
|
+
bad: lineText,
|
|
4471
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
for (const stmt of statements) {
|
|
4478
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
4479
|
+
if (target.targetFeature && target.targetFeature !== feat.name) {
|
|
4480
|
+
const evidence = {
|
|
4481
|
+
fromFeature: feat.name,
|
|
4482
|
+
toFeature: target.targetFeature,
|
|
4483
|
+
file,
|
|
4484
|
+
relativePath: relPath,
|
|
4485
|
+
line: stmt.line,
|
|
4486
|
+
code: stmt.rawCode
|
|
4487
|
+
};
|
|
4488
|
+
const cycleResult = graph.addEdge(evidence);
|
|
4489
|
+
if (cycleResult.createsCycle) {
|
|
4490
|
+
const partner = cycleResult.directPartner;
|
|
4491
|
+
detectedCycles.push({
|
|
4492
|
+
featureA: feat.name,
|
|
4493
|
+
featureB: target.targetFeature,
|
|
4494
|
+
cycle: cycleResult.cycleChain,
|
|
4495
|
+
evidenceAtoB: evidence,
|
|
4496
|
+
evidenceBtoA: partner || evidence
|
|
4497
|
+
});
|
|
4498
|
+
addViolation({
|
|
4499
|
+
feature: feat.name,
|
|
4500
|
+
layer,
|
|
4501
|
+
file,
|
|
4502
|
+
relativePath: relPath,
|
|
4503
|
+
line: stmt.line,
|
|
4504
|
+
category: "Circular Feature Dependency Violation",
|
|
4505
|
+
code: stmt.rawCode,
|
|
4506
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4507
|
+
severity: "error",
|
|
4508
|
+
advice: `Circular feature dependency detected between '${feat.name}' and '${target.targetFeature}'. Dependency flow must be strictly one-way: for any two features, only one can know about the other exists. If '${feat.name}' needs to impose a side effect on '${target.targetFeature}' (or vice versa), it must do so via an event, never via a direct dependency.`,
|
|
4509
|
+
fixExample: {
|
|
4510
|
+
bad: stmt.rawCode,
|
|
4511
|
+
good: `// Emit a Domain/Application Event (e.g. publish '${feat.name}Changed') instead of depending on '${target.targetFeature}'.`
|
|
4512
|
+
}
|
|
4513
|
+
});
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (layer === "domain") {
|
|
4517
|
+
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
4518
|
+
addViolation({
|
|
4519
|
+
feature: feat.name,
|
|
4520
|
+
layer,
|
|
4521
|
+
file,
|
|
4522
|
+
relativePath: relPath,
|
|
4523
|
+
line: stmt.line,
|
|
4524
|
+
category: "Domain Purity Violation",
|
|
4525
|
+
code: stmt.rawCode,
|
|
4526
|
+
ruleId: "D1_OUTER_IMPORT",
|
|
4527
|
+
severity: "error",
|
|
4528
|
+
advice: `Domain layer must not import from outer layers (${target.targetLayer}). Keep the domain core pure.`,
|
|
4529
|
+
fixExample: {
|
|
4530
|
+
bad: stmt.rawCode,
|
|
4531
|
+
good: "// Define interfaces, types, or value objects directly within the domain layer."
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
if (target.isDiFramework) {
|
|
4536
|
+
addViolation({
|
|
4537
|
+
feature: feat.name,
|
|
4538
|
+
layer,
|
|
4539
|
+
file,
|
|
4540
|
+
relativePath: relPath,
|
|
4541
|
+
line: stmt.line,
|
|
4542
|
+
category: "Domain Dependence Violation",
|
|
4543
|
+
code: stmt.rawCode,
|
|
4544
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4545
|
+
severity: "error",
|
|
4546
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
4547
|
+
fixExample: {
|
|
4548
|
+
bad: stmt.rawCode,
|
|
4549
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
4550
|
+
}
|
|
4551
|
+
});
|
|
4552
|
+
}
|
|
4553
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
4554
|
+
addViolation({
|
|
4555
|
+
feature: feat.name,
|
|
4556
|
+
layer,
|
|
4557
|
+
file,
|
|
4558
|
+
relativePath: relPath,
|
|
4559
|
+
line: stmt.line,
|
|
4560
|
+
category: "Domain Purity Violation",
|
|
4561
|
+
code: stmt.rawCode,
|
|
4562
|
+
ruleId: "D3_SHARED_NON_DOMAIN_IMPORT",
|
|
4563
|
+
severity: "error",
|
|
4564
|
+
advice: "Domain layer can only import pure value objects from shared/<module>/domain/*. Ports, infrastructure, and root classes must not be imported.",
|
|
4565
|
+
fixExample: {
|
|
4566
|
+
bad: stmt.rawCode,
|
|
4567
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/domain/...";`
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
4570
|
+
}
|
|
4571
|
+
if (target.isExternal && !target.isDiFramework) {
|
|
4572
|
+
addViolation({
|
|
4573
|
+
feature: feat.name,
|
|
4574
|
+
layer,
|
|
4575
|
+
file,
|
|
4576
|
+
relativePath: relPath,
|
|
4577
|
+
line: stmt.line,
|
|
4578
|
+
category: "Domain Purity Violation",
|
|
4579
|
+
code: stmt.rawCode,
|
|
4580
|
+
ruleId: "D4_EXTERNAL_LIBRARY",
|
|
4581
|
+
severity: "error",
|
|
4582
|
+
advice: `Domain layer must be pure and must not depend on external runtime libraries ('${stmt.specifier}').`,
|
|
4583
|
+
fixExample: {
|
|
4584
|
+
bad: stmt.rawCode,
|
|
4585
|
+
good: "// Model pure business interfaces without external libraries."
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
} else if (layer === "useCases") {
|
|
4590
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4591
|
+
addViolation({
|
|
4592
|
+
feature: feat.name,
|
|
4593
|
+
layer,
|
|
4594
|
+
file,
|
|
4595
|
+
relativePath: relPath,
|
|
4596
|
+
line: stmt.line,
|
|
4597
|
+
category: "Use Case Intra-Feature Violation",
|
|
4598
|
+
code: stmt.rawCode,
|
|
4599
|
+
ruleId: "U1_INTRA_USE_CASE",
|
|
4600
|
+
severity: "error",
|
|
4601
|
+
advice: `Use cases cannot depend on other use cases of the same feature ('${feat.name}'). Extract shared business logic to a domain repository or domain service.`,
|
|
4602
|
+
fixExample: {
|
|
4603
|
+
bad: stmt.rawCode,
|
|
4604
|
+
good: "// Inject a domain interface or domain service instead of another use case."
|
|
4605
|
+
}
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
if (target.targetLayer === "useCases" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4609
|
+
addViolation({
|
|
4610
|
+
feature: feat.name,
|
|
4611
|
+
layer,
|
|
4612
|
+
file,
|
|
4613
|
+
relativePath: relPath,
|
|
4614
|
+
line: stmt.line,
|
|
4615
|
+
category: "Use Case Cross-Feature Use Case Violation",
|
|
4616
|
+
code: stmt.rawCode,
|
|
4617
|
+
ruleId: "U2_CROSS_FEATURE_USE_CASE",
|
|
4618
|
+
severity: "error",
|
|
4619
|
+
advice: `Use cases cannot depend directly on use cases of foreign feature '${target.targetFeature}'. Treat features as separate microservices: define an interface in '${feat.name}/domain/' specifying what is needed, and implement it in '${feat.name}/infrastructure/'.`,
|
|
4620
|
+
fixExample: {
|
|
4621
|
+
bad: stmt.rawCode,
|
|
4622
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/ using '${target.targetFileName}'.`
|
|
4623
|
+
}
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
if (target.targetLayer === "infrastructure") {
|
|
4627
|
+
addViolation({
|
|
4628
|
+
feature: feat.name,
|
|
4629
|
+
layer,
|
|
4630
|
+
file,
|
|
4631
|
+
relativePath: relPath,
|
|
4632
|
+
line: stmt.line,
|
|
4633
|
+
category: "Use Case Layer Violation",
|
|
4634
|
+
code: stmt.rawCode,
|
|
4635
|
+
ruleId: "U3_DIRECT_INFRASTRUCTURE",
|
|
4636
|
+
severity: "error",
|
|
4637
|
+
advice: "Use cases cannot import concrete infrastructure directly. Depend on Domain Ports of the same feature via DI.",
|
|
4638
|
+
fixExample: {
|
|
4639
|
+
bad: stmt.rawCode,
|
|
4640
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4641
|
+
}
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4645
|
+
addViolation({
|
|
4646
|
+
feature: feat.name,
|
|
4647
|
+
layer,
|
|
4648
|
+
file,
|
|
4649
|
+
relativePath: relPath,
|
|
4650
|
+
line: stmt.line,
|
|
4651
|
+
category: "Use Case Cross-Feature Port Violation",
|
|
4652
|
+
code: stmt.rawCode,
|
|
4653
|
+
ruleId: "U4_CROSS_FEATURE_PORT",
|
|
4654
|
+
severity: "error",
|
|
4655
|
+
advice: `Use cases cannot depend on domain interfaces/ports of foreign feature '${target.targetFeature}'. Define an interface in '${feat.name}/domain/' (consumer-driven port) and implement it in '${feat.name}/infrastructure/'.`,
|
|
4656
|
+
fixExample: {
|
|
4657
|
+
bad: stmt.rawCode,
|
|
4658
|
+
good: `// Define a local interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4659
|
+
}
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
if (target.targetLayer === "services" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4663
|
+
addViolation({
|
|
4664
|
+
feature: feat.name,
|
|
4665
|
+
layer,
|
|
4666
|
+
file,
|
|
4667
|
+
relativePath: relPath,
|
|
4668
|
+
line: stmt.line,
|
|
4669
|
+
category: "Use Case Layer Violation",
|
|
4670
|
+
code: stmt.rawCode,
|
|
4671
|
+
ruleId: "U5_CROSS_FEATURE_SERVICE",
|
|
4672
|
+
severity: "error",
|
|
4673
|
+
advice: `Use cases cannot depend on services of foreign feature '${target.targetFeature}'.`,
|
|
4674
|
+
fixExample: {
|
|
4675
|
+
bad: stmt.rawCode,
|
|
4676
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4677
|
+
}
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4681
|
+
addViolation({
|
|
4682
|
+
feature: feat.name,
|
|
4683
|
+
layer,
|
|
4684
|
+
file,
|
|
4685
|
+
relativePath: relPath,
|
|
4686
|
+
line: stmt.line,
|
|
4687
|
+
category: "Shared Module Root Class Violation",
|
|
4688
|
+
code: stmt.rawCode,
|
|
4689
|
+
ruleId: "U6_SHARED_NON_ROOT",
|
|
4690
|
+
severity: "error",
|
|
4691
|
+
advice: `Use cases can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.`,
|
|
4692
|
+
fixExample: {
|
|
4693
|
+
bad: stmt.rawCode,
|
|
4694
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4695
|
+
}
|
|
4696
|
+
});
|
|
4697
|
+
}
|
|
4698
|
+
} else if (layer === "services") {
|
|
4699
|
+
if (target.targetLayer === "useCases") {
|
|
4700
|
+
addViolation({
|
|
4701
|
+
feature: feat.name,
|
|
4702
|
+
layer,
|
|
4703
|
+
file,
|
|
4704
|
+
relativePath: relPath,
|
|
4705
|
+
line: stmt.line,
|
|
4706
|
+
category: "Service Intra-Feature Violation",
|
|
4707
|
+
code: stmt.rawCode,
|
|
4708
|
+
ruleId: "S1_USE_CASE_DEPENDENCY",
|
|
4709
|
+
severity: "error",
|
|
4710
|
+
advice: "Services cannot depend on use cases of any feature.",
|
|
4711
|
+
fixExample: {
|
|
4712
|
+
bad: stmt.rawCode,
|
|
4713
|
+
good: "// Inject domain repository ports directly instead of use cases."
|
|
4714
|
+
}
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
4717
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4718
|
+
addViolation({
|
|
4719
|
+
feature: feat.name,
|
|
4720
|
+
layer,
|
|
4721
|
+
file,
|
|
4722
|
+
relativePath: relPath,
|
|
4723
|
+
line: stmt.line,
|
|
4724
|
+
category: "Service Cross-Feature Port Violation",
|
|
4725
|
+
code: stmt.rawCode,
|
|
4726
|
+
ruleId: "S2_CROSS_FEATURE_PORT",
|
|
4727
|
+
severity: "error",
|
|
4728
|
+
advice: `Services cannot depend on domain ports of foreign feature '${target.targetFeature}'.`,
|
|
4729
|
+
fixExample: {
|
|
4730
|
+
bad: stmt.rawCode,
|
|
4731
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4732
|
+
}
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
if (target.targetLayer === "infrastructure") {
|
|
4736
|
+
addViolation({
|
|
4737
|
+
feature: feat.name,
|
|
4738
|
+
layer,
|
|
4739
|
+
file,
|
|
4740
|
+
relativePath: relPath,
|
|
4741
|
+
line: stmt.line,
|
|
4742
|
+
category: "Service Layer Violation",
|
|
4743
|
+
code: stmt.rawCode,
|
|
4744
|
+
ruleId: "S3_DIRECT_INFRASTRUCTURE",
|
|
4745
|
+
severity: "error",
|
|
4746
|
+
advice: "Services cannot import concrete infrastructure directly. Depend on Domain Ports via DI.",
|
|
4747
|
+
fixExample: {
|
|
4748
|
+
bad: stmt.rawCode,
|
|
4749
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4750
|
+
}
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4754
|
+
addViolation({
|
|
4755
|
+
feature: feat.name,
|
|
4756
|
+
layer,
|
|
4757
|
+
file,
|
|
4758
|
+
relativePath: relPath,
|
|
4759
|
+
line: stmt.line,
|
|
4760
|
+
category: "Shared Module Root Class Violation",
|
|
4761
|
+
code: stmt.rawCode,
|
|
4762
|
+
ruleId: "S4_SHARED_NON_ROOT",
|
|
4763
|
+
severity: "error",
|
|
4764
|
+
advice: "Services can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4765
|
+
fixExample: {
|
|
4766
|
+
bad: stmt.rawCode,
|
|
4767
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4768
|
+
}
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
} else if (layer === "infrastructure") {
|
|
4772
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4773
|
+
addViolation({
|
|
4774
|
+
feature: feat.name,
|
|
4775
|
+
layer,
|
|
4776
|
+
file,
|
|
4777
|
+
relativePath: relPath,
|
|
4778
|
+
line: stmt.line,
|
|
4779
|
+
category: "Adapter Layer Violation",
|
|
4780
|
+
code: stmt.rawCode,
|
|
4781
|
+
ruleId: "I1_INTRA_USE_CASE",
|
|
4782
|
+
severity: "error",
|
|
4783
|
+
advice: `Infrastructure adapters cannot depend on use cases of the same feature ('${feat.name}'). Adapters implement domain ports.`,
|
|
4784
|
+
fixExample: {
|
|
4785
|
+
bad: stmt.rawCode,
|
|
4786
|
+
good: "// Adapters implement domain ports and should not invoke local application use cases."
|
|
4787
|
+
}
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4791
|
+
addViolation({
|
|
4792
|
+
feature: feat.name,
|
|
4793
|
+
layer,
|
|
4794
|
+
file,
|
|
4795
|
+
relativePath: relPath,
|
|
4796
|
+
line: stmt.line,
|
|
4797
|
+
category: "Shared Module Root Class Violation",
|
|
4798
|
+
code: stmt.rawCode,
|
|
4799
|
+
ruleId: "I2_SHARED_NON_ROOT",
|
|
4800
|
+
severity: "error",
|
|
4801
|
+
advice: "Infrastructure adapters can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4802
|
+
fixExample: {
|
|
4803
|
+
bad: stmt.rawCode,
|
|
4804
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
4807
|
+
}
|
|
4808
|
+
} else if (layer === "presentation") {
|
|
4809
|
+
if (target.targetLayer === "infrastructure") {
|
|
4810
|
+
addViolation({
|
|
4811
|
+
feature: feat.name,
|
|
4812
|
+
layer,
|
|
4813
|
+
file,
|
|
4814
|
+
relativePath: relPath,
|
|
4815
|
+
line: stmt.line,
|
|
4816
|
+
category: "Presentation Layer Violation",
|
|
4817
|
+
code: stmt.rawCode,
|
|
4818
|
+
ruleId: "P1_DIRECT_INFRASTRUCTURE",
|
|
4819
|
+
severity: "error",
|
|
4820
|
+
advice: "Presentation handlers must not import infrastructure directly. Inject Use Cases via DI.",
|
|
4821
|
+
fixExample: {
|
|
4822
|
+
bad: stmt.rawCode,
|
|
4823
|
+
good: "// Inject the corresponding Use Case via static deps and invoke its execute() method."
|
|
4824
|
+
}
|
|
4825
|
+
});
|
|
4826
|
+
}
|
|
4827
|
+
if (target.targetLayer === "domain" && (target.isTargetRepo || target.isTargetPort)) {
|
|
4828
|
+
addViolation({
|
|
4829
|
+
feature: feat.name,
|
|
4830
|
+
layer,
|
|
4831
|
+
file,
|
|
4832
|
+
relativePath: relPath,
|
|
4833
|
+
line: stmt.line,
|
|
4834
|
+
category: "Presentation Layer Violation",
|
|
4835
|
+
code: stmt.rawCode,
|
|
4836
|
+
ruleId: "P2_DIRECT_REPOSITORY",
|
|
4837
|
+
severity: "error",
|
|
4838
|
+
advice: "Presentation handlers must not depend on domain repositories or ports directly. Inject Use Cases via DI.",
|
|
4839
|
+
fixExample: {
|
|
4840
|
+
bad: stmt.rawCode,
|
|
4841
|
+
good: "// Presentation layer must only orchestrate Use Cases."
|
|
4842
|
+
}
|
|
4843
|
+
});
|
|
4844
|
+
}
|
|
4845
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4846
|
+
addViolation({
|
|
4847
|
+
feature: feat.name,
|
|
4848
|
+
layer,
|
|
4849
|
+
file,
|
|
4850
|
+
relativePath: relPath,
|
|
4851
|
+
line: stmt.line,
|
|
4852
|
+
category: "Shared Module Root Class Violation",
|
|
4853
|
+
code: stmt.rawCode,
|
|
4854
|
+
ruleId: "P3_SHARED_NON_ROOT",
|
|
4855
|
+
severity: "error",
|
|
4856
|
+
advice: "Presentation handlers can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4857
|
+
fixExample: {
|
|
4858
|
+
bad: stmt.rawCode,
|
|
4859
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
const directCycles = graph.findDirectCycles();
|
|
4868
|
+
for (const c of directCycles) {
|
|
4869
|
+
const existing = detectedCycles.find(
|
|
4870
|
+
(dc) => dc.featureA === c.featureA && dc.featureB === c.featureB || dc.featureA === c.featureB && dc.featureB === c.featureA
|
|
4871
|
+
);
|
|
4872
|
+
if (!existing) {
|
|
4873
|
+
detectedCycles.push(c);
|
|
4874
|
+
addViolation({
|
|
4875
|
+
feature: c.featureA,
|
|
4876
|
+
file: c.evidenceAtoB.file,
|
|
4877
|
+
relativePath: c.evidenceAtoB.relativePath,
|
|
4878
|
+
line: c.evidenceAtoB.line,
|
|
4879
|
+
category: "Circular Feature Dependency Violation",
|
|
4880
|
+
code: c.evidenceAtoB.code,
|
|
4881
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4882
|
+
severity: "error",
|
|
4883
|
+
advice: `Circular feature dependency detected between '${c.featureA}' and '${c.featureB}'. Dependency flow must be strictly one-way: for any two features, only one can know about the other exists. If the unaware feature needs to impose a side effect on the other, it must do so via an event, never via a direct dependency.`
|
|
4884
|
+
});
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
const durationMs = Date.now() - startTime;
|
|
4888
|
+
const cleanFeatures = results.filter((r) => r.passed).length;
|
|
4889
|
+
const violatedFeatures = results.filter((r) => !r.passed).length;
|
|
4890
|
+
const summary = {
|
|
4891
|
+
totalFeatures: results.length,
|
|
4892
|
+
cleanFeatures,
|
|
4893
|
+
violatedFeatures,
|
|
4894
|
+
totalViolations: allViolations.length,
|
|
4895
|
+
durationMs,
|
|
4896
|
+
results,
|
|
4897
|
+
allViolations,
|
|
4898
|
+
circularDependencies: detectedCycles
|
|
4899
|
+
};
|
|
4900
|
+
if (allViolations.length > 0 && options.throwOnError !== false) {
|
|
4901
|
+
const errorHeader = `Dependency rule audit failed with ${allViolations.length} violation(s) across ${violatedFeatures} feature(s):`;
|
|
4902
|
+
const compiledDetails = allViolations.map(
|
|
4903
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4904
|
+
Rule: ${v.ruleId}
|
|
4905
|
+
Code: ${v.code}
|
|
4906
|
+
Advice: ${v.advice}`
|
|
4907
|
+
).join("\n\n");
|
|
4908
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4909
|
+
|
|
4910
|
+
${compiledDetails}
|
|
4911
|
+
`, allViolations, false);
|
|
4912
|
+
}
|
|
4913
|
+
return summary;
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
// src/commands/lint.ts
|
|
4917
|
+
function printLintBanner(mode) {
|
|
4918
|
+
console.log("");
|
|
4919
|
+
const title = " Mason Clean Architecture Dependency Linter ";
|
|
4920
|
+
const line = "\u2501".repeat(title.length + 2);
|
|
4921
|
+
console.log(pc15.cyan(` \u250F${line}\u2513`));
|
|
4922
|
+
console.log(` ${pc15.cyan("\u2503")} ${pc15.bgCyan(pc15.black(title))} ${pc15.cyan("\u2503")}`);
|
|
4923
|
+
console.log(pc15.cyan(` \u2517${line}\u251B`));
|
|
4924
|
+
const modeText = mode === "failFast" ? `${pc15.yellow("Fail-Fast")} ${pc15.dim("(default: stops at first error)")}` : `${pc15.magenta("Complete")} ${pc15.dim("(--complete: compiles all errors)")}`;
|
|
4925
|
+
console.log(` Mode: ${modeText}`);
|
|
4926
|
+
console.log("");
|
|
4927
|
+
}
|
|
4928
|
+
function formatViolation(v, showFixHint = true) {
|
|
4929
|
+
const lines = [];
|
|
4930
|
+
const categoryBadge = pc15.red(pc15.bold(`[${v.category}]`));
|
|
4931
|
+
const location = pc15.white(`${v.relativePath}:${pc15.yellow(String(v.line))}`);
|
|
4932
|
+
lines.push(` ${categoryBadge} ${location}`);
|
|
4933
|
+
lines.push(` ${pc15.dim("Rule ID:")} ${pc15.yellow(v.ruleId)}`);
|
|
4934
|
+
lines.push(` ${pc15.dim("Code:")} ${pc15.magenta(v.code)}`);
|
|
4935
|
+
lines.push(` ${pc15.dim("Advice:")} ${pc15.cyan(v.advice)}`);
|
|
4936
|
+
if (showFixHint && v.fixExample) {
|
|
4937
|
+
lines.push(` ${pc15.dim("Example:")}`);
|
|
4938
|
+
lines.push(` ${pc15.red(`- ${v.fixExample.bad}`)}`);
|
|
4939
|
+
lines.push(` ${pc15.green(`+ ${v.fixExample.good}`)}`);
|
|
4940
|
+
}
|
|
4941
|
+
return lines.join("\n");
|
|
4942
|
+
}
|
|
4943
|
+
function formatFeatureResult(res, options = {}) {
|
|
4944
|
+
const lines = [];
|
|
4945
|
+
if (res.passed) {
|
|
4946
|
+
lines.push(
|
|
4947
|
+
` ${pc15.green("\u2714")} ${pc15.bold(res.featureName)} ${pc15.dim(
|
|
4948
|
+
`(${res.scannedFilesCount} files checked - clean)`
|
|
4949
|
+
)}`
|
|
4950
|
+
);
|
|
4951
|
+
return lines.join("\n");
|
|
4952
|
+
}
|
|
4953
|
+
const countBadge = pc15.red(
|
|
4954
|
+
`(${res.violations.length} violation${res.violations.length > 1 ? "s" : ""})`
|
|
4955
|
+
);
|
|
4956
|
+
lines.push(`
|
|
4957
|
+
${pc15.red("\u2716")} ${pc15.bold(pc15.white(res.featureName))} ${countBadge}`);
|
|
4958
|
+
lines.push(` ${pc15.dim("Path:")} ${res.featureDir}`);
|
|
4959
|
+
lines.push("");
|
|
4960
|
+
for (const v of res.violations) {
|
|
4961
|
+
lines.push(formatViolation(v, options.fixHint !== false));
|
|
4962
|
+
lines.push("");
|
|
4963
|
+
}
|
|
4964
|
+
return lines.join("\n");
|
|
4965
|
+
}
|
|
4966
|
+
function printTerminalReport(summary, options = {}) {
|
|
4967
|
+
for (const res of summary.results) {
|
|
4968
|
+
const formatted = formatFeatureResult(res, options);
|
|
4969
|
+
if (formatted.trim()) {
|
|
4970
|
+
console.log(formatted);
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
console.log("\u2501".repeat(70));
|
|
4974
|
+
const statusIcon = summary.totalViolations === 0 ? pc15.green("\u2714 PASS") : pc15.red("\u2716 FAIL");
|
|
4975
|
+
console.log(
|
|
4976
|
+
` ${statusIcon} ${pc15.bold("Dependency Rules Audit Result")} ${pc15.dim(
|
|
4977
|
+
`(${summary.durationMs}ms)`
|
|
4978
|
+
)}`
|
|
4979
|
+
);
|
|
4980
|
+
console.log(
|
|
4981
|
+
` ${pc15.bold("Features:")} ${pc15.cyan(String(summary.totalFeatures))} total | ${pc15.green(
|
|
4982
|
+
String(summary.cleanFeatures)
|
|
4983
|
+
)} clean | ${summary.violatedFeatures > 0 ? pc15.red(String(summary.violatedFeatures)) : pc15.green("0")} with violations`
|
|
4984
|
+
);
|
|
4985
|
+
console.log(
|
|
4986
|
+
` ${pc15.bold("Violations:")} ${summary.totalViolations > 0 ? pc15.red(pc15.bold(String(summary.totalViolations))) : pc15.green("0")} rule violations found`
|
|
4987
|
+
);
|
|
4988
|
+
console.log("\u2501".repeat(70));
|
|
4989
|
+
console.log("");
|
|
4990
|
+
}
|
|
4991
|
+
async function handleLint(flags = {}) {
|
|
4992
|
+
const complete = Boolean(flags.complete);
|
|
4993
|
+
const mode = complete ? "complete" : "failFast";
|
|
4994
|
+
const quiet = Boolean(flags.quiet);
|
|
4995
|
+
if (!quiet) {
|
|
4996
|
+
printLintBanner(mode);
|
|
4997
|
+
}
|
|
4998
|
+
const options = {
|
|
4999
|
+
complete,
|
|
5000
|
+
quiet,
|
|
5001
|
+
targetDir: flags.targetDir || flags.target,
|
|
5002
|
+
projectRoot: flags.projectRoot,
|
|
5003
|
+
fixHint: flags.fixHint !== false,
|
|
5004
|
+
throwOnError: false
|
|
5005
|
+
};
|
|
5006
|
+
try {
|
|
5007
|
+
const summary = lintCodebase(options);
|
|
5008
|
+
if (!quiet) {
|
|
5009
|
+
printTerminalReport(summary, options);
|
|
5010
|
+
}
|
|
5011
|
+
if (summary.totalViolations > 0) {
|
|
5012
|
+
const errorHeader = `Dependency rule audit failed with ${summary.totalViolations} violation(s) across ${summary.violatedFeatures} feature(s):`;
|
|
5013
|
+
const compiledDetails = summary.allViolations.map(
|
|
5014
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
5015
|
+
Rule: ${v.ruleId}
|
|
5016
|
+
Code: ${v.code}
|
|
5017
|
+
Advice: ${v.advice}`
|
|
5018
|
+
).join("\n\n");
|
|
5019
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
5020
|
+
|
|
5021
|
+
${compiledDetails}
|
|
5022
|
+
`, summary.allViolations, false);
|
|
5023
|
+
}
|
|
5024
|
+
if (!quiet) {
|
|
5025
|
+
console.log(
|
|
5026
|
+
pc15.green(`\u2714 All dependency rules are being followed! (${summary.totalFeatures} features checked)`)
|
|
5027
|
+
);
|
|
5028
|
+
console.log("");
|
|
5029
|
+
}
|
|
5030
|
+
return summary;
|
|
5031
|
+
} catch (err) {
|
|
5032
|
+
if (err instanceof DependencyRuleError && err.isFailFast) {
|
|
5033
|
+
if (!quiet) {
|
|
5034
|
+
console.log(pc15.red(`
|
|
5035
|
+
\u2716 [FAIL FAST] Terminating on first detected dependency rule violation:`));
|
|
5036
|
+
const v = err.violations[0];
|
|
5037
|
+
if (v) {
|
|
5038
|
+
console.log(formatViolation(v, options.fixHint !== false));
|
|
5039
|
+
}
|
|
5040
|
+
console.log("");
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
throw err;
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
4163
5046
|
|
|
4164
5047
|
// src/cli.ts
|
|
4165
5048
|
function createCli() {
|
|
@@ -4167,7 +5050,7 @@ function createCli() {
|
|
|
4167
5050
|
cli.command("[...args]", "Interactive scaffolding and import wizard").option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").option("--registry", "Run in registry mode").option("--point-to <mapping>", "Point a dependency to an existing local module (e.g. --point-to time:mytime)").option("--alias <mapping>", "Import a dependency fresh under an alias name (e.g. --alias time:time-mason)").option("--tests", "Include test files (use --no-tests to exclude)").action(async (args, flags) => {
|
|
4168
5051
|
if (args && args.length > 0) {
|
|
4169
5052
|
p14.log.error(
|
|
4170
|
-
|
|
5053
|
+
pc15.red(
|
|
4171
5054
|
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
4172
5055
|
)
|
|
4173
5056
|
);
|
|
@@ -4233,14 +5116,21 @@ function createCli() {
|
|
|
4233
5116
|
value: "import",
|
|
4234
5117
|
label: "Import from registry",
|
|
4235
5118
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
5119
|
+
},
|
|
5120
|
+
{
|
|
5121
|
+
value: "lint",
|
|
5122
|
+
label: "Lint dependency rules",
|
|
5123
|
+
hint: "Scan codebase and verify Clean Architecture dependency rules"
|
|
4236
5124
|
}
|
|
4237
5125
|
]
|
|
4238
5126
|
})
|
|
4239
5127
|
);
|
|
4240
5128
|
if (choice === "create") {
|
|
4241
5129
|
await handleCreate(void 0, void 0, flags);
|
|
4242
|
-
} else {
|
|
5130
|
+
} else if (choice === "import") {
|
|
4243
5131
|
await handleImport(void 0, void 0, flags);
|
|
5132
|
+
} else if (choice === "lint") {
|
|
5133
|
+
await handleLint(flags);
|
|
4244
5134
|
}
|
|
4245
5135
|
} catch (err) {
|
|
4246
5136
|
handleCliError(err);
|
|
@@ -4266,6 +5156,22 @@ function createCli() {
|
|
|
4266
5156
|
handleCliError(err);
|
|
4267
5157
|
}
|
|
4268
5158
|
});
|
|
5159
|
+
cli.command(
|
|
5160
|
+
"lint [target]",
|
|
5161
|
+
"Scan codebase and verify Clean Architecture dependency rules"
|
|
5162
|
+
).option(
|
|
5163
|
+
"-c, --complete",
|
|
5164
|
+
"Scan entire codebase without failing fast, compiling all errors at the end"
|
|
5165
|
+
).option("-q, --quiet", "Suppress non-error output").example("mason lint").example("mason lint --complete").example("mason lint ./src").action(async (target, flags = {}) => {
|
|
5166
|
+
try {
|
|
5167
|
+
await handleLint({
|
|
5168
|
+
...flags,
|
|
5169
|
+
targetDir: target || flags.target || flags.dest
|
|
5170
|
+
});
|
|
5171
|
+
} catch (err) {
|
|
5172
|
+
handleCliError(err);
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
4269
5175
|
const createSubtypes = [
|
|
4270
5176
|
"feature",
|
|
4271
5177
|
"shared",
|
|
@@ -4297,7 +5203,10 @@ function handleCliError(err) {
|
|
|
4297
5203
|
process.exit(0);
|
|
4298
5204
|
}
|
|
4299
5205
|
const message = err instanceof Error ? err.message : String(err);
|
|
4300
|
-
p14.log.error(
|
|
5206
|
+
p14.log.error(pc15.red(`Error: ${message}`));
|
|
5207
|
+
if (process.env.VITEST) {
|
|
5208
|
+
throw err;
|
|
5209
|
+
}
|
|
4301
5210
|
process.exit(1);
|
|
4302
5211
|
}
|
|
4303
5212
|
async function runCli(argv = process.argv) {
|