From e6b65367bdcf68415ec1747dc9b3b0134cff8281 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 May 2026 10:00:45 +0000 Subject: [PATCH 1/5] feat(folder-structure-cruiser): allow imports from direct children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Komoszyński --- packages/folder-structure-cruiser/README.md | 4 ++ .../__tests__/cross-feature-imports.spec.ts | 15 +++++ packages/folder-structure-cruiser/src/bin.ts | 15 ++++- .../commands/validateCrossFeatureImports.ts | 11 +++- .../src/lib/checkCrossFeatureImports.ts | 55 ++++++++++++++++++- 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/packages/folder-structure-cruiser/README.md b/packages/folder-structure-cruiser/README.md index 1ef98a08..11c9ff90 100644 --- a/packages/folder-structure-cruiser/README.md +++ b/packages/folder-structure-cruiser/README.md @@ -21,6 +21,7 @@ Validates cross-feature nested imports according to folder structure rules. - `configPath: string` - Path to the dependency-cruiser configuration file (e.g., `.dependency-cruiser.js`) - `tsConfigPath: string` - Optional path to TypeScript configuration file for enhanced type resolution - `webpackConfigPath?: string` - Optional path to webpack configuration file for webpack alias resolution + - `allowImportsFromDirectChildrenOf?: string[]` - Optional list of directories whose direct children can be imported **Returns:** `Promise` - The function doesn't return a value but outputs results to console @@ -52,6 +53,9 @@ npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --direct # With both TypeScript and webpack config npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --directory "packages/admin" --config "./.dependency-cruiser.json" --tsConfig "./tsconfig.base.json" --webpackConfig "./webpack.config.js" + +# Allow imports from direct children of selected folders +npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --directory "packages/admin" --config "./.dependency-cruiser.json" --allow-imports-from-direct-children-of "src/features" ``` ### Shared Component Validation diff --git a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts index 8bf9f316..3b043f3a 100644 --- a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts +++ b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts @@ -50,6 +50,21 @@ describe("cross-feature-imports validation", () => { ) }, 30000) + it("should allow imports from direct children of configured folder", async () => { + const dirname = import.meta.dirname + const testDir = join(dirname, "test-structure") + const filePath = join(testDir, "polls/SnapshotPollEditor/index.tsx") + const configPath = join(dirname, "../.dependency-cruiser.json") + + await validateCrossFeatureImports({ + directories: [filePath], + configPath: configPath, + allowImportsFromDirectChildrenOf: ["surveys"], + }) + + expect(consoleErrorSpy).not.toHaveBeenCalled() + }, 30000) + it("should detect violations in ActivityEditor (nested sibling child import)", async () => { const dirname = import.meta.dirname const testDir = join(dirname, "test-structure") diff --git a/packages/folder-structure-cruiser/src/bin.ts b/packages/folder-structure-cruiser/src/bin.ts index 88d79787..2d5d8a1d 100644 --- a/packages/folder-structure-cruiser/src/bin.ts +++ b/packages/folder-structure-cruiser/src/bin.ts @@ -30,13 +30,24 @@ program .option("-c, --config ", "Path to config file") .option("-t, --tsConfig ", "Path to ts config file") .option("-w, --webpackConfig ", "Path to webpack config file") + .option( + "-a, --allow-imports-from-direct-children-of ", + "Allow imports from direct children of listed directories", + ) .action(async options => { const directories = options.directory ? [options.directory] : [".*"] const configPath = options.config ?? "" const tsConfigPath = options.tsConfig const webpackConfigPath = options.webpackConfig - - await validateCrossFeatureImports({ directories, configPath, tsConfigPath, webpackConfigPath }) + const allowImportsFromDirectChildrenOf = options.allowImportsFromDirectChildrenOf + + await validateCrossFeatureImports({ + directories, + configPath, + tsConfigPath, + webpackConfigPath, + allowImportsFromDirectChildrenOf, + }) }) program.parse() diff --git a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts index 42d87cec..4fe4cc5f 100644 --- a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts +++ b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts @@ -3,6 +3,10 @@ import { formatMessages } from "../lib/formatMessages.js" import { CruiseParams, getCruiseResult } from "../lib/getCruiseResult.js" import { logger } from "../lib/logger.js" +export type ValidateCrossFeatureImportsParams = CruiseParams & { + allowImportsFromDirectChildrenOf?: string[] +} + /** * Validates cross-feature nested imports according to folder structure rules. * @@ -19,6 +23,7 @@ import { logger } from "../lib/logger.js" * @param cruiseParams.configPath - Path to the dependency-cruiser configuration file (e.g., .dependency-cruiser.js) * @param cruiseParams.tsConfigPath - Optional path to TypeScript configuration file for enhanced type resolution * @param cruiseParams.webpackConfigPath - Optional path to webpack configuration file for webpack alias resolution + * @param cruiseParams.allowImportsFromDirectChildrenOf - Optional directory paths whose direct children can be imported * * @returns Promise - The function doesn't return a value but outputs results to console * @@ -60,11 +65,13 @@ import { logger } from "../lib/logger.js" * } * ``` */ -export async function validateCrossFeatureImports(cruiseParams: CruiseParams) { +export async function validateCrossFeatureImports(cruiseParams: ValidateCrossFeatureImportsParams) { try { const cruiseResult = await getCruiseResult(cruiseParams) - const { messages: errorMessages, totalCruised } = checkCrossFeatureImports(cruiseResult) + const { messages: errorMessages, totalCruised } = checkCrossFeatureImports(cruiseResult, { + allowImportsFromDirectChildrenOf: cruiseParams.allowImportsFromDirectChildrenOf, + }) if (errorMessages.length === 0) { logger.success("✅ No issues found!") diff --git a/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts b/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts index 2b40c06d..595f310d 100644 --- a/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts +++ b/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts @@ -3,10 +3,55 @@ import { findCommonPathsPrefixLength } from "./findCommonPathsPrefix.js" import { Message } from "./formatMessages.js" type CheckResult = { messages: Message[]; totalCruised: number } +type CheckCrossFeatureImportsOptions = { + allowImportsFromDirectChildrenOf?: string[] +} + +const INDEX_FILE_PATTERN = /^index(?:\..+)?$/ + +function normalizePath(path: string): string[] { + return path.split("/").filter(segment => segment.length > 0 && segment !== ".") +} + +function findSubPathIndex(path: string[], subPath: string[]): number { + if (subPath.length === 0 || path.length < subPath.length) { + return -1 + } + + for (let i = 0; i <= path.length - subPath.length; i++) { + const isMatch = subPath.every((segment, offset) => path[i + offset] === segment) + + if (isMatch) { + return i + } + } + + return -1 +} + +function isDirectChildImportOfDirectory(dependencyPath: string[], directoryPath: string[]): boolean { + const directoryPathStartIndex = findSubPathIndex(dependencyPath, directoryPath) + + if (directoryPathStartIndex < 0) { + return false + } + + const relativePathFromDirectory = dependencyPath.slice(directoryPathStartIndex + directoryPath.length) + + if (relativePathFromDirectory.length === 1) { + return true + } + + return relativePathFromDirectory.length === 2 && INDEX_FILE_PATTERN.test(relativePathFromDirectory[1]) +} -export function checkCrossFeatureImports(result: IReporterOutput): CheckResult { +export function checkCrossFeatureImports( + result: IReporterOutput, + options: CheckCrossFeatureImportsOptions = {}, +): CheckResult { const output = typeof result.output === "object" ? result.output : undefined const modules = output?.modules ?? [] + const allowedDirectChildrenDirectories = (options.allowImportsFromDirectChildrenOf ?? []).map(normalizePath) const errorMessages: Message[] = [] @@ -22,10 +67,14 @@ export function checkCrossFeatureImports(result: IReporterOutput): CheckResult { dependencies.forEach(dependency => { const dependencyPath = dependency.resolved.split("/") const commonPrefixPathLength = findCommonPathsPrefixLength([modulePath, dependencyPath]) + const shouldAllowDirectChildrenImport = allowedDirectChildrenDirectories.some(directoryPath => + isDirectChildImportOfDirectory(dependencyPath, directoryPath), + ) if ( - !commonPrefixPathLength || - (commonPrefixPathLength < modulePath.length && dependencyPath.length > commonPrefixPathLength + 2) + !shouldAllowDirectChildrenImport && + (!commonPrefixPathLength || + (commonPrefixPathLength < modulePath.length && dependencyPath.length > commonPrefixPathLength + 2)) ) { errorMessages.push({ source: module.source, From bc1a634d3200145cb1b5f32f31e78415792483b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 May 2026 10:03:02 +0000 Subject: [PATCH 2/5] test(folder-structure-cruiser): scope direct-children assertion --- .../__tests__/cross-feature-imports.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts index 3b043f3a..795c2260 100644 --- a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts +++ b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts @@ -62,7 +62,10 @@ describe("cross-feature-imports validation", () => { allowImportsFromDirectChildrenOf: ["surveys"], }) - expect(consoleErrorSpy).not.toHaveBeenCalled() + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining("SnapshotPollEditor/index.tsx → __tests__/test-structure/surveys/SurveyEditor/index.tsx"), + ) }, 30000) it("should detect violations in ActivityEditor (nested sibling child import)", async () => { From 8f8187f174a45827f7641e537927d35a4de0dc4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 May 2026 10:09:37 +0000 Subject: [PATCH 3/5] refactor(folder-structure-cruiser): read direct-child imports from config --- package-lock.json | 126 +++++++----------- packages/folder-structure-cruiser/README.md | 11 +- .../__tests__/cross-feature-imports.spec.ts | 3 +- .../allow-direct-children.config.json | 8 ++ .../folder-structure-cruiser/package.json | 3 +- packages/folder-structure-cruiser/src/bin.ts | 6 - .../commands/validateCrossFeatureImports.ts | 11 +- .../lib/getFolderStructureCruiserConfig.ts | 84 ++++++++++++ 8 files changed, 152 insertions(+), 100 deletions(-) create mode 100644 packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json create mode 100644 packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts diff --git a/package-lock.json b/package-lock.json index 7feb69fa..ceddffbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -261,7 +261,6 @@ "node_modules/@babel/core": { "version": "7.28.4", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2378,7 +2377,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2399,7 +2397,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2525,17 +2522,20 @@ "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { "version": "0.9.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@emotion/unitless": { "version": "0.10.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", @@ -5220,7 +5220,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -6084,7 +6083,6 @@ "version": "0.24.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime": "0.24.1", "@module-federation/webpack-bundler-runtime": "0.24.1" @@ -6221,7 +6219,6 @@ "version": "0.21.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime": "0.21.6", "@module-federation/webpack-bundler-runtime": "0.21.6" @@ -6595,7 +6592,6 @@ "version": "11.1.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -7754,7 +7750,6 @@ "version": "5.2.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -8092,7 +8087,6 @@ "resolved": "https://registry.npmjs.org/@openfeature/web-sdk/-/web-sdk-1.7.2.tgz", "integrity": "sha512-8QwhoxVNN2bFFkpWjbCyHCdkVjt/UTVn0o+OwcUUQoZnvPn46Oo1BxJQxUTibl/D/dAM/YQhxmg7ep7gYRxX4g==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@openfeature/core": "^1.9.0" } @@ -8102,7 +8096,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -9323,7 +9316,6 @@ "version": "1.6.8", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -9782,7 +9774,6 @@ "version": "8.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -9992,7 +9983,6 @@ "version": "1.9.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@swc-node/core": "^1.13.1", "@swc-node/sourcemap-support": "^0.5.0", @@ -10098,7 +10088,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.2", "@swc/types": "0.1.7" @@ -10314,7 +10303,6 @@ "version": "0.5.17", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -10415,7 +10403,6 @@ "node_modules/@tanstack/react-query": { "version": "5.90.2", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.2" }, @@ -10445,7 +10432,6 @@ "node_modules/@tanstack/react-router": { "version": "1.128.0", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/history": "1.121.34", "@tanstack/react-store": "^0.7.0", @@ -10488,6 +10474,7 @@ "node_modules/@tanstack/react-router-devtools/node_modules/@tanstack/history": { "version": "1.154.14", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10547,6 +10534,7 @@ "node_modules/@tanstack/react-router-devtools/node_modules/@tanstack/store": { "version": "0.8.0", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -10554,7 +10542,8 @@ }, "node_modules/@tanstack/react-router-devtools/node_modules/cookie-es": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tanstack/react-store": { "version": "0.7.7", @@ -10824,7 +10813,6 @@ "node_modules/@testing-library/react": { "version": "16.3.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -11055,7 +11043,6 @@ "node_modules/@types/eslint-plugin-jsx-a11y": { "version": "6.10.1", "license": "MIT", - "peer": true, "dependencies": { "eslint": "^9" } @@ -11181,7 +11168,6 @@ "node_modules/@types/node": { "version": "22.18.10", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -11205,7 +11191,6 @@ "version": "19.2.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -11214,7 +11199,6 @@ "version": "19.2.1", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -11284,7 +11268,8 @@ }, "node_modules/@types/stylis": { "version": "4.2.7", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -11317,7 +11302,6 @@ "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.0", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", @@ -11352,7 +11336,6 @@ "node_modules/@typescript-eslint/parser": { "version": "8.46.0", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -12609,7 +12592,6 @@ "version": "4.0.18", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -12664,7 +12646,6 @@ "version": "3.5.27", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.27", @@ -13128,7 +13109,6 @@ "node_modules/@zkochan/js-yaml": { "version": "0.0.7", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -13173,7 +13153,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -13201,11 +13180,13 @@ }, "node_modules/acorn-jsx-walk": { "version": "2.0.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/acorn-loose": { "version": "8.5.2", "license": "MIT", + "peer": true, "dependencies": { "acorn": "^8.15.0" }, @@ -13270,7 +13251,6 @@ "node_modules/ajv": { "version": "8.17.1", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13960,7 +13940,6 @@ "node_modules/axios": { "version": "1.12.2", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -14115,7 +14094,6 @@ "version": "3.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -14260,7 +14238,6 @@ "node_modules/bare-events": { "version": "2.8.0", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -14579,7 +14556,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -14875,6 +14851,7 @@ "node_modules/camelize": { "version": "1.0.1", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16581,7 +16558,6 @@ "node_modules/cosmiconfig": { "version": "9.0.0", "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -16746,6 +16722,7 @@ "node_modules/css-color-keywords": { "version": "1.0.0", "license": "ISC", + "peer": true, "engines": { "node": ">=4" } @@ -16774,6 +16751,7 @@ "node_modules/css-to-react-native": { "version": "3.2.0", "license": "MIT", + "peer": true, "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", @@ -16854,8 +16832,7 @@ }, "node_modules/csstype": { "version": "3.2.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -16952,7 +16929,6 @@ "node_modules/date-fns": { "version": "4.1.0", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -16976,8 +16952,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/de-indent": { "version": "1.0.2", @@ -17195,6 +17170,7 @@ "node_modules/dependency-cruiser": { "version": "17.3.7", "license": "MIT", + "peer": true, "dependencies": { "acorn": "8.15.0", "acorn-jsx": "5.3.2", @@ -17230,6 +17206,7 @@ "node_modules/dependency-cruiser/node_modules/ignore": { "version": "7.0.5", "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -17237,6 +17214,7 @@ "node_modules/dependency-cruiser/node_modules/picomatch": { "version": "4.0.3", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -18098,7 +18076,6 @@ "node_modules/eslint": { "version": "9.39.2", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -18156,7 +18133,6 @@ "node_modules/eslint-config-prettier": { "version": "10.1.5", "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -18537,7 +18513,6 @@ "node_modules/eslint-plugin-react": { "version": "7.35.0", "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -21407,6 +21382,7 @@ "node_modules/interpret": { "version": "3.1.1", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -21707,6 +21683,7 @@ "node_modules/is-installed-globally": { "version": "1.0.0", "license": "MIT", + "peer": true, "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" @@ -21787,6 +21764,7 @@ "node_modules/is-path-inside": { "version": "4.0.0", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -23535,7 +23513,6 @@ "version": "22.1.0", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "abab": "^2.0.6", "cssstyle": "^3.0.0", @@ -23637,6 +23614,8 @@ }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -23850,6 +23829,7 @@ "node_modules/kleur": { "version": "3.0.3", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -24535,7 +24515,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -24925,8 +24904,7 @@ }, "node_modules/lodash": { "version": "4.17.21", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -25159,7 +25137,6 @@ "version": "3.7.2", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" } @@ -26499,7 +26476,6 @@ "version": "22.4.4", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -27752,7 +27728,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -27985,7 +27960,6 @@ "node_modules/prettier": { "version": "3.6.2", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -28086,6 +28060,7 @@ "node_modules/prompts": { "version": "2.4.2", "license": "MIT", + "peer": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -29119,7 +29094,6 @@ "node_modules/react": { "version": "19.2.0", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -29127,7 +29101,6 @@ "node_modules/react-dom": { "version": "19.2.0", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -29439,6 +29412,7 @@ "node_modules/rechoir": { "version": "0.8.0", "license": "MIT", + "peer": true, "dependencies": { "resolve": "^1.20.0" }, @@ -29461,8 +29435,7 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -29756,7 +29729,6 @@ "node_modules/rollup": { "version": "4.52.4", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -29882,7 +29854,6 @@ "node_modules/rxjs": { "version": "7.8.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -29939,6 +29910,7 @@ "node_modules/safe-regex": { "version": "2.1.1", "license": "MIT", + "peer": true, "dependencies": { "regexp-tree": "~0.1.1" } @@ -30142,7 +30114,6 @@ "node_modules/seroval": { "version": "1.5.0", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -30238,7 +30209,8 @@ }, "node_modules/shallowequal": { "version": "1.1.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/sharp": { "version": "0.33.5", @@ -30463,7 +30435,8 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/slash": { "version": "3.0.0", @@ -31132,6 +31105,7 @@ "node_modules/styled-components": { "version": "6.3.8", "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", @@ -31177,6 +31151,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -31199,7 +31174,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -31476,7 +31450,6 @@ "node_modules/stylelint/node_modules/postcss-selector-parser": { "version": "7.1.0", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -32026,8 +31999,7 @@ }, "node_modules/tiny-invariant": { "version": "1.3.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -32240,7 +32212,6 @@ "version": "10.9.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -32302,6 +32273,7 @@ "node_modules/tsconfig-paths-webpack-plugin": { "version": "4.2.0", "license": "MIT", + "peer": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", @@ -32315,6 +32287,7 @@ "node_modules/tsconfig-paths-webpack-plugin/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -32328,6 +32301,7 @@ "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { "version": "4.1.2", "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -32342,6 +32316,7 @@ "node_modules/tsconfig-paths-webpack-plugin/node_modules/supports-color": { "version": "7.2.0", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -32551,7 +32526,6 @@ "node_modules/typescript": { "version": "5.9.3", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -32563,7 +32537,6 @@ "node_modules/typescript-eslint": { "version": "8.46.0", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.0", "@typescript-eslint/parser": "8.46.0", @@ -33022,7 +32995,6 @@ "version": "6.2.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cypress/request": "3.0.9", "@verdaccio/auth": "8.0.0-next-8.29", @@ -33196,7 +33168,6 @@ "node_modules/vite": { "version": "7.3.1", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -33781,7 +33752,6 @@ "node_modules/vitest": { "version": "4.0.18", "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -33917,6 +33887,7 @@ "node_modules/watskeburt": { "version": "5.0.2", "license": "MIT", + "peer": true, "bin": { "watskeburt": "dist/run-cli.js" }, @@ -34071,7 +34042,6 @@ "version": "5.102.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -34489,7 +34459,6 @@ "version": "8.18.0", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -34687,7 +34656,6 @@ "node_modules/zod": { "version": "4.1.12", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -35108,7 +35076,8 @@ "dependencies": { "@leancodepl/logger": "10.3.0", "chalk": ">=5.0.0", - "commander": "^14.0.0" + "commander": "^14.0.0", + "json5": "^2.2.3" }, "bin": { "folder-structure-cruiser": "dist/bin.js" @@ -35276,7 +35245,6 @@ "version": "18.3.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, diff --git a/packages/folder-structure-cruiser/README.md b/packages/folder-structure-cruiser/README.md index 11c9ff90..7c814629 100644 --- a/packages/folder-structure-cruiser/README.md +++ b/packages/folder-structure-cruiser/README.md @@ -21,7 +21,6 @@ Validates cross-feature nested imports according to folder structure rules. - `configPath: string` - Path to the dependency-cruiser configuration file (e.g., `.dependency-cruiser.js`) - `tsConfigPath: string` - Optional path to TypeScript configuration file for enhanced type resolution - `webpackConfigPath?: string` - Optional path to webpack configuration file for webpack alias resolution - - `allowImportsFromDirectChildrenOf?: string[]` - Optional list of directories whose direct children can be imported **Returns:** `Promise` - The function doesn't return a value but outputs results to console @@ -53,9 +52,6 @@ npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --direct # With both TypeScript and webpack config npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --directory "packages/admin" --config "./.dependency-cruiser.json" --tsConfig "./tsconfig.base.json" --webpackConfig "./webpack.config.js" - -# Allow imports from direct children of selected folders -npx @leancodepl/folder-structure-cruiser validate-cross-feature-imports --directory "packages/admin" --config "./.dependency-cruiser.json" --allow-imports-from-direct-children-of "src/features" ``` ### Shared Component Validation @@ -85,7 +81,12 @@ npx depcruise --ts-config ./tsconfig.base.json --webpack-config ./webpack.config ```json { - "extends": ["@leancodepl/folder-structure-cruiser/.dependency-cruiser.json"] + "extends": ["@leancodepl/folder-structure-cruiser/.dependency-cruiser.json"], + "folderStructureCruiser": { + "crossFeatureImports": { + "allowImportsFromDirectChildrenOf": ["src/features"] + } + } } ``` diff --git a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts index 795c2260..afa3059b 100644 --- a/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts +++ b/packages/folder-structure-cruiser/__tests__/cross-feature-imports.spec.ts @@ -54,12 +54,11 @@ describe("cross-feature-imports validation", () => { const dirname = import.meta.dirname const testDir = join(dirname, "test-structure") const filePath = join(testDir, "polls/SnapshotPollEditor/index.tsx") - const configPath = join(dirname, "../.dependency-cruiser.json") + const configPath = join(dirname, "test-configs/allow-direct-children.config.json") await validateCrossFeatureImports({ directories: [filePath], configPath: configPath, - allowImportsFromDirectChildrenOf: ["surveys"], }) expect(consoleErrorSpy).not.toHaveBeenCalledWith( diff --git a/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json b/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json new file mode 100644 index 00000000..a61c74df --- /dev/null +++ b/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json @@ -0,0 +1,8 @@ +{ + "extends": ["../../.dependency-cruiser.json"], + "folderStructureCruiser": { + "crossFeatureImports": { + "allowImportsFromDirectChildrenOf": ["surveys"] + } + } +} diff --git a/packages/folder-structure-cruiser/package.json b/packages/folder-structure-cruiser/package.json index 34e2e500..dca55e28 100644 --- a/packages/folder-structure-cruiser/package.json +++ b/packages/folder-structure-cruiser/package.json @@ -21,7 +21,8 @@ "dependencies": { "@leancodepl/logger": "10.3.0", "chalk": ">=5.0.0", - "commander": "^14.0.0" + "commander": "^14.0.0", + "json5": "^2.2.3" }, "devDependencies": { "vitest": "*" diff --git a/packages/folder-structure-cruiser/src/bin.ts b/packages/folder-structure-cruiser/src/bin.ts index 2d5d8a1d..c64a7dd9 100644 --- a/packages/folder-structure-cruiser/src/bin.ts +++ b/packages/folder-structure-cruiser/src/bin.ts @@ -30,23 +30,17 @@ program .option("-c, --config ", "Path to config file") .option("-t, --tsConfig ", "Path to ts config file") .option("-w, --webpackConfig ", "Path to webpack config file") - .option( - "-a, --allow-imports-from-direct-children-of ", - "Allow imports from direct children of listed directories", - ) .action(async options => { const directories = options.directory ? [options.directory] : [".*"] const configPath = options.config ?? "" const tsConfigPath = options.tsConfig const webpackConfigPath = options.webpackConfig - const allowImportsFromDirectChildrenOf = options.allowImportsFromDirectChildrenOf await validateCrossFeatureImports({ directories, configPath, tsConfigPath, webpackConfigPath, - allowImportsFromDirectChildrenOf, }) }) diff --git a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts index 4fe4cc5f..b2176151 100644 --- a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts +++ b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts @@ -1,12 +1,9 @@ import { checkCrossFeatureImports } from "../lib/checkCrossFeatureImports.js" import { formatMessages } from "../lib/formatMessages.js" import { CruiseParams, getCruiseResult } from "../lib/getCruiseResult.js" +import { getFolderStructureCruiserConfig } from "../lib/getFolderStructureCruiserConfig.js" import { logger } from "../lib/logger.js" -export type ValidateCrossFeatureImportsParams = CruiseParams & { - allowImportsFromDirectChildrenOf?: string[] -} - /** * Validates cross-feature nested imports according to folder structure rules. * @@ -23,7 +20,6 @@ export type ValidateCrossFeatureImportsParams = CruiseParams & { * @param cruiseParams.configPath - Path to the dependency-cruiser configuration file (e.g., .dependency-cruiser.js) * @param cruiseParams.tsConfigPath - Optional path to TypeScript configuration file for enhanced type resolution * @param cruiseParams.webpackConfigPath - Optional path to webpack configuration file for webpack alias resolution - * @param cruiseParams.allowImportsFromDirectChildrenOf - Optional directory paths whose direct children can be imported * * @returns Promise - The function doesn't return a value but outputs results to console * @@ -65,12 +61,13 @@ export type ValidateCrossFeatureImportsParams = CruiseParams & { * } * ``` */ -export async function validateCrossFeatureImports(cruiseParams: ValidateCrossFeatureImportsParams) { +export async function validateCrossFeatureImports(cruiseParams: CruiseParams) { try { + const folderStructureCruiserConfig = await getFolderStructureCruiserConfig(cruiseParams.configPath) const cruiseResult = await getCruiseResult(cruiseParams) const { messages: errorMessages, totalCruised } = checkCrossFeatureImports(cruiseResult, { - allowImportsFromDirectChildrenOf: cruiseParams.allowImportsFromDirectChildrenOf, + allowImportsFromDirectChildrenOf: folderStructureCruiserConfig.allowImportsFromDirectChildrenOf, }) if (errorMessages.length === 0) { diff --git a/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts new file mode 100644 index 00000000..ce542c60 --- /dev/null +++ b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts @@ -0,0 +1,84 @@ +import { readFile } from "node:fs/promises" +import { createRequire } from "node:module" +import { dirname, extname, isAbsolute, resolve } from "node:path" +import { pathToFileURL } from "node:url" +import json5 from "json5" + +type FolderStructureCruiserConfigShape = { + extends?: string | string[] + folderStructureCruiser?: { + crossFeatureImports?: { + allowImportsFromDirectChildrenOf?: string[] + } + } +} + +export type FolderStructureCruiserConfig = { + allowImportsFromDirectChildrenOf: string[] +} + +const require = createRequire(import.meta.url) + +function parseStringArray(value: unknown): string[] { + return Array.isArray(value) && value.every(item => typeof item === "string") ? value : [] +} + +async function readConfig(configPath: string): Promise { + const configFileExtension = extname(configPath) + + if ([".js", ".cjs", ".mjs", ""].includes(configFileExtension)) { + const importedConfig = (await import(pathToFileURL(configPath).href)) as { default?: unknown } + return (importedConfig.default ?? importedConfig) as FolderStructureCruiserConfigShape + } + + const rawConfig = await readFile(configPath, "utf8") + return json5.parse(rawConfig) as FolderStructureCruiserConfigShape +} + +function resolveConfigPath(configPath: string, baseDirectory: string): string { + if (isAbsolute(configPath)) { + return configPath + } + + return require.resolve(configPath, { paths: [baseDirectory] }) +} + +async function collectAllowImportsFromDirectChildrenOf( + configPath: string, + visitedConfigs = new Set(), +): Promise { + const resolvedConfigPath = resolveConfigPath(configPath, process.cwd()) + + if (visitedConfigs.has(resolvedConfigPath)) { + return [] + } + + visitedConfigs.add(resolvedConfigPath) + const config = await readConfig(resolvedConfigPath) + const configDirectory = dirname(resolvedConfigPath) + const inheritedConfigPaths = Array.isArray(config.extends) ? config.extends : config.extends ? [config.extends] : [] + const inheritedAllowedImports = ( + await Promise.all( + inheritedConfigPaths.map(inheritedConfigPath => + collectAllowImportsFromDirectChildrenOf(resolveConfigPath(inheritedConfigPath, configDirectory), visitedConfigs), + ), + ) + ).flat() + const localAllowedImports = parseStringArray( + config.folderStructureCruiser?.crossFeatureImports?.allowImportsFromDirectChildrenOf, + ) + + return [...new Set([...inheritedAllowedImports, ...localAllowedImports])] +} + +export async function getFolderStructureCruiserConfig(configPath: string): Promise { + if (!configPath) { + return { allowImportsFromDirectChildrenOf: [] } + } + + const allowImportsFromDirectChildrenOf = await collectAllowImportsFromDirectChildrenOf(resolve(configPath)) + + return { + allowImportsFromDirectChildrenOf, + } +} From 443d71ca3962463fc970f7120c704a4dd34276a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 May 2026 10:14:19 +0000 Subject: [PATCH 4/5] fix(folder-structure-cruiser): resolve CI lint errors --- .../src/lib/checkCrossFeatureImports.ts | 4 ++-- .../src/lib/getFolderStructureCruiserConfig.ts | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts b/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts index 595f310d..c35e84e1 100644 --- a/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts +++ b/packages/folder-structure-cruiser/src/lib/checkCrossFeatureImports.ts @@ -7,7 +7,7 @@ type CheckCrossFeatureImportsOptions = { allowImportsFromDirectChildrenOf?: string[] } -const INDEX_FILE_PATTERN = /^index(?:\..+)?$/ +const indexFilePattern = /^index(?:\..+)?$/ function normalizePath(path: string): string[] { return path.split("/").filter(segment => segment.length > 0 && segment !== ".") @@ -42,7 +42,7 @@ function isDirectChildImportOfDirectory(dependencyPath: string[], directoryPath: return true } - return relativePathFromDirectory.length === 2 && INDEX_FILE_PATTERN.test(relativePathFromDirectory[1]) + return relativePathFromDirectory.length === 2 && indexFilePattern.test(relativePathFromDirectory[1]) } export function checkCrossFeatureImports( diff --git a/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts index ce542c60..2a9fda5a 100644 --- a/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts +++ b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts @@ -1,8 +1,8 @@ +import json5 from "json5" import { readFile } from "node:fs/promises" import { createRequire } from "node:module" import { dirname, extname, isAbsolute, resolve } from "node:path" import { pathToFileURL } from "node:url" -import json5 from "json5" type FolderStructureCruiserConfigShape = { extends?: string | string[] @@ -26,7 +26,7 @@ function parseStringArray(value: unknown): string[] { async function readConfig(configPath: string): Promise { const configFileExtension = extname(configPath) - if ([".js", ".cjs", ".mjs", ""].includes(configFileExtension)) { + if (["", ".cjs", ".js", ".mjs"].includes(configFileExtension)) { const importedConfig = (await import(pathToFileURL(configPath).href)) as { default?: unknown } return (importedConfig.default ?? importedConfig) as FolderStructureCruiserConfigShape } @@ -57,13 +57,12 @@ async function collectAllowImportsFromDirectChildrenOf( const config = await readConfig(resolvedConfigPath) const configDirectory = dirname(resolvedConfigPath) const inheritedConfigPaths = Array.isArray(config.extends) ? config.extends : config.extends ? [config.extends] : [] - const inheritedAllowedImports = ( - await Promise.all( - inheritedConfigPaths.map(inheritedConfigPath => - collectAllowImportsFromDirectChildrenOf(resolveConfigPath(inheritedConfigPath, configDirectory), visitedConfigs), - ), - ) - ).flat() + const inheritedAllowedImportsNested = await Promise.all( + inheritedConfigPaths.map(inheritedConfigPath => + collectAllowImportsFromDirectChildrenOf(resolveConfigPath(inheritedConfigPath, configDirectory), visitedConfigs), + ), + ) + const inheritedAllowedImports = inheritedAllowedImportsNested.flat() const localAllowedImports = parseStringArray( config.folderStructureCruiser?.crossFeatureImports?.allowImportsFromDirectChildrenOf, ) From 0d5796d26f77d33b80580018f4d93024d729ddeb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 May 2026 10:26:07 +0000 Subject: [PATCH 5/5] refactor(folder-structure-cruiser): use dependency-cruiser options namespace --- package-lock.json | 3 +- packages/folder-structure-cruiser/README.md | 8 ++- .../allow-direct-children.config.json | 8 ++- .../folder-structure-cruiser/package.json | 3 +- .../lib/getFolderStructureCruiserConfig.ts | 63 ++----------------- 5 files changed, 18 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index ceddffbe..ade9f89e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35076,8 +35076,7 @@ "dependencies": { "@leancodepl/logger": "10.3.0", "chalk": ">=5.0.0", - "commander": "^14.0.0", - "json5": "^2.2.3" + "commander": "^14.0.0" }, "bin": { "folder-structure-cruiser": "dist/bin.js" diff --git a/packages/folder-structure-cruiser/README.md b/packages/folder-structure-cruiser/README.md index 7c814629..22105ee0 100644 --- a/packages/folder-structure-cruiser/README.md +++ b/packages/folder-structure-cruiser/README.md @@ -82,9 +82,11 @@ npx depcruise --ts-config ./tsconfig.base.json --webpack-config ./webpack.config ```json { "extends": ["@leancodepl/folder-structure-cruiser/.dependency-cruiser.json"], - "folderStructureCruiser": { - "crossFeatureImports": { - "allowImportsFromDirectChildrenOf": ["src/features"] + "options": { + "folderStructureCruiser": { + "crossFeatureImports": { + "allowImportsFromDirectChildrenOf": ["src/features"] + } } } } diff --git a/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json b/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json index a61c74df..7990e352 100644 --- a/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json +++ b/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json @@ -1,8 +1,10 @@ { "extends": ["../../.dependency-cruiser.json"], - "folderStructureCruiser": { - "crossFeatureImports": { - "allowImportsFromDirectChildrenOf": ["surveys"] + "options": { + "folderStructureCruiser": { + "crossFeatureImports": { + "allowImportsFromDirectChildrenOf": ["surveys"] + } } } } diff --git a/packages/folder-structure-cruiser/package.json b/packages/folder-structure-cruiser/package.json index dca55e28..34e2e500 100644 --- a/packages/folder-structure-cruiser/package.json +++ b/packages/folder-structure-cruiser/package.json @@ -21,8 +21,7 @@ "dependencies": { "@leancodepl/logger": "10.3.0", "chalk": ">=5.0.0", - "commander": "^14.0.0", - "json5": "^2.2.3" + "commander": "^14.0.0" }, "devDependencies": { "vitest": "*" diff --git a/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts index 2a9fda5a..c3ed395b 100644 --- a/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts +++ b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts @@ -1,11 +1,6 @@ -import json5 from "json5" -import { readFile } from "node:fs/promises" -import { createRequire } from "node:module" -import { dirname, extname, isAbsolute, resolve } from "node:path" -import { pathToFileURL } from "node:url" +import extractDepcruiseOptions from "dependency-cruiser/config-utl/extract-depcruise-options" -type FolderStructureCruiserConfigShape = { - extends?: string | string[] +type CruiseOptionsWithFolderStructureCruiser = { folderStructureCruiser?: { crossFeatureImports?: { allowImportsFromDirectChildrenOf?: string[] @@ -17,65 +12,19 @@ export type FolderStructureCruiserConfig = { allowImportsFromDirectChildrenOf: string[] } -const require = createRequire(import.meta.url) - function parseStringArray(value: unknown): string[] { return Array.isArray(value) && value.every(item => typeof item === "string") ? value : [] } -async function readConfig(configPath: string): Promise { - const configFileExtension = extname(configPath) - - if (["", ".cjs", ".js", ".mjs"].includes(configFileExtension)) { - const importedConfig = (await import(pathToFileURL(configPath).href)) as { default?: unknown } - return (importedConfig.default ?? importedConfig) as FolderStructureCruiserConfigShape - } - - const rawConfig = await readFile(configPath, "utf8") - return json5.parse(rawConfig) as FolderStructureCruiserConfigShape -} - -function resolveConfigPath(configPath: string, baseDirectory: string): string { - if (isAbsolute(configPath)) { - return configPath - } - - return require.resolve(configPath, { paths: [baseDirectory] }) -} - -async function collectAllowImportsFromDirectChildrenOf( - configPath: string, - visitedConfigs = new Set(), -): Promise { - const resolvedConfigPath = resolveConfigPath(configPath, process.cwd()) - - if (visitedConfigs.has(resolvedConfigPath)) { - return [] - } - - visitedConfigs.add(resolvedConfigPath) - const config = await readConfig(resolvedConfigPath) - const configDirectory = dirname(resolvedConfigPath) - const inheritedConfigPaths = Array.isArray(config.extends) ? config.extends : config.extends ? [config.extends] : [] - const inheritedAllowedImportsNested = await Promise.all( - inheritedConfigPaths.map(inheritedConfigPath => - collectAllowImportsFromDirectChildrenOf(resolveConfigPath(inheritedConfigPath, configDirectory), visitedConfigs), - ), - ) - const inheritedAllowedImports = inheritedAllowedImportsNested.flat() - const localAllowedImports = parseStringArray( - config.folderStructureCruiser?.crossFeatureImports?.allowImportsFromDirectChildrenOf, - ) - - return [...new Set([...inheritedAllowedImports, ...localAllowedImports])] -} - export async function getFolderStructureCruiserConfig(configPath: string): Promise { if (!configPath) { return { allowImportsFromDirectChildrenOf: [] } } - const allowImportsFromDirectChildrenOf = await collectAllowImportsFromDirectChildrenOf(resolve(configPath)) + const depcruiseOptions = (await extractDepcruiseOptions(configPath)) as CruiseOptionsWithFolderStructureCruiser + const allowImportsFromDirectChildrenOf = parseStringArray( + depcruiseOptions.folderStructureCruiser?.crossFeatureImports?.allowImportsFromDirectChildrenOf, + ) return { allowImportsFromDirectChildrenOf,