@solid-stack/mason 1.0.20 → 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/README.md +713 -137
- 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 +3 -2
- package/schema.json +29 -0
package/dist/bin.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
var cac = require('cac');
|
|
5
5
|
var p14 = require('@clack/prompts');
|
|
6
|
-
var
|
|
6
|
+
var pc15 = require('picocolors');
|
|
7
7
|
var fs9 = require('fs');
|
|
8
8
|
var path2 = require('path');
|
|
9
9
|
var url = require('url');
|
|
@@ -33,7 +33,7 @@ function _interopNamespace(e) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
var p14__namespace = /*#__PURE__*/_interopNamespace(p14);
|
|
36
|
-
var
|
|
36
|
+
var pc15__default = /*#__PURE__*/_interopDefault(pc15);
|
|
37
37
|
var fs9__default = /*#__PURE__*/_interopDefault(fs9);
|
|
38
38
|
var path2__default = /*#__PURE__*/_interopDefault(path2);
|
|
39
39
|
var Handlebars__default = /*#__PURE__*/_interopDefault(Handlebars);
|
|
@@ -76,17 +76,17 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
76
76
|
function printGeneratedFiles(files, isDryRun = false) {
|
|
77
77
|
console.log("");
|
|
78
78
|
if (isDryRun) {
|
|
79
|
-
p14__namespace.log.warn(
|
|
79
|
+
p14__namespace.log.warn(pc15__default.default.yellow(pc15__default.default.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
80
80
|
}
|
|
81
81
|
for (const f of files) {
|
|
82
82
|
if (f.action === "created") {
|
|
83
|
-
p14__namespace.log.success(`${
|
|
83
|
+
p14__namespace.log.success(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(created)")}`);
|
|
84
84
|
} else if (f.action === "overwritten") {
|
|
85
|
-
p14__namespace.log.warn(`${
|
|
85
|
+
p14__namespace.log.warn(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(overwritten)")}`);
|
|
86
86
|
} else if (f.action === "skipped") {
|
|
87
|
-
p14__namespace.log.info(`${
|
|
87
|
+
p14__namespace.log.info(`${pc15__default.default.dim(f.relativePath)} ${pc15__default.default.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
88
88
|
} else {
|
|
89
|
-
p14__namespace.log.info(`${
|
|
89
|
+
p14__namespace.log.info(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(preview)")}`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -1773,13 +1773,13 @@ function handleCancel(result) {
|
|
|
1773
1773
|
}
|
|
1774
1774
|
function printBanner() {
|
|
1775
1775
|
console.log("");
|
|
1776
|
-
p14__namespace.intro(
|
|
1776
|
+
p14__namespace.intro(pc15__default.default.bgCyan(pc15__default.default.black(" @solid-stack/mason ")));
|
|
1777
1777
|
}
|
|
1778
1778
|
async function promptProperties(options = {}) {
|
|
1779
1779
|
const properties = options.defaults ? [...options.defaults] : [];
|
|
1780
1780
|
if (properties.length > 0) {
|
|
1781
1781
|
const summary = properties.map((p16) => `${p16.name}: ${p16.type}${p16.optional ? "?" : ""}`).join(", ");
|
|
1782
|
-
p14__namespace.log.info(
|
|
1782
|
+
p14__namespace.log.info(pc15__default.default.dim(`Pre-configured fields: ${summary}`));
|
|
1783
1783
|
const addMore = handleCancel(
|
|
1784
1784
|
await p14__namespace.confirm({
|
|
1785
1785
|
message: "Would you like to add custom fields/properties?",
|
|
@@ -1790,7 +1790,7 @@ async function promptProperties(options = {}) {
|
|
|
1790
1790
|
return properties;
|
|
1791
1791
|
}
|
|
1792
1792
|
}
|
|
1793
|
-
p14__namespace.log.info(
|
|
1793
|
+
p14__namespace.log.info(pc15__default.default.cyan("Enter properties (leave name blank when finished):"));
|
|
1794
1794
|
while (true) {
|
|
1795
1795
|
const name = handleCancel(
|
|
1796
1796
|
await p14__namespace.text({
|
|
@@ -1838,7 +1838,7 @@ async function promptProperties(options = {}) {
|
|
|
1838
1838
|
optional
|
|
1839
1839
|
});
|
|
1840
1840
|
p14__namespace.log.success(
|
|
1841
|
-
|
|
1841
|
+
pc15__default.default.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
1842
1842
|
);
|
|
1843
1843
|
}
|
|
1844
1844
|
return properties;
|
|
@@ -2098,7 +2098,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2098
2098
|
}
|
|
2099
2099
|
}
|
|
2100
2100
|
const s = p14__namespace.spinner();
|
|
2101
|
-
s.start(`Generating feature '${
|
|
2101
|
+
s.start(`Generating feature '${pc15__default.default.cyan(normalizedName)}'...`);
|
|
2102
2102
|
const options = {
|
|
2103
2103
|
name: normalizedName,
|
|
2104
2104
|
projectRoot,
|
|
@@ -2145,7 +2145,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2145
2145
|
s.stop("Feature scaffolded successfully!");
|
|
2146
2146
|
printGeneratedFiles(files, options.dryRun);
|
|
2147
2147
|
p14__namespace.outro(
|
|
2148
|
-
|
|
2148
|
+
pc15__default.default.green(
|
|
2149
2149
|
`\u2713 Feature '${normalizedName}' initialized in ${path2__default.default.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2150
2150
|
)
|
|
2151
2151
|
);
|
|
@@ -2301,7 +2301,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2301
2301
|
);
|
|
2302
2302
|
}
|
|
2303
2303
|
const s = p14__namespace.spinner();
|
|
2304
|
-
s.start(`Generating shared module '${
|
|
2304
|
+
s.start(`Generating shared module '${pc15__default.default.cyan(normalizedName)}'...`);
|
|
2305
2305
|
const files = generateShared({
|
|
2306
2306
|
name: normalizedName,
|
|
2307
2307
|
description,
|
|
@@ -2315,7 +2315,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2315
2315
|
s.stop("Shared module generated successfully!");
|
|
2316
2316
|
printGeneratedFiles(files, Boolean(flags.dryRun));
|
|
2317
2317
|
p14__namespace.outro(
|
|
2318
|
-
|
|
2318
|
+
pc15__default.default.green(
|
|
2319
2319
|
`\u2713 Shared module '${normalizedName}' initialized in ${path2__default.default.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2320
2320
|
)
|
|
2321
2321
|
);
|
|
@@ -2443,7 +2443,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2443
2443
|
}
|
|
2444
2444
|
}
|
|
2445
2445
|
const s = p14__namespace.spinner();
|
|
2446
|
-
s.start(`Generating use case '${
|
|
2446
|
+
s.start(`Generating use case '${pc15__default.default.cyan(ucName)}'...`);
|
|
2447
2447
|
const options = {
|
|
2448
2448
|
name: ucName,
|
|
2449
2449
|
feature,
|
|
@@ -2475,7 +2475,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2475
2475
|
}
|
|
2476
2476
|
s.stop("Use case generated successfully!");
|
|
2477
2477
|
printGeneratedFiles(files, options.dryRun);
|
|
2478
|
-
p14__namespace.outro(
|
|
2478
|
+
p14__namespace.outro(pc15__default.default.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2479
2479
|
}
|
|
2480
2480
|
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2481
2481
|
const projectRoot = findProjectRoot();
|
|
@@ -2546,7 +2546,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2546
2546
|
});
|
|
2547
2547
|
}
|
|
2548
2548
|
const s = p14__namespace.spinner();
|
|
2549
|
-
s.start(`Generating domain model '${
|
|
2549
|
+
s.start(`Generating domain model '${pc15__default.default.cyan(entityName)}'...`);
|
|
2550
2550
|
const options = {
|
|
2551
2551
|
name: entityName,
|
|
2552
2552
|
feature,
|
|
@@ -2559,7 +2559,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2559
2559
|
const file = generateDomain(options);
|
|
2560
2560
|
s.stop("Domain model generated successfully!");
|
|
2561
2561
|
printGeneratedFiles([file], options.dryRun);
|
|
2562
|
-
p14__namespace.outro(
|
|
2562
|
+
p14__namespace.outro(pc15__default.default.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2563
2563
|
}
|
|
2564
2564
|
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2565
2565
|
const projectRoot = findProjectRoot();
|
|
@@ -2728,7 +2728,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2728
2728
|
registerInProvider = !flags.noProvider;
|
|
2729
2729
|
}
|
|
2730
2730
|
const s = p14__namespace.spinner();
|
|
2731
|
-
s.start(`Generating infrastructure class '${
|
|
2731
|
+
s.start(`Generating infrastructure class '${pc15__default.default.cyan(className)}'...`);
|
|
2732
2732
|
const files = [];
|
|
2733
2733
|
const options = {
|
|
2734
2734
|
name: className,
|
|
@@ -2763,7 +2763,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2763
2763
|
}
|
|
2764
2764
|
s.stop("Infrastructure class generated successfully!");
|
|
2765
2765
|
printGeneratedFiles(files, options.dryRun);
|
|
2766
|
-
p14__namespace.outro(
|
|
2766
|
+
p14__namespace.outro(pc15__default.default.green(`Infrastructure class '${className}' created!`));
|
|
2767
2767
|
}
|
|
2768
2768
|
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
2769
2769
|
const projectRoot = findProjectRoot();
|
|
@@ -2876,7 +2876,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2876
2876
|
}
|
|
2877
2877
|
}
|
|
2878
2878
|
const s = p14__namespace.spinner();
|
|
2879
|
-
s.start(`Generating interface '${
|
|
2879
|
+
s.start(`Generating interface '${pc15__default.default.cyan(ifaceName)}'...`);
|
|
2880
2880
|
const files = [];
|
|
2881
2881
|
const options = {
|
|
2882
2882
|
name: ifaceName,
|
|
@@ -2925,7 +2925,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2925
2925
|
}
|
|
2926
2926
|
s.stop("Interface generated successfully!");
|
|
2927
2927
|
printGeneratedFiles(files, options.dryRun);
|
|
2928
|
-
p14__namespace.outro(
|
|
2928
|
+
p14__namespace.outro(pc15__default.default.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
2929
2929
|
}
|
|
2930
2930
|
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
2931
2931
|
const projectRoot = findProjectRoot();
|
|
@@ -3042,7 +3042,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3042
3042
|
}
|
|
3043
3043
|
}
|
|
3044
3044
|
const s = p14__namespace.spinner();
|
|
3045
|
-
s.start(`Generating HTTP handler '${
|
|
3045
|
+
s.start(`Generating HTTP handler '${pc15__default.default.cyan(className)}'...`);
|
|
3046
3046
|
const options = {
|
|
3047
3047
|
name: className,
|
|
3048
3048
|
feature,
|
|
@@ -3060,7 +3060,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3060
3060
|
const files = generateHttpHandler(options);
|
|
3061
3061
|
s.stop("HTTP handler generated successfully!");
|
|
3062
3062
|
printGeneratedFiles(files, options.dryRun);
|
|
3063
|
-
p14__namespace.outro(
|
|
3063
|
+
p14__namespace.outro(pc15__default.default.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
3064
3064
|
}
|
|
3065
3065
|
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
3066
3066
|
const formattedDeps = dependencies.map((dep) => ({
|
|
@@ -3216,7 +3216,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3216
3216
|
);
|
|
3217
3217
|
}
|
|
3218
3218
|
const s = p14__namespace.spinner();
|
|
3219
|
-
s.start(`Generating injectable '${
|
|
3219
|
+
s.start(`Generating injectable '${pc15__default.default.cyan(className)}'...`);
|
|
3220
3220
|
const options = {
|
|
3221
3221
|
name: className,
|
|
3222
3222
|
feature,
|
|
@@ -3230,7 +3230,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3230
3230
|
const files = generateInjectable(options);
|
|
3231
3231
|
s.stop("Injectable class generated successfully!");
|
|
3232
3232
|
printGeneratedFiles(files, options.dryRun);
|
|
3233
|
-
p14__namespace.outro(
|
|
3233
|
+
p14__namespace.outro(pc15__default.default.green(`Injectable '${className}' created successfully!`));
|
|
3234
3234
|
}
|
|
3235
3235
|
async function runInteractiveMenu(flags = {}) {
|
|
3236
3236
|
printBanner();
|
|
@@ -3344,7 +3344,7 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3344
3344
|
break;
|
|
3345
3345
|
default:
|
|
3346
3346
|
p14__namespace.log.error(
|
|
3347
|
-
|
|
3347
|
+
pc15__default.default.red(
|
|
3348
3348
|
`Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3349
3349
|
)
|
|
3350
3350
|
);
|
|
@@ -3580,7 +3580,7 @@ function installNpmPackages(projectRoot, packages, pm) {
|
|
|
3580
3580
|
installCmd = `npm install ${packages.join(" ")}`;
|
|
3581
3581
|
break;
|
|
3582
3582
|
}
|
|
3583
|
-
p14__namespace.log.info(`Installing missing dependencies with ${manager}: ${
|
|
3583
|
+
p14__namespace.log.info(`Installing missing dependencies with ${manager}: ${pc15__default.default.cyan(packages.join(", "))}`);
|
|
3584
3584
|
child_process.execSync(installCmd, {
|
|
3585
3585
|
cwd: projectRoot,
|
|
3586
3586
|
stdio: "inherit"
|
|
@@ -3736,19 +3736,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3736
3736
|
);
|
|
3737
3737
|
}
|
|
3738
3738
|
if (type !== "shared" && type !== "feature") {
|
|
3739
|
-
p14__namespace.log.error(
|
|
3739
|
+
p14__namespace.log.error(pc15__default.default.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3740
3740
|
process.exit(1);
|
|
3741
3741
|
}
|
|
3742
3742
|
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3743
3743
|
const s = p14__namespace.spinner();
|
|
3744
|
-
s.start(`Connecting to registry at ${
|
|
3744
|
+
s.start(`Connecting to registry at ${pc15__default.default.dim(registryBase)}...`);
|
|
3745
3745
|
let index;
|
|
3746
3746
|
try {
|
|
3747
3747
|
index = await fetchRegistryIndex(registryBase);
|
|
3748
3748
|
s.stop("Registry index loaded.");
|
|
3749
3749
|
} catch (err) {
|
|
3750
|
-
s.stop(
|
|
3751
|
-
p14__namespace.log.error(
|
|
3750
|
+
s.stop(pc15__default.default.red("Failed to load registry index."));
|
|
3751
|
+
p14__namespace.log.error(pc15__default.default.red(err.message || String(err)));
|
|
3752
3752
|
process.exit(1);
|
|
3753
3753
|
}
|
|
3754
3754
|
const availableList = type === "shared" ? index.shared : index.features;
|
|
@@ -3772,9 +3772,9 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3772
3772
|
}
|
|
3773
3773
|
if (!candidateNames.includes(name)) {
|
|
3774
3774
|
const closest = findClosestMatch(name, candidateNames);
|
|
3775
|
-
p14__namespace.log.error(
|
|
3775
|
+
p14__namespace.log.error(pc15__default.default.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3776
3776
|
if (closest) {
|
|
3777
|
-
p14__namespace.log.info(
|
|
3777
|
+
p14__namespace.log.info(pc15__default.default.yellow(`Did you mean '${pc15__default.default.cyan(closest)}'?`));
|
|
3778
3778
|
}
|
|
3779
3779
|
process.exit(1);
|
|
3780
3780
|
}
|
|
@@ -3786,7 +3786,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3786
3786
|
try {
|
|
3787
3787
|
manifest = await fetchModuleManifest(registryBase, modType, modName);
|
|
3788
3788
|
} catch (err) {
|
|
3789
|
-
p14__namespace.log.error(
|
|
3789
|
+
p14__namespace.log.error(pc15__default.default.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
|
|
3790
3790
|
process.exit(1);
|
|
3791
3791
|
}
|
|
3792
3792
|
nodes.set(key, {
|
|
@@ -3826,7 +3826,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3826
3826
|
buildTopologicalOrder(rootKey);
|
|
3827
3827
|
const resolutions = /* @__PURE__ */ new Map();
|
|
3828
3828
|
const globalAliasMap = /* @__PURE__ */ new Map();
|
|
3829
|
-
p14__namespace.log.step(`Importing ${type}: ${
|
|
3829
|
+
p14__namespace.log.step(`Importing ${type}: ${pc15__default.default.cyan(name)}...`);
|
|
3830
3830
|
for (const key of orderedKeys) {
|
|
3831
3831
|
const node = nodes.get(key);
|
|
3832
3832
|
const { type: modType, name: registryName, manifest } = node;
|
|
@@ -3859,7 +3859,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3859
3859
|
if (programmaticPointTo) {
|
|
3860
3860
|
if (!checkModuleExists(projectRoot, modType, programmaticPointTo)) {
|
|
3861
3861
|
p14__namespace.log.error(
|
|
3862
|
-
|
|
3862
|
+
pc15__default.default.red(
|
|
3863
3863
|
`Module '${programmaticPointTo}' specified in --point-to does not exist in ${folder}.`
|
|
3864
3864
|
)
|
|
3865
3865
|
);
|
|
@@ -3895,7 +3895,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3895
3895
|
}
|
|
3896
3896
|
const existingDir = getExistingModuleDir(projectRoot, modType, registryName);
|
|
3897
3897
|
if (!existingDir) {
|
|
3898
|
-
p14__namespace.log.info(` \u21B3 Resolving required ${modType} dependency: ${
|
|
3898
|
+
p14__namespace.log.info(` \u21B3 Resolving required ${modType} dependency: ${pc15__default.default.cyan(registryName)}`);
|
|
3899
3899
|
resolutions.set(key, {
|
|
3900
3900
|
type: modType,
|
|
3901
3901
|
registryName,
|
|
@@ -4126,7 +4126,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4126
4126
|
const isOverwrite = Boolean(flags.overwrite || res.action === "overwrite");
|
|
4127
4127
|
if (key !== rootKey) {
|
|
4128
4128
|
p14__namespace.log.step(
|
|
4129
|
-
`Importing ${res.type}: ${
|
|
4129
|
+
`Importing ${res.type}: ${pc15__default.default.cyan(res.targetName)}${res.targetName !== res.registryName ? pc15__default.default.dim(` (from ${res.registryName})`) : ""}...`
|
|
4130
4130
|
);
|
|
4131
4131
|
}
|
|
4132
4132
|
if (res.action === "overwrite" && !flags.dryRun) {
|
|
@@ -4145,7 +4145,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4145
4145
|
const localFilePath = path2__default.default.join(targetDir, file);
|
|
4146
4146
|
if (fs9__default.default.existsSync(localFilePath) && !isOverwrite) {
|
|
4147
4147
|
p14__namespace.log.warn(
|
|
4148
|
-
` \u21B3 Skipping existing file: ${
|
|
4148
|
+
` \u21B3 Skipping existing file: ${pc15__default.default.dim(path2__default.default.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
|
|
4149
4149
|
);
|
|
4150
4150
|
continue;
|
|
4151
4151
|
}
|
|
@@ -4157,13 +4157,13 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4157
4157
|
}
|
|
4158
4158
|
const displayPath = path2__default.default.relative(projectRoot, localFilePath);
|
|
4159
4159
|
totalWrittenFiles.push(displayPath);
|
|
4160
|
-
p14__namespace.log.message(` ${
|
|
4160
|
+
p14__namespace.log.message(` ${pc15__default.default.green("+")} ${displayPath}`);
|
|
4161
4161
|
}
|
|
4162
4162
|
}
|
|
4163
4163
|
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
4164
4164
|
if (missingNpm.length > 0) {
|
|
4165
4165
|
if (flags.skipInstall || flags.dryRun) {
|
|
4166
|
-
p14__namespace.log.info(`Required npm dependencies to install: ${
|
|
4166
|
+
p14__namespace.log.info(`Required npm dependencies to install: ${pc15__default.default.cyan(missingNpm.join(", "))}`);
|
|
4167
4167
|
} else {
|
|
4168
4168
|
try {
|
|
4169
4169
|
installNpmPackages(projectRoot, missingNpm);
|
|
@@ -4177,18 +4177,901 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4177
4177
|
}
|
|
4178
4178
|
}
|
|
4179
4179
|
if (flags.dryRun) {
|
|
4180
|
-
p14__namespace.log.info(
|
|
4180
|
+
p14__namespace.log.info(pc15__default.default.yellow("Dry run complete: No files were written to disk."));
|
|
4181
4181
|
} else {
|
|
4182
4182
|
const rootRes = resolutions.get(rootKey);
|
|
4183
4183
|
const baseDir = getModuleBaseDir(projectRoot, type);
|
|
4184
4184
|
const displayTarget = path2__default.default.relative(projectRoot, path2__default.default.join(baseDir, rootRes.targetName));
|
|
4185
4185
|
p14__namespace.log.success(
|
|
4186
|
-
|
|
4186
|
+
pc15__default.default.green(
|
|
4187
4187
|
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
|
|
4188
4188
|
)
|
|
4189
4189
|
);
|
|
4190
4190
|
}
|
|
4191
4191
|
}
|
|
4192
|
+
var DependencyRuleError = class extends Error {
|
|
4193
|
+
violations;
|
|
4194
|
+
isFailFast;
|
|
4195
|
+
constructor(message, violations, isFailFast = false) {
|
|
4196
|
+
super(message);
|
|
4197
|
+
this.name = "DependencyRuleError";
|
|
4198
|
+
this.violations = violations;
|
|
4199
|
+
this.isFailFast = isFailFast;
|
|
4200
|
+
}
|
|
4201
|
+
};
|
|
4202
|
+
function isTestFile2(filePath, projectRoot) {
|
|
4203
|
+
const norm = (projectRoot ? path2__default.default.relative(projectRoot, filePath) : filePath).replace(/\\/g, "/");
|
|
4204
|
+
const base = path2__default.default.basename(norm);
|
|
4205
|
+
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);
|
|
4206
|
+
}
|
|
4207
|
+
function scanDirFiles(dir, projectRoot) {
|
|
4208
|
+
if (!fs9__default.default.existsSync(dir)) return [];
|
|
4209
|
+
const entries = fs9__default.default.readdirSync(dir, { withFileTypes: true });
|
|
4210
|
+
const files = [];
|
|
4211
|
+
for (const entry of entries) {
|
|
4212
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
const fullPath = path2__default.default.join(dir, entry.name);
|
|
4216
|
+
if (entry.isDirectory()) {
|
|
4217
|
+
files.push(...scanDirFiles(fullPath, projectRoot));
|
|
4218
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !isTestFile2(fullPath, projectRoot)) {
|
|
4219
|
+
files.push(fullPath);
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
return files;
|
|
4223
|
+
}
|
|
4224
|
+
function classifyFile(fullPath, projectRoot) {
|
|
4225
|
+
const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4226
|
+
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4227
|
+
if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
|
|
4228
|
+
const feature = featureMatch[1];
|
|
4229
|
+
const subfolder = featureMatch[2];
|
|
4230
|
+
let layer;
|
|
4231
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4232
|
+
layer = "domain";
|
|
4233
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4234
|
+
layer = "useCases";
|
|
4235
|
+
} else if (subfolder === "services") {
|
|
4236
|
+
layer = "services";
|
|
4237
|
+
} else if (subfolder === "infrastructure") {
|
|
4238
|
+
layer = "infrastructure";
|
|
4239
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4240
|
+
layer = "presentation";
|
|
4241
|
+
}
|
|
4242
|
+
return { feature, layer };
|
|
4243
|
+
}
|
|
4244
|
+
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4245
|
+
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
4246
|
+
return { isExternal: true, isDiFramework: true };
|
|
4247
|
+
}
|
|
4248
|
+
if (specifier.startsWith("node:") || ["crypto", "fs", "path", "http", "https", "url", "os", "child_process", "stream", "express", "cors", "dotenv"].includes(specifier)) {
|
|
4249
|
+
return { isExternal: true, isDiFramework: false };
|
|
4250
|
+
}
|
|
4251
|
+
let resolvedRel = null;
|
|
4252
|
+
if (specifier.startsWith(".")) {
|
|
4253
|
+
const abs = path2__default.default.resolve(path2__default.default.dirname(sourceFileAbs), specifier);
|
|
4254
|
+
resolvedRel = path2__default.default.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
4255
|
+
} else if (specifier.startsWith("@/features/") || specifier.startsWith("~/features/")) {
|
|
4256
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4257
|
+
} else if (specifier.startsWith("@features/")) {
|
|
4258
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4259
|
+
} else if (specifier.startsWith("@/shared/") || specifier.startsWith("~/shared/")) {
|
|
4260
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4261
|
+
} else if (specifier.startsWith("@shared/")) {
|
|
4262
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4263
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4264
|
+
resolvedRel = "src/" + specifier.slice(2);
|
|
4265
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4266
|
+
resolvedRel = specifier;
|
|
4267
|
+
}
|
|
4268
|
+
if (!resolvedRel) {
|
|
4269
|
+
return { isExternal: true, isDiFramework: false };
|
|
4270
|
+
}
|
|
4271
|
+
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4272
|
+
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4273
|
+
const targetFeature = featMatch[1];
|
|
4274
|
+
const targetSubfolder = featMatch[2];
|
|
4275
|
+
const rest = featMatch[3] || "";
|
|
4276
|
+
let targetLayer;
|
|
4277
|
+
if (targetSubfolder === "domain" || targetSubfolder === "errors") {
|
|
4278
|
+
targetLayer = "domain";
|
|
4279
|
+
} else if (targetSubfolder === "useCases" || targetSubfolder === "usecases") {
|
|
4280
|
+
targetLayer = "useCases";
|
|
4281
|
+
} else if (targetSubfolder === "services") {
|
|
4282
|
+
targetLayer = "services";
|
|
4283
|
+
} else if (targetSubfolder === "infrastructure") {
|
|
4284
|
+
targetLayer = "infrastructure";
|
|
4285
|
+
} else if (targetSubfolder === "pxExpress" || targetSubfolder === "presentation" || targetSubfolder === "handlers" || targetSubfolder.startsWith("px")) {
|
|
4286
|
+
targetLayer = "presentation";
|
|
4287
|
+
}
|
|
4288
|
+
const basePart = rest ? path2__default.default.basename(rest) : targetSubfolder;
|
|
4289
|
+
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4290
|
+
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4291
|
+
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4292
|
+
return {
|
|
4293
|
+
isExternal: false,
|
|
4294
|
+
isDiFramework: false,
|
|
4295
|
+
targetFeature,
|
|
4296
|
+
targetLayer,
|
|
4297
|
+
targetSubfolder,
|
|
4298
|
+
targetFileName,
|
|
4299
|
+
isTargetPort,
|
|
4300
|
+
isTargetRepo
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4304
|
+
if (sharedMatch) {
|
|
4305
|
+
const targetSharedModule = sharedMatch[1];
|
|
4306
|
+
const sharedRest = sharedMatch[2] || "";
|
|
4307
|
+
const isSharedRoot = !sharedRest.includes("/");
|
|
4308
|
+
const isSharedPorts = sharedRest.startsWith("ports/") || sharedRest === "ports";
|
|
4309
|
+
const isSharedInfra = sharedRest.startsWith("infrastructure/") || sharedRest === "infrastructure";
|
|
4310
|
+
const isSharedDomain = sharedRest.startsWith("domain/") || sharedRest === "domain";
|
|
4311
|
+
return {
|
|
4312
|
+
isExternal: false,
|
|
4313
|
+
isDiFramework: false,
|
|
4314
|
+
targetSharedModule,
|
|
4315
|
+
isSharedRoot,
|
|
4316
|
+
isSharedPorts,
|
|
4317
|
+
isSharedInfra,
|
|
4318
|
+
isSharedDomain
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
return { isExternal: true, isDiFramework: false };
|
|
4322
|
+
}
|
|
4323
|
+
function extractImportStatements(content) {
|
|
4324
|
+
const statements = [];
|
|
4325
|
+
const lines = content.split(/\r?\n/);
|
|
4326
|
+
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;
|
|
4327
|
+
let match;
|
|
4328
|
+
while ((match = regex.exec(content)) !== null) {
|
|
4329
|
+
const specifier = match[1];
|
|
4330
|
+
const charIndex = match.index;
|
|
4331
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4332
|
+
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
4333
|
+
statements.push({
|
|
4334
|
+
specifier,
|
|
4335
|
+
line: lineNum,
|
|
4336
|
+
rawCode
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
return statements;
|
|
4340
|
+
}
|
|
4341
|
+
var FeatureDependencyGraph = class {
|
|
4342
|
+
edges = /* @__PURE__ */ new Map();
|
|
4343
|
+
addEdge(evidence) {
|
|
4344
|
+
const { fromFeature, toFeature } = evidence;
|
|
4345
|
+
if (fromFeature === toFeature) {
|
|
4346
|
+
return { createsCycle: false };
|
|
4347
|
+
}
|
|
4348
|
+
if (!this.edges.has(fromFeature)) {
|
|
4349
|
+
this.edges.set(fromFeature, /* @__PURE__ */ new Map());
|
|
4350
|
+
}
|
|
4351
|
+
this.edges.get(fromFeature).set(toFeature, evidence);
|
|
4352
|
+
const reverse = this.edges.get(toFeature)?.get(fromFeature);
|
|
4353
|
+
if (reverse) {
|
|
4354
|
+
return { createsCycle: true, directPartner: reverse, cycleChain: [fromFeature, toFeature, fromFeature] };
|
|
4355
|
+
}
|
|
4356
|
+
const cycle = this.findCycleFrom(fromFeature);
|
|
4357
|
+
if (cycle && cycle.length > 0) {
|
|
4358
|
+
return { createsCycle: true, cycleChain: cycle };
|
|
4359
|
+
}
|
|
4360
|
+
return { createsCycle: false };
|
|
4361
|
+
}
|
|
4362
|
+
getEdges() {
|
|
4363
|
+
const list = [];
|
|
4364
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4365
|
+
for (const [to, ev] of toMap.entries()) {
|
|
4366
|
+
list.push({ from, to, evidence: ev });
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
return list;
|
|
4370
|
+
}
|
|
4371
|
+
findDirectCycles() {
|
|
4372
|
+
const cycles = [];
|
|
4373
|
+
const checkedPairs = /* @__PURE__ */ new Set();
|
|
4374
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4375
|
+
for (const [to, evAtoB] of toMap.entries()) {
|
|
4376
|
+
const pairKey = [from, to].sort().join("<->");
|
|
4377
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
4378
|
+
const evBtoA = this.edges.get(to)?.get(from);
|
|
4379
|
+
if (evBtoA) {
|
|
4380
|
+
checkedPairs.add(pairKey);
|
|
4381
|
+
cycles.push({
|
|
4382
|
+
featureA: from,
|
|
4383
|
+
featureB: to,
|
|
4384
|
+
cycle: [from, to, from],
|
|
4385
|
+
evidenceAtoB: evAtoB,
|
|
4386
|
+
evidenceBtoA: evBtoA
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
return cycles;
|
|
4392
|
+
}
|
|
4393
|
+
findCycleFrom(start) {
|
|
4394
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4395
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
4396
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4397
|
+
const dfs = (curr) => {
|
|
4398
|
+
visited.add(curr);
|
|
4399
|
+
inStack.add(curr);
|
|
4400
|
+
const neighbors = this.edges.get(curr);
|
|
4401
|
+
if (neighbors) {
|
|
4402
|
+
for (const next of neighbors.keys()) {
|
|
4403
|
+
if (!visited.has(next)) {
|
|
4404
|
+
parent.set(next, curr);
|
|
4405
|
+
const cycle = dfs(next);
|
|
4406
|
+
if (cycle) return cycle;
|
|
4407
|
+
} else if (inStack.has(next)) {
|
|
4408
|
+
const cycle = [next];
|
|
4409
|
+
let p16 = curr;
|
|
4410
|
+
while (p16 !== next) {
|
|
4411
|
+
cycle.push(p16);
|
|
4412
|
+
p16 = parent.get(p16) || next;
|
|
4413
|
+
}
|
|
4414
|
+
cycle.push(next);
|
|
4415
|
+
cycle.reverse();
|
|
4416
|
+
return cycle;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
inStack.delete(curr);
|
|
4421
|
+
return null;
|
|
4422
|
+
};
|
|
4423
|
+
return dfs(start);
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4426
|
+
function discoverAllFeatures(projectRoot) {
|
|
4427
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
4428
|
+
if (!fs9__default.default.existsSync(featuresBase)) return [];
|
|
4429
|
+
const entries = fs9__default.default.readdirSync(featuresBase, { withFileTypes: true });
|
|
4430
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
4431
|
+
name: e.name,
|
|
4432
|
+
dir: path2__default.default.join(featuresBase, e.name)
|
|
4433
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4434
|
+
}
|
|
4435
|
+
function lintCodebase(options = {}) {
|
|
4436
|
+
const startTime = Date.now();
|
|
4437
|
+
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
4438
|
+
const failFast = !options.complete;
|
|
4439
|
+
const features = discoverAllFeatures(projectRoot);
|
|
4440
|
+
const graph = new FeatureDependencyGraph();
|
|
4441
|
+
const allViolations = [];
|
|
4442
|
+
const results = [];
|
|
4443
|
+
const detectedCycles = [];
|
|
4444
|
+
const addViolation = (v) => {
|
|
4445
|
+
allViolations.push(v);
|
|
4446
|
+
const featureResult = results.find((r) => r.featureName === v.feature);
|
|
4447
|
+
if (featureResult) {
|
|
4448
|
+
featureResult.violations.push(v);
|
|
4449
|
+
featureResult.passed = false;
|
|
4450
|
+
}
|
|
4451
|
+
if (failFast) {
|
|
4452
|
+
const errorMsg = `[FAIL FAST] Dependency rule violation detected:
|
|
4453
|
+
Feature: ${v.feature}
|
|
4454
|
+
Category: ${v.category}
|
|
4455
|
+
Rule: ${v.ruleId}
|
|
4456
|
+
Location: ${v.relativePath}:${v.line}
|
|
4457
|
+
Code: ${v.code}
|
|
4458
|
+
Explanation: ${v.advice}`;
|
|
4459
|
+
throw new DependencyRuleError(errorMsg, [v], true);
|
|
4460
|
+
}
|
|
4461
|
+
};
|
|
4462
|
+
for (const feat of features) {
|
|
4463
|
+
results.push({
|
|
4464
|
+
featureName: feat.name,
|
|
4465
|
+
featureDir: feat.dir,
|
|
4466
|
+
scannedFilesCount: 0,
|
|
4467
|
+
passed: true,
|
|
4468
|
+
violations: [],
|
|
4469
|
+
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
for (const feat of features) {
|
|
4473
|
+
const featResult = results.find((r) => r.featureName === feat.name);
|
|
4474
|
+
const allFiles = scanDirFiles(feat.dir, projectRoot);
|
|
4475
|
+
featResult.scannedFilesCount = allFiles.length;
|
|
4476
|
+
for (const file of allFiles) {
|
|
4477
|
+
const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
|
|
4478
|
+
const { layer } = classifyFile(file, projectRoot);
|
|
4479
|
+
if (!layer) continue;
|
|
4480
|
+
const content = fs9__default.default.readFileSync(file, "utf-8");
|
|
4481
|
+
const statements = extractImportStatements(content);
|
|
4482
|
+
if (layer === "domain") {
|
|
4483
|
+
const lines = content.split(/\r?\n/);
|
|
4484
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4485
|
+
const lineText = lines[i].trim();
|
|
4486
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
4487
|
+
addViolation({
|
|
4488
|
+
feature: feat.name,
|
|
4489
|
+
layer,
|
|
4490
|
+
file,
|
|
4491
|
+
relativePath: relPath,
|
|
4492
|
+
line: i + 1,
|
|
4493
|
+
category: "Domain Dependence Violation",
|
|
4494
|
+
code: lineText,
|
|
4495
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4496
|
+
severity: "error",
|
|
4497
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
4498
|
+
fixExample: {
|
|
4499
|
+
bad: lineText,
|
|
4500
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
4501
|
+
}
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
for (const stmt of statements) {
|
|
4507
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
4508
|
+
if (target.targetFeature && target.targetFeature !== feat.name) {
|
|
4509
|
+
const evidence = {
|
|
4510
|
+
fromFeature: feat.name,
|
|
4511
|
+
toFeature: target.targetFeature,
|
|
4512
|
+
file,
|
|
4513
|
+
relativePath: relPath,
|
|
4514
|
+
line: stmt.line,
|
|
4515
|
+
code: stmt.rawCode
|
|
4516
|
+
};
|
|
4517
|
+
const cycleResult = graph.addEdge(evidence);
|
|
4518
|
+
if (cycleResult.createsCycle) {
|
|
4519
|
+
const partner = cycleResult.directPartner;
|
|
4520
|
+
detectedCycles.push({
|
|
4521
|
+
featureA: feat.name,
|
|
4522
|
+
featureB: target.targetFeature,
|
|
4523
|
+
cycle: cycleResult.cycleChain,
|
|
4524
|
+
evidenceAtoB: evidence,
|
|
4525
|
+
evidenceBtoA: partner || evidence
|
|
4526
|
+
});
|
|
4527
|
+
addViolation({
|
|
4528
|
+
feature: feat.name,
|
|
4529
|
+
layer,
|
|
4530
|
+
file,
|
|
4531
|
+
relativePath: relPath,
|
|
4532
|
+
line: stmt.line,
|
|
4533
|
+
category: "Circular Feature Dependency Violation",
|
|
4534
|
+
code: stmt.rawCode,
|
|
4535
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4536
|
+
severity: "error",
|
|
4537
|
+
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.`,
|
|
4538
|
+
fixExample: {
|
|
4539
|
+
bad: stmt.rawCode,
|
|
4540
|
+
good: `// Emit a Domain/Application Event (e.g. publish '${feat.name}Changed') instead of depending on '${target.targetFeature}'.`
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
if (layer === "domain") {
|
|
4546
|
+
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
4547
|
+
addViolation({
|
|
4548
|
+
feature: feat.name,
|
|
4549
|
+
layer,
|
|
4550
|
+
file,
|
|
4551
|
+
relativePath: relPath,
|
|
4552
|
+
line: stmt.line,
|
|
4553
|
+
category: "Domain Purity Violation",
|
|
4554
|
+
code: stmt.rawCode,
|
|
4555
|
+
ruleId: "D1_OUTER_IMPORT",
|
|
4556
|
+
severity: "error",
|
|
4557
|
+
advice: `Domain layer must not import from outer layers (${target.targetLayer}). Keep the domain core pure.`,
|
|
4558
|
+
fixExample: {
|
|
4559
|
+
bad: stmt.rawCode,
|
|
4560
|
+
good: "// Define interfaces, types, or value objects directly within the domain layer."
|
|
4561
|
+
}
|
|
4562
|
+
});
|
|
4563
|
+
}
|
|
4564
|
+
if (target.isDiFramework) {
|
|
4565
|
+
addViolation({
|
|
4566
|
+
feature: feat.name,
|
|
4567
|
+
layer,
|
|
4568
|
+
file,
|
|
4569
|
+
relativePath: relPath,
|
|
4570
|
+
line: stmt.line,
|
|
4571
|
+
category: "Domain Dependence Violation",
|
|
4572
|
+
code: stmt.rawCode,
|
|
4573
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4574
|
+
severity: "error",
|
|
4575
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
4576
|
+
fixExample: {
|
|
4577
|
+
bad: stmt.rawCode,
|
|
4578
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
4583
|
+
addViolation({
|
|
4584
|
+
feature: feat.name,
|
|
4585
|
+
layer,
|
|
4586
|
+
file,
|
|
4587
|
+
relativePath: relPath,
|
|
4588
|
+
line: stmt.line,
|
|
4589
|
+
category: "Domain Purity Violation",
|
|
4590
|
+
code: stmt.rawCode,
|
|
4591
|
+
ruleId: "D3_SHARED_NON_DOMAIN_IMPORT",
|
|
4592
|
+
severity: "error",
|
|
4593
|
+
advice: "Domain layer can only import pure value objects from shared/<module>/domain/*. Ports, infrastructure, and root classes must not be imported.",
|
|
4594
|
+
fixExample: {
|
|
4595
|
+
bad: stmt.rawCode,
|
|
4596
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/domain/...";`
|
|
4597
|
+
}
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
if (target.isExternal && !target.isDiFramework) {
|
|
4601
|
+
addViolation({
|
|
4602
|
+
feature: feat.name,
|
|
4603
|
+
layer,
|
|
4604
|
+
file,
|
|
4605
|
+
relativePath: relPath,
|
|
4606
|
+
line: stmt.line,
|
|
4607
|
+
category: "Domain Purity Violation",
|
|
4608
|
+
code: stmt.rawCode,
|
|
4609
|
+
ruleId: "D4_EXTERNAL_LIBRARY",
|
|
4610
|
+
severity: "error",
|
|
4611
|
+
advice: `Domain layer must be pure and must not depend on external runtime libraries ('${stmt.specifier}').`,
|
|
4612
|
+
fixExample: {
|
|
4613
|
+
bad: stmt.rawCode,
|
|
4614
|
+
good: "// Model pure business interfaces without external libraries."
|
|
4615
|
+
}
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
} else if (layer === "useCases") {
|
|
4619
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4620
|
+
addViolation({
|
|
4621
|
+
feature: feat.name,
|
|
4622
|
+
layer,
|
|
4623
|
+
file,
|
|
4624
|
+
relativePath: relPath,
|
|
4625
|
+
line: stmt.line,
|
|
4626
|
+
category: "Use Case Intra-Feature Violation",
|
|
4627
|
+
code: stmt.rawCode,
|
|
4628
|
+
ruleId: "U1_INTRA_USE_CASE",
|
|
4629
|
+
severity: "error",
|
|
4630
|
+
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.`,
|
|
4631
|
+
fixExample: {
|
|
4632
|
+
bad: stmt.rawCode,
|
|
4633
|
+
good: "// Inject a domain interface or domain service instead of another use case."
|
|
4634
|
+
}
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
if (target.targetLayer === "useCases" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4638
|
+
addViolation({
|
|
4639
|
+
feature: feat.name,
|
|
4640
|
+
layer,
|
|
4641
|
+
file,
|
|
4642
|
+
relativePath: relPath,
|
|
4643
|
+
line: stmt.line,
|
|
4644
|
+
category: "Use Case Cross-Feature Use Case Violation",
|
|
4645
|
+
code: stmt.rawCode,
|
|
4646
|
+
ruleId: "U2_CROSS_FEATURE_USE_CASE",
|
|
4647
|
+
severity: "error",
|
|
4648
|
+
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/'.`,
|
|
4649
|
+
fixExample: {
|
|
4650
|
+
bad: stmt.rawCode,
|
|
4651
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/ using '${target.targetFileName}'.`
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
if (target.targetLayer === "infrastructure") {
|
|
4656
|
+
addViolation({
|
|
4657
|
+
feature: feat.name,
|
|
4658
|
+
layer,
|
|
4659
|
+
file,
|
|
4660
|
+
relativePath: relPath,
|
|
4661
|
+
line: stmt.line,
|
|
4662
|
+
category: "Use Case Layer Violation",
|
|
4663
|
+
code: stmt.rawCode,
|
|
4664
|
+
ruleId: "U3_DIRECT_INFRASTRUCTURE",
|
|
4665
|
+
severity: "error",
|
|
4666
|
+
advice: "Use cases cannot import concrete infrastructure directly. Depend on Domain Ports of the same feature via DI.",
|
|
4667
|
+
fixExample: {
|
|
4668
|
+
bad: stmt.rawCode,
|
|
4669
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4670
|
+
}
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4674
|
+
addViolation({
|
|
4675
|
+
feature: feat.name,
|
|
4676
|
+
layer,
|
|
4677
|
+
file,
|
|
4678
|
+
relativePath: relPath,
|
|
4679
|
+
line: stmt.line,
|
|
4680
|
+
category: "Use Case Cross-Feature Port Violation",
|
|
4681
|
+
code: stmt.rawCode,
|
|
4682
|
+
ruleId: "U4_CROSS_FEATURE_PORT",
|
|
4683
|
+
severity: "error",
|
|
4684
|
+
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/'.`,
|
|
4685
|
+
fixExample: {
|
|
4686
|
+
bad: stmt.rawCode,
|
|
4687
|
+
good: `// Define a local interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4688
|
+
}
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
if (target.targetLayer === "services" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4692
|
+
addViolation({
|
|
4693
|
+
feature: feat.name,
|
|
4694
|
+
layer,
|
|
4695
|
+
file,
|
|
4696
|
+
relativePath: relPath,
|
|
4697
|
+
line: stmt.line,
|
|
4698
|
+
category: "Use Case Layer Violation",
|
|
4699
|
+
code: stmt.rawCode,
|
|
4700
|
+
ruleId: "U5_CROSS_FEATURE_SERVICE",
|
|
4701
|
+
severity: "error",
|
|
4702
|
+
advice: `Use cases cannot depend on services of foreign feature '${target.targetFeature}'.`,
|
|
4703
|
+
fixExample: {
|
|
4704
|
+
bad: stmt.rawCode,
|
|
4705
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4710
|
+
addViolation({
|
|
4711
|
+
feature: feat.name,
|
|
4712
|
+
layer,
|
|
4713
|
+
file,
|
|
4714
|
+
relativePath: relPath,
|
|
4715
|
+
line: stmt.line,
|
|
4716
|
+
category: "Shared Module Root Class Violation",
|
|
4717
|
+
code: stmt.rawCode,
|
|
4718
|
+
ruleId: "U6_SHARED_NON_ROOT",
|
|
4719
|
+
severity: "error",
|
|
4720
|
+
advice: `Use cases can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.`,
|
|
4721
|
+
fixExample: {
|
|
4722
|
+
bad: stmt.rawCode,
|
|
4723
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4724
|
+
}
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
} else if (layer === "services") {
|
|
4728
|
+
if (target.targetLayer === "useCases") {
|
|
4729
|
+
addViolation({
|
|
4730
|
+
feature: feat.name,
|
|
4731
|
+
layer,
|
|
4732
|
+
file,
|
|
4733
|
+
relativePath: relPath,
|
|
4734
|
+
line: stmt.line,
|
|
4735
|
+
category: "Service Intra-Feature Violation",
|
|
4736
|
+
code: stmt.rawCode,
|
|
4737
|
+
ruleId: "S1_USE_CASE_DEPENDENCY",
|
|
4738
|
+
severity: "error",
|
|
4739
|
+
advice: "Services cannot depend on use cases of any feature.",
|
|
4740
|
+
fixExample: {
|
|
4741
|
+
bad: stmt.rawCode,
|
|
4742
|
+
good: "// Inject domain repository ports directly instead of use cases."
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4747
|
+
addViolation({
|
|
4748
|
+
feature: feat.name,
|
|
4749
|
+
layer,
|
|
4750
|
+
file,
|
|
4751
|
+
relativePath: relPath,
|
|
4752
|
+
line: stmt.line,
|
|
4753
|
+
category: "Service Cross-Feature Port Violation",
|
|
4754
|
+
code: stmt.rawCode,
|
|
4755
|
+
ruleId: "S2_CROSS_FEATURE_PORT",
|
|
4756
|
+
severity: "error",
|
|
4757
|
+
advice: `Services cannot depend on domain ports of foreign feature '${target.targetFeature}'.`,
|
|
4758
|
+
fixExample: {
|
|
4759
|
+
bad: stmt.rawCode,
|
|
4760
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
if (target.targetLayer === "infrastructure") {
|
|
4765
|
+
addViolation({
|
|
4766
|
+
feature: feat.name,
|
|
4767
|
+
layer,
|
|
4768
|
+
file,
|
|
4769
|
+
relativePath: relPath,
|
|
4770
|
+
line: stmt.line,
|
|
4771
|
+
category: "Service Layer Violation",
|
|
4772
|
+
code: stmt.rawCode,
|
|
4773
|
+
ruleId: "S3_DIRECT_INFRASTRUCTURE",
|
|
4774
|
+
severity: "error",
|
|
4775
|
+
advice: "Services cannot import concrete infrastructure directly. Depend on Domain Ports via DI.",
|
|
4776
|
+
fixExample: {
|
|
4777
|
+
bad: stmt.rawCode,
|
|
4778
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4779
|
+
}
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4783
|
+
addViolation({
|
|
4784
|
+
feature: feat.name,
|
|
4785
|
+
layer,
|
|
4786
|
+
file,
|
|
4787
|
+
relativePath: relPath,
|
|
4788
|
+
line: stmt.line,
|
|
4789
|
+
category: "Shared Module Root Class Violation",
|
|
4790
|
+
code: stmt.rawCode,
|
|
4791
|
+
ruleId: "S4_SHARED_NON_ROOT",
|
|
4792
|
+
severity: "error",
|
|
4793
|
+
advice: "Services can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4794
|
+
fixExample: {
|
|
4795
|
+
bad: stmt.rawCode,
|
|
4796
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4797
|
+
}
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
} else if (layer === "infrastructure") {
|
|
4801
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4802
|
+
addViolation({
|
|
4803
|
+
feature: feat.name,
|
|
4804
|
+
layer,
|
|
4805
|
+
file,
|
|
4806
|
+
relativePath: relPath,
|
|
4807
|
+
line: stmt.line,
|
|
4808
|
+
category: "Adapter Layer Violation",
|
|
4809
|
+
code: stmt.rawCode,
|
|
4810
|
+
ruleId: "I1_INTRA_USE_CASE",
|
|
4811
|
+
severity: "error",
|
|
4812
|
+
advice: `Infrastructure adapters cannot depend on use cases of the same feature ('${feat.name}'). Adapters implement domain ports.`,
|
|
4813
|
+
fixExample: {
|
|
4814
|
+
bad: stmt.rawCode,
|
|
4815
|
+
good: "// Adapters implement domain ports and should not invoke local application use cases."
|
|
4816
|
+
}
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4820
|
+
addViolation({
|
|
4821
|
+
feature: feat.name,
|
|
4822
|
+
layer,
|
|
4823
|
+
file,
|
|
4824
|
+
relativePath: relPath,
|
|
4825
|
+
line: stmt.line,
|
|
4826
|
+
category: "Shared Module Root Class Violation",
|
|
4827
|
+
code: stmt.rawCode,
|
|
4828
|
+
ruleId: "I2_SHARED_NON_ROOT",
|
|
4829
|
+
severity: "error",
|
|
4830
|
+
advice: "Infrastructure adapters can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4831
|
+
fixExample: {
|
|
4832
|
+
bad: stmt.rawCode,
|
|
4833
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4834
|
+
}
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
} else if (layer === "presentation") {
|
|
4838
|
+
if (target.targetLayer === "infrastructure") {
|
|
4839
|
+
addViolation({
|
|
4840
|
+
feature: feat.name,
|
|
4841
|
+
layer,
|
|
4842
|
+
file,
|
|
4843
|
+
relativePath: relPath,
|
|
4844
|
+
line: stmt.line,
|
|
4845
|
+
category: "Presentation Layer Violation",
|
|
4846
|
+
code: stmt.rawCode,
|
|
4847
|
+
ruleId: "P1_DIRECT_INFRASTRUCTURE",
|
|
4848
|
+
severity: "error",
|
|
4849
|
+
advice: "Presentation handlers must not import infrastructure directly. Inject Use Cases via DI.",
|
|
4850
|
+
fixExample: {
|
|
4851
|
+
bad: stmt.rawCode,
|
|
4852
|
+
good: "// Inject the corresponding Use Case via static deps and invoke its execute() method."
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
if (target.targetLayer === "domain" && (target.isTargetRepo || target.isTargetPort)) {
|
|
4857
|
+
addViolation({
|
|
4858
|
+
feature: feat.name,
|
|
4859
|
+
layer,
|
|
4860
|
+
file,
|
|
4861
|
+
relativePath: relPath,
|
|
4862
|
+
line: stmt.line,
|
|
4863
|
+
category: "Presentation Layer Violation",
|
|
4864
|
+
code: stmt.rawCode,
|
|
4865
|
+
ruleId: "P2_DIRECT_REPOSITORY",
|
|
4866
|
+
severity: "error",
|
|
4867
|
+
advice: "Presentation handlers must not depend on domain repositories or ports directly. Inject Use Cases via DI.",
|
|
4868
|
+
fixExample: {
|
|
4869
|
+
bad: stmt.rawCode,
|
|
4870
|
+
good: "// Presentation layer must only orchestrate Use Cases."
|
|
4871
|
+
}
|
|
4872
|
+
});
|
|
4873
|
+
}
|
|
4874
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4875
|
+
addViolation({
|
|
4876
|
+
feature: feat.name,
|
|
4877
|
+
layer,
|
|
4878
|
+
file,
|
|
4879
|
+
relativePath: relPath,
|
|
4880
|
+
line: stmt.line,
|
|
4881
|
+
category: "Shared Module Root Class Violation",
|
|
4882
|
+
code: stmt.rawCode,
|
|
4883
|
+
ruleId: "P3_SHARED_NON_ROOT",
|
|
4884
|
+
severity: "error",
|
|
4885
|
+
advice: "Presentation handlers can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4886
|
+
fixExample: {
|
|
4887
|
+
bad: stmt.rawCode,
|
|
4888
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4889
|
+
}
|
|
4890
|
+
});
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
const directCycles = graph.findDirectCycles();
|
|
4897
|
+
for (const c of directCycles) {
|
|
4898
|
+
const existing = detectedCycles.find(
|
|
4899
|
+
(dc) => dc.featureA === c.featureA && dc.featureB === c.featureB || dc.featureA === c.featureB && dc.featureB === c.featureA
|
|
4900
|
+
);
|
|
4901
|
+
if (!existing) {
|
|
4902
|
+
detectedCycles.push(c);
|
|
4903
|
+
addViolation({
|
|
4904
|
+
feature: c.featureA,
|
|
4905
|
+
file: c.evidenceAtoB.file,
|
|
4906
|
+
relativePath: c.evidenceAtoB.relativePath,
|
|
4907
|
+
line: c.evidenceAtoB.line,
|
|
4908
|
+
category: "Circular Feature Dependency Violation",
|
|
4909
|
+
code: c.evidenceAtoB.code,
|
|
4910
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4911
|
+
severity: "error",
|
|
4912
|
+
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.`
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
const durationMs = Date.now() - startTime;
|
|
4917
|
+
const cleanFeatures = results.filter((r) => r.passed).length;
|
|
4918
|
+
const violatedFeatures = results.filter((r) => !r.passed).length;
|
|
4919
|
+
const summary = {
|
|
4920
|
+
totalFeatures: results.length,
|
|
4921
|
+
cleanFeatures,
|
|
4922
|
+
violatedFeatures,
|
|
4923
|
+
totalViolations: allViolations.length,
|
|
4924
|
+
durationMs,
|
|
4925
|
+
results,
|
|
4926
|
+
allViolations,
|
|
4927
|
+
circularDependencies: detectedCycles
|
|
4928
|
+
};
|
|
4929
|
+
if (allViolations.length > 0 && options.throwOnError !== false) {
|
|
4930
|
+
const errorHeader = `Dependency rule audit failed with ${allViolations.length} violation(s) across ${violatedFeatures} feature(s):`;
|
|
4931
|
+
const compiledDetails = allViolations.map(
|
|
4932
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4933
|
+
Rule: ${v.ruleId}
|
|
4934
|
+
Code: ${v.code}
|
|
4935
|
+
Advice: ${v.advice}`
|
|
4936
|
+
).join("\n\n");
|
|
4937
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4938
|
+
|
|
4939
|
+
${compiledDetails}
|
|
4940
|
+
`, allViolations, false);
|
|
4941
|
+
}
|
|
4942
|
+
return summary;
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
// src/commands/lint.ts
|
|
4946
|
+
function printLintBanner(mode) {
|
|
4947
|
+
console.log("");
|
|
4948
|
+
const title = " Mason Clean Architecture Dependency Linter ";
|
|
4949
|
+
const line = "\u2501".repeat(title.length + 2);
|
|
4950
|
+
console.log(pc15__default.default.cyan(` \u250F${line}\u2513`));
|
|
4951
|
+
console.log(` ${pc15__default.default.cyan("\u2503")} ${pc15__default.default.bgCyan(pc15__default.default.black(title))} ${pc15__default.default.cyan("\u2503")}`);
|
|
4952
|
+
console.log(pc15__default.default.cyan(` \u2517${line}\u251B`));
|
|
4953
|
+
const modeText = mode === "failFast" ? `${pc15__default.default.yellow("Fail-Fast")} ${pc15__default.default.dim("(default: stops at first error)")}` : `${pc15__default.default.magenta("Complete")} ${pc15__default.default.dim("(--complete: compiles all errors)")}`;
|
|
4954
|
+
console.log(` Mode: ${modeText}`);
|
|
4955
|
+
console.log("");
|
|
4956
|
+
}
|
|
4957
|
+
function formatViolation(v, showFixHint = true) {
|
|
4958
|
+
const lines = [];
|
|
4959
|
+
const categoryBadge = pc15__default.default.red(pc15__default.default.bold(`[${v.category}]`));
|
|
4960
|
+
const location = pc15__default.default.white(`${v.relativePath}:${pc15__default.default.yellow(String(v.line))}`);
|
|
4961
|
+
lines.push(` ${categoryBadge} ${location}`);
|
|
4962
|
+
lines.push(` ${pc15__default.default.dim("Rule ID:")} ${pc15__default.default.yellow(v.ruleId)}`);
|
|
4963
|
+
lines.push(` ${pc15__default.default.dim("Code:")} ${pc15__default.default.magenta(v.code)}`);
|
|
4964
|
+
lines.push(` ${pc15__default.default.dim("Advice:")} ${pc15__default.default.cyan(v.advice)}`);
|
|
4965
|
+
if (showFixHint && v.fixExample) {
|
|
4966
|
+
lines.push(` ${pc15__default.default.dim("Example:")}`);
|
|
4967
|
+
lines.push(` ${pc15__default.default.red(`- ${v.fixExample.bad}`)}`);
|
|
4968
|
+
lines.push(` ${pc15__default.default.green(`+ ${v.fixExample.good}`)}`);
|
|
4969
|
+
}
|
|
4970
|
+
return lines.join("\n");
|
|
4971
|
+
}
|
|
4972
|
+
function formatFeatureResult(res, options = {}) {
|
|
4973
|
+
const lines = [];
|
|
4974
|
+
if (res.passed) {
|
|
4975
|
+
lines.push(
|
|
4976
|
+
` ${pc15__default.default.green("\u2714")} ${pc15__default.default.bold(res.featureName)} ${pc15__default.default.dim(
|
|
4977
|
+
`(${res.scannedFilesCount} files checked - clean)`
|
|
4978
|
+
)}`
|
|
4979
|
+
);
|
|
4980
|
+
return lines.join("\n");
|
|
4981
|
+
}
|
|
4982
|
+
const countBadge = pc15__default.default.red(
|
|
4983
|
+
`(${res.violations.length} violation${res.violations.length > 1 ? "s" : ""})`
|
|
4984
|
+
);
|
|
4985
|
+
lines.push(`
|
|
4986
|
+
${pc15__default.default.red("\u2716")} ${pc15__default.default.bold(pc15__default.default.white(res.featureName))} ${countBadge}`);
|
|
4987
|
+
lines.push(` ${pc15__default.default.dim("Path:")} ${res.featureDir}`);
|
|
4988
|
+
lines.push("");
|
|
4989
|
+
for (const v of res.violations) {
|
|
4990
|
+
lines.push(formatViolation(v, options.fixHint !== false));
|
|
4991
|
+
lines.push("");
|
|
4992
|
+
}
|
|
4993
|
+
return lines.join("\n");
|
|
4994
|
+
}
|
|
4995
|
+
function printTerminalReport(summary, options = {}) {
|
|
4996
|
+
for (const res of summary.results) {
|
|
4997
|
+
const formatted = formatFeatureResult(res, options);
|
|
4998
|
+
if (formatted.trim()) {
|
|
4999
|
+
console.log(formatted);
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
console.log("\u2501".repeat(70));
|
|
5003
|
+
const statusIcon = summary.totalViolations === 0 ? pc15__default.default.green("\u2714 PASS") : pc15__default.default.red("\u2716 FAIL");
|
|
5004
|
+
console.log(
|
|
5005
|
+
` ${statusIcon} ${pc15__default.default.bold("Dependency Rules Audit Result")} ${pc15__default.default.dim(
|
|
5006
|
+
`(${summary.durationMs}ms)`
|
|
5007
|
+
)}`
|
|
5008
|
+
);
|
|
5009
|
+
console.log(
|
|
5010
|
+
` ${pc15__default.default.bold("Features:")} ${pc15__default.default.cyan(String(summary.totalFeatures))} total | ${pc15__default.default.green(
|
|
5011
|
+
String(summary.cleanFeatures)
|
|
5012
|
+
)} clean | ${summary.violatedFeatures > 0 ? pc15__default.default.red(String(summary.violatedFeatures)) : pc15__default.default.green("0")} with violations`
|
|
5013
|
+
);
|
|
5014
|
+
console.log(
|
|
5015
|
+
` ${pc15__default.default.bold("Violations:")} ${summary.totalViolations > 0 ? pc15__default.default.red(pc15__default.default.bold(String(summary.totalViolations))) : pc15__default.default.green("0")} rule violations found`
|
|
5016
|
+
);
|
|
5017
|
+
console.log("\u2501".repeat(70));
|
|
5018
|
+
console.log("");
|
|
5019
|
+
}
|
|
5020
|
+
async function handleLint(flags = {}) {
|
|
5021
|
+
const complete = Boolean(flags.complete);
|
|
5022
|
+
const mode = complete ? "complete" : "failFast";
|
|
5023
|
+
const quiet = Boolean(flags.quiet);
|
|
5024
|
+
if (!quiet) {
|
|
5025
|
+
printLintBanner(mode);
|
|
5026
|
+
}
|
|
5027
|
+
const options = {
|
|
5028
|
+
complete,
|
|
5029
|
+
quiet,
|
|
5030
|
+
targetDir: flags.targetDir || flags.target,
|
|
5031
|
+
projectRoot: flags.projectRoot,
|
|
5032
|
+
fixHint: flags.fixHint !== false,
|
|
5033
|
+
throwOnError: false
|
|
5034
|
+
};
|
|
5035
|
+
try {
|
|
5036
|
+
const summary = lintCodebase(options);
|
|
5037
|
+
if (!quiet) {
|
|
5038
|
+
printTerminalReport(summary, options);
|
|
5039
|
+
}
|
|
5040
|
+
if (summary.totalViolations > 0) {
|
|
5041
|
+
const errorHeader = `Dependency rule audit failed with ${summary.totalViolations} violation(s) across ${summary.violatedFeatures} feature(s):`;
|
|
5042
|
+
const compiledDetails = summary.allViolations.map(
|
|
5043
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
5044
|
+
Rule: ${v.ruleId}
|
|
5045
|
+
Code: ${v.code}
|
|
5046
|
+
Advice: ${v.advice}`
|
|
5047
|
+
).join("\n\n");
|
|
5048
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
5049
|
+
|
|
5050
|
+
${compiledDetails}
|
|
5051
|
+
`, summary.allViolations, false);
|
|
5052
|
+
}
|
|
5053
|
+
if (!quiet) {
|
|
5054
|
+
console.log(
|
|
5055
|
+
pc15__default.default.green(`\u2714 All dependency rules are being followed! (${summary.totalFeatures} features checked)`)
|
|
5056
|
+
);
|
|
5057
|
+
console.log("");
|
|
5058
|
+
}
|
|
5059
|
+
return summary;
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
if (err instanceof DependencyRuleError && err.isFailFast) {
|
|
5062
|
+
if (!quiet) {
|
|
5063
|
+
console.log(pc15__default.default.red(`
|
|
5064
|
+
\u2716 [FAIL FAST] Terminating on first detected dependency rule violation:`));
|
|
5065
|
+
const v = err.violations[0];
|
|
5066
|
+
if (v) {
|
|
5067
|
+
console.log(formatViolation(v, options.fixHint !== false));
|
|
5068
|
+
}
|
|
5069
|
+
console.log("");
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
throw err;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
4192
5075
|
|
|
4193
5076
|
// src/cli.ts
|
|
4194
5077
|
function createCli() {
|
|
@@ -4196,7 +5079,7 @@ function createCli() {
|
|
|
4196
5079
|
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) => {
|
|
4197
5080
|
if (args && args.length > 0) {
|
|
4198
5081
|
p14__namespace.log.error(
|
|
4199
|
-
|
|
5082
|
+
pc15__default.default.red(
|
|
4200
5083
|
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
4201
5084
|
)
|
|
4202
5085
|
);
|
|
@@ -4262,14 +5145,21 @@ function createCli() {
|
|
|
4262
5145
|
value: "import",
|
|
4263
5146
|
label: "Import from registry",
|
|
4264
5147
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
5148
|
+
},
|
|
5149
|
+
{
|
|
5150
|
+
value: "lint",
|
|
5151
|
+
label: "Lint dependency rules",
|
|
5152
|
+
hint: "Scan codebase and verify Clean Architecture dependency rules"
|
|
4265
5153
|
}
|
|
4266
5154
|
]
|
|
4267
5155
|
})
|
|
4268
5156
|
);
|
|
4269
5157
|
if (choice === "create") {
|
|
4270
5158
|
await handleCreate(void 0, void 0, flags);
|
|
4271
|
-
} else {
|
|
5159
|
+
} else if (choice === "import") {
|
|
4272
5160
|
await handleImport(void 0, void 0, flags);
|
|
5161
|
+
} else if (choice === "lint") {
|
|
5162
|
+
await handleLint(flags);
|
|
4273
5163
|
}
|
|
4274
5164
|
} catch (err) {
|
|
4275
5165
|
handleCliError(err);
|
|
@@ -4295,6 +5185,22 @@ function createCli() {
|
|
|
4295
5185
|
handleCliError(err);
|
|
4296
5186
|
}
|
|
4297
5187
|
});
|
|
5188
|
+
cli.command(
|
|
5189
|
+
"lint [target]",
|
|
5190
|
+
"Scan codebase and verify Clean Architecture dependency rules"
|
|
5191
|
+
).option(
|
|
5192
|
+
"-c, --complete",
|
|
5193
|
+
"Scan entire codebase without failing fast, compiling all errors at the end"
|
|
5194
|
+
).option("-q, --quiet", "Suppress non-error output").example("mason lint").example("mason lint --complete").example("mason lint ./src").action(async (target, flags = {}) => {
|
|
5195
|
+
try {
|
|
5196
|
+
await handleLint({
|
|
5197
|
+
...flags,
|
|
5198
|
+
targetDir: target || flags.target || flags.dest
|
|
5199
|
+
});
|
|
5200
|
+
} catch (err) {
|
|
5201
|
+
handleCliError(err);
|
|
5202
|
+
}
|
|
5203
|
+
});
|
|
4298
5204
|
const createSubtypes = [
|
|
4299
5205
|
"feature",
|
|
4300
5206
|
"shared",
|
|
@@ -4326,7 +5232,10 @@ function handleCliError(err) {
|
|
|
4326
5232
|
process.exit(0);
|
|
4327
5233
|
}
|
|
4328
5234
|
const message = err instanceof Error ? err.message : String(err);
|
|
4329
|
-
p14__namespace.log.error(
|
|
5235
|
+
p14__namespace.log.error(pc15__default.default.red(`Error: ${message}`));
|
|
5236
|
+
if (process.env.VITEST) {
|
|
5237
|
+
throw err;
|
|
5238
|
+
}
|
|
4330
5239
|
process.exit(1);
|
|
4331
5240
|
}
|
|
4332
5241
|
async function runCli(argv = process.argv) {
|