@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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs4 from 'fs';
|
|
2
2
|
import path2 from 'path';
|
|
3
3
|
import * as p14 from '@clack/prompts';
|
|
4
|
-
import
|
|
4
|
+
import pc15 from 'picocolors';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
@@ -112,17 +112,17 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
112
112
|
function printGeneratedFiles(files, isDryRun = false) {
|
|
113
113
|
console.log("");
|
|
114
114
|
if (isDryRun) {
|
|
115
|
-
p14.log.warn(
|
|
115
|
+
p14.log.warn(pc15.yellow(pc15.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
116
116
|
}
|
|
117
117
|
for (const f of files) {
|
|
118
118
|
if (f.action === "created") {
|
|
119
|
-
p14.log.success(`${
|
|
119
|
+
p14.log.success(`${pc15.bold(f.relativePath)} ${pc15.dim("(created)")}`);
|
|
120
120
|
} else if (f.action === "overwritten") {
|
|
121
|
-
p14.log.warn(`${
|
|
121
|
+
p14.log.warn(`${pc15.bold(f.relativePath)} ${pc15.dim("(overwritten)")}`);
|
|
122
122
|
} else if (f.action === "skipped") {
|
|
123
|
-
p14.log.info(`${
|
|
123
|
+
p14.log.info(`${pc15.dim(f.relativePath)} ${pc15.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
124
124
|
} else {
|
|
125
|
-
p14.log.info(`${
|
|
125
|
+
p14.log.info(`${pc15.bold(f.relativePath)} ${pc15.dim("(preview)")}`);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -332,13 +332,13 @@ function handleCancel(result) {
|
|
|
332
332
|
}
|
|
333
333
|
function printBanner() {
|
|
334
334
|
console.log("");
|
|
335
|
-
p14.intro(
|
|
335
|
+
p14.intro(pc15.bgCyan(pc15.black(" @solid-stack/mason ")));
|
|
336
336
|
}
|
|
337
337
|
async function promptProperties(options = {}) {
|
|
338
338
|
const properties = options.defaults ? [...options.defaults] : [];
|
|
339
339
|
if (properties.length > 0) {
|
|
340
340
|
const summary = properties.map((p16) => `${p16.name}: ${p16.type}${p16.optional ? "?" : ""}`).join(", ");
|
|
341
|
-
p14.log.info(
|
|
341
|
+
p14.log.info(pc15.dim(`Pre-configured fields: ${summary}`));
|
|
342
342
|
const addMore = handleCancel(
|
|
343
343
|
await p14.confirm({
|
|
344
344
|
message: "Would you like to add custom fields/properties?",
|
|
@@ -349,7 +349,7 @@ async function promptProperties(options = {}) {
|
|
|
349
349
|
return properties;
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
|
-
p14.log.info(
|
|
352
|
+
p14.log.info(pc15.cyan("Enter properties (leave name blank when finished):"));
|
|
353
353
|
while (true) {
|
|
354
354
|
const name = handleCancel(
|
|
355
355
|
await p14.text({
|
|
@@ -397,7 +397,7 @@ async function promptProperties(options = {}) {
|
|
|
397
397
|
optional
|
|
398
398
|
});
|
|
399
399
|
p14.log.success(
|
|
400
|
-
|
|
400
|
+
pc15.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
401
401
|
);
|
|
402
402
|
}
|
|
403
403
|
return properties;
|
|
@@ -631,7 +631,7 @@ function installNpmPackages(projectRoot, packages, pm) {
|
|
|
631
631
|
installCmd = `npm install ${packages.join(" ")}`;
|
|
632
632
|
break;
|
|
633
633
|
}
|
|
634
|
-
p14.log.info(`Installing missing dependencies with ${manager}: ${
|
|
634
|
+
p14.log.info(`Installing missing dependencies with ${manager}: ${pc15.cyan(packages.join(", "))}`);
|
|
635
635
|
execSync(installCmd, {
|
|
636
636
|
cwd: projectRoot,
|
|
637
637
|
stdio: "inherit"
|
|
@@ -2348,7 +2348,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2348
2348
|
}
|
|
2349
2349
|
}
|
|
2350
2350
|
const s = p14.spinner();
|
|
2351
|
-
s.start(`Generating feature '${
|
|
2351
|
+
s.start(`Generating feature '${pc15.cyan(normalizedName)}'...`);
|
|
2352
2352
|
const options = {
|
|
2353
2353
|
name: normalizedName,
|
|
2354
2354
|
projectRoot,
|
|
@@ -2395,7 +2395,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2395
2395
|
s.stop("Feature scaffolded successfully!");
|
|
2396
2396
|
printGeneratedFiles(files, options.dryRun);
|
|
2397
2397
|
p14.outro(
|
|
2398
|
-
|
|
2398
|
+
pc15.green(
|
|
2399
2399
|
`\u2713 Feature '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2400
2400
|
)
|
|
2401
2401
|
);
|
|
@@ -2551,7 +2551,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2551
2551
|
);
|
|
2552
2552
|
}
|
|
2553
2553
|
const s = p14.spinner();
|
|
2554
|
-
s.start(`Generating shared module '${
|
|
2554
|
+
s.start(`Generating shared module '${pc15.cyan(normalizedName)}'...`);
|
|
2555
2555
|
const files = generateShared({
|
|
2556
2556
|
name: normalizedName,
|
|
2557
2557
|
description,
|
|
@@ -2565,7 +2565,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2565
2565
|
s.stop("Shared module generated successfully!");
|
|
2566
2566
|
printGeneratedFiles(files, Boolean(flags.dryRun));
|
|
2567
2567
|
p14.outro(
|
|
2568
|
-
|
|
2568
|
+
pc15.green(
|
|
2569
2569
|
`\u2713 Shared module '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2570
2570
|
)
|
|
2571
2571
|
);
|
|
@@ -2693,7 +2693,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2693
2693
|
}
|
|
2694
2694
|
}
|
|
2695
2695
|
const s = p14.spinner();
|
|
2696
|
-
s.start(`Generating use case '${
|
|
2696
|
+
s.start(`Generating use case '${pc15.cyan(ucName)}'...`);
|
|
2697
2697
|
const options = {
|
|
2698
2698
|
name: ucName,
|
|
2699
2699
|
feature,
|
|
@@ -2725,7 +2725,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2725
2725
|
}
|
|
2726
2726
|
s.stop("Use case generated successfully!");
|
|
2727
2727
|
printGeneratedFiles(files, options.dryRun);
|
|
2728
|
-
p14.outro(
|
|
2728
|
+
p14.outro(pc15.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2729
2729
|
}
|
|
2730
2730
|
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2731
2731
|
const projectRoot = findProjectRoot();
|
|
@@ -2796,7 +2796,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2796
2796
|
});
|
|
2797
2797
|
}
|
|
2798
2798
|
const s = p14.spinner();
|
|
2799
|
-
s.start(`Generating domain model '${
|
|
2799
|
+
s.start(`Generating domain model '${pc15.cyan(entityName)}'...`);
|
|
2800
2800
|
const options = {
|
|
2801
2801
|
name: entityName,
|
|
2802
2802
|
feature,
|
|
@@ -2809,7 +2809,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2809
2809
|
const file = generateDomain(options);
|
|
2810
2810
|
s.stop("Domain model generated successfully!");
|
|
2811
2811
|
printGeneratedFiles([file], options.dryRun);
|
|
2812
|
-
p14.outro(
|
|
2812
|
+
p14.outro(pc15.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2813
2813
|
}
|
|
2814
2814
|
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2815
2815
|
const projectRoot = findProjectRoot();
|
|
@@ -2978,7 +2978,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2978
2978
|
registerInProvider = !flags.noProvider;
|
|
2979
2979
|
}
|
|
2980
2980
|
const s = p14.spinner();
|
|
2981
|
-
s.start(`Generating infrastructure class '${
|
|
2981
|
+
s.start(`Generating infrastructure class '${pc15.cyan(className)}'...`);
|
|
2982
2982
|
const files = [];
|
|
2983
2983
|
const options = {
|
|
2984
2984
|
name: className,
|
|
@@ -3013,7 +3013,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
3013
3013
|
}
|
|
3014
3014
|
s.stop("Infrastructure class generated successfully!");
|
|
3015
3015
|
printGeneratedFiles(files, options.dryRun);
|
|
3016
|
-
p14.outro(
|
|
3016
|
+
p14.outro(pc15.green(`Infrastructure class '${className}' created!`));
|
|
3017
3017
|
}
|
|
3018
3018
|
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
3019
3019
|
const projectRoot = findProjectRoot();
|
|
@@ -3126,7 +3126,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
3126
3126
|
}
|
|
3127
3127
|
}
|
|
3128
3128
|
const s = p14.spinner();
|
|
3129
|
-
s.start(`Generating interface '${
|
|
3129
|
+
s.start(`Generating interface '${pc15.cyan(ifaceName)}'...`);
|
|
3130
3130
|
const files = [];
|
|
3131
3131
|
const options = {
|
|
3132
3132
|
name: ifaceName,
|
|
@@ -3175,7 +3175,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
3175
3175
|
}
|
|
3176
3176
|
s.stop("Interface generated successfully!");
|
|
3177
3177
|
printGeneratedFiles(files, options.dryRun);
|
|
3178
|
-
p14.outro(
|
|
3178
|
+
p14.outro(pc15.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
3179
3179
|
}
|
|
3180
3180
|
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
3181
3181
|
const projectRoot = findProjectRoot();
|
|
@@ -3292,7 +3292,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3292
3292
|
}
|
|
3293
3293
|
}
|
|
3294
3294
|
const s = p14.spinner();
|
|
3295
|
-
s.start(`Generating HTTP handler '${
|
|
3295
|
+
s.start(`Generating HTTP handler '${pc15.cyan(className)}'...`);
|
|
3296
3296
|
const options = {
|
|
3297
3297
|
name: className,
|
|
3298
3298
|
feature,
|
|
@@ -3310,7 +3310,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3310
3310
|
const files = generateHttpHandler(options);
|
|
3311
3311
|
s.stop("HTTP handler generated successfully!");
|
|
3312
3312
|
printGeneratedFiles(files, options.dryRun);
|
|
3313
|
-
p14.outro(
|
|
3313
|
+
p14.outro(pc15.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
3314
3314
|
}
|
|
3315
3315
|
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
3316
3316
|
const formattedDeps = dependencies.map((dep) => ({
|
|
@@ -3466,7 +3466,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3466
3466
|
);
|
|
3467
3467
|
}
|
|
3468
3468
|
const s = p14.spinner();
|
|
3469
|
-
s.start(`Generating injectable '${
|
|
3469
|
+
s.start(`Generating injectable '${pc15.cyan(className)}'...`);
|
|
3470
3470
|
const options = {
|
|
3471
3471
|
name: className,
|
|
3472
3472
|
feature,
|
|
@@ -3480,7 +3480,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3480
3480
|
const files = generateInjectable(options);
|
|
3481
3481
|
s.stop("Injectable class generated successfully!");
|
|
3482
3482
|
printGeneratedFiles(files, options.dryRun);
|
|
3483
|
-
p14.outro(
|
|
3483
|
+
p14.outro(pc15.green(`Injectable '${className}' created successfully!`));
|
|
3484
3484
|
}
|
|
3485
3485
|
async function runInteractiveMenu(flags = {}) {
|
|
3486
3486
|
printBanner();
|
|
@@ -3594,7 +3594,7 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3594
3594
|
break;
|
|
3595
3595
|
default:
|
|
3596
3596
|
p14.log.error(
|
|
3597
|
-
|
|
3597
|
+
pc15.red(
|
|
3598
3598
|
`Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3599
3599
|
)
|
|
3600
3600
|
);
|
|
@@ -3714,19 +3714,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3714
3714
|
);
|
|
3715
3715
|
}
|
|
3716
3716
|
if (type !== "shared" && type !== "feature") {
|
|
3717
|
-
p14.log.error(
|
|
3717
|
+
p14.log.error(pc15.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3718
3718
|
process.exit(1);
|
|
3719
3719
|
}
|
|
3720
3720
|
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3721
3721
|
const s = p14.spinner();
|
|
3722
|
-
s.start(`Connecting to registry at ${
|
|
3722
|
+
s.start(`Connecting to registry at ${pc15.dim(registryBase)}...`);
|
|
3723
3723
|
let index;
|
|
3724
3724
|
try {
|
|
3725
3725
|
index = await fetchRegistryIndex(registryBase);
|
|
3726
3726
|
s.stop("Registry index loaded.");
|
|
3727
3727
|
} catch (err) {
|
|
3728
|
-
s.stop(
|
|
3729
|
-
p14.log.error(
|
|
3728
|
+
s.stop(pc15.red("Failed to load registry index."));
|
|
3729
|
+
p14.log.error(pc15.red(err.message || String(err)));
|
|
3730
3730
|
process.exit(1);
|
|
3731
3731
|
}
|
|
3732
3732
|
const availableList = type === "shared" ? index.shared : index.features;
|
|
@@ -3750,9 +3750,9 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3750
3750
|
}
|
|
3751
3751
|
if (!candidateNames.includes(name)) {
|
|
3752
3752
|
const closest = findClosestMatch(name, candidateNames);
|
|
3753
|
-
p14.log.error(
|
|
3753
|
+
p14.log.error(pc15.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3754
3754
|
if (closest) {
|
|
3755
|
-
p14.log.info(
|
|
3755
|
+
p14.log.info(pc15.yellow(`Did you mean '${pc15.cyan(closest)}'?`));
|
|
3756
3756
|
}
|
|
3757
3757
|
process.exit(1);
|
|
3758
3758
|
}
|
|
@@ -3764,7 +3764,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3764
3764
|
try {
|
|
3765
3765
|
manifest = await fetchModuleManifest(registryBase, modType, modName);
|
|
3766
3766
|
} catch (err) {
|
|
3767
|
-
p14.log.error(
|
|
3767
|
+
p14.log.error(pc15.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
|
|
3768
3768
|
process.exit(1);
|
|
3769
3769
|
}
|
|
3770
3770
|
nodes.set(key, {
|
|
@@ -3804,7 +3804,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3804
3804
|
buildTopologicalOrder(rootKey);
|
|
3805
3805
|
const resolutions = /* @__PURE__ */ new Map();
|
|
3806
3806
|
const globalAliasMap = /* @__PURE__ */ new Map();
|
|
3807
|
-
p14.log.step(`Importing ${type}: ${
|
|
3807
|
+
p14.log.step(`Importing ${type}: ${pc15.cyan(name)}...`);
|
|
3808
3808
|
for (const key of orderedKeys) {
|
|
3809
3809
|
const node = nodes.get(key);
|
|
3810
3810
|
const { type: modType, name: registryName, manifest } = node;
|
|
@@ -3837,7 +3837,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3837
3837
|
if (programmaticPointTo) {
|
|
3838
3838
|
if (!checkModuleExists(projectRoot, modType, programmaticPointTo)) {
|
|
3839
3839
|
p14.log.error(
|
|
3840
|
-
|
|
3840
|
+
pc15.red(
|
|
3841
3841
|
`Module '${programmaticPointTo}' specified in --point-to does not exist in ${folder}.`
|
|
3842
3842
|
)
|
|
3843
3843
|
);
|
|
@@ -3873,7 +3873,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3873
3873
|
}
|
|
3874
3874
|
const existingDir = getExistingModuleDir(projectRoot, modType, registryName);
|
|
3875
3875
|
if (!existingDir) {
|
|
3876
|
-
p14.log.info(` \u21B3 Resolving required ${modType} dependency: ${
|
|
3876
|
+
p14.log.info(` \u21B3 Resolving required ${modType} dependency: ${pc15.cyan(registryName)}`);
|
|
3877
3877
|
resolutions.set(key, {
|
|
3878
3878
|
type: modType,
|
|
3879
3879
|
registryName,
|
|
@@ -4104,7 +4104,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4104
4104
|
const isOverwrite = Boolean(flags.overwrite || res.action === "overwrite");
|
|
4105
4105
|
if (key !== rootKey) {
|
|
4106
4106
|
p14.log.step(
|
|
4107
|
-
`Importing ${res.type}: ${
|
|
4107
|
+
`Importing ${res.type}: ${pc15.cyan(res.targetName)}${res.targetName !== res.registryName ? pc15.dim(` (from ${res.registryName})`) : ""}...`
|
|
4108
4108
|
);
|
|
4109
4109
|
}
|
|
4110
4110
|
if (res.action === "overwrite" && !flags.dryRun) {
|
|
@@ -4123,7 +4123,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4123
4123
|
const localFilePath = path2.join(targetDir, file);
|
|
4124
4124
|
if (fs4.existsSync(localFilePath) && !isOverwrite) {
|
|
4125
4125
|
p14.log.warn(
|
|
4126
|
-
` \u21B3 Skipping existing file: ${
|
|
4126
|
+
` \u21B3 Skipping existing file: ${pc15.dim(path2.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
|
|
4127
4127
|
);
|
|
4128
4128
|
continue;
|
|
4129
4129
|
}
|
|
@@ -4135,13 +4135,13 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4135
4135
|
}
|
|
4136
4136
|
const displayPath = path2.relative(projectRoot, localFilePath);
|
|
4137
4137
|
totalWrittenFiles.push(displayPath);
|
|
4138
|
-
p14.log.message(` ${
|
|
4138
|
+
p14.log.message(` ${pc15.green("+")} ${displayPath}`);
|
|
4139
4139
|
}
|
|
4140
4140
|
}
|
|
4141
4141
|
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
4142
4142
|
if (missingNpm.length > 0) {
|
|
4143
4143
|
if (flags.skipInstall || flags.dryRun) {
|
|
4144
|
-
p14.log.info(`Required npm dependencies to install: ${
|
|
4144
|
+
p14.log.info(`Required npm dependencies to install: ${pc15.cyan(missingNpm.join(", "))}`);
|
|
4145
4145
|
} else {
|
|
4146
4146
|
try {
|
|
4147
4147
|
installNpmPackages(projectRoot, missingNpm);
|
|
@@ -4155,18 +4155,901 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4155
4155
|
}
|
|
4156
4156
|
}
|
|
4157
4157
|
if (flags.dryRun) {
|
|
4158
|
-
p14.log.info(
|
|
4158
|
+
p14.log.info(pc15.yellow("Dry run complete: No files were written to disk."));
|
|
4159
4159
|
} else {
|
|
4160
4160
|
const rootRes = resolutions.get(rootKey);
|
|
4161
4161
|
const baseDir = getModuleBaseDir(projectRoot, type);
|
|
4162
4162
|
const displayTarget = path2.relative(projectRoot, path2.join(baseDir, rootRes.targetName));
|
|
4163
4163
|
p14.log.success(
|
|
4164
|
-
|
|
4164
|
+
pc15.green(
|
|
4165
4165
|
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
|
|
4166
4166
|
)
|
|
4167
4167
|
);
|
|
4168
4168
|
}
|
|
4169
4169
|
}
|
|
4170
|
+
var DependencyRuleError = class extends Error {
|
|
4171
|
+
violations;
|
|
4172
|
+
isFailFast;
|
|
4173
|
+
constructor(message, violations, isFailFast = false) {
|
|
4174
|
+
super(message);
|
|
4175
|
+
this.name = "DependencyRuleError";
|
|
4176
|
+
this.violations = violations;
|
|
4177
|
+
this.isFailFast = isFailFast;
|
|
4178
|
+
}
|
|
4179
|
+
};
|
|
4180
|
+
function isTestFile2(filePath, projectRoot) {
|
|
4181
|
+
const norm = (projectRoot ? path2.relative(projectRoot, filePath) : filePath).replace(/\\/g, "/");
|
|
4182
|
+
const base = path2.basename(norm);
|
|
4183
|
+
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);
|
|
4184
|
+
}
|
|
4185
|
+
function scanDirFiles(dir, projectRoot) {
|
|
4186
|
+
if (!fs4.existsSync(dir)) return [];
|
|
4187
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
4188
|
+
const files = [];
|
|
4189
|
+
for (const entry of entries) {
|
|
4190
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
const fullPath = path2.join(dir, entry.name);
|
|
4194
|
+
if (entry.isDirectory()) {
|
|
4195
|
+
files.push(...scanDirFiles(fullPath, projectRoot));
|
|
4196
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !isTestFile2(fullPath, projectRoot)) {
|
|
4197
|
+
files.push(fullPath);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
return files;
|
|
4201
|
+
}
|
|
4202
|
+
function classifyFile(fullPath, projectRoot) {
|
|
4203
|
+
const rel = path2.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4204
|
+
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4205
|
+
if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
|
|
4206
|
+
const feature = featureMatch[1];
|
|
4207
|
+
const subfolder = featureMatch[2];
|
|
4208
|
+
let layer;
|
|
4209
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4210
|
+
layer = "domain";
|
|
4211
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4212
|
+
layer = "useCases";
|
|
4213
|
+
} else if (subfolder === "services") {
|
|
4214
|
+
layer = "services";
|
|
4215
|
+
} else if (subfolder === "infrastructure") {
|
|
4216
|
+
layer = "infrastructure";
|
|
4217
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4218
|
+
layer = "presentation";
|
|
4219
|
+
}
|
|
4220
|
+
return { feature, layer };
|
|
4221
|
+
}
|
|
4222
|
+
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4223
|
+
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
4224
|
+
return { isExternal: true, isDiFramework: true };
|
|
4225
|
+
}
|
|
4226
|
+
if (specifier.startsWith("node:") || ["crypto", "fs", "path", "http", "https", "url", "os", "child_process", "stream", "express", "cors", "dotenv"].includes(specifier)) {
|
|
4227
|
+
return { isExternal: true, isDiFramework: false };
|
|
4228
|
+
}
|
|
4229
|
+
let resolvedRel = null;
|
|
4230
|
+
if (specifier.startsWith(".")) {
|
|
4231
|
+
const abs = path2.resolve(path2.dirname(sourceFileAbs), specifier);
|
|
4232
|
+
resolvedRel = path2.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
4233
|
+
} else if (specifier.startsWith("@/features/") || specifier.startsWith("~/features/")) {
|
|
4234
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4235
|
+
} else if (specifier.startsWith("@features/")) {
|
|
4236
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4237
|
+
} else if (specifier.startsWith("@/shared/") || specifier.startsWith("~/shared/")) {
|
|
4238
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4239
|
+
} else if (specifier.startsWith("@shared/")) {
|
|
4240
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4241
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4242
|
+
resolvedRel = "src/" + specifier.slice(2);
|
|
4243
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4244
|
+
resolvedRel = specifier;
|
|
4245
|
+
}
|
|
4246
|
+
if (!resolvedRel) {
|
|
4247
|
+
return { isExternal: true, isDiFramework: false };
|
|
4248
|
+
}
|
|
4249
|
+
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4250
|
+
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4251
|
+
const targetFeature = featMatch[1];
|
|
4252
|
+
const targetSubfolder = featMatch[2];
|
|
4253
|
+
const rest = featMatch[3] || "";
|
|
4254
|
+
let targetLayer;
|
|
4255
|
+
if (targetSubfolder === "domain" || targetSubfolder === "errors") {
|
|
4256
|
+
targetLayer = "domain";
|
|
4257
|
+
} else if (targetSubfolder === "useCases" || targetSubfolder === "usecases") {
|
|
4258
|
+
targetLayer = "useCases";
|
|
4259
|
+
} else if (targetSubfolder === "services") {
|
|
4260
|
+
targetLayer = "services";
|
|
4261
|
+
} else if (targetSubfolder === "infrastructure") {
|
|
4262
|
+
targetLayer = "infrastructure";
|
|
4263
|
+
} else if (targetSubfolder === "pxExpress" || targetSubfolder === "presentation" || targetSubfolder === "handlers" || targetSubfolder.startsWith("px")) {
|
|
4264
|
+
targetLayer = "presentation";
|
|
4265
|
+
}
|
|
4266
|
+
const basePart = rest ? path2.basename(rest) : targetSubfolder;
|
|
4267
|
+
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4268
|
+
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4269
|
+
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4270
|
+
return {
|
|
4271
|
+
isExternal: false,
|
|
4272
|
+
isDiFramework: false,
|
|
4273
|
+
targetFeature,
|
|
4274
|
+
targetLayer,
|
|
4275
|
+
targetSubfolder,
|
|
4276
|
+
targetFileName,
|
|
4277
|
+
isTargetPort,
|
|
4278
|
+
isTargetRepo
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4282
|
+
if (sharedMatch) {
|
|
4283
|
+
const targetSharedModule = sharedMatch[1];
|
|
4284
|
+
const sharedRest = sharedMatch[2] || "";
|
|
4285
|
+
const isSharedRoot = !sharedRest.includes("/");
|
|
4286
|
+
const isSharedPorts = sharedRest.startsWith("ports/") || sharedRest === "ports";
|
|
4287
|
+
const isSharedInfra = sharedRest.startsWith("infrastructure/") || sharedRest === "infrastructure";
|
|
4288
|
+
const isSharedDomain = sharedRest.startsWith("domain/") || sharedRest === "domain";
|
|
4289
|
+
return {
|
|
4290
|
+
isExternal: false,
|
|
4291
|
+
isDiFramework: false,
|
|
4292
|
+
targetSharedModule,
|
|
4293
|
+
isSharedRoot,
|
|
4294
|
+
isSharedPorts,
|
|
4295
|
+
isSharedInfra,
|
|
4296
|
+
isSharedDomain
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
return { isExternal: true, isDiFramework: false };
|
|
4300
|
+
}
|
|
4301
|
+
function extractImportStatements(content) {
|
|
4302
|
+
const statements = [];
|
|
4303
|
+
const lines = content.split(/\r?\n/);
|
|
4304
|
+
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;
|
|
4305
|
+
let match;
|
|
4306
|
+
while ((match = regex.exec(content)) !== null) {
|
|
4307
|
+
const specifier = match[1];
|
|
4308
|
+
const charIndex = match.index;
|
|
4309
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4310
|
+
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
4311
|
+
statements.push({
|
|
4312
|
+
specifier,
|
|
4313
|
+
line: lineNum,
|
|
4314
|
+
rawCode
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
return statements;
|
|
4318
|
+
}
|
|
4319
|
+
var FeatureDependencyGraph = class {
|
|
4320
|
+
edges = /* @__PURE__ */ new Map();
|
|
4321
|
+
addEdge(evidence) {
|
|
4322
|
+
const { fromFeature, toFeature } = evidence;
|
|
4323
|
+
if (fromFeature === toFeature) {
|
|
4324
|
+
return { createsCycle: false };
|
|
4325
|
+
}
|
|
4326
|
+
if (!this.edges.has(fromFeature)) {
|
|
4327
|
+
this.edges.set(fromFeature, /* @__PURE__ */ new Map());
|
|
4328
|
+
}
|
|
4329
|
+
this.edges.get(fromFeature).set(toFeature, evidence);
|
|
4330
|
+
const reverse = this.edges.get(toFeature)?.get(fromFeature);
|
|
4331
|
+
if (reverse) {
|
|
4332
|
+
return { createsCycle: true, directPartner: reverse, cycleChain: [fromFeature, toFeature, fromFeature] };
|
|
4333
|
+
}
|
|
4334
|
+
const cycle = this.findCycleFrom(fromFeature);
|
|
4335
|
+
if (cycle && cycle.length > 0) {
|
|
4336
|
+
return { createsCycle: true, cycleChain: cycle };
|
|
4337
|
+
}
|
|
4338
|
+
return { createsCycle: false };
|
|
4339
|
+
}
|
|
4340
|
+
getEdges() {
|
|
4341
|
+
const list = [];
|
|
4342
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4343
|
+
for (const [to, ev] of toMap.entries()) {
|
|
4344
|
+
list.push({ from, to, evidence: ev });
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
return list;
|
|
4348
|
+
}
|
|
4349
|
+
findDirectCycles() {
|
|
4350
|
+
const cycles = [];
|
|
4351
|
+
const checkedPairs = /* @__PURE__ */ new Set();
|
|
4352
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4353
|
+
for (const [to, evAtoB] of toMap.entries()) {
|
|
4354
|
+
const pairKey = [from, to].sort().join("<->");
|
|
4355
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
4356
|
+
const evBtoA = this.edges.get(to)?.get(from);
|
|
4357
|
+
if (evBtoA) {
|
|
4358
|
+
checkedPairs.add(pairKey);
|
|
4359
|
+
cycles.push({
|
|
4360
|
+
featureA: from,
|
|
4361
|
+
featureB: to,
|
|
4362
|
+
cycle: [from, to, from],
|
|
4363
|
+
evidenceAtoB: evAtoB,
|
|
4364
|
+
evidenceBtoA: evBtoA
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
return cycles;
|
|
4370
|
+
}
|
|
4371
|
+
findCycleFrom(start) {
|
|
4372
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4373
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
4374
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4375
|
+
const dfs = (curr) => {
|
|
4376
|
+
visited.add(curr);
|
|
4377
|
+
inStack.add(curr);
|
|
4378
|
+
const neighbors = this.edges.get(curr);
|
|
4379
|
+
if (neighbors) {
|
|
4380
|
+
for (const next of neighbors.keys()) {
|
|
4381
|
+
if (!visited.has(next)) {
|
|
4382
|
+
parent.set(next, curr);
|
|
4383
|
+
const cycle = dfs(next);
|
|
4384
|
+
if (cycle) return cycle;
|
|
4385
|
+
} else if (inStack.has(next)) {
|
|
4386
|
+
const cycle = [next];
|
|
4387
|
+
let p16 = curr;
|
|
4388
|
+
while (p16 !== next) {
|
|
4389
|
+
cycle.push(p16);
|
|
4390
|
+
p16 = parent.get(p16) || next;
|
|
4391
|
+
}
|
|
4392
|
+
cycle.push(next);
|
|
4393
|
+
cycle.reverse();
|
|
4394
|
+
return cycle;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
inStack.delete(curr);
|
|
4399
|
+
return null;
|
|
4400
|
+
};
|
|
4401
|
+
return dfs(start);
|
|
4402
|
+
}
|
|
4403
|
+
};
|
|
4404
|
+
function discoverAllFeatures(projectRoot) {
|
|
4405
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
4406
|
+
if (!fs4.existsSync(featuresBase)) return [];
|
|
4407
|
+
const entries = fs4.readdirSync(featuresBase, { withFileTypes: true });
|
|
4408
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
4409
|
+
name: e.name,
|
|
4410
|
+
dir: path2.join(featuresBase, e.name)
|
|
4411
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4412
|
+
}
|
|
4413
|
+
function lintCodebase(options = {}) {
|
|
4414
|
+
const startTime = Date.now();
|
|
4415
|
+
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
4416
|
+
const failFast = !options.complete;
|
|
4417
|
+
const features = discoverAllFeatures(projectRoot);
|
|
4418
|
+
const graph = new FeatureDependencyGraph();
|
|
4419
|
+
const allViolations = [];
|
|
4420
|
+
const results = [];
|
|
4421
|
+
const detectedCycles = [];
|
|
4422
|
+
const addViolation = (v) => {
|
|
4423
|
+
allViolations.push(v);
|
|
4424
|
+
const featureResult = results.find((r) => r.featureName === v.feature);
|
|
4425
|
+
if (featureResult) {
|
|
4426
|
+
featureResult.violations.push(v);
|
|
4427
|
+
featureResult.passed = false;
|
|
4428
|
+
}
|
|
4429
|
+
if (failFast) {
|
|
4430
|
+
const errorMsg = `[FAIL FAST] Dependency rule violation detected:
|
|
4431
|
+
Feature: ${v.feature}
|
|
4432
|
+
Category: ${v.category}
|
|
4433
|
+
Rule: ${v.ruleId}
|
|
4434
|
+
Location: ${v.relativePath}:${v.line}
|
|
4435
|
+
Code: ${v.code}
|
|
4436
|
+
Explanation: ${v.advice}`;
|
|
4437
|
+
throw new DependencyRuleError(errorMsg, [v], true);
|
|
4438
|
+
}
|
|
4439
|
+
};
|
|
4440
|
+
for (const feat of features) {
|
|
4441
|
+
results.push({
|
|
4442
|
+
featureName: feat.name,
|
|
4443
|
+
featureDir: feat.dir,
|
|
4444
|
+
scannedFilesCount: 0,
|
|
4445
|
+
passed: true,
|
|
4446
|
+
violations: [],
|
|
4447
|
+
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
for (const feat of features) {
|
|
4451
|
+
const featResult = results.find((r) => r.featureName === feat.name);
|
|
4452
|
+
const allFiles = scanDirFiles(feat.dir, projectRoot);
|
|
4453
|
+
featResult.scannedFilesCount = allFiles.length;
|
|
4454
|
+
for (const file of allFiles) {
|
|
4455
|
+
const relPath = path2.relative(projectRoot, file).replace(/\\/g, "/");
|
|
4456
|
+
const { layer } = classifyFile(file, projectRoot);
|
|
4457
|
+
if (!layer) continue;
|
|
4458
|
+
const content = fs4.readFileSync(file, "utf-8");
|
|
4459
|
+
const statements = extractImportStatements(content);
|
|
4460
|
+
if (layer === "domain") {
|
|
4461
|
+
const lines = content.split(/\r?\n/);
|
|
4462
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4463
|
+
const lineText = lines[i].trim();
|
|
4464
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
4465
|
+
addViolation({
|
|
4466
|
+
feature: feat.name,
|
|
4467
|
+
layer,
|
|
4468
|
+
file,
|
|
4469
|
+
relativePath: relPath,
|
|
4470
|
+
line: i + 1,
|
|
4471
|
+
category: "Domain Dependence Violation",
|
|
4472
|
+
code: lineText,
|
|
4473
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4474
|
+
severity: "error",
|
|
4475
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
4476
|
+
fixExample: {
|
|
4477
|
+
bad: lineText,
|
|
4478
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
4479
|
+
}
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
for (const stmt of statements) {
|
|
4485
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
4486
|
+
if (target.targetFeature && target.targetFeature !== feat.name) {
|
|
4487
|
+
const evidence = {
|
|
4488
|
+
fromFeature: feat.name,
|
|
4489
|
+
toFeature: target.targetFeature,
|
|
4490
|
+
file,
|
|
4491
|
+
relativePath: relPath,
|
|
4492
|
+
line: stmt.line,
|
|
4493
|
+
code: stmt.rawCode
|
|
4494
|
+
};
|
|
4495
|
+
const cycleResult = graph.addEdge(evidence);
|
|
4496
|
+
if (cycleResult.createsCycle) {
|
|
4497
|
+
const partner = cycleResult.directPartner;
|
|
4498
|
+
detectedCycles.push({
|
|
4499
|
+
featureA: feat.name,
|
|
4500
|
+
featureB: target.targetFeature,
|
|
4501
|
+
cycle: cycleResult.cycleChain,
|
|
4502
|
+
evidenceAtoB: evidence,
|
|
4503
|
+
evidenceBtoA: partner || evidence
|
|
4504
|
+
});
|
|
4505
|
+
addViolation({
|
|
4506
|
+
feature: feat.name,
|
|
4507
|
+
layer,
|
|
4508
|
+
file,
|
|
4509
|
+
relativePath: relPath,
|
|
4510
|
+
line: stmt.line,
|
|
4511
|
+
category: "Circular Feature Dependency Violation",
|
|
4512
|
+
code: stmt.rawCode,
|
|
4513
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4514
|
+
severity: "error",
|
|
4515
|
+
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.`,
|
|
4516
|
+
fixExample: {
|
|
4517
|
+
bad: stmt.rawCode,
|
|
4518
|
+
good: `// Emit a Domain/Application Event (e.g. publish '${feat.name}Changed') instead of depending on '${target.targetFeature}'.`
|
|
4519
|
+
}
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
if (layer === "domain") {
|
|
4524
|
+
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
4525
|
+
addViolation({
|
|
4526
|
+
feature: feat.name,
|
|
4527
|
+
layer,
|
|
4528
|
+
file,
|
|
4529
|
+
relativePath: relPath,
|
|
4530
|
+
line: stmt.line,
|
|
4531
|
+
category: "Domain Purity Violation",
|
|
4532
|
+
code: stmt.rawCode,
|
|
4533
|
+
ruleId: "D1_OUTER_IMPORT",
|
|
4534
|
+
severity: "error",
|
|
4535
|
+
advice: `Domain layer must not import from outer layers (${target.targetLayer}). Keep the domain core pure.`,
|
|
4536
|
+
fixExample: {
|
|
4537
|
+
bad: stmt.rawCode,
|
|
4538
|
+
good: "// Define interfaces, types, or value objects directly within the domain layer."
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
if (target.isDiFramework) {
|
|
4543
|
+
addViolation({
|
|
4544
|
+
feature: feat.name,
|
|
4545
|
+
layer,
|
|
4546
|
+
file,
|
|
4547
|
+
relativePath: relPath,
|
|
4548
|
+
line: stmt.line,
|
|
4549
|
+
category: "Domain Dependence Violation",
|
|
4550
|
+
code: stmt.rawCode,
|
|
4551
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4552
|
+
severity: "error",
|
|
4553
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
4554
|
+
fixExample: {
|
|
4555
|
+
bad: stmt.rawCode,
|
|
4556
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
4557
|
+
}
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
4561
|
+
addViolation({
|
|
4562
|
+
feature: feat.name,
|
|
4563
|
+
layer,
|
|
4564
|
+
file,
|
|
4565
|
+
relativePath: relPath,
|
|
4566
|
+
line: stmt.line,
|
|
4567
|
+
category: "Domain Purity Violation",
|
|
4568
|
+
code: stmt.rawCode,
|
|
4569
|
+
ruleId: "D3_SHARED_NON_DOMAIN_IMPORT",
|
|
4570
|
+
severity: "error",
|
|
4571
|
+
advice: "Domain layer can only import pure value objects from shared/<module>/domain/*. Ports, infrastructure, and root classes must not be imported.",
|
|
4572
|
+
fixExample: {
|
|
4573
|
+
bad: stmt.rawCode,
|
|
4574
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/domain/...";`
|
|
4575
|
+
}
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
if (target.isExternal && !target.isDiFramework) {
|
|
4579
|
+
addViolation({
|
|
4580
|
+
feature: feat.name,
|
|
4581
|
+
layer,
|
|
4582
|
+
file,
|
|
4583
|
+
relativePath: relPath,
|
|
4584
|
+
line: stmt.line,
|
|
4585
|
+
category: "Domain Purity Violation",
|
|
4586
|
+
code: stmt.rawCode,
|
|
4587
|
+
ruleId: "D4_EXTERNAL_LIBRARY",
|
|
4588
|
+
severity: "error",
|
|
4589
|
+
advice: `Domain layer must be pure and must not depend on external runtime libraries ('${stmt.specifier}').`,
|
|
4590
|
+
fixExample: {
|
|
4591
|
+
bad: stmt.rawCode,
|
|
4592
|
+
good: "// Model pure business interfaces without external libraries."
|
|
4593
|
+
}
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
} else if (layer === "useCases") {
|
|
4597
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4598
|
+
addViolation({
|
|
4599
|
+
feature: feat.name,
|
|
4600
|
+
layer,
|
|
4601
|
+
file,
|
|
4602
|
+
relativePath: relPath,
|
|
4603
|
+
line: stmt.line,
|
|
4604
|
+
category: "Use Case Intra-Feature Violation",
|
|
4605
|
+
code: stmt.rawCode,
|
|
4606
|
+
ruleId: "U1_INTRA_USE_CASE",
|
|
4607
|
+
severity: "error",
|
|
4608
|
+
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.`,
|
|
4609
|
+
fixExample: {
|
|
4610
|
+
bad: stmt.rawCode,
|
|
4611
|
+
good: "// Inject a domain interface or domain service instead of another use case."
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
if (target.targetLayer === "useCases" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4616
|
+
addViolation({
|
|
4617
|
+
feature: feat.name,
|
|
4618
|
+
layer,
|
|
4619
|
+
file,
|
|
4620
|
+
relativePath: relPath,
|
|
4621
|
+
line: stmt.line,
|
|
4622
|
+
category: "Use Case Cross-Feature Use Case Violation",
|
|
4623
|
+
code: stmt.rawCode,
|
|
4624
|
+
ruleId: "U2_CROSS_FEATURE_USE_CASE",
|
|
4625
|
+
severity: "error",
|
|
4626
|
+
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/'.`,
|
|
4627
|
+
fixExample: {
|
|
4628
|
+
bad: stmt.rawCode,
|
|
4629
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/ using '${target.targetFileName}'.`
|
|
4630
|
+
}
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
if (target.targetLayer === "infrastructure") {
|
|
4634
|
+
addViolation({
|
|
4635
|
+
feature: feat.name,
|
|
4636
|
+
layer,
|
|
4637
|
+
file,
|
|
4638
|
+
relativePath: relPath,
|
|
4639
|
+
line: stmt.line,
|
|
4640
|
+
category: "Use Case Layer Violation",
|
|
4641
|
+
code: stmt.rawCode,
|
|
4642
|
+
ruleId: "U3_DIRECT_INFRASTRUCTURE",
|
|
4643
|
+
severity: "error",
|
|
4644
|
+
advice: "Use cases cannot import concrete infrastructure directly. Depend on Domain Ports of the same feature via DI.",
|
|
4645
|
+
fixExample: {
|
|
4646
|
+
bad: stmt.rawCode,
|
|
4647
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4648
|
+
}
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4652
|
+
addViolation({
|
|
4653
|
+
feature: feat.name,
|
|
4654
|
+
layer,
|
|
4655
|
+
file,
|
|
4656
|
+
relativePath: relPath,
|
|
4657
|
+
line: stmt.line,
|
|
4658
|
+
category: "Use Case Cross-Feature Port Violation",
|
|
4659
|
+
code: stmt.rawCode,
|
|
4660
|
+
ruleId: "U4_CROSS_FEATURE_PORT",
|
|
4661
|
+
severity: "error",
|
|
4662
|
+
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/'.`,
|
|
4663
|
+
fixExample: {
|
|
4664
|
+
bad: stmt.rawCode,
|
|
4665
|
+
good: `// Define a local interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4666
|
+
}
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
if (target.targetLayer === "services" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4670
|
+
addViolation({
|
|
4671
|
+
feature: feat.name,
|
|
4672
|
+
layer,
|
|
4673
|
+
file,
|
|
4674
|
+
relativePath: relPath,
|
|
4675
|
+
line: stmt.line,
|
|
4676
|
+
category: "Use Case Layer Violation",
|
|
4677
|
+
code: stmt.rawCode,
|
|
4678
|
+
ruleId: "U5_CROSS_FEATURE_SERVICE",
|
|
4679
|
+
severity: "error",
|
|
4680
|
+
advice: `Use cases cannot depend on services of foreign feature '${target.targetFeature}'.`,
|
|
4681
|
+
fixExample: {
|
|
4682
|
+
bad: stmt.rawCode,
|
|
4683
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4684
|
+
}
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4688
|
+
addViolation({
|
|
4689
|
+
feature: feat.name,
|
|
4690
|
+
layer,
|
|
4691
|
+
file,
|
|
4692
|
+
relativePath: relPath,
|
|
4693
|
+
line: stmt.line,
|
|
4694
|
+
category: "Shared Module Root Class Violation",
|
|
4695
|
+
code: stmt.rawCode,
|
|
4696
|
+
ruleId: "U6_SHARED_NON_ROOT",
|
|
4697
|
+
severity: "error",
|
|
4698
|
+
advice: `Use cases can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.`,
|
|
4699
|
+
fixExample: {
|
|
4700
|
+
bad: stmt.rawCode,
|
|
4701
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4702
|
+
}
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
} else if (layer === "services") {
|
|
4706
|
+
if (target.targetLayer === "useCases") {
|
|
4707
|
+
addViolation({
|
|
4708
|
+
feature: feat.name,
|
|
4709
|
+
layer,
|
|
4710
|
+
file,
|
|
4711
|
+
relativePath: relPath,
|
|
4712
|
+
line: stmt.line,
|
|
4713
|
+
category: "Service Intra-Feature Violation",
|
|
4714
|
+
code: stmt.rawCode,
|
|
4715
|
+
ruleId: "S1_USE_CASE_DEPENDENCY",
|
|
4716
|
+
severity: "error",
|
|
4717
|
+
advice: "Services cannot depend on use cases of any feature.",
|
|
4718
|
+
fixExample: {
|
|
4719
|
+
bad: stmt.rawCode,
|
|
4720
|
+
good: "// Inject domain repository ports directly instead of use cases."
|
|
4721
|
+
}
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4725
|
+
addViolation({
|
|
4726
|
+
feature: feat.name,
|
|
4727
|
+
layer,
|
|
4728
|
+
file,
|
|
4729
|
+
relativePath: relPath,
|
|
4730
|
+
line: stmt.line,
|
|
4731
|
+
category: "Service Cross-Feature Port Violation",
|
|
4732
|
+
code: stmt.rawCode,
|
|
4733
|
+
ruleId: "S2_CROSS_FEATURE_PORT",
|
|
4734
|
+
severity: "error",
|
|
4735
|
+
advice: `Services cannot depend on domain ports of foreign feature '${target.targetFeature}'.`,
|
|
4736
|
+
fixExample: {
|
|
4737
|
+
bad: stmt.rawCode,
|
|
4738
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4739
|
+
}
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
if (target.targetLayer === "infrastructure") {
|
|
4743
|
+
addViolation({
|
|
4744
|
+
feature: feat.name,
|
|
4745
|
+
layer,
|
|
4746
|
+
file,
|
|
4747
|
+
relativePath: relPath,
|
|
4748
|
+
line: stmt.line,
|
|
4749
|
+
category: "Service Layer Violation",
|
|
4750
|
+
code: stmt.rawCode,
|
|
4751
|
+
ruleId: "S3_DIRECT_INFRASTRUCTURE",
|
|
4752
|
+
severity: "error",
|
|
4753
|
+
advice: "Services cannot import concrete infrastructure directly. Depend on Domain Ports via DI.",
|
|
4754
|
+
fixExample: {
|
|
4755
|
+
bad: stmt.rawCode,
|
|
4756
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4757
|
+
}
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4761
|
+
addViolation({
|
|
4762
|
+
feature: feat.name,
|
|
4763
|
+
layer,
|
|
4764
|
+
file,
|
|
4765
|
+
relativePath: relPath,
|
|
4766
|
+
line: stmt.line,
|
|
4767
|
+
category: "Shared Module Root Class Violation",
|
|
4768
|
+
code: stmt.rawCode,
|
|
4769
|
+
ruleId: "S4_SHARED_NON_ROOT",
|
|
4770
|
+
severity: "error",
|
|
4771
|
+
advice: "Services can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4772
|
+
fixExample: {
|
|
4773
|
+
bad: stmt.rawCode,
|
|
4774
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4775
|
+
}
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
} else if (layer === "infrastructure") {
|
|
4779
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4780
|
+
addViolation({
|
|
4781
|
+
feature: feat.name,
|
|
4782
|
+
layer,
|
|
4783
|
+
file,
|
|
4784
|
+
relativePath: relPath,
|
|
4785
|
+
line: stmt.line,
|
|
4786
|
+
category: "Adapter Layer Violation",
|
|
4787
|
+
code: stmt.rawCode,
|
|
4788
|
+
ruleId: "I1_INTRA_USE_CASE",
|
|
4789
|
+
severity: "error",
|
|
4790
|
+
advice: `Infrastructure adapters cannot depend on use cases of the same feature ('${feat.name}'). Adapters implement domain ports.`,
|
|
4791
|
+
fixExample: {
|
|
4792
|
+
bad: stmt.rawCode,
|
|
4793
|
+
good: "// Adapters implement domain ports and should not invoke local application use cases."
|
|
4794
|
+
}
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4798
|
+
addViolation({
|
|
4799
|
+
feature: feat.name,
|
|
4800
|
+
layer,
|
|
4801
|
+
file,
|
|
4802
|
+
relativePath: relPath,
|
|
4803
|
+
line: stmt.line,
|
|
4804
|
+
category: "Shared Module Root Class Violation",
|
|
4805
|
+
code: stmt.rawCode,
|
|
4806
|
+
ruleId: "I2_SHARED_NON_ROOT",
|
|
4807
|
+
severity: "error",
|
|
4808
|
+
advice: "Infrastructure adapters can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4809
|
+
fixExample: {
|
|
4810
|
+
bad: stmt.rawCode,
|
|
4811
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
} else if (layer === "presentation") {
|
|
4816
|
+
if (target.targetLayer === "infrastructure") {
|
|
4817
|
+
addViolation({
|
|
4818
|
+
feature: feat.name,
|
|
4819
|
+
layer,
|
|
4820
|
+
file,
|
|
4821
|
+
relativePath: relPath,
|
|
4822
|
+
line: stmt.line,
|
|
4823
|
+
category: "Presentation Layer Violation",
|
|
4824
|
+
code: stmt.rawCode,
|
|
4825
|
+
ruleId: "P1_DIRECT_INFRASTRUCTURE",
|
|
4826
|
+
severity: "error",
|
|
4827
|
+
advice: "Presentation handlers must not import infrastructure directly. Inject Use Cases via DI.",
|
|
4828
|
+
fixExample: {
|
|
4829
|
+
bad: stmt.rawCode,
|
|
4830
|
+
good: "// Inject the corresponding Use Case via static deps and invoke its execute() method."
|
|
4831
|
+
}
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
if (target.targetLayer === "domain" && (target.isTargetRepo || target.isTargetPort)) {
|
|
4835
|
+
addViolation({
|
|
4836
|
+
feature: feat.name,
|
|
4837
|
+
layer,
|
|
4838
|
+
file,
|
|
4839
|
+
relativePath: relPath,
|
|
4840
|
+
line: stmt.line,
|
|
4841
|
+
category: "Presentation Layer Violation",
|
|
4842
|
+
code: stmt.rawCode,
|
|
4843
|
+
ruleId: "P2_DIRECT_REPOSITORY",
|
|
4844
|
+
severity: "error",
|
|
4845
|
+
advice: "Presentation handlers must not depend on domain repositories or ports directly. Inject Use Cases via DI.",
|
|
4846
|
+
fixExample: {
|
|
4847
|
+
bad: stmt.rawCode,
|
|
4848
|
+
good: "// Presentation layer must only orchestrate Use Cases."
|
|
4849
|
+
}
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4853
|
+
addViolation({
|
|
4854
|
+
feature: feat.name,
|
|
4855
|
+
layer,
|
|
4856
|
+
file,
|
|
4857
|
+
relativePath: relPath,
|
|
4858
|
+
line: stmt.line,
|
|
4859
|
+
category: "Shared Module Root Class Violation",
|
|
4860
|
+
code: stmt.rawCode,
|
|
4861
|
+
ruleId: "P3_SHARED_NON_ROOT",
|
|
4862
|
+
severity: "error",
|
|
4863
|
+
advice: "Presentation handlers can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4864
|
+
fixExample: {
|
|
4865
|
+
bad: stmt.rawCode,
|
|
4866
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
const directCycles = graph.findDirectCycles();
|
|
4875
|
+
for (const c of directCycles) {
|
|
4876
|
+
const existing = detectedCycles.find(
|
|
4877
|
+
(dc) => dc.featureA === c.featureA && dc.featureB === c.featureB || dc.featureA === c.featureB && dc.featureB === c.featureA
|
|
4878
|
+
);
|
|
4879
|
+
if (!existing) {
|
|
4880
|
+
detectedCycles.push(c);
|
|
4881
|
+
addViolation({
|
|
4882
|
+
feature: c.featureA,
|
|
4883
|
+
file: c.evidenceAtoB.file,
|
|
4884
|
+
relativePath: c.evidenceAtoB.relativePath,
|
|
4885
|
+
line: c.evidenceAtoB.line,
|
|
4886
|
+
category: "Circular Feature Dependency Violation",
|
|
4887
|
+
code: c.evidenceAtoB.code,
|
|
4888
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4889
|
+
severity: "error",
|
|
4890
|
+
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.`
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
const durationMs = Date.now() - startTime;
|
|
4895
|
+
const cleanFeatures = results.filter((r) => r.passed).length;
|
|
4896
|
+
const violatedFeatures = results.filter((r) => !r.passed).length;
|
|
4897
|
+
const summary = {
|
|
4898
|
+
totalFeatures: results.length,
|
|
4899
|
+
cleanFeatures,
|
|
4900
|
+
violatedFeatures,
|
|
4901
|
+
totalViolations: allViolations.length,
|
|
4902
|
+
durationMs,
|
|
4903
|
+
results,
|
|
4904
|
+
allViolations,
|
|
4905
|
+
circularDependencies: detectedCycles
|
|
4906
|
+
};
|
|
4907
|
+
if (allViolations.length > 0 && options.throwOnError !== false) {
|
|
4908
|
+
const errorHeader = `Dependency rule audit failed with ${allViolations.length} violation(s) across ${violatedFeatures} feature(s):`;
|
|
4909
|
+
const compiledDetails = allViolations.map(
|
|
4910
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4911
|
+
Rule: ${v.ruleId}
|
|
4912
|
+
Code: ${v.code}
|
|
4913
|
+
Advice: ${v.advice}`
|
|
4914
|
+
).join("\n\n");
|
|
4915
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4916
|
+
|
|
4917
|
+
${compiledDetails}
|
|
4918
|
+
`, allViolations, false);
|
|
4919
|
+
}
|
|
4920
|
+
return summary;
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
// src/commands/lint.ts
|
|
4924
|
+
function printLintBanner(mode) {
|
|
4925
|
+
console.log("");
|
|
4926
|
+
const title = " Mason Clean Architecture Dependency Linter ";
|
|
4927
|
+
const line = "\u2501".repeat(title.length + 2);
|
|
4928
|
+
console.log(pc15.cyan(` \u250F${line}\u2513`));
|
|
4929
|
+
console.log(` ${pc15.cyan("\u2503")} ${pc15.bgCyan(pc15.black(title))} ${pc15.cyan("\u2503")}`);
|
|
4930
|
+
console.log(pc15.cyan(` \u2517${line}\u251B`));
|
|
4931
|
+
const modeText = mode === "failFast" ? `${pc15.yellow("Fail-Fast")} ${pc15.dim("(default: stops at first error)")}` : `${pc15.magenta("Complete")} ${pc15.dim("(--complete: compiles all errors)")}`;
|
|
4932
|
+
console.log(` Mode: ${modeText}`);
|
|
4933
|
+
console.log("");
|
|
4934
|
+
}
|
|
4935
|
+
function formatViolation(v, showFixHint = true) {
|
|
4936
|
+
const lines = [];
|
|
4937
|
+
const categoryBadge = pc15.red(pc15.bold(`[${v.category}]`));
|
|
4938
|
+
const location = pc15.white(`${v.relativePath}:${pc15.yellow(String(v.line))}`);
|
|
4939
|
+
lines.push(` ${categoryBadge} ${location}`);
|
|
4940
|
+
lines.push(` ${pc15.dim("Rule ID:")} ${pc15.yellow(v.ruleId)}`);
|
|
4941
|
+
lines.push(` ${pc15.dim("Code:")} ${pc15.magenta(v.code)}`);
|
|
4942
|
+
lines.push(` ${pc15.dim("Advice:")} ${pc15.cyan(v.advice)}`);
|
|
4943
|
+
if (showFixHint && v.fixExample) {
|
|
4944
|
+
lines.push(` ${pc15.dim("Example:")}`);
|
|
4945
|
+
lines.push(` ${pc15.red(`- ${v.fixExample.bad}`)}`);
|
|
4946
|
+
lines.push(` ${pc15.green(`+ ${v.fixExample.good}`)}`);
|
|
4947
|
+
}
|
|
4948
|
+
return lines.join("\n");
|
|
4949
|
+
}
|
|
4950
|
+
function formatFeatureResult(res, options = {}) {
|
|
4951
|
+
const lines = [];
|
|
4952
|
+
if (res.passed) {
|
|
4953
|
+
lines.push(
|
|
4954
|
+
` ${pc15.green("\u2714")} ${pc15.bold(res.featureName)} ${pc15.dim(
|
|
4955
|
+
`(${res.scannedFilesCount} files checked - clean)`
|
|
4956
|
+
)}`
|
|
4957
|
+
);
|
|
4958
|
+
return lines.join("\n");
|
|
4959
|
+
}
|
|
4960
|
+
const countBadge = pc15.red(
|
|
4961
|
+
`(${res.violations.length} violation${res.violations.length > 1 ? "s" : ""})`
|
|
4962
|
+
);
|
|
4963
|
+
lines.push(`
|
|
4964
|
+
${pc15.red("\u2716")} ${pc15.bold(pc15.white(res.featureName))} ${countBadge}`);
|
|
4965
|
+
lines.push(` ${pc15.dim("Path:")} ${res.featureDir}`);
|
|
4966
|
+
lines.push("");
|
|
4967
|
+
for (const v of res.violations) {
|
|
4968
|
+
lines.push(formatViolation(v, options.fixHint !== false));
|
|
4969
|
+
lines.push("");
|
|
4970
|
+
}
|
|
4971
|
+
return lines.join("\n");
|
|
4972
|
+
}
|
|
4973
|
+
function printTerminalReport(summary, options = {}) {
|
|
4974
|
+
for (const res of summary.results) {
|
|
4975
|
+
const formatted = formatFeatureResult(res, options);
|
|
4976
|
+
if (formatted.trim()) {
|
|
4977
|
+
console.log(formatted);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
console.log("\u2501".repeat(70));
|
|
4981
|
+
const statusIcon = summary.totalViolations === 0 ? pc15.green("\u2714 PASS") : pc15.red("\u2716 FAIL");
|
|
4982
|
+
console.log(
|
|
4983
|
+
` ${statusIcon} ${pc15.bold("Dependency Rules Audit Result")} ${pc15.dim(
|
|
4984
|
+
`(${summary.durationMs}ms)`
|
|
4985
|
+
)}`
|
|
4986
|
+
);
|
|
4987
|
+
console.log(
|
|
4988
|
+
` ${pc15.bold("Features:")} ${pc15.cyan(String(summary.totalFeatures))} total | ${pc15.green(
|
|
4989
|
+
String(summary.cleanFeatures)
|
|
4990
|
+
)} clean | ${summary.violatedFeatures > 0 ? pc15.red(String(summary.violatedFeatures)) : pc15.green("0")} with violations`
|
|
4991
|
+
);
|
|
4992
|
+
console.log(
|
|
4993
|
+
` ${pc15.bold("Violations:")} ${summary.totalViolations > 0 ? pc15.red(pc15.bold(String(summary.totalViolations))) : pc15.green("0")} rule violations found`
|
|
4994
|
+
);
|
|
4995
|
+
console.log("\u2501".repeat(70));
|
|
4996
|
+
console.log("");
|
|
4997
|
+
}
|
|
4998
|
+
async function handleLint(flags = {}) {
|
|
4999
|
+
const complete = Boolean(flags.complete);
|
|
5000
|
+
const mode = complete ? "complete" : "failFast";
|
|
5001
|
+
const quiet = Boolean(flags.quiet);
|
|
5002
|
+
if (!quiet) {
|
|
5003
|
+
printLintBanner(mode);
|
|
5004
|
+
}
|
|
5005
|
+
const options = {
|
|
5006
|
+
complete,
|
|
5007
|
+
quiet,
|
|
5008
|
+
targetDir: flags.targetDir || flags.target,
|
|
5009
|
+
projectRoot: flags.projectRoot,
|
|
5010
|
+
fixHint: flags.fixHint !== false,
|
|
5011
|
+
throwOnError: false
|
|
5012
|
+
};
|
|
5013
|
+
try {
|
|
5014
|
+
const summary = lintCodebase(options);
|
|
5015
|
+
if (!quiet) {
|
|
5016
|
+
printTerminalReport(summary, options);
|
|
5017
|
+
}
|
|
5018
|
+
if (summary.totalViolations > 0) {
|
|
5019
|
+
const errorHeader = `Dependency rule audit failed with ${summary.totalViolations} violation(s) across ${summary.violatedFeatures} feature(s):`;
|
|
5020
|
+
const compiledDetails = summary.allViolations.map(
|
|
5021
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
5022
|
+
Rule: ${v.ruleId}
|
|
5023
|
+
Code: ${v.code}
|
|
5024
|
+
Advice: ${v.advice}`
|
|
5025
|
+
).join("\n\n");
|
|
5026
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
5027
|
+
|
|
5028
|
+
${compiledDetails}
|
|
5029
|
+
`, summary.allViolations, false);
|
|
5030
|
+
}
|
|
5031
|
+
if (!quiet) {
|
|
5032
|
+
console.log(
|
|
5033
|
+
pc15.green(`\u2714 All dependency rules are being followed! (${summary.totalFeatures} features checked)`)
|
|
5034
|
+
);
|
|
5035
|
+
console.log("");
|
|
5036
|
+
}
|
|
5037
|
+
return summary;
|
|
5038
|
+
} catch (err) {
|
|
5039
|
+
if (err instanceof DependencyRuleError && err.isFailFast) {
|
|
5040
|
+
if (!quiet) {
|
|
5041
|
+
console.log(pc15.red(`
|
|
5042
|
+
\u2716 [FAIL FAST] Terminating on first detected dependency rule violation:`));
|
|
5043
|
+
const v = err.violations[0];
|
|
5044
|
+
if (v) {
|
|
5045
|
+
console.log(formatViolation(v, options.fixHint !== false));
|
|
5046
|
+
}
|
|
5047
|
+
console.log("");
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
throw err;
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
4170
5053
|
|
|
4171
5054
|
// src/cli.ts
|
|
4172
5055
|
function createCli() {
|
|
@@ -4174,7 +5057,7 @@ function createCli() {
|
|
|
4174
5057
|
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) => {
|
|
4175
5058
|
if (args && args.length > 0) {
|
|
4176
5059
|
p14.log.error(
|
|
4177
|
-
|
|
5060
|
+
pc15.red(
|
|
4178
5061
|
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
4179
5062
|
)
|
|
4180
5063
|
);
|
|
@@ -4240,14 +5123,21 @@ function createCli() {
|
|
|
4240
5123
|
value: "import",
|
|
4241
5124
|
label: "Import from registry",
|
|
4242
5125
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
5126
|
+
},
|
|
5127
|
+
{
|
|
5128
|
+
value: "lint",
|
|
5129
|
+
label: "Lint dependency rules",
|
|
5130
|
+
hint: "Scan codebase and verify Clean Architecture dependency rules"
|
|
4243
5131
|
}
|
|
4244
5132
|
]
|
|
4245
5133
|
})
|
|
4246
5134
|
);
|
|
4247
5135
|
if (choice === "create") {
|
|
4248
5136
|
await handleCreate(void 0, void 0, flags);
|
|
4249
|
-
} else {
|
|
5137
|
+
} else if (choice === "import") {
|
|
4250
5138
|
await handleImport(void 0, void 0, flags);
|
|
5139
|
+
} else if (choice === "lint") {
|
|
5140
|
+
await handleLint(flags);
|
|
4251
5141
|
}
|
|
4252
5142
|
} catch (err) {
|
|
4253
5143
|
handleCliError(err);
|
|
@@ -4273,6 +5163,22 @@ function createCli() {
|
|
|
4273
5163
|
handleCliError(err);
|
|
4274
5164
|
}
|
|
4275
5165
|
});
|
|
5166
|
+
cli.command(
|
|
5167
|
+
"lint [target]",
|
|
5168
|
+
"Scan codebase and verify Clean Architecture dependency rules"
|
|
5169
|
+
).option(
|
|
5170
|
+
"-c, --complete",
|
|
5171
|
+
"Scan entire codebase without failing fast, compiling all errors at the end"
|
|
5172
|
+
).option("-q, --quiet", "Suppress non-error output").example("mason lint").example("mason lint --complete").example("mason lint ./src").action(async (target, flags = {}) => {
|
|
5173
|
+
try {
|
|
5174
|
+
await handleLint({
|
|
5175
|
+
...flags,
|
|
5176
|
+
targetDir: target || flags.target || flags.dest
|
|
5177
|
+
});
|
|
5178
|
+
} catch (err) {
|
|
5179
|
+
handleCliError(err);
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
4276
5182
|
const createSubtypes = [
|
|
4277
5183
|
"feature",
|
|
4278
5184
|
"shared",
|
|
@@ -4304,7 +5210,10 @@ function handleCliError(err) {
|
|
|
4304
5210
|
process.exit(0);
|
|
4305
5211
|
}
|
|
4306
5212
|
const message = err instanceof Error ? err.message : String(err);
|
|
4307
|
-
p14.log.error(
|
|
5213
|
+
p14.log.error(pc15.red(`Error: ${message}`));
|
|
5214
|
+
if (process.env.VITEST) {
|
|
5215
|
+
throw err;
|
|
5216
|
+
}
|
|
4308
5217
|
process.exit(1);
|
|
4309
5218
|
}
|
|
4310
5219
|
async function runCli(argv = process.argv) {
|
|
@@ -4312,6 +5221,6 @@ async function runCli(argv = process.argv) {
|
|
|
4312
5221
|
cli.parse(argv, { run: true });
|
|
4313
5222
|
}
|
|
4314
5223
|
|
|
4315
|
-
export { DEFAULT_REGISTRY_BASE, PromptCancelledError, computeDirectoryHash, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateShared, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getAllFiles, getConfiguredFeaturesDir, getConfiguredSharedDir, getEffectiveMode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleImport, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, listDomainEntities, listDomainInterfaces, listExistingFeatures, listExistingShared, listUseCases, loadInterfaceHierarchy, loadMasonConfig, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveFeaturesDir, resolveSharedDir, runCli, runSharedCommand, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
5224
|
+
export { DEFAULT_REGISTRY_BASE, DependencyRuleError, FeatureDependencyGraph, PromptCancelledError, computeDirectoryHash, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateShared, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getAllFiles, getConfiguredFeaturesDir, getConfiguredSharedDir, getEffectiveMode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleImport, handleLint, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, lintCodebase, listDomainEntities, listDomainInterfaces, listExistingFeatures, listExistingShared, listUseCases, loadInterfaceHierarchy, loadMasonConfig, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveFeaturesDir, resolveSharedDir, runCli, runSharedCommand, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
4316
5225
|
//# sourceMappingURL=index.js.map
|
|
4317
5226
|
//# sourceMappingURL=index.js.map
|