diff --git a/package-lock.json b/package-lock.json index 7feb69fa..ade9f89e 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" } @@ -35276,7 +35244,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 1ef98a08..22105ee0 100644 --- a/packages/folder-structure-cruiser/README.md +++ b/packages/folder-structure-cruiser/README.md @@ -81,7 +81,14 @@ 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"], + "options": { + "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 8bf9f316..afa3059b 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,23 @@ 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, "test-configs/allow-direct-children.config.json") + + await validateCrossFeatureImports({ + directories: [filePath], + configPath: configPath, + }) + + 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 () => { const dirname = import.meta.dirname const testDir = join(dirname, "test-structure") 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..7990e352 --- /dev/null +++ b/packages/folder-structure-cruiser/__tests__/test-configs/allow-direct-children.config.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../.dependency-cruiser.json"], + "options": { + "folderStructureCruiser": { + "crossFeatureImports": { + "allowImportsFromDirectChildrenOf": ["surveys"] + } + } + } +} diff --git a/packages/folder-structure-cruiser/src/bin.ts b/packages/folder-structure-cruiser/src/bin.ts index 88d79787..c64a7dd9 100644 --- a/packages/folder-structure-cruiser/src/bin.ts +++ b/packages/folder-structure-cruiser/src/bin.ts @@ -36,7 +36,12 @@ program const tsConfigPath = options.tsConfig const webpackConfigPath = options.webpackConfig - await validateCrossFeatureImports({ directories, configPath, tsConfigPath, webpackConfigPath }) + await validateCrossFeatureImports({ + directories, + configPath, + tsConfigPath, + webpackConfigPath, + }) }) program.parse() diff --git a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts index 42d87cec..b2176151 100644 --- a/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts +++ b/packages/folder-structure-cruiser/src/commands/validateCrossFeatureImports.ts @@ -1,6 +1,7 @@ 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" /** @@ -62,9 +63,12 @@ import { logger } from "../lib/logger.js" */ export async function validateCrossFeatureImports(cruiseParams: CruiseParams) { try { + const folderStructureCruiserConfig = await getFolderStructureCruiserConfig(cruiseParams.configPath) const cruiseResult = await getCruiseResult(cruiseParams) - const { messages: errorMessages, totalCruised } = checkCrossFeatureImports(cruiseResult) + const { messages: errorMessages, totalCruised } = checkCrossFeatureImports(cruiseResult, { + allowImportsFromDirectChildrenOf: folderStructureCruiserConfig.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..c35e84e1 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 indexFilePattern = /^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 && indexFilePattern.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, 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..c3ed395b --- /dev/null +++ b/packages/folder-structure-cruiser/src/lib/getFolderStructureCruiserConfig.ts @@ -0,0 +1,32 @@ +import extractDepcruiseOptions from "dependency-cruiser/config-utl/extract-depcruise-options" + +type CruiseOptionsWithFolderStructureCruiser = { + folderStructureCruiser?: { + crossFeatureImports?: { + allowImportsFromDirectChildrenOf?: string[] + } + } +} + +export type FolderStructureCruiserConfig = { + allowImportsFromDirectChildrenOf: string[] +} + +function parseStringArray(value: unknown): string[] { + return Array.isArray(value) && value.every(item => typeof item === "string") ? value : [] +} + +export async function getFolderStructureCruiserConfig(configPath: string): Promise { + if (!configPath) { + return { allowImportsFromDirectChildrenOf: [] } + } + + const depcruiseOptions = (await extractDepcruiseOptions(configPath)) as CruiseOptionsWithFolderStructureCruiser + const allowImportsFromDirectChildrenOf = parseStringArray( + depcruiseOptions.folderStructureCruiser?.crossFeatureImports?.allowImportsFromDirectChildrenOf, + ) + + return { + allowImportsFromDirectChildrenOf, + } +}