@leancodepl/folder-structure-cruiser 10.3.0 → 10.4.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  All notable changes to this project will be documented in this file. See
4
4
  [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [10.4.0](https://github.com/leancodepl/js_corelibrary/compare/v10.3.1...v10.4.0) (2026-06-23)
7
+
8
+ ### Features
9
+
10
+ - skip opaque segments and external dependencies in cross-feature-imports check
11
+ ([b2cb6dd](https://github.com/leancodepl/js_corelibrary/commit/b2cb6ddf79345444c5dd20bdaadcb8901c99e510))
12
+
13
+ # Change Log
14
+
15
+ All notable changes to this project will be documented in this file. See
16
+ [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
17
+
18
+ ## [10.3.1](https://github.com/leancodepl/js_corelibrary/compare/v10.3.0...v10.3.1) (2026-05-20)
19
+
20
+ ### Bug Fixes
21
+
22
+ - cross feature imports test
23
+ ([ef8c45a](https://github.com/leancodepl/js_corelibrary/commit/ef8c45adec390da0032987d7bd29338e433e45e1))
24
+ - **folder-structure-cruiser:** address review feedback
25
+ ([4714a5b](https://github.com/leancodepl/js_corelibrary/commit/4714a5b8106a670adbfa921742c5ce91fdb334cd))
26
+ - **folder-structure-cruiser:** return non-zero exit code on violations
27
+ ([1ad84d5](https://github.com/leancodepl/js_corelibrary/commit/1ad84d5720396e12a1ccc64eacb85fe1e9e034cb))
28
+
29
+ # Change Log
30
+
31
+ All notable changes to this project will be documented in this file. See
32
+ [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
33
+
6
34
  # [10.3.0](https://github.com/leancodepl/js_corelibrary/compare/v10.2.1...v10.3.0) (2026-04-16)
7
35
 
8
36
  **Note:** Version bump only for package @leancodepl/folder-structure-cruiser
package/README.md CHANGED
@@ -22,7 +22,7 @@ Validates cross-feature nested imports according to folder structure rules.
22
22
  - `tsConfigPath: string` - Optional path to TypeScript configuration file for enhanced type resolution
23
23
  - `webpackConfigPath?: string` - Optional path to webpack configuration file for webpack alias resolution
24
24
 
25
- **Returns:** `Promise<void>` - The function doesn't return a value but outputs results to console
25
+ **Returns:** `Promise<number>` - Number of detected violations
26
26
 
27
27
  **Throws:** `Error` - Throws an error if the dependency analysis fails or configuration is invalid
28
28
 
@@ -38,7 +38,7 @@ Validates if shared components are located at the first shared level.
38
38
  - `tsConfigPath: string` - Optional path to TypeScript configuration file for enhanced type resolution
39
39
  - `webpackConfigPath?: string` - Optional path to webpack configuration file for webpack alias resolution
40
40
 
41
- **Returns:** `Promise<void>` - The function doesn't return a value but outputs results to console
41
+ **Returns:** `Promise<number>` - Number of detected violations
42
42
 
43
43
  **Throws:** `Error` - Throws an error if the dependency analysis fails or configuration is invalid
44
44
 
@@ -97,6 +97,10 @@ Imports are allowed only if they meet one of these conditions:
97
97
  2. **Sibling's immediate child**: Import is from an immediate sibling's or ancestors siblings' child
98
98
  - ✅ `src/feature1/subfeature1/ComponentA` → `src/feature1/subfeature2/ComponentB`
99
99
 
100
+ 3. **Through an opaque directory**: any directory whose name ends with an underscore is [opaque](#opaque-directories)
101
+ and doesn't count toward the nesting depth
102
+ - ✅ `src/feature1/ComponentA` → `src/features_/feature2/ComponentB`
103
+
100
104
  ### Shared Component Detection
101
105
 
102
106
  Identifies components that should be moved to shared levels when:
@@ -116,6 +120,23 @@ Create a `.dependency-cruiser.json` file in your project root:
116
120
 
117
121
  This configuration serves as a base config that can be extended with your own rules.
118
122
 
123
+ ### Opaque directories
124
+
125
+ Some directories only group features together (e.g. a `features/` folder that holds every feature) rather than being a
126
+ feature themselves. Counting them toward the nesting depth would make every cross-feature import look one level too
127
+ deep. Mark such a directory as **opaque** by ending its name with an underscore — `features_`, `modules_`, etc. Opaque
128
+ segments are skipped when measuring how deep an import reaches, so the directory behaves as if it weren't there.
129
+
130
+ This needs no configuration; it's driven purely by the directory name. With a `src/features_/` container:
131
+
132
+ - ✅ `src/features_/featureA/X` → `src/features_/featureB/Y` (`features_` is skipped, so these read as sibling features)
133
+ - ✅ `src/featureA/X` → `src/features_/featureB/Y` (`featureB` is a top-level feature once `features_` is skipped)
134
+ - ❌ `src/featureA/X` → `src/features_/featureB/internal/Y` (`internal/Y` is still a grandchild of `featureB`, one level
135
+ too deep)
136
+
137
+ The trailing-underscore convention is matched verbatim, so a sibling directory like `features` (no underscore) keeps
138
+ counting normally.
139
+
119
140
  ## Nx Configuration
120
141
 
121
142
  Configure folder-structure-cruiser commands as Nx target in your `project.json`. Example configuration:
package/dist/bin.cjs CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const o=require("commander"),r=require("./validateSharedComponent-BqvIbK3d.cjs");o.program.name("folder-structure-cruiser").description("CLI tool for validating folder structure rules");o.program.command("validate-shared-components").description("Validate if shared components are located at the first shared level").option("-d, --directory <dir>","Directory to analyze",".").option("-c, --config <path_to_config>","Path to config file").option("-t, --tsConfig <path_to_ts_config>","Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>","Path to webpack config file").action(async t=>{const e=t.directory?[t.directory]:[".*"],i=t.config??"",a=t.tsConfig,c=t.webpackConfig;await r.validateSharedComponent({directories:e,configPath:i,tsConfigPath:a,webpackConfigPath:c})});o.program.command("validate-cross-feature-imports").description("Validate if cross-feature nested imports are allowed").option("-d, --directory <dir>","Directory to analyze",".").option("-c, --config <path_to_config>","Path to config file").option("-t, --tsConfig <path_to_ts_config>","Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>","Path to webpack config file").action(async t=>{const e=t.directory?[t.directory]:[".*"],i=t.config??"",a=t.tsConfig,c=t.webpackConfig;await r.validateCrossFeatureImports({directories:e,configPath:i,tsConfigPath:a,webpackConfigPath:c})});o.program.parse();
2
+ "use strict";const t=require("commander"),r=require("./validateSharedComponent-BHinLxNm.cjs");t.program.name("folder-structure-cruiser").description("CLI tool for validating folder structure rules");function s(o){o>0&&(process.exitCode=1)}function f(o){r.logger.error(o instanceof Error?o:new Error(String(o))),process.exitCode=1}t.program.command("validate-shared-components").description("Validate if shared components are located at the first shared level").option("-d, --directory <dir>","Directory to analyze",".").option("-c, --config <path_to_config>","Path to config file").option("-t, --tsConfig <path_to_ts_config>","Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>","Path to webpack config file").action(async o=>{const e=o.directory?[o.directory]:[".*"],i=o.config??"",a=o.tsConfig,n=o.webpackConfig,c=await r.validateSharedComponent({directories:e,configPath:i,tsConfigPath:a,webpackConfigPath:n});s(c)});t.program.command("validate-cross-feature-imports").description("Validate if cross-feature nested imports are allowed").option("-d, --directory <dir>","Directory to analyze",".").option("-c, --config <path_to_config>","Path to config file").option("-t, --tsConfig <path_to_ts_config>","Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>","Path to webpack config file").action(async o=>{const e=o.directory?[o.directory]:[".*"],i=o.config??"",a=o.tsConfig,n=o.webpackConfig,c=await r.validateCrossFeatureImports({directories:e,configPath:i,tsConfigPath:a,webpackConfigPath:n});s(c)});t.program.parseAsync().catch(f);
package/dist/bin.js CHANGED
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { program as o } from "commander";
3
- import { a as n, v as r } from "./validateSharedComponent-YmSG2wR9.js";
4
- o.name("folder-structure-cruiser").description("CLI tool for validating folder structure rules");
5
- o.command("validate-shared-components").description("Validate if shared components are located at the first shared level").option("-d, --directory <dir>", "Directory to analyze", ".").option("-c, --config <path_to_config>", "Path to config file").option("-t, --tsConfig <path_to_ts_config>", "Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>", "Path to webpack config file").action(async (t) => {
6
- const i = t.directory ? [t.directory] : [".*"], a = t.config ?? "", e = t.tsConfig, c = t.webpackConfig;
7
- await n({ directories: i, configPath: a, tsConfigPath: e, webpackConfigPath: c });
2
+ import { program as t } from "commander";
3
+ import { a as s, v as f, l as d } from "./validateSharedComponent-D24GPyRZ.js";
4
+ t.name("folder-structure-cruiser").description("CLI tool for validating folder structure rules");
5
+ function r(o) {
6
+ o > 0 && (process.exitCode = 1);
7
+ }
8
+ function g(o) {
9
+ d.error(o instanceof Error ? o : new Error(String(o))), process.exitCode = 1;
10
+ }
11
+ t.command("validate-shared-components").description("Validate if shared components are located at the first shared level").option("-d, --directory <dir>", "Directory to analyze", ".").option("-c, --config <path_to_config>", "Path to config file").option("-t, --tsConfig <path_to_ts_config>", "Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>", "Path to webpack config file").action(async (o) => {
12
+ const i = o.directory ? [o.directory] : [".*"], a = o.config ?? "", e = o.tsConfig, n = o.webpackConfig, c = await s({ directories: i, configPath: a, tsConfigPath: e, webpackConfigPath: n });
13
+ r(c);
8
14
  });
9
- o.command("validate-cross-feature-imports").description("Validate if cross-feature nested imports are allowed").option("-d, --directory <dir>", "Directory to analyze", ".").option("-c, --config <path_to_config>", "Path to config file").option("-t, --tsConfig <path_to_ts_config>", "Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>", "Path to webpack config file").action(async (t) => {
10
- const i = t.directory ? [t.directory] : [".*"], a = t.config ?? "", e = t.tsConfig, c = t.webpackConfig;
11
- await r({ directories: i, configPath: a, tsConfigPath: e, webpackConfigPath: c });
15
+ t.command("validate-cross-feature-imports").description("Validate if cross-feature nested imports are allowed").option("-d, --directory <dir>", "Directory to analyze", ".").option("-c, --config <path_to_config>", "Path to config file").option("-t, --tsConfig <path_to_ts_config>", "Path to ts config file").option("-w, --webpackConfig <path_to_webpack_config>", "Path to webpack config file").action(async (o) => {
16
+ const i = o.directory ? [o.directory] : [".*"], a = o.config ?? "", e = o.tsConfig, n = o.webpackConfig, c = await f({ directories: i, configPath: a, tsConfigPath: e, webpackConfigPath: n });
17
+ r(c);
12
18
  });
13
- o.parse();
19
+ t.parseAsync().catch(g);
@@ -2,10 +2,11 @@ import { CruiseParams } from '../lib/getCruiseResult.js';
2
2
  /**
3
3
  * Validates cross-feature nested imports according to folder structure rules.
4
4
  *
5
- * This function analyzes the codebase using dependency-cruiser to identify violations
6
- * of cross-feature import restrictions. It checks if modules with multiple dependents
7
- * are properly structured to avoid cross-feature nested imports that violate the
8
- * established folder structure rules.
5
+ * Analyzes the module graph of the given directories and reports imports that
6
+ * reach into another feature deeper than its immediate children. Directories
7
+ * whose name ends with an underscore (e.g. `features_`) are opaque and don't
8
+ * count toward the nesting depth. Imports of Node built-ins and npm packages
9
+ * are left out of the analysis.
9
10
  *
10
11
  * The function will output violations to the console, showing which modules have
11
12
  * cross-feature import issues that need to be resolved.
@@ -16,7 +17,7 @@ import { CruiseParams } from '../lib/getCruiseResult.js';
16
17
  * @param cruiseParams.tsConfigPath - Optional path to TypeScript configuration file for enhanced type resolution
17
18
  * @param cruiseParams.webpackConfigPath - Optional path to webpack configuration file for webpack alias resolution
18
19
  *
19
- * @returns Promise<void> - The function doesn't return a value but outputs results to console
20
+ * @returns Promise<number> - Number of detected violations
20
21
  *
21
22
  * @throws {Error} - Throws an error if the dependency analysis fails or configuration is invalid
22
23
  *
@@ -56,5 +57,5 @@ import { CruiseParams } from '../lib/getCruiseResult.js';
56
57
  * }
57
58
  * ```
58
59
  */
59
- export declare function validateCrossFeatureImports(cruiseParams: CruiseParams): Promise<void>;
60
+ export declare function validateCrossFeatureImports(cruiseParams: CruiseParams): Promise<number>;
60
61
  //# sourceMappingURL=validateCrossFeatureImports.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateCrossFeatureImports.d.ts","sourceRoot":"","sources":["../../src/commands/validateCrossFeatureImports.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAmB,MAAM,2BAA2B,CAAA;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,2BAA2B,CAAC,YAAY,EAAE,YAAY,iBAkB3E"}
1
+ {"version":3,"file":"validateCrossFeatureImports.d.ts","sourceRoot":"","sources":["../../src/commands/validateCrossFeatureImports.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAmB,MAAM,2BAA2B,CAAA;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAsB,2BAA2B,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB7F"}
@@ -16,7 +16,7 @@ import { CruiseParams } from '../lib/getCruiseResult.js';
16
16
  * @param cruiseParams.tsConfigPath - Optional path to TypeScript configuration file for enhanced type resolution
17
17
  * @param cruiseParams.webpackConfigPath - Optional path to webpack configuration file for webpack alias resolution
18
18
  *
19
- * @returns Promise<void> - The function doesn't return a value but outputs results to console
19
+ * @returns Promise<number> - Number of detected violations
20
20
  *
21
21
  * @throws {Error} - Throws an error if the dependency analysis fails or configuration is invalid
22
22
  *
@@ -56,5 +56,5 @@ import { CruiseParams } from '../lib/getCruiseResult.js';
56
56
  * }
57
57
  * ```
58
58
  */
59
- export declare function validateSharedComponent(cruiseParams: CruiseParams): Promise<void>;
59
+ export declare function validateSharedComponent(cruiseParams: CruiseParams): Promise<number>;
60
60
  //# sourceMappingURL=validateSharedComponent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateSharedComponent.d.ts","sourceRoot":"","sources":["../../src/commands/validateSharedComponent.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAmB,MAAM,2BAA2B,CAAA;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,YAAY,iBAkBvE"}
1
+ {"version":3,"file":"validateSharedComponent.d.ts","sourceRoot":"","sources":["../../src/commands/validateSharedComponent.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAmB,MAAM,2BAA2B,CAAA;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBzF"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./validateSharedComponent-BqvIbK3d.cjs");exports.validateCrossFeatureImports=e.validateCrossFeatureImports;exports.validateSharedComponent=e.validateSharedComponent;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./validateSharedComponent-BHinLxNm.cjs");exports.validateCrossFeatureImports=e.validateCrossFeatureImports;exports.validateSharedComponent=e.validateSharedComponent;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { v as o, a as r } from "./validateSharedComponent-YmSG2wR9.js";
1
+ import { v as o, a as r } from "./validateSharedComponent-D24GPyRZ.js";
2
2
  export {
3
3
  o as validateCrossFeatureImports,
4
4
  r as validateSharedComponent
@@ -1 +1 @@
1
- {"version":3,"file":"checkCrossFeatureImports.d.ts","sourceRoot":"","sources":["../../src/lib/checkCrossFeatureImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEpD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAE7C,KAAK,WAAW,GAAG;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAqC7E"}
1
+ {"version":3,"file":"checkCrossFeatureImports.d.ts","sourceRoot":"","sources":["../../src/lib/checkCrossFeatureImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEpD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAE7C,KAAK,WAAW,GAAG;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CA0C7E"}
@@ -0,0 +1,3 @@
1
+ "use strict";const x=require("chalk"),P=require("dependency-cruiser"),y=require("dependency-cruiser/config-utl/extract-depcruise-options"),$=require("dependency-cruiser/config-utl/extract-ts-config"),q=require("dependency-cruiser/config-utl/extract-webpack-resolve-config"),F=require("@leancodepl/logger/cli");function g(e){if(e.length===0)return[];if(e.length===1)return e[0];const t=[],n=Math.min(...e.map(s=>s.length));for(let s=0;s<n;s++){const o=e[0][s];if(e.every(r=>r[s]===o))t.push(o);else break}return t}function p(e){return g(e).length}function M(e){const t=typeof e.output=="object"?e.output:void 0,n=t?.modules??[],s=[];for(const o of n){if(o.coreModule)continue;const r=o.dependencies||[],i=f(o.source.split("/"));r.forEach(u=>{if(k(u.dependencyTypes))return;const l=f(u.resolved.split("/")),c=p([i,l]);(!c||c<i.length&&l.length>c+j)&&s.push({source:o.source,target:u.resolved,rule:"cross-feature-nested-imports",severity:"error"})})}return{messages:s,totalCruised:t?.summary.totalCruised??0}}const j=2;function k(e){return e?.some(t=>t==="core"||t==="unknown"||t.startsWith("npm"))??!1}function w(e){return/[^_]_$/.test(e)}function f(e){return e.filter(t=>!w(t))}const{bold:m}=x;function h(e){return e.map(t=>`${t.rule}: ${m(t.source)} → ${m(t.target)}`)}async function C({directories:e=[".*"],configPath:t,tsConfigPath:n,webpackConfigPath:s}){const o=await y(t),r=s?await q(s):void 0,i=n?$(n):void 0;return await P.cruise(e,{...o},r,{tsConfig:i})}const a=F.createCliLogger();async function L(e){const t=await C(e),{messages:n,totalCruised:s}=M(t);if(n.length===0&&a.success("✅ No issues found!"),n.length>0){const o=h(n);a.error(o.join(`
2
+ `)),a.error(`Found ${n.length} violation(s). ${s} modules cruised.`)}return n.length}function R(e,t,n){const s=e.at(-1)??"";if(!/index\.(tsx?|jsx?|ts|js)$/.test(s))return!1;const o=e.at(-3)??"",r=t.at(-1);return o===r&&n===e.length-2}function S(e){const t=typeof e.output=="object"?e.output:void 0,n=t?.modules??[],s=[];for(const o of n){if(o.coreModule)continue;const r=o.dependents||[];if(r.length<=1)continue;const i=o.source.split("/"),u=i.length;if(u<=2)continue;const l=r.map(v=>v.split("/")),c=g(l),d=p([i,c]);R(i,c,d)||d<u-1&&s.push({source:o.source,target:c.join("/"),rule:"not-shared-level",severity:"info"})}return{messages:s,totalCruised:t?.summary.totalCruised??0}}async function b(e){const t=await C(e),{messages:n,totalCruised:s}=S(t);if(n.length===0&&a.success("✅ No issues found!"),n.length>0){const o=h(n);a.info(o.join(`
3
+ `)),a.info(`Found ${n.length} violation(s). ${s} modules cruised.`)}return n.length}exports.logger=a;exports.validateCrossFeatureImports=L;exports.validateSharedComponent=b;
@@ -0,0 +1,125 @@
1
+ import C from "chalk";
2
+ import { cruise as P } from "dependency-cruiser";
3
+ import y from "dependency-cruiser/config-utl/extract-depcruise-options";
4
+ import $ from "dependency-cruiser/config-utl/extract-ts-config";
5
+ import M from "dependency-cruiser/config-utl/extract-webpack-resolve-config";
6
+ import { createCliLogger as j } from "@leancodepl/logger/cli";
7
+ function p(e) {
8
+ if (e.length === 0) return [];
9
+ if (e.length === 1) return e[0];
10
+ const t = [], o = Math.min(...e.map((n) => n.length));
11
+ for (let n = 0; n < o; n++) {
12
+ const s = e[0][n];
13
+ if (e.every((r) => r[n] === s))
14
+ t.push(s);
15
+ else
16
+ break;
17
+ }
18
+ return t;
19
+ }
20
+ function g(e) {
21
+ return p(e).length;
22
+ }
23
+ function F(e) {
24
+ const t = typeof e.output == "object" ? e.output : void 0, o = t?.modules ?? [], n = [];
25
+ for (const s of o) {
26
+ if (s.coreModule)
27
+ continue;
28
+ const r = s.dependencies || [], i = m(s.source.split("/"));
29
+ r.forEach((u) => {
30
+ if (w(u.dependencyTypes))
31
+ return;
32
+ const l = m(u.resolved.split("/")), c = g([i, l]);
33
+ (!c || c < i.length && l.length > c + k) && n.push({
34
+ source: s.source,
35
+ target: u.resolved,
36
+ rule: "cross-feature-nested-imports",
37
+ severity: "error"
38
+ });
39
+ });
40
+ }
41
+ return {
42
+ messages: n,
43
+ totalCruised: t?.summary.totalCruised ?? 0
44
+ };
45
+ }
46
+ const k = 2;
47
+ function w(e) {
48
+ return e?.some((t) => t === "core" || t === "unknown" || t.startsWith("npm")) ?? !1;
49
+ }
50
+ function L(e) {
51
+ return /[^_]_$/.test(e);
52
+ }
53
+ function m(e) {
54
+ return e.filter((t) => !L(t));
55
+ }
56
+ const { bold: d } = C;
57
+ function h(e) {
58
+ return e.map((t) => `${t.rule}: ${d(t.source)} → ${d(t.target)}`);
59
+ }
60
+ async function x({
61
+ directories: e = [".*"],
62
+ configPath: t,
63
+ tsConfigPath: o,
64
+ webpackConfigPath: n
65
+ }) {
66
+ const s = await y(t), r = n ? await M(n) : void 0, i = o ? $(o) : void 0;
67
+ return await P(e, { ...s }, r, {
68
+ tsConfig: i
69
+ });
70
+ }
71
+ const a = j();
72
+ async function E(e) {
73
+ const t = await x(e), { messages: o, totalCruised: n } = F(t);
74
+ if (o.length === 0 && a.success("✅ No issues found!"), o.length > 0) {
75
+ const s = h(o);
76
+ a.error(s.join(`
77
+ `)), a.error(`Found ${o.length} violation(s). ${n} modules cruised.`);
78
+ }
79
+ return o.length;
80
+ }
81
+ function R(e, t, o) {
82
+ const n = e.at(-1) ?? "";
83
+ if (!/index\.(tsx?|jsx?|ts|js)$/.test(n))
84
+ return !1;
85
+ const s = e.at(-3) ?? "", r = t.at(-1);
86
+ return s === r && o === e.length - 2;
87
+ }
88
+ function b(e) {
89
+ const t = typeof e.output == "object" ? e.output : void 0, o = t?.modules ?? [], n = [];
90
+ for (const s of o) {
91
+ if (s.coreModule)
92
+ continue;
93
+ const r = s.dependents || [];
94
+ if (r.length <= 1)
95
+ continue;
96
+ const i = s.source.split("/"), u = i.length;
97
+ if (u <= 2)
98
+ continue;
99
+ const l = r.map((v) => v.split("/")), c = p(l), f = g([i, c]);
100
+ R(i, c, f) || f < u - 1 && n.push({
101
+ source: s.source,
102
+ target: c.join("/"),
103
+ rule: "not-shared-level",
104
+ severity: "info"
105
+ });
106
+ }
107
+ return {
108
+ messages: n,
109
+ totalCruised: t?.summary.totalCruised ?? 0
110
+ };
111
+ }
112
+ async function T(e) {
113
+ const t = await x(e), { messages: o, totalCruised: n } = b(t);
114
+ if (o.length === 0 && a.success("✅ No issues found!"), o.length > 0) {
115
+ const s = h(o);
116
+ a.info(s.join(`
117
+ `)), a.info(`Found ${o.length} violation(s). ${n} modules cruised.`);
118
+ }
119
+ return o.length;
120
+ }
121
+ export {
122
+ T as a,
123
+ a as l,
124
+ E as v
125
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leancodepl/folder-structure-cruiser",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@
19
19
  "folder-structure-cruiser": "./dist/bin.js"
20
20
  },
21
21
  "dependencies": {
22
- "@leancodepl/logger": "10.3.0",
22
+ "@leancodepl/logger": "10.4.0",
23
23
  "chalk": ">=5.0.0",
24
24
  "commander": "^14.0.0"
25
25
  },
@@ -1,3 +0,0 @@
1
- "use strict";const v=require("chalk"),x=require("dependency-cruiser"),y=require("dependency-cruiser/config-utl/extract-depcruise-options"),P=require("dependency-cruiser/config-utl/extract-ts-config"),F=require("dependency-cruiser/config-utl/extract-webpack-resolve-config"),M=require("@leancodepl/logger/cli");function m(e){if(e.length===0)return[];if(e.length===1)return e[0];const t=[],s=Math.min(...e.map(o=>o.length));for(let o=0;o<s;o++){const n=e[0][o];if(e.every(r=>r[o]===n))t.push(n);else break}return t}function g(e){return m(e).length}function $(e){const t=typeof e.output=="object"?e.output:void 0,s=t?.modules??[],o=[];for(const n of s){if(n.coreModule)continue;const r=n.dependencies||[],i=n.source.split("/");r.forEach(a=>{const l=a.resolved.split("/"),u=g([i,l]);(!u||u<i.length&&l.length>u+2)&&o.push({source:n.source,target:a.resolved,rule:"cross-feature-nested-imports",severity:"error"})})}return{messages:o,totalCruised:t?.summary.totalCruised??0}}const{bold:f}=v;function p(e){return e.map(t=>`${t.rule}: ${f(t.source)} → ${f(t.target)}`)}async function h({directories:e=[".*"],configPath:t,tsConfigPath:s,webpackConfigPath:o}){const n=await y(t),r=o?await F(o):void 0,i=s?P(s):void 0;return await x.cruise(e,{...n},r,{tsConfig:i})}const c=M.createCliLogger();async function j(e){try{const t=await h(e),{messages:s,totalCruised:o}=$(t);if(s.length===0&&c.success("✅ No issues found!"),s.length>0){const n=p(s);c.error(n.join(`
2
- `)),c.error(`Found ${s.length} violations(s). ${o} modules cruised.`)}}catch(t){c.error(t)}}function q(e,t,s){const o=e.at(-1)??"";if(!/index\.(tsx?|jsx?|ts|js)$/.test(o))return!1;const n=e.at(-3)??"",r=t.at(-1);return n===r&&s===e.length-2}function L(e){const t=typeof e.output=="object"?e.output:void 0,s=t?.modules??[],o=[];for(const n of s){if(n.coreModule)continue;const r=n.dependents||[];if(r.length<=1)continue;const i=n.source.split("/"),a=i.length;if(a<=2)continue;const l=r.map(C=>C.split("/")),u=m(l),d=g([i,u]);q(i,u,d)||d<a-1&&o.push({source:n.source,target:u.join("/"),rule:"not-shared-level",severity:"info"})}return{messages:o,totalCruised:t?.summary.totalCruised??0}}async function R(e){try{const t=await h(e),{messages:s,totalCruised:o}=L(t);if(s.length===0&&c.success("✅ No issues found!"),s.length>0){const n=p(s);c.info(n.join(`
3
- `)),c.info(`Found ${s.length} violations(s). ${o} modules cruised.`)}}catch(t){c.error(t)}}exports.validateCrossFeatureImports=j;exports.validateSharedComponent=R;
@@ -1,118 +0,0 @@
1
- import C from "chalk";
2
- import { cruise as x } from "dependency-cruiser";
3
- import P from "dependency-cruiser/config-utl/extract-depcruise-options";
4
- import y from "dependency-cruiser/config-utl/extract-ts-config";
5
- import M from "dependency-cruiser/config-utl/extract-webpack-resolve-config";
6
- import { createCliLogger as $ } from "@leancodepl/logger/cli";
7
- function m(e) {
8
- if (e.length === 0) return [];
9
- if (e.length === 1) return e[0];
10
- const t = [], s = Math.min(...e.map((o) => o.length));
11
- for (let o = 0; o < s; o++) {
12
- const n = e[0][o];
13
- if (e.every((r) => r[o] === n))
14
- t.push(n);
15
- else
16
- break;
17
- }
18
- return t;
19
- }
20
- function p(e) {
21
- return m(e).length;
22
- }
23
- function j(e) {
24
- const t = typeof e.output == "object" ? e.output : void 0, s = t?.modules ?? [], o = [];
25
- for (const n of s) {
26
- if (n.coreModule)
27
- continue;
28
- const r = n.dependencies || [], i = n.source.split("/");
29
- r.forEach((a) => {
30
- const l = a.resolved.split("/"), u = p([i, l]);
31
- (!u || u < i.length && l.length > u + 2) && o.push({
32
- source: n.source,
33
- target: a.resolved,
34
- rule: "cross-feature-nested-imports",
35
- severity: "error"
36
- });
37
- });
38
- }
39
- return {
40
- messages: o,
41
- totalCruised: t?.summary.totalCruised ?? 0
42
- };
43
- }
44
- const { bold: d } = C;
45
- function g(e) {
46
- return e.map((t) => `${t.rule}: ${d(t.source)} → ${d(t.target)}`);
47
- }
48
- async function h({
49
- directories: e = [".*"],
50
- configPath: t,
51
- tsConfigPath: s,
52
- webpackConfigPath: o
53
- }) {
54
- const n = await P(t), r = o ? await M(o) : void 0, i = s ? y(s) : void 0;
55
- return await x(e, { ...n }, r, {
56
- tsConfig: i
57
- });
58
- }
59
- const c = $();
60
- async function N(e) {
61
- try {
62
- const t = await h(e), { messages: s, totalCruised: o } = j(t);
63
- if (s.length === 0 && c.success("✅ No issues found!"), s.length > 0) {
64
- const n = g(s);
65
- c.error(n.join(`
66
- `)), c.error(`Found ${s.length} violations(s). ${o} modules cruised.`);
67
- }
68
- } catch (t) {
69
- c.error(t);
70
- }
71
- }
72
- function F(e, t, s) {
73
- const o = e.at(-1) ?? "";
74
- if (!/index\.(tsx?|jsx?|ts|js)$/.test(o))
75
- return !1;
76
- const n = e.at(-3) ?? "", r = t.at(-1);
77
- return n === r && s === e.length - 2;
78
- }
79
- function L(e) {
80
- const t = typeof e.output == "object" ? e.output : void 0, s = t?.modules ?? [], o = [];
81
- for (const n of s) {
82
- if (n.coreModule)
83
- continue;
84
- const r = n.dependents || [];
85
- if (r.length <= 1)
86
- continue;
87
- const i = n.source.split("/"), a = i.length;
88
- if (a <= 2)
89
- continue;
90
- const l = r.map((v) => v.split("/")), u = m(l), f = p([i, u]);
91
- F(i, u, f) || f < a - 1 && o.push({
92
- source: n.source,
93
- target: u.join("/"),
94
- rule: "not-shared-level",
95
- severity: "info"
96
- });
97
- }
98
- return {
99
- messages: o,
100
- totalCruised: t?.summary.totalCruised ?? 0
101
- };
102
- }
103
- async function S(e) {
104
- try {
105
- const t = await h(e), { messages: s, totalCruised: o } = L(t);
106
- if (s.length === 0 && c.success("✅ No issues found!"), s.length > 0) {
107
- const n = g(s);
108
- c.info(n.join(`
109
- `)), c.info(`Found ${s.length} violations(s). ${o} modules cruised.`);
110
- }
111
- } catch (t) {
112
- c.error(t);
113
- }
114
- }
115
- export {
116
- S as a,
117
- N as v
118
- };