@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/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var fs4 = require('fs');
|
|
4
4
|
var path2 = require('path');
|
|
5
5
|
var p14 = require('@clack/prompts');
|
|
6
|
-
var
|
|
6
|
+
var pc15 = require('picocolors');
|
|
7
7
|
var url = require('url');
|
|
8
8
|
var child_process = require('child_process');
|
|
9
9
|
var crypto = require('crypto');
|
|
@@ -34,7 +34,7 @@ function _interopNamespace(e) {
|
|
|
34
34
|
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
|
|
35
35
|
var path2__default = /*#__PURE__*/_interopDefault(path2);
|
|
36
36
|
var p14__namespace = /*#__PURE__*/_interopNamespace(p14);
|
|
37
|
-
var
|
|
37
|
+
var pc15__default = /*#__PURE__*/_interopDefault(pc15);
|
|
38
38
|
var Handlebars__default = /*#__PURE__*/_interopDefault(Handlebars);
|
|
39
39
|
|
|
40
40
|
// src/utils/formatting.ts
|
|
@@ -141,17 +141,17 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
141
141
|
function printGeneratedFiles(files, isDryRun = false) {
|
|
142
142
|
console.log("");
|
|
143
143
|
if (isDryRun) {
|
|
144
|
-
p14__namespace.log.warn(
|
|
144
|
+
p14__namespace.log.warn(pc15__default.default.yellow(pc15__default.default.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
145
145
|
}
|
|
146
146
|
for (const f of files) {
|
|
147
147
|
if (f.action === "created") {
|
|
148
|
-
p14__namespace.log.success(`${
|
|
148
|
+
p14__namespace.log.success(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(created)")}`);
|
|
149
149
|
} else if (f.action === "overwritten") {
|
|
150
|
-
p14__namespace.log.warn(`${
|
|
150
|
+
p14__namespace.log.warn(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(overwritten)")}`);
|
|
151
151
|
} else if (f.action === "skipped") {
|
|
152
|
-
p14__namespace.log.info(`${
|
|
152
|
+
p14__namespace.log.info(`${pc15__default.default.dim(f.relativePath)} ${pc15__default.default.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
153
153
|
} else {
|
|
154
|
-
p14__namespace.log.info(`${
|
|
154
|
+
p14__namespace.log.info(`${pc15__default.default.bold(f.relativePath)} ${pc15__default.default.dim("(preview)")}`);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -361,13 +361,13 @@ function handleCancel(result) {
|
|
|
361
361
|
}
|
|
362
362
|
function printBanner() {
|
|
363
363
|
console.log("");
|
|
364
|
-
p14__namespace.intro(
|
|
364
|
+
p14__namespace.intro(pc15__default.default.bgCyan(pc15__default.default.black(" @solid-stack/mason ")));
|
|
365
365
|
}
|
|
366
366
|
async function promptProperties(options = {}) {
|
|
367
367
|
const properties = options.defaults ? [...options.defaults] : [];
|
|
368
368
|
if (properties.length > 0) {
|
|
369
369
|
const summary = properties.map((p16) => `${p16.name}: ${p16.type}${p16.optional ? "?" : ""}`).join(", ");
|
|
370
|
-
p14__namespace.log.info(
|
|
370
|
+
p14__namespace.log.info(pc15__default.default.dim(`Pre-configured fields: ${summary}`));
|
|
371
371
|
const addMore = handleCancel(
|
|
372
372
|
await p14__namespace.confirm({
|
|
373
373
|
message: "Would you like to add custom fields/properties?",
|
|
@@ -378,7 +378,7 @@ async function promptProperties(options = {}) {
|
|
|
378
378
|
return properties;
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
|
-
p14__namespace.log.info(
|
|
381
|
+
p14__namespace.log.info(pc15__default.default.cyan("Enter properties (leave name blank when finished):"));
|
|
382
382
|
while (true) {
|
|
383
383
|
const name = handleCancel(
|
|
384
384
|
await p14__namespace.text({
|
|
@@ -426,7 +426,7 @@ async function promptProperties(options = {}) {
|
|
|
426
426
|
optional
|
|
427
427
|
});
|
|
428
428
|
p14__namespace.log.success(
|
|
429
|
-
|
|
429
|
+
pc15__default.default.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
430
430
|
);
|
|
431
431
|
}
|
|
432
432
|
return properties;
|
|
@@ -660,7 +660,7 @@ function installNpmPackages(projectRoot, packages, pm) {
|
|
|
660
660
|
installCmd = `npm install ${packages.join(" ")}`;
|
|
661
661
|
break;
|
|
662
662
|
}
|
|
663
|
-
p14__namespace.log.info(`Installing missing dependencies with ${manager}: ${
|
|
663
|
+
p14__namespace.log.info(`Installing missing dependencies with ${manager}: ${pc15__default.default.cyan(packages.join(", "))}`);
|
|
664
664
|
child_process.execSync(installCmd, {
|
|
665
665
|
cwd: projectRoot,
|
|
666
666
|
stdio: "inherit"
|
|
@@ -2377,7 +2377,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2377
2377
|
}
|
|
2378
2378
|
}
|
|
2379
2379
|
const s = p14__namespace.spinner();
|
|
2380
|
-
s.start(`Generating feature '${
|
|
2380
|
+
s.start(`Generating feature '${pc15__default.default.cyan(normalizedName)}'...`);
|
|
2381
2381
|
const options = {
|
|
2382
2382
|
name: normalizedName,
|
|
2383
2383
|
projectRoot,
|
|
@@ -2424,7 +2424,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2424
2424
|
s.stop("Feature scaffolded successfully!");
|
|
2425
2425
|
printGeneratedFiles(files, options.dryRun);
|
|
2426
2426
|
p14__namespace.outro(
|
|
2427
|
-
|
|
2427
|
+
pc15__default.default.green(
|
|
2428
2428
|
`\u2713 Feature '${normalizedName}' initialized in ${path2__default.default.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2429
2429
|
)
|
|
2430
2430
|
);
|
|
@@ -2580,7 +2580,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2580
2580
|
);
|
|
2581
2581
|
}
|
|
2582
2582
|
const s = p14__namespace.spinner();
|
|
2583
|
-
s.start(`Generating shared module '${
|
|
2583
|
+
s.start(`Generating shared module '${pc15__default.default.cyan(normalizedName)}'...`);
|
|
2584
2584
|
const files = generateShared({
|
|
2585
2585
|
name: normalizedName,
|
|
2586
2586
|
description,
|
|
@@ -2594,7 +2594,7 @@ async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
|
2594
2594
|
s.stop("Shared module generated successfully!");
|
|
2595
2595
|
printGeneratedFiles(files, Boolean(flags.dryRun));
|
|
2596
2596
|
p14__namespace.outro(
|
|
2597
|
-
|
|
2597
|
+
pc15__default.default.green(
|
|
2598
2598
|
`\u2713 Shared module '${normalizedName}' initialized in ${path2__default.default.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2599
2599
|
)
|
|
2600
2600
|
);
|
|
@@ -2722,7 +2722,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2722
2722
|
}
|
|
2723
2723
|
}
|
|
2724
2724
|
const s = p14__namespace.spinner();
|
|
2725
|
-
s.start(`Generating use case '${
|
|
2725
|
+
s.start(`Generating use case '${pc15__default.default.cyan(ucName)}'...`);
|
|
2726
2726
|
const options = {
|
|
2727
2727
|
name: ucName,
|
|
2728
2728
|
feature,
|
|
@@ -2754,7 +2754,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2754
2754
|
}
|
|
2755
2755
|
s.stop("Use case generated successfully!");
|
|
2756
2756
|
printGeneratedFiles(files, options.dryRun);
|
|
2757
|
-
p14__namespace.outro(
|
|
2757
|
+
p14__namespace.outro(pc15__default.default.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2758
2758
|
}
|
|
2759
2759
|
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2760
2760
|
const projectRoot = findProjectRoot();
|
|
@@ -2825,7 +2825,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2825
2825
|
});
|
|
2826
2826
|
}
|
|
2827
2827
|
const s = p14__namespace.spinner();
|
|
2828
|
-
s.start(`Generating domain model '${
|
|
2828
|
+
s.start(`Generating domain model '${pc15__default.default.cyan(entityName)}'...`);
|
|
2829
2829
|
const options = {
|
|
2830
2830
|
name: entityName,
|
|
2831
2831
|
feature,
|
|
@@ -2838,7 +2838,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2838
2838
|
const file = generateDomain(options);
|
|
2839
2839
|
s.stop("Domain model generated successfully!");
|
|
2840
2840
|
printGeneratedFiles([file], options.dryRun);
|
|
2841
|
-
p14__namespace.outro(
|
|
2841
|
+
p14__namespace.outro(pc15__default.default.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2842
2842
|
}
|
|
2843
2843
|
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2844
2844
|
const projectRoot = findProjectRoot();
|
|
@@ -3007,7 +3007,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
3007
3007
|
registerInProvider = !flags.noProvider;
|
|
3008
3008
|
}
|
|
3009
3009
|
const s = p14__namespace.spinner();
|
|
3010
|
-
s.start(`Generating infrastructure class '${
|
|
3010
|
+
s.start(`Generating infrastructure class '${pc15__default.default.cyan(className)}'...`);
|
|
3011
3011
|
const files = [];
|
|
3012
3012
|
const options = {
|
|
3013
3013
|
name: className,
|
|
@@ -3042,7 +3042,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
3042
3042
|
}
|
|
3043
3043
|
s.stop("Infrastructure class generated successfully!");
|
|
3044
3044
|
printGeneratedFiles(files, options.dryRun);
|
|
3045
|
-
p14__namespace.outro(
|
|
3045
|
+
p14__namespace.outro(pc15__default.default.green(`Infrastructure class '${className}' created!`));
|
|
3046
3046
|
}
|
|
3047
3047
|
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
3048
3048
|
const projectRoot = findProjectRoot();
|
|
@@ -3155,7 +3155,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
3155
3155
|
}
|
|
3156
3156
|
}
|
|
3157
3157
|
const s = p14__namespace.spinner();
|
|
3158
|
-
s.start(`Generating interface '${
|
|
3158
|
+
s.start(`Generating interface '${pc15__default.default.cyan(ifaceName)}'...`);
|
|
3159
3159
|
const files = [];
|
|
3160
3160
|
const options = {
|
|
3161
3161
|
name: ifaceName,
|
|
@@ -3204,7 +3204,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
3204
3204
|
}
|
|
3205
3205
|
s.stop("Interface generated successfully!");
|
|
3206
3206
|
printGeneratedFiles(files, options.dryRun);
|
|
3207
|
-
p14__namespace.outro(
|
|
3207
|
+
p14__namespace.outro(pc15__default.default.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
3208
3208
|
}
|
|
3209
3209
|
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
3210
3210
|
const projectRoot = findProjectRoot();
|
|
@@ -3321,7 +3321,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3321
3321
|
}
|
|
3322
3322
|
}
|
|
3323
3323
|
const s = p14__namespace.spinner();
|
|
3324
|
-
s.start(`Generating HTTP handler '${
|
|
3324
|
+
s.start(`Generating HTTP handler '${pc15__default.default.cyan(className)}'...`);
|
|
3325
3325
|
const options = {
|
|
3326
3326
|
name: className,
|
|
3327
3327
|
feature,
|
|
@@ -3339,7 +3339,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
3339
3339
|
const files = generateHttpHandler(options);
|
|
3340
3340
|
s.stop("HTTP handler generated successfully!");
|
|
3341
3341
|
printGeneratedFiles(files, options.dryRun);
|
|
3342
|
-
p14__namespace.outro(
|
|
3342
|
+
p14__namespace.outro(pc15__default.default.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
3343
3343
|
}
|
|
3344
3344
|
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
3345
3345
|
const formattedDeps = dependencies.map((dep) => ({
|
|
@@ -3495,7 +3495,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3495
3495
|
);
|
|
3496
3496
|
}
|
|
3497
3497
|
const s = p14__namespace.spinner();
|
|
3498
|
-
s.start(`Generating injectable '${
|
|
3498
|
+
s.start(`Generating injectable '${pc15__default.default.cyan(className)}'...`);
|
|
3499
3499
|
const options = {
|
|
3500
3500
|
name: className,
|
|
3501
3501
|
feature,
|
|
@@ -3509,7 +3509,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3509
3509
|
const files = generateInjectable(options);
|
|
3510
3510
|
s.stop("Injectable class generated successfully!");
|
|
3511
3511
|
printGeneratedFiles(files, options.dryRun);
|
|
3512
|
-
p14__namespace.outro(
|
|
3512
|
+
p14__namespace.outro(pc15__default.default.green(`Injectable '${className}' created successfully!`));
|
|
3513
3513
|
}
|
|
3514
3514
|
async function runInteractiveMenu(flags = {}) {
|
|
3515
3515
|
printBanner();
|
|
@@ -3623,7 +3623,7 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3623
3623
|
break;
|
|
3624
3624
|
default:
|
|
3625
3625
|
p14__namespace.log.error(
|
|
3626
|
-
|
|
3626
|
+
pc15__default.default.red(
|
|
3627
3627
|
`Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3628
3628
|
)
|
|
3629
3629
|
);
|
|
@@ -3743,19 +3743,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3743
3743
|
);
|
|
3744
3744
|
}
|
|
3745
3745
|
if (type !== "shared" && type !== "feature") {
|
|
3746
|
-
p14__namespace.log.error(
|
|
3746
|
+
p14__namespace.log.error(pc15__default.default.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3747
3747
|
process.exit(1);
|
|
3748
3748
|
}
|
|
3749
3749
|
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3750
3750
|
const s = p14__namespace.spinner();
|
|
3751
|
-
s.start(`Connecting to registry at ${
|
|
3751
|
+
s.start(`Connecting to registry at ${pc15__default.default.dim(registryBase)}...`);
|
|
3752
3752
|
let index;
|
|
3753
3753
|
try {
|
|
3754
3754
|
index = await fetchRegistryIndex(registryBase);
|
|
3755
3755
|
s.stop("Registry index loaded.");
|
|
3756
3756
|
} catch (err) {
|
|
3757
|
-
s.stop(
|
|
3758
|
-
p14__namespace.log.error(
|
|
3757
|
+
s.stop(pc15__default.default.red("Failed to load registry index."));
|
|
3758
|
+
p14__namespace.log.error(pc15__default.default.red(err.message || String(err)));
|
|
3759
3759
|
process.exit(1);
|
|
3760
3760
|
}
|
|
3761
3761
|
const availableList = type === "shared" ? index.shared : index.features;
|
|
@@ -3779,9 +3779,9 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3779
3779
|
}
|
|
3780
3780
|
if (!candidateNames.includes(name)) {
|
|
3781
3781
|
const closest = findClosestMatch(name, candidateNames);
|
|
3782
|
-
p14__namespace.log.error(
|
|
3782
|
+
p14__namespace.log.error(pc15__default.default.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3783
3783
|
if (closest) {
|
|
3784
|
-
p14__namespace.log.info(
|
|
3784
|
+
p14__namespace.log.info(pc15__default.default.yellow(`Did you mean '${pc15__default.default.cyan(closest)}'?`));
|
|
3785
3785
|
}
|
|
3786
3786
|
process.exit(1);
|
|
3787
3787
|
}
|
|
@@ -3793,7 +3793,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3793
3793
|
try {
|
|
3794
3794
|
manifest = await fetchModuleManifest(registryBase, modType, modName);
|
|
3795
3795
|
} catch (err) {
|
|
3796
|
-
p14__namespace.log.error(
|
|
3796
|
+
p14__namespace.log.error(pc15__default.default.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
|
|
3797
3797
|
process.exit(1);
|
|
3798
3798
|
}
|
|
3799
3799
|
nodes.set(key, {
|
|
@@ -3833,7 +3833,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3833
3833
|
buildTopologicalOrder(rootKey);
|
|
3834
3834
|
const resolutions = /* @__PURE__ */ new Map();
|
|
3835
3835
|
const globalAliasMap = /* @__PURE__ */ new Map();
|
|
3836
|
-
p14__namespace.log.step(`Importing ${type}: ${
|
|
3836
|
+
p14__namespace.log.step(`Importing ${type}: ${pc15__default.default.cyan(name)}...`);
|
|
3837
3837
|
for (const key of orderedKeys) {
|
|
3838
3838
|
const node = nodes.get(key);
|
|
3839
3839
|
const { type: modType, name: registryName, manifest } = node;
|
|
@@ -3866,7 +3866,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3866
3866
|
if (programmaticPointTo) {
|
|
3867
3867
|
if (!checkModuleExists(projectRoot, modType, programmaticPointTo)) {
|
|
3868
3868
|
p14__namespace.log.error(
|
|
3869
|
-
|
|
3869
|
+
pc15__default.default.red(
|
|
3870
3870
|
`Module '${programmaticPointTo}' specified in --point-to does not exist in ${folder}.`
|
|
3871
3871
|
)
|
|
3872
3872
|
);
|
|
@@ -3902,7 +3902,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3902
3902
|
}
|
|
3903
3903
|
const existingDir = getExistingModuleDir(projectRoot, modType, registryName);
|
|
3904
3904
|
if (!existingDir) {
|
|
3905
|
-
p14__namespace.log.info(` \u21B3 Resolving required ${modType} dependency: ${
|
|
3905
|
+
p14__namespace.log.info(` \u21B3 Resolving required ${modType} dependency: ${pc15__default.default.cyan(registryName)}`);
|
|
3906
3906
|
resolutions.set(key, {
|
|
3907
3907
|
type: modType,
|
|
3908
3908
|
registryName,
|
|
@@ -4133,7 +4133,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4133
4133
|
const isOverwrite = Boolean(flags.overwrite || res.action === "overwrite");
|
|
4134
4134
|
if (key !== rootKey) {
|
|
4135
4135
|
p14__namespace.log.step(
|
|
4136
|
-
`Importing ${res.type}: ${
|
|
4136
|
+
`Importing ${res.type}: ${pc15__default.default.cyan(res.targetName)}${res.targetName !== res.registryName ? pc15__default.default.dim(` (from ${res.registryName})`) : ""}...`
|
|
4137
4137
|
);
|
|
4138
4138
|
}
|
|
4139
4139
|
if (res.action === "overwrite" && !flags.dryRun) {
|
|
@@ -4152,7 +4152,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4152
4152
|
const localFilePath = path2__default.default.join(targetDir, file);
|
|
4153
4153
|
if (fs4__default.default.existsSync(localFilePath) && !isOverwrite) {
|
|
4154
4154
|
p14__namespace.log.warn(
|
|
4155
|
-
` \u21B3 Skipping existing file: ${
|
|
4155
|
+
` \u21B3 Skipping existing file: ${pc15__default.default.dim(path2__default.default.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
|
|
4156
4156
|
);
|
|
4157
4157
|
continue;
|
|
4158
4158
|
}
|
|
@@ -4164,13 +4164,13 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4164
4164
|
}
|
|
4165
4165
|
const displayPath = path2__default.default.relative(projectRoot, localFilePath);
|
|
4166
4166
|
totalWrittenFiles.push(displayPath);
|
|
4167
|
-
p14__namespace.log.message(` ${
|
|
4167
|
+
p14__namespace.log.message(` ${pc15__default.default.green("+")} ${displayPath}`);
|
|
4168
4168
|
}
|
|
4169
4169
|
}
|
|
4170
4170
|
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
4171
4171
|
if (missingNpm.length > 0) {
|
|
4172
4172
|
if (flags.skipInstall || flags.dryRun) {
|
|
4173
|
-
p14__namespace.log.info(`Required npm dependencies to install: ${
|
|
4173
|
+
p14__namespace.log.info(`Required npm dependencies to install: ${pc15__default.default.cyan(missingNpm.join(", "))}`);
|
|
4174
4174
|
} else {
|
|
4175
4175
|
try {
|
|
4176
4176
|
installNpmPackages(projectRoot, missingNpm);
|
|
@@ -4184,18 +4184,901 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
4184
4184
|
}
|
|
4185
4185
|
}
|
|
4186
4186
|
if (flags.dryRun) {
|
|
4187
|
-
p14__namespace.log.info(
|
|
4187
|
+
p14__namespace.log.info(pc15__default.default.yellow("Dry run complete: No files were written to disk."));
|
|
4188
4188
|
} else {
|
|
4189
4189
|
const rootRes = resolutions.get(rootKey);
|
|
4190
4190
|
const baseDir = getModuleBaseDir(projectRoot, type);
|
|
4191
4191
|
const displayTarget = path2__default.default.relative(projectRoot, path2__default.default.join(baseDir, rootRes.targetName));
|
|
4192
4192
|
p14__namespace.log.success(
|
|
4193
|
-
|
|
4193
|
+
pc15__default.default.green(
|
|
4194
4194
|
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
|
|
4195
4195
|
)
|
|
4196
4196
|
);
|
|
4197
4197
|
}
|
|
4198
4198
|
}
|
|
4199
|
+
var DependencyRuleError = class extends Error {
|
|
4200
|
+
violations;
|
|
4201
|
+
isFailFast;
|
|
4202
|
+
constructor(message, violations, isFailFast = false) {
|
|
4203
|
+
super(message);
|
|
4204
|
+
this.name = "DependencyRuleError";
|
|
4205
|
+
this.violations = violations;
|
|
4206
|
+
this.isFailFast = isFailFast;
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
function isTestFile2(filePath, projectRoot) {
|
|
4210
|
+
const norm = (projectRoot ? path2__default.default.relative(projectRoot, filePath) : filePath).replace(/\\/g, "/");
|
|
4211
|
+
const base = path2__default.default.basename(norm);
|
|
4212
|
+
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);
|
|
4213
|
+
}
|
|
4214
|
+
function scanDirFiles(dir, projectRoot) {
|
|
4215
|
+
if (!fs4__default.default.existsSync(dir)) return [];
|
|
4216
|
+
const entries = fs4__default.default.readdirSync(dir, { withFileTypes: true });
|
|
4217
|
+
const files = [];
|
|
4218
|
+
for (const entry of entries) {
|
|
4219
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
const fullPath = path2__default.default.join(dir, entry.name);
|
|
4223
|
+
if (entry.isDirectory()) {
|
|
4224
|
+
files.push(...scanDirFiles(fullPath, projectRoot));
|
|
4225
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) && !isTestFile2(fullPath, projectRoot)) {
|
|
4226
|
+
files.push(fullPath);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
return files;
|
|
4230
|
+
}
|
|
4231
|
+
function classifyFile(fullPath, projectRoot) {
|
|
4232
|
+
const rel = path2__default.default.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
4233
|
+
const featureMatch = rel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
4234
|
+
if (!featureMatch || !featureMatch[1] || !featureMatch[2]) return {};
|
|
4235
|
+
const feature = featureMatch[1];
|
|
4236
|
+
const subfolder = featureMatch[2];
|
|
4237
|
+
let layer;
|
|
4238
|
+
if (subfolder === "domain" || subfolder === "errors") {
|
|
4239
|
+
layer = "domain";
|
|
4240
|
+
} else if (subfolder === "useCases" || subfolder === "usecases") {
|
|
4241
|
+
layer = "useCases";
|
|
4242
|
+
} else if (subfolder === "services") {
|
|
4243
|
+
layer = "services";
|
|
4244
|
+
} else if (subfolder === "infrastructure") {
|
|
4245
|
+
layer = "infrastructure";
|
|
4246
|
+
} else if (subfolder === "pxExpress" || subfolder === "presentation" || subfolder === "handlers" || subfolder.startsWith("px")) {
|
|
4247
|
+
layer = "presentation";
|
|
4248
|
+
}
|
|
4249
|
+
return { feature, layer };
|
|
4250
|
+
}
|
|
4251
|
+
function classifyTarget(specifier, sourceFileAbs, projectRoot) {
|
|
4252
|
+
if (specifier === "@solid-stack/di" || specifier.startsWith("@solid-stack/di/")) {
|
|
4253
|
+
return { isExternal: true, isDiFramework: true };
|
|
4254
|
+
}
|
|
4255
|
+
if (specifier.startsWith("node:") || ["crypto", "fs", "path", "http", "https", "url", "os", "child_process", "stream", "express", "cors", "dotenv"].includes(specifier)) {
|
|
4256
|
+
return { isExternal: true, isDiFramework: false };
|
|
4257
|
+
}
|
|
4258
|
+
let resolvedRel = null;
|
|
4259
|
+
if (specifier.startsWith(".")) {
|
|
4260
|
+
const abs = path2__default.default.resolve(path2__default.default.dirname(sourceFileAbs), specifier);
|
|
4261
|
+
resolvedRel = path2__default.default.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
4262
|
+
} else if (specifier.startsWith("@/features/") || specifier.startsWith("~/features/")) {
|
|
4263
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4264
|
+
} else if (specifier.startsWith("@features/")) {
|
|
4265
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4266
|
+
} else if (specifier.startsWith("@/shared/") || specifier.startsWith("~/shared/")) {
|
|
4267
|
+
resolvedRel = specifier.replace(/^[@~]\//, "src/");
|
|
4268
|
+
} else if (specifier.startsWith("@shared/")) {
|
|
4269
|
+
resolvedRel = "src/" + specifier.slice(1);
|
|
4270
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
4271
|
+
resolvedRel = "src/" + specifier.slice(2);
|
|
4272
|
+
} else if (specifier.startsWith("features/") || specifier.startsWith("shared/")) {
|
|
4273
|
+
resolvedRel = specifier;
|
|
4274
|
+
}
|
|
4275
|
+
if (!resolvedRel) {
|
|
4276
|
+
return { isExternal: true, isDiFramework: false };
|
|
4277
|
+
}
|
|
4278
|
+
const featMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?features\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
|
|
4279
|
+
if (featMatch && featMatch[1] && featMatch[2]) {
|
|
4280
|
+
const targetFeature = featMatch[1];
|
|
4281
|
+
const targetSubfolder = featMatch[2];
|
|
4282
|
+
const rest = featMatch[3] || "";
|
|
4283
|
+
let targetLayer;
|
|
4284
|
+
if (targetSubfolder === "domain" || targetSubfolder === "errors") {
|
|
4285
|
+
targetLayer = "domain";
|
|
4286
|
+
} else if (targetSubfolder === "useCases" || targetSubfolder === "usecases") {
|
|
4287
|
+
targetLayer = "useCases";
|
|
4288
|
+
} else if (targetSubfolder === "services") {
|
|
4289
|
+
targetLayer = "services";
|
|
4290
|
+
} else if (targetSubfolder === "infrastructure") {
|
|
4291
|
+
targetLayer = "infrastructure";
|
|
4292
|
+
} else if (targetSubfolder === "pxExpress" || targetSubfolder === "presentation" || targetSubfolder === "handlers" || targetSubfolder.startsWith("px")) {
|
|
4293
|
+
targetLayer = "presentation";
|
|
4294
|
+
}
|
|
4295
|
+
const basePart = rest ? path2__default.default.basename(rest) : targetSubfolder;
|
|
4296
|
+
const targetFileName = basePart.replace(/\.(js|ts|mjs|cjs)$/, "");
|
|
4297
|
+
const isTargetPort = /^I[A-Z]/.test(targetFileName);
|
|
4298
|
+
const isTargetRepo = isTargetPort && targetFileName.includes("Repo");
|
|
4299
|
+
return {
|
|
4300
|
+
isExternal: false,
|
|
4301
|
+
isDiFramework: false,
|
|
4302
|
+
targetFeature,
|
|
4303
|
+
targetLayer,
|
|
4304
|
+
targetSubfolder,
|
|
4305
|
+
targetFileName,
|
|
4306
|
+
isTargetPort,
|
|
4307
|
+
isTargetRepo
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
const sharedMatch = resolvedRel.match(/(?:^|\/)(?:src\/)?shared\/([^/]+)(?:\/(.*))?$/);
|
|
4311
|
+
if (sharedMatch) {
|
|
4312
|
+
const targetSharedModule = sharedMatch[1];
|
|
4313
|
+
const sharedRest = sharedMatch[2] || "";
|
|
4314
|
+
const isSharedRoot = !sharedRest.includes("/");
|
|
4315
|
+
const isSharedPorts = sharedRest.startsWith("ports/") || sharedRest === "ports";
|
|
4316
|
+
const isSharedInfra = sharedRest.startsWith("infrastructure/") || sharedRest === "infrastructure";
|
|
4317
|
+
const isSharedDomain = sharedRest.startsWith("domain/") || sharedRest === "domain";
|
|
4318
|
+
return {
|
|
4319
|
+
isExternal: false,
|
|
4320
|
+
isDiFramework: false,
|
|
4321
|
+
targetSharedModule,
|
|
4322
|
+
isSharedRoot,
|
|
4323
|
+
isSharedPorts,
|
|
4324
|
+
isSharedInfra,
|
|
4325
|
+
isSharedDomain
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
return { isExternal: true, isDiFramework: false };
|
|
4329
|
+
}
|
|
4330
|
+
function extractImportStatements(content) {
|
|
4331
|
+
const statements = [];
|
|
4332
|
+
const lines = content.split(/\r?\n/);
|
|
4333
|
+
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;
|
|
4334
|
+
let match;
|
|
4335
|
+
while ((match = regex.exec(content)) !== null) {
|
|
4336
|
+
const specifier = match[1];
|
|
4337
|
+
const charIndex = match.index;
|
|
4338
|
+
const lineNum = content.substring(0, charIndex).split("\n").length;
|
|
4339
|
+
const rawCode = lines[lineNum - 1]?.trim() || match[0].trim();
|
|
4340
|
+
statements.push({
|
|
4341
|
+
specifier,
|
|
4342
|
+
line: lineNum,
|
|
4343
|
+
rawCode
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
return statements;
|
|
4347
|
+
}
|
|
4348
|
+
var FeatureDependencyGraph = class {
|
|
4349
|
+
edges = /* @__PURE__ */ new Map();
|
|
4350
|
+
addEdge(evidence) {
|
|
4351
|
+
const { fromFeature, toFeature } = evidence;
|
|
4352
|
+
if (fromFeature === toFeature) {
|
|
4353
|
+
return { createsCycle: false };
|
|
4354
|
+
}
|
|
4355
|
+
if (!this.edges.has(fromFeature)) {
|
|
4356
|
+
this.edges.set(fromFeature, /* @__PURE__ */ new Map());
|
|
4357
|
+
}
|
|
4358
|
+
this.edges.get(fromFeature).set(toFeature, evidence);
|
|
4359
|
+
const reverse = this.edges.get(toFeature)?.get(fromFeature);
|
|
4360
|
+
if (reverse) {
|
|
4361
|
+
return { createsCycle: true, directPartner: reverse, cycleChain: [fromFeature, toFeature, fromFeature] };
|
|
4362
|
+
}
|
|
4363
|
+
const cycle = this.findCycleFrom(fromFeature);
|
|
4364
|
+
if (cycle && cycle.length > 0) {
|
|
4365
|
+
return { createsCycle: true, cycleChain: cycle };
|
|
4366
|
+
}
|
|
4367
|
+
return { createsCycle: false };
|
|
4368
|
+
}
|
|
4369
|
+
getEdges() {
|
|
4370
|
+
const list = [];
|
|
4371
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4372
|
+
for (const [to, ev] of toMap.entries()) {
|
|
4373
|
+
list.push({ from, to, evidence: ev });
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
return list;
|
|
4377
|
+
}
|
|
4378
|
+
findDirectCycles() {
|
|
4379
|
+
const cycles = [];
|
|
4380
|
+
const checkedPairs = /* @__PURE__ */ new Set();
|
|
4381
|
+
for (const [from, toMap] of this.edges.entries()) {
|
|
4382
|
+
for (const [to, evAtoB] of toMap.entries()) {
|
|
4383
|
+
const pairKey = [from, to].sort().join("<->");
|
|
4384
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
4385
|
+
const evBtoA = this.edges.get(to)?.get(from);
|
|
4386
|
+
if (evBtoA) {
|
|
4387
|
+
checkedPairs.add(pairKey);
|
|
4388
|
+
cycles.push({
|
|
4389
|
+
featureA: from,
|
|
4390
|
+
featureB: to,
|
|
4391
|
+
cycle: [from, to, from],
|
|
4392
|
+
evidenceAtoB: evAtoB,
|
|
4393
|
+
evidenceBtoA: evBtoA
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
return cycles;
|
|
4399
|
+
}
|
|
4400
|
+
findCycleFrom(start) {
|
|
4401
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4402
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
4403
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4404
|
+
const dfs = (curr) => {
|
|
4405
|
+
visited.add(curr);
|
|
4406
|
+
inStack.add(curr);
|
|
4407
|
+
const neighbors = this.edges.get(curr);
|
|
4408
|
+
if (neighbors) {
|
|
4409
|
+
for (const next of neighbors.keys()) {
|
|
4410
|
+
if (!visited.has(next)) {
|
|
4411
|
+
parent.set(next, curr);
|
|
4412
|
+
const cycle = dfs(next);
|
|
4413
|
+
if (cycle) return cycle;
|
|
4414
|
+
} else if (inStack.has(next)) {
|
|
4415
|
+
const cycle = [next];
|
|
4416
|
+
let p16 = curr;
|
|
4417
|
+
while (p16 !== next) {
|
|
4418
|
+
cycle.push(p16);
|
|
4419
|
+
p16 = parent.get(p16) || next;
|
|
4420
|
+
}
|
|
4421
|
+
cycle.push(next);
|
|
4422
|
+
cycle.reverse();
|
|
4423
|
+
return cycle;
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
inStack.delete(curr);
|
|
4428
|
+
return null;
|
|
4429
|
+
};
|
|
4430
|
+
return dfs(start);
|
|
4431
|
+
}
|
|
4432
|
+
};
|
|
4433
|
+
function discoverAllFeatures(projectRoot) {
|
|
4434
|
+
const featuresBase = resolveFeaturesDir(projectRoot);
|
|
4435
|
+
if (!fs4__default.default.existsSync(featuresBase)) return [];
|
|
4436
|
+
const entries = fs4__default.default.readdirSync(featuresBase, { withFileTypes: true });
|
|
4437
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "__tests__" && e.name !== "tests").map((e) => ({
|
|
4438
|
+
name: e.name,
|
|
4439
|
+
dir: path2__default.default.join(featuresBase, e.name)
|
|
4440
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4441
|
+
}
|
|
4442
|
+
function lintCodebase(options = {}) {
|
|
4443
|
+
const startTime = Date.now();
|
|
4444
|
+
const projectRoot = options.projectRoot || findProjectRoot(options.targetDir || process.cwd());
|
|
4445
|
+
const failFast = !options.complete;
|
|
4446
|
+
const features = discoverAllFeatures(projectRoot);
|
|
4447
|
+
const graph = new FeatureDependencyGraph();
|
|
4448
|
+
const allViolations = [];
|
|
4449
|
+
const results = [];
|
|
4450
|
+
const detectedCycles = [];
|
|
4451
|
+
const addViolation = (v) => {
|
|
4452
|
+
allViolations.push(v);
|
|
4453
|
+
const featureResult = results.find((r) => r.featureName === v.feature);
|
|
4454
|
+
if (featureResult) {
|
|
4455
|
+
featureResult.violations.push(v);
|
|
4456
|
+
featureResult.passed = false;
|
|
4457
|
+
}
|
|
4458
|
+
if (failFast) {
|
|
4459
|
+
const errorMsg = `[FAIL FAST] Dependency rule violation detected:
|
|
4460
|
+
Feature: ${v.feature}
|
|
4461
|
+
Category: ${v.category}
|
|
4462
|
+
Rule: ${v.ruleId}
|
|
4463
|
+
Location: ${v.relativePath}:${v.line}
|
|
4464
|
+
Code: ${v.code}
|
|
4465
|
+
Explanation: ${v.advice}`;
|
|
4466
|
+
throw new DependencyRuleError(errorMsg, [v], true);
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
for (const feat of features) {
|
|
4470
|
+
results.push({
|
|
4471
|
+
featureName: feat.name,
|
|
4472
|
+
featureDir: feat.dir,
|
|
4473
|
+
scannedFilesCount: 0,
|
|
4474
|
+
passed: true,
|
|
4475
|
+
violations: [],
|
|
4476
|
+
layersChecked: ["domain", "useCases", "services", "infrastructure", "presentation"]
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
for (const feat of features) {
|
|
4480
|
+
const featResult = results.find((r) => r.featureName === feat.name);
|
|
4481
|
+
const allFiles = scanDirFiles(feat.dir, projectRoot);
|
|
4482
|
+
featResult.scannedFilesCount = allFiles.length;
|
|
4483
|
+
for (const file of allFiles) {
|
|
4484
|
+
const relPath = path2__default.default.relative(projectRoot, file).replace(/\\/g, "/");
|
|
4485
|
+
const { layer } = classifyFile(file, projectRoot);
|
|
4486
|
+
if (!layer) continue;
|
|
4487
|
+
const content = fs4__default.default.readFileSync(file, "utf-8");
|
|
4488
|
+
const statements = extractImportStatements(content);
|
|
4489
|
+
if (layer === "domain") {
|
|
4490
|
+
const lines = content.split(/\r?\n/);
|
|
4491
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4492
|
+
const lineText = lines[i].trim();
|
|
4493
|
+
if (lineText.includes("@MakeInjectable") || lineText.includes("public static deps =")) {
|
|
4494
|
+
addViolation({
|
|
4495
|
+
feature: feat.name,
|
|
4496
|
+
layer,
|
|
4497
|
+
file,
|
|
4498
|
+
relativePath: relPath,
|
|
4499
|
+
line: i + 1,
|
|
4500
|
+
category: "Domain Dependence Violation",
|
|
4501
|
+
code: lineText,
|
|
4502
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4503
|
+
severity: "error",
|
|
4504
|
+
advice: "Domain layer must have zero DI dependence. Remove @MakeInjectable decorators and static deps declarations.",
|
|
4505
|
+
fixExample: {
|
|
4506
|
+
bad: lineText,
|
|
4507
|
+
good: "// Domain interfaces and value objects must remain plain TypeScript classes/interfaces."
|
|
4508
|
+
}
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
for (const stmt of statements) {
|
|
4514
|
+
const target = classifyTarget(stmt.specifier, file, projectRoot);
|
|
4515
|
+
if (target.targetFeature && target.targetFeature !== feat.name) {
|
|
4516
|
+
const evidence = {
|
|
4517
|
+
fromFeature: feat.name,
|
|
4518
|
+
toFeature: target.targetFeature,
|
|
4519
|
+
file,
|
|
4520
|
+
relativePath: relPath,
|
|
4521
|
+
line: stmt.line,
|
|
4522
|
+
code: stmt.rawCode
|
|
4523
|
+
};
|
|
4524
|
+
const cycleResult = graph.addEdge(evidence);
|
|
4525
|
+
if (cycleResult.createsCycle) {
|
|
4526
|
+
const partner = cycleResult.directPartner;
|
|
4527
|
+
detectedCycles.push({
|
|
4528
|
+
featureA: feat.name,
|
|
4529
|
+
featureB: target.targetFeature,
|
|
4530
|
+
cycle: cycleResult.cycleChain,
|
|
4531
|
+
evidenceAtoB: evidence,
|
|
4532
|
+
evidenceBtoA: partner || evidence
|
|
4533
|
+
});
|
|
4534
|
+
addViolation({
|
|
4535
|
+
feature: feat.name,
|
|
4536
|
+
layer,
|
|
4537
|
+
file,
|
|
4538
|
+
relativePath: relPath,
|
|
4539
|
+
line: stmt.line,
|
|
4540
|
+
category: "Circular Feature Dependency Violation",
|
|
4541
|
+
code: stmt.rawCode,
|
|
4542
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4543
|
+
severity: "error",
|
|
4544
|
+
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.`,
|
|
4545
|
+
fixExample: {
|
|
4546
|
+
bad: stmt.rawCode,
|
|
4547
|
+
good: `// Emit a Domain/Application Event (e.g. publish '${feat.name}Changed') instead of depending on '${target.targetFeature}'.`
|
|
4548
|
+
}
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
if (layer === "domain") {
|
|
4553
|
+
if (target.targetLayer && ["useCases", "services", "infrastructure", "presentation"].includes(target.targetLayer)) {
|
|
4554
|
+
addViolation({
|
|
4555
|
+
feature: feat.name,
|
|
4556
|
+
layer,
|
|
4557
|
+
file,
|
|
4558
|
+
relativePath: relPath,
|
|
4559
|
+
line: stmt.line,
|
|
4560
|
+
category: "Domain Purity Violation",
|
|
4561
|
+
code: stmt.rawCode,
|
|
4562
|
+
ruleId: "D1_OUTER_IMPORT",
|
|
4563
|
+
severity: "error",
|
|
4564
|
+
advice: `Domain layer must not import from outer layers (${target.targetLayer}). Keep the domain core pure.`,
|
|
4565
|
+
fixExample: {
|
|
4566
|
+
bad: stmt.rawCode,
|
|
4567
|
+
good: "// Define interfaces, types, or value objects directly within the domain layer."
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
4570
|
+
}
|
|
4571
|
+
if (target.isDiFramework) {
|
|
4572
|
+
addViolation({
|
|
4573
|
+
feature: feat.name,
|
|
4574
|
+
layer,
|
|
4575
|
+
file,
|
|
4576
|
+
relativePath: relPath,
|
|
4577
|
+
line: stmt.line,
|
|
4578
|
+
category: "Domain Dependence Violation",
|
|
4579
|
+
code: stmt.rawCode,
|
|
4580
|
+
ruleId: "D2_DI_DEPENDENCE",
|
|
4581
|
+
severity: "error",
|
|
4582
|
+
advice: "Domain layer must have zero DI dependence. Domain interfaces and value objects are plain classes/types.",
|
|
4583
|
+
fixExample: {
|
|
4584
|
+
bad: stmt.rawCode,
|
|
4585
|
+
good: "// Remove @solid-stack/di import. Domain entities are pure TypeScript."
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
if (target.targetSharedModule && !target.isSharedDomain) {
|
|
4590
|
+
addViolation({
|
|
4591
|
+
feature: feat.name,
|
|
4592
|
+
layer,
|
|
4593
|
+
file,
|
|
4594
|
+
relativePath: relPath,
|
|
4595
|
+
line: stmt.line,
|
|
4596
|
+
category: "Domain Purity Violation",
|
|
4597
|
+
code: stmt.rawCode,
|
|
4598
|
+
ruleId: "D3_SHARED_NON_DOMAIN_IMPORT",
|
|
4599
|
+
severity: "error",
|
|
4600
|
+
advice: "Domain layer can only import pure value objects from shared/<module>/domain/*. Ports, infrastructure, and root classes must not be imported.",
|
|
4601
|
+
fixExample: {
|
|
4602
|
+
bad: stmt.rawCode,
|
|
4603
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/domain/...";`
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
if (target.isExternal && !target.isDiFramework) {
|
|
4608
|
+
addViolation({
|
|
4609
|
+
feature: feat.name,
|
|
4610
|
+
layer,
|
|
4611
|
+
file,
|
|
4612
|
+
relativePath: relPath,
|
|
4613
|
+
line: stmt.line,
|
|
4614
|
+
category: "Domain Purity Violation",
|
|
4615
|
+
code: stmt.rawCode,
|
|
4616
|
+
ruleId: "D4_EXTERNAL_LIBRARY",
|
|
4617
|
+
severity: "error",
|
|
4618
|
+
advice: `Domain layer must be pure and must not depend on external runtime libraries ('${stmt.specifier}').`,
|
|
4619
|
+
fixExample: {
|
|
4620
|
+
bad: stmt.rawCode,
|
|
4621
|
+
good: "// Model pure business interfaces without external libraries."
|
|
4622
|
+
}
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
} else if (layer === "useCases") {
|
|
4626
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4627
|
+
addViolation({
|
|
4628
|
+
feature: feat.name,
|
|
4629
|
+
layer,
|
|
4630
|
+
file,
|
|
4631
|
+
relativePath: relPath,
|
|
4632
|
+
line: stmt.line,
|
|
4633
|
+
category: "Use Case Intra-Feature Violation",
|
|
4634
|
+
code: stmt.rawCode,
|
|
4635
|
+
ruleId: "U1_INTRA_USE_CASE",
|
|
4636
|
+
severity: "error",
|
|
4637
|
+
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.`,
|
|
4638
|
+
fixExample: {
|
|
4639
|
+
bad: stmt.rawCode,
|
|
4640
|
+
good: "// Inject a domain interface or domain service instead of another use case."
|
|
4641
|
+
}
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
if (target.targetLayer === "useCases" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4645
|
+
addViolation({
|
|
4646
|
+
feature: feat.name,
|
|
4647
|
+
layer,
|
|
4648
|
+
file,
|
|
4649
|
+
relativePath: relPath,
|
|
4650
|
+
line: stmt.line,
|
|
4651
|
+
category: "Use Case Cross-Feature Use Case Violation",
|
|
4652
|
+
code: stmt.rawCode,
|
|
4653
|
+
ruleId: "U2_CROSS_FEATURE_USE_CASE",
|
|
4654
|
+
severity: "error",
|
|
4655
|
+
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/'.`,
|
|
4656
|
+
fixExample: {
|
|
4657
|
+
bad: stmt.rawCode,
|
|
4658
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/ using '${target.targetFileName}'.`
|
|
4659
|
+
}
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
if (target.targetLayer === "infrastructure") {
|
|
4663
|
+
addViolation({
|
|
4664
|
+
feature: feat.name,
|
|
4665
|
+
layer,
|
|
4666
|
+
file,
|
|
4667
|
+
relativePath: relPath,
|
|
4668
|
+
line: stmt.line,
|
|
4669
|
+
category: "Use Case Layer Violation",
|
|
4670
|
+
code: stmt.rawCode,
|
|
4671
|
+
ruleId: "U3_DIRECT_INFRASTRUCTURE",
|
|
4672
|
+
severity: "error",
|
|
4673
|
+
advice: "Use cases cannot import concrete infrastructure directly. Depend on Domain Ports of the same feature via DI.",
|
|
4674
|
+
fixExample: {
|
|
4675
|
+
bad: stmt.rawCode,
|
|
4676
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4677
|
+
}
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4681
|
+
addViolation({
|
|
4682
|
+
feature: feat.name,
|
|
4683
|
+
layer,
|
|
4684
|
+
file,
|
|
4685
|
+
relativePath: relPath,
|
|
4686
|
+
line: stmt.line,
|
|
4687
|
+
category: "Use Case Cross-Feature Port Violation",
|
|
4688
|
+
code: stmt.rawCode,
|
|
4689
|
+
ruleId: "U4_CROSS_FEATURE_PORT",
|
|
4690
|
+
severity: "error",
|
|
4691
|
+
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/'.`,
|
|
4692
|
+
fixExample: {
|
|
4693
|
+
bad: stmt.rawCode,
|
|
4694
|
+
good: `// Define a local interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4695
|
+
}
|
|
4696
|
+
});
|
|
4697
|
+
}
|
|
4698
|
+
if (target.targetLayer === "services" && target.targetFeature && target.targetFeature !== feat.name) {
|
|
4699
|
+
addViolation({
|
|
4700
|
+
feature: feat.name,
|
|
4701
|
+
layer,
|
|
4702
|
+
file,
|
|
4703
|
+
relativePath: relPath,
|
|
4704
|
+
line: stmt.line,
|
|
4705
|
+
category: "Use Case Layer Violation",
|
|
4706
|
+
code: stmt.rawCode,
|
|
4707
|
+
ruleId: "U5_CROSS_FEATURE_SERVICE",
|
|
4708
|
+
severity: "error",
|
|
4709
|
+
advice: `Use cases cannot depend on services of foreign feature '${target.targetFeature}'.`,
|
|
4710
|
+
fixExample: {
|
|
4711
|
+
bad: stmt.rawCode,
|
|
4712
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4713
|
+
}
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4717
|
+
addViolation({
|
|
4718
|
+
feature: feat.name,
|
|
4719
|
+
layer,
|
|
4720
|
+
file,
|
|
4721
|
+
relativePath: relPath,
|
|
4722
|
+
line: stmt.line,
|
|
4723
|
+
category: "Shared Module Root Class Violation",
|
|
4724
|
+
code: stmt.rawCode,
|
|
4725
|
+
ruleId: "U6_SHARED_NON_ROOT",
|
|
4726
|
+
severity: "error",
|
|
4727
|
+
advice: `Use cases can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.`,
|
|
4728
|
+
fixExample: {
|
|
4729
|
+
bad: stmt.rawCode,
|
|
4730
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4731
|
+
}
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
} else if (layer === "services") {
|
|
4735
|
+
if (target.targetLayer === "useCases") {
|
|
4736
|
+
addViolation({
|
|
4737
|
+
feature: feat.name,
|
|
4738
|
+
layer,
|
|
4739
|
+
file,
|
|
4740
|
+
relativePath: relPath,
|
|
4741
|
+
line: stmt.line,
|
|
4742
|
+
category: "Service Intra-Feature Violation",
|
|
4743
|
+
code: stmt.rawCode,
|
|
4744
|
+
ruleId: "S1_USE_CASE_DEPENDENCY",
|
|
4745
|
+
severity: "error",
|
|
4746
|
+
advice: "Services cannot depend on use cases of any feature.",
|
|
4747
|
+
fixExample: {
|
|
4748
|
+
bad: stmt.rawCode,
|
|
4749
|
+
good: "// Inject domain repository ports directly instead of use cases."
|
|
4750
|
+
}
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
if (target.targetLayer === "domain" && target.targetFeature && target.targetFeature !== feat.name && target.isTargetPort) {
|
|
4754
|
+
addViolation({
|
|
4755
|
+
feature: feat.name,
|
|
4756
|
+
layer,
|
|
4757
|
+
file,
|
|
4758
|
+
relativePath: relPath,
|
|
4759
|
+
line: stmt.line,
|
|
4760
|
+
category: "Service Cross-Feature Port Violation",
|
|
4761
|
+
code: stmt.rawCode,
|
|
4762
|
+
ruleId: "S2_CROSS_FEATURE_PORT",
|
|
4763
|
+
severity: "error",
|
|
4764
|
+
advice: `Services cannot depend on domain ports of foreign feature '${target.targetFeature}'.`,
|
|
4765
|
+
fixExample: {
|
|
4766
|
+
bad: stmt.rawCode,
|
|
4767
|
+
good: `// Define an interface in ${feat.name}/domain/ and implement it in ${feat.name}/infrastructure/.`
|
|
4768
|
+
}
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
if (target.targetLayer === "infrastructure") {
|
|
4772
|
+
addViolation({
|
|
4773
|
+
feature: feat.name,
|
|
4774
|
+
layer,
|
|
4775
|
+
file,
|
|
4776
|
+
relativePath: relPath,
|
|
4777
|
+
line: stmt.line,
|
|
4778
|
+
category: "Service Layer Violation",
|
|
4779
|
+
code: stmt.rawCode,
|
|
4780
|
+
ruleId: "S3_DIRECT_INFRASTRUCTURE",
|
|
4781
|
+
severity: "error",
|
|
4782
|
+
advice: "Services cannot import concrete infrastructure directly. Depend on Domain Ports via DI.",
|
|
4783
|
+
fixExample: {
|
|
4784
|
+
bad: stmt.rawCode,
|
|
4785
|
+
good: "// Depend on the domain interface port declared in static deps."
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4790
|
+
addViolation({
|
|
4791
|
+
feature: feat.name,
|
|
4792
|
+
layer,
|
|
4793
|
+
file,
|
|
4794
|
+
relativePath: relPath,
|
|
4795
|
+
line: stmt.line,
|
|
4796
|
+
category: "Shared Module Root Class Violation",
|
|
4797
|
+
code: stmt.rawCode,
|
|
4798
|
+
ruleId: "S4_SHARED_NON_ROOT",
|
|
4799
|
+
severity: "error",
|
|
4800
|
+
advice: "Services can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4801
|
+
fixExample: {
|
|
4802
|
+
bad: stmt.rawCode,
|
|
4803
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4804
|
+
}
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
} else if (layer === "infrastructure") {
|
|
4808
|
+
if (target.targetLayer === "useCases" && target.targetFeature === feat.name) {
|
|
4809
|
+
addViolation({
|
|
4810
|
+
feature: feat.name,
|
|
4811
|
+
layer,
|
|
4812
|
+
file,
|
|
4813
|
+
relativePath: relPath,
|
|
4814
|
+
line: stmt.line,
|
|
4815
|
+
category: "Adapter Layer Violation",
|
|
4816
|
+
code: stmt.rawCode,
|
|
4817
|
+
ruleId: "I1_INTRA_USE_CASE",
|
|
4818
|
+
severity: "error",
|
|
4819
|
+
advice: `Infrastructure adapters cannot depend on use cases of the same feature ('${feat.name}'). Adapters implement domain ports.`,
|
|
4820
|
+
fixExample: {
|
|
4821
|
+
bad: stmt.rawCode,
|
|
4822
|
+
good: "// Adapters implement domain ports and should not invoke local application use cases."
|
|
4823
|
+
}
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4827
|
+
addViolation({
|
|
4828
|
+
feature: feat.name,
|
|
4829
|
+
layer,
|
|
4830
|
+
file,
|
|
4831
|
+
relativePath: relPath,
|
|
4832
|
+
line: stmt.line,
|
|
4833
|
+
category: "Shared Module Root Class Violation",
|
|
4834
|
+
code: stmt.rawCode,
|
|
4835
|
+
ruleId: "I2_SHARED_NON_ROOT",
|
|
4836
|
+
severity: "error",
|
|
4837
|
+
advice: "Infrastructure adapters can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4838
|
+
fixExample: {
|
|
4839
|
+
bad: stmt.rawCode,
|
|
4840
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4841
|
+
}
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4844
|
+
} else if (layer === "presentation") {
|
|
4845
|
+
if (target.targetLayer === "infrastructure") {
|
|
4846
|
+
addViolation({
|
|
4847
|
+
feature: feat.name,
|
|
4848
|
+
layer,
|
|
4849
|
+
file,
|
|
4850
|
+
relativePath: relPath,
|
|
4851
|
+
line: stmt.line,
|
|
4852
|
+
category: "Presentation Layer Violation",
|
|
4853
|
+
code: stmt.rawCode,
|
|
4854
|
+
ruleId: "P1_DIRECT_INFRASTRUCTURE",
|
|
4855
|
+
severity: "error",
|
|
4856
|
+
advice: "Presentation handlers must not import infrastructure directly. Inject Use Cases via DI.",
|
|
4857
|
+
fixExample: {
|
|
4858
|
+
bad: stmt.rawCode,
|
|
4859
|
+
good: "// Inject the corresponding Use Case via static deps and invoke its execute() method."
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
if (target.targetLayer === "domain" && (target.isTargetRepo || target.isTargetPort)) {
|
|
4864
|
+
addViolation({
|
|
4865
|
+
feature: feat.name,
|
|
4866
|
+
layer,
|
|
4867
|
+
file,
|
|
4868
|
+
relativePath: relPath,
|
|
4869
|
+
line: stmt.line,
|
|
4870
|
+
category: "Presentation Layer Violation",
|
|
4871
|
+
code: stmt.rawCode,
|
|
4872
|
+
ruleId: "P2_DIRECT_REPOSITORY",
|
|
4873
|
+
severity: "error",
|
|
4874
|
+
advice: "Presentation handlers must not depend on domain repositories or ports directly. Inject Use Cases via DI.",
|
|
4875
|
+
fixExample: {
|
|
4876
|
+
bad: stmt.rawCode,
|
|
4877
|
+
good: "// Presentation layer must only orchestrate Use Cases."
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4881
|
+
if (target.targetSharedModule && (target.isSharedPorts || target.isSharedInfra)) {
|
|
4882
|
+
addViolation({
|
|
4883
|
+
feature: feat.name,
|
|
4884
|
+
layer,
|
|
4885
|
+
file,
|
|
4886
|
+
relativePath: relPath,
|
|
4887
|
+
line: stmt.line,
|
|
4888
|
+
category: "Shared Module Root Class Violation",
|
|
4889
|
+
code: stmt.rawCode,
|
|
4890
|
+
ruleId: "P3_SHARED_NON_ROOT",
|
|
4891
|
+
severity: "error",
|
|
4892
|
+
advice: "Presentation handlers can only depend on root classes of shared modules. Never import from shared ports or infrastructure directly.",
|
|
4893
|
+
fixExample: {
|
|
4894
|
+
bad: stmt.rawCode,
|
|
4895
|
+
good: `import { ... } from "@/shared/${target.targetSharedModule}/<RootClass>.js";`
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
const directCycles = graph.findDirectCycles();
|
|
4904
|
+
for (const c of directCycles) {
|
|
4905
|
+
const existing = detectedCycles.find(
|
|
4906
|
+
(dc) => dc.featureA === c.featureA && dc.featureB === c.featureB || dc.featureA === c.featureB && dc.featureB === c.featureA
|
|
4907
|
+
);
|
|
4908
|
+
if (!existing) {
|
|
4909
|
+
detectedCycles.push(c);
|
|
4910
|
+
addViolation({
|
|
4911
|
+
feature: c.featureA,
|
|
4912
|
+
file: c.evidenceAtoB.file,
|
|
4913
|
+
relativePath: c.evidenceAtoB.relativePath,
|
|
4914
|
+
line: c.evidenceAtoB.line,
|
|
4915
|
+
category: "Circular Feature Dependency Violation",
|
|
4916
|
+
code: c.evidenceAtoB.code,
|
|
4917
|
+
ruleId: "C1_CIRCULAR_FEATURE_DEPENDENCY",
|
|
4918
|
+
severity: "error",
|
|
4919
|
+
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.`
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
const durationMs = Date.now() - startTime;
|
|
4924
|
+
const cleanFeatures = results.filter((r) => r.passed).length;
|
|
4925
|
+
const violatedFeatures = results.filter((r) => !r.passed).length;
|
|
4926
|
+
const summary = {
|
|
4927
|
+
totalFeatures: results.length,
|
|
4928
|
+
cleanFeatures,
|
|
4929
|
+
violatedFeatures,
|
|
4930
|
+
totalViolations: allViolations.length,
|
|
4931
|
+
durationMs,
|
|
4932
|
+
results,
|
|
4933
|
+
allViolations,
|
|
4934
|
+
circularDependencies: detectedCycles
|
|
4935
|
+
};
|
|
4936
|
+
if (allViolations.length > 0 && options.throwOnError !== false) {
|
|
4937
|
+
const errorHeader = `Dependency rule audit failed with ${allViolations.length} violation(s) across ${violatedFeatures} feature(s):`;
|
|
4938
|
+
const compiledDetails = allViolations.map(
|
|
4939
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
4940
|
+
Rule: ${v.ruleId}
|
|
4941
|
+
Code: ${v.code}
|
|
4942
|
+
Advice: ${v.advice}`
|
|
4943
|
+
).join("\n\n");
|
|
4944
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
4945
|
+
|
|
4946
|
+
${compiledDetails}
|
|
4947
|
+
`, allViolations, false);
|
|
4948
|
+
}
|
|
4949
|
+
return summary;
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
// src/commands/lint.ts
|
|
4953
|
+
function printLintBanner(mode) {
|
|
4954
|
+
console.log("");
|
|
4955
|
+
const title = " Mason Clean Architecture Dependency Linter ";
|
|
4956
|
+
const line = "\u2501".repeat(title.length + 2);
|
|
4957
|
+
console.log(pc15__default.default.cyan(` \u250F${line}\u2513`));
|
|
4958
|
+
console.log(` ${pc15__default.default.cyan("\u2503")} ${pc15__default.default.bgCyan(pc15__default.default.black(title))} ${pc15__default.default.cyan("\u2503")}`);
|
|
4959
|
+
console.log(pc15__default.default.cyan(` \u2517${line}\u251B`));
|
|
4960
|
+
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)")}`;
|
|
4961
|
+
console.log(` Mode: ${modeText}`);
|
|
4962
|
+
console.log("");
|
|
4963
|
+
}
|
|
4964
|
+
function formatViolation(v, showFixHint = true) {
|
|
4965
|
+
const lines = [];
|
|
4966
|
+
const categoryBadge = pc15__default.default.red(pc15__default.default.bold(`[${v.category}]`));
|
|
4967
|
+
const location = pc15__default.default.white(`${v.relativePath}:${pc15__default.default.yellow(String(v.line))}`);
|
|
4968
|
+
lines.push(` ${categoryBadge} ${location}`);
|
|
4969
|
+
lines.push(` ${pc15__default.default.dim("Rule ID:")} ${pc15__default.default.yellow(v.ruleId)}`);
|
|
4970
|
+
lines.push(` ${pc15__default.default.dim("Code:")} ${pc15__default.default.magenta(v.code)}`);
|
|
4971
|
+
lines.push(` ${pc15__default.default.dim("Advice:")} ${pc15__default.default.cyan(v.advice)}`);
|
|
4972
|
+
if (showFixHint && v.fixExample) {
|
|
4973
|
+
lines.push(` ${pc15__default.default.dim("Example:")}`);
|
|
4974
|
+
lines.push(` ${pc15__default.default.red(`- ${v.fixExample.bad}`)}`);
|
|
4975
|
+
lines.push(` ${pc15__default.default.green(`+ ${v.fixExample.good}`)}`);
|
|
4976
|
+
}
|
|
4977
|
+
return lines.join("\n");
|
|
4978
|
+
}
|
|
4979
|
+
function formatFeatureResult(res, options = {}) {
|
|
4980
|
+
const lines = [];
|
|
4981
|
+
if (res.passed) {
|
|
4982
|
+
lines.push(
|
|
4983
|
+
` ${pc15__default.default.green("\u2714")} ${pc15__default.default.bold(res.featureName)} ${pc15__default.default.dim(
|
|
4984
|
+
`(${res.scannedFilesCount} files checked - clean)`
|
|
4985
|
+
)}`
|
|
4986
|
+
);
|
|
4987
|
+
return lines.join("\n");
|
|
4988
|
+
}
|
|
4989
|
+
const countBadge = pc15__default.default.red(
|
|
4990
|
+
`(${res.violations.length} violation${res.violations.length > 1 ? "s" : ""})`
|
|
4991
|
+
);
|
|
4992
|
+
lines.push(`
|
|
4993
|
+
${pc15__default.default.red("\u2716")} ${pc15__default.default.bold(pc15__default.default.white(res.featureName))} ${countBadge}`);
|
|
4994
|
+
lines.push(` ${pc15__default.default.dim("Path:")} ${res.featureDir}`);
|
|
4995
|
+
lines.push("");
|
|
4996
|
+
for (const v of res.violations) {
|
|
4997
|
+
lines.push(formatViolation(v, options.fixHint !== false));
|
|
4998
|
+
lines.push("");
|
|
4999
|
+
}
|
|
5000
|
+
return lines.join("\n");
|
|
5001
|
+
}
|
|
5002
|
+
function printTerminalReport(summary, options = {}) {
|
|
5003
|
+
for (const res of summary.results) {
|
|
5004
|
+
const formatted = formatFeatureResult(res, options);
|
|
5005
|
+
if (formatted.trim()) {
|
|
5006
|
+
console.log(formatted);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
console.log("\u2501".repeat(70));
|
|
5010
|
+
const statusIcon = summary.totalViolations === 0 ? pc15__default.default.green("\u2714 PASS") : pc15__default.default.red("\u2716 FAIL");
|
|
5011
|
+
console.log(
|
|
5012
|
+
` ${statusIcon} ${pc15__default.default.bold("Dependency Rules Audit Result")} ${pc15__default.default.dim(
|
|
5013
|
+
`(${summary.durationMs}ms)`
|
|
5014
|
+
)}`
|
|
5015
|
+
);
|
|
5016
|
+
console.log(
|
|
5017
|
+
` ${pc15__default.default.bold("Features:")} ${pc15__default.default.cyan(String(summary.totalFeatures))} total | ${pc15__default.default.green(
|
|
5018
|
+
String(summary.cleanFeatures)
|
|
5019
|
+
)} clean | ${summary.violatedFeatures > 0 ? pc15__default.default.red(String(summary.violatedFeatures)) : pc15__default.default.green("0")} with violations`
|
|
5020
|
+
);
|
|
5021
|
+
console.log(
|
|
5022
|
+
` ${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`
|
|
5023
|
+
);
|
|
5024
|
+
console.log("\u2501".repeat(70));
|
|
5025
|
+
console.log("");
|
|
5026
|
+
}
|
|
5027
|
+
async function handleLint(flags = {}) {
|
|
5028
|
+
const complete = Boolean(flags.complete);
|
|
5029
|
+
const mode = complete ? "complete" : "failFast";
|
|
5030
|
+
const quiet = Boolean(flags.quiet);
|
|
5031
|
+
if (!quiet) {
|
|
5032
|
+
printLintBanner(mode);
|
|
5033
|
+
}
|
|
5034
|
+
const options = {
|
|
5035
|
+
complete,
|
|
5036
|
+
quiet,
|
|
5037
|
+
targetDir: flags.targetDir || flags.target,
|
|
5038
|
+
projectRoot: flags.projectRoot,
|
|
5039
|
+
fixHint: flags.fixHint !== false,
|
|
5040
|
+
throwOnError: false
|
|
5041
|
+
};
|
|
5042
|
+
try {
|
|
5043
|
+
const summary = lintCodebase(options);
|
|
5044
|
+
if (!quiet) {
|
|
5045
|
+
printTerminalReport(summary, options);
|
|
5046
|
+
}
|
|
5047
|
+
if (summary.totalViolations > 0) {
|
|
5048
|
+
const errorHeader = `Dependency rule audit failed with ${summary.totalViolations} violation(s) across ${summary.violatedFeatures} feature(s):`;
|
|
5049
|
+
const compiledDetails = summary.allViolations.map(
|
|
5050
|
+
(v, i) => `${i + 1}. [${v.category}] ${v.relativePath}:${v.line}
|
|
5051
|
+
Rule: ${v.ruleId}
|
|
5052
|
+
Code: ${v.code}
|
|
5053
|
+
Advice: ${v.advice}`
|
|
5054
|
+
).join("\n\n");
|
|
5055
|
+
throw new DependencyRuleError(`${errorHeader}
|
|
5056
|
+
|
|
5057
|
+
${compiledDetails}
|
|
5058
|
+
`, summary.allViolations, false);
|
|
5059
|
+
}
|
|
5060
|
+
if (!quiet) {
|
|
5061
|
+
console.log(
|
|
5062
|
+
pc15__default.default.green(`\u2714 All dependency rules are being followed! (${summary.totalFeatures} features checked)`)
|
|
5063
|
+
);
|
|
5064
|
+
console.log("");
|
|
5065
|
+
}
|
|
5066
|
+
return summary;
|
|
5067
|
+
} catch (err) {
|
|
5068
|
+
if (err instanceof DependencyRuleError && err.isFailFast) {
|
|
5069
|
+
if (!quiet) {
|
|
5070
|
+
console.log(pc15__default.default.red(`
|
|
5071
|
+
\u2716 [FAIL FAST] Terminating on first detected dependency rule violation:`));
|
|
5072
|
+
const v = err.violations[0];
|
|
5073
|
+
if (v) {
|
|
5074
|
+
console.log(formatViolation(v, options.fixHint !== false));
|
|
5075
|
+
}
|
|
5076
|
+
console.log("");
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
throw err;
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
4199
5082
|
|
|
4200
5083
|
// src/cli.ts
|
|
4201
5084
|
function createCli() {
|
|
@@ -4203,7 +5086,7 @@ function createCli() {
|
|
|
4203
5086
|
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) => {
|
|
4204
5087
|
if (args && args.length > 0) {
|
|
4205
5088
|
p14__namespace.log.error(
|
|
4206
|
-
|
|
5089
|
+
pc15__default.default.red(
|
|
4207
5090
|
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
4208
5091
|
)
|
|
4209
5092
|
);
|
|
@@ -4269,14 +5152,21 @@ function createCli() {
|
|
|
4269
5152
|
value: "import",
|
|
4270
5153
|
label: "Import from registry",
|
|
4271
5154
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
5155
|
+
},
|
|
5156
|
+
{
|
|
5157
|
+
value: "lint",
|
|
5158
|
+
label: "Lint dependency rules",
|
|
5159
|
+
hint: "Scan codebase and verify Clean Architecture dependency rules"
|
|
4272
5160
|
}
|
|
4273
5161
|
]
|
|
4274
5162
|
})
|
|
4275
5163
|
);
|
|
4276
5164
|
if (choice === "create") {
|
|
4277
5165
|
await handleCreate(void 0, void 0, flags);
|
|
4278
|
-
} else {
|
|
5166
|
+
} else if (choice === "import") {
|
|
4279
5167
|
await handleImport(void 0, void 0, flags);
|
|
5168
|
+
} else if (choice === "lint") {
|
|
5169
|
+
await handleLint(flags);
|
|
4280
5170
|
}
|
|
4281
5171
|
} catch (err) {
|
|
4282
5172
|
handleCliError(err);
|
|
@@ -4302,6 +5192,22 @@ function createCli() {
|
|
|
4302
5192
|
handleCliError(err);
|
|
4303
5193
|
}
|
|
4304
5194
|
});
|
|
5195
|
+
cli.command(
|
|
5196
|
+
"lint [target]",
|
|
5197
|
+
"Scan codebase and verify Clean Architecture dependency rules"
|
|
5198
|
+
).option(
|
|
5199
|
+
"-c, --complete",
|
|
5200
|
+
"Scan entire codebase without failing fast, compiling all errors at the end"
|
|
5201
|
+
).option("-q, --quiet", "Suppress non-error output").example("mason lint").example("mason lint --complete").example("mason lint ./src").action(async (target, flags = {}) => {
|
|
5202
|
+
try {
|
|
5203
|
+
await handleLint({
|
|
5204
|
+
...flags,
|
|
5205
|
+
targetDir: target || flags.target || flags.dest
|
|
5206
|
+
});
|
|
5207
|
+
} catch (err) {
|
|
5208
|
+
handleCliError(err);
|
|
5209
|
+
}
|
|
5210
|
+
});
|
|
4305
5211
|
const createSubtypes = [
|
|
4306
5212
|
"feature",
|
|
4307
5213
|
"shared",
|
|
@@ -4333,7 +5239,10 @@ function handleCliError(err) {
|
|
|
4333
5239
|
process.exit(0);
|
|
4334
5240
|
}
|
|
4335
5241
|
const message = err instanceof Error ? err.message : String(err);
|
|
4336
|
-
p14__namespace.log.error(
|
|
5242
|
+
p14__namespace.log.error(pc15__default.default.red(`Error: ${message}`));
|
|
5243
|
+
if (process.env.VITEST) {
|
|
5244
|
+
throw err;
|
|
5245
|
+
}
|
|
4337
5246
|
process.exit(1);
|
|
4338
5247
|
}
|
|
4339
5248
|
async function runCli(argv = process.argv) {
|
|
@@ -4342,6 +5251,8 @@ async function runCli(argv = process.argv) {
|
|
|
4342
5251
|
}
|
|
4343
5252
|
|
|
4344
5253
|
exports.DEFAULT_REGISTRY_BASE = DEFAULT_REGISTRY_BASE;
|
|
5254
|
+
exports.DependencyRuleError = DependencyRuleError;
|
|
5255
|
+
exports.FeatureDependencyGraph = FeatureDependencyGraph;
|
|
4345
5256
|
exports.PromptCancelledError = PromptCancelledError;
|
|
4346
5257
|
exports.computeDirectoryHash = computeDirectoryHash;
|
|
4347
5258
|
exports.concreteInfraTemplate = concreteInfraTemplate;
|
|
@@ -4386,6 +5297,7 @@ exports.getTemplatesDir = getTemplatesDir;
|
|
|
4386
5297
|
exports.handleCancel = handleCancel;
|
|
4387
5298
|
exports.handleCreate = handleCreate;
|
|
4388
5299
|
exports.handleImport = handleImport;
|
|
5300
|
+
exports.handleLint = handleLint;
|
|
4389
5301
|
exports.hbs = hbs;
|
|
4390
5302
|
exports.httpHandlerTemplate = httpHandlerTemplate;
|
|
4391
5303
|
exports.injectableInterfaceTemplate = injectableInterfaceTemplate;
|
|
@@ -4394,6 +5306,7 @@ exports.installNpmPackages = installNpmPackages;
|
|
|
4394
5306
|
exports.interfaceTemplate = interfaceTemplate;
|
|
4395
5307
|
exports.isValidIdentifier = isValidIdentifier;
|
|
4396
5308
|
exports.levenshteinDistance = levenshteinDistance;
|
|
5309
|
+
exports.lintCodebase = lintCodebase;
|
|
4397
5310
|
exports.listDomainEntities = listDomainEntities;
|
|
4398
5311
|
exports.listDomainInterfaces = listDomainInterfaces;
|
|
4399
5312
|
exports.listExistingFeatures = listExistingFeatures;
|