From 54a7f4237ac9ba846b1318f475ff2c3a07d0b3cb Mon Sep 17 00:00:00 2001 From: Sander Verweij Date: Thu, 9 May 2024 13:54:36 +0200 Subject: [PATCH] fix(config-util): adds utility that extracts dependency-cruiser options from a dependency-cruiser config (#933) ## Description - adds utility that extracts dependency-cruiser options from a dependency-cruiser config, and that returns an `ICruiseOptions` one can use as input parameter for the `cruise` function. This function also sets the `validate` attribute to `true` if there's a rule set in the config (and to `false` otherwise), so there's no need to do that anymore either. - corrects the documented method signature of `extractDepcruiseConfig` to what it really returns (an `IConfiguration`) - updates the docs/api.md documentation so all examples (1) run as intended (2) reflect current situation of the API. ## Motivation and Context fixes #932 `extractDepcruiseConfig` returns an `IConfiguration` object while the cruise function needs an `ICruiseOptions`. Changing the signature of `extractDepcruiseConfig` would constitute a breaking change - and `extractDepcruiseConfig` also has its own uses => we need an _additional_ utility function that translates a dependency-cruiser configuration file into an `ICruiseOptions` object. This PR adds that. ## Example ```typescript import { cruise, type ICruiseOptions, type IReporterOutput, type IResolveOptions, } from "dependency-cruiser"; import extractDepcruiseOptions from "dependency-cruiser/config-utl/extract-depcruise-options"; import extractTSConfig from "dependency-cruiser/config-utl/extract-ts-config"; import extractWebpackResolveConfig from "dependency-cruiser/config-utl/extract-webpack-resolve-config"; try { const lArrayOfFilesAndDirectoriesToCruise = ["src"]; const depcruiseOptions: ICruiseOptions = await extractDepcruiseOptions( "./.dependency-cruiser.json", ); const lWebpackResolveConfig = (await extractWebpackResolveConfig( "./webpack.config.js", )) as IResolveOptions; const tsConfig = extractTSConfig("./tsconfig.json"); const cruiseResult: IReporterOutput = await cruise( lArrayOfFilesAndDirectoriesToCruise, depcruiseOptions, lWebpackResolveConfig, { tsConfig, }, ); console.dir(cruiseResult.output, { depth: 10 }); } catch (pError) { console.error(pError); } ``` ## How Has This Been Tested? - [x] green ci - [x] additional integration tests ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] Documentation only change - [ ] Refactor (non-breaking change which fixes an issue without changing functionality) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist - [x] :book: - My change doesn't require a documentation update, or ... - it _does_ and I have updated it - [x] :balance_scale: - The contribution will be subject to [The MIT license](https://github.com/sverweij/dependency-cruiser/blob/main/LICENSE), and I'm OK with that. - The contribution is my own original work. - I am ok with the stuff in [**CONTRIBUTING.md**](https://github.com/sverweij/dependency-cruiser/blob/main/.github/CONTRIBUTING.md). --- .dependency-cruiser.json | 7 +- doc/api.md | 63 ++++++++-------- package.json | 4 ++ .../extract-depcruise-config/index.mjs | 2 +- .../extract-depcruise-config/read-config.mjs | 5 ++ src/config-utl/extract-depcruise-options.mjs | 28 ++++++++ .../__mocks__/depcruiseconfig/empty.mjs | 1 + ...es.sub-not-allowed-error-with-options.json | 30 ++++++++ .../rules.sub-not-allowed-error.json | 21 ++++++ .../extract-depcruise-options.spec.mjs | 71 +++++++++++++++++++ .../config-utl/extract-depcruise-config.d.mts | 14 ++-- .../extract-depcruise-options.d.mts | 18 +++++ types/dependency-cruiser.d.mts | 2 +- 13 files changed, 229 insertions(+), 37 deletions(-) create mode 100644 src/config-utl/extract-depcruise-options.mjs create mode 100644 test/config-utl/__mocks__/depcruiseconfig/empty.mjs create mode 100644 test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error-with-options.json create mode 100644 test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error.json create mode 100644 test/config-utl/extract-depcruise-options.spec.mjs create mode 100644 types/config-utl/extract-depcruise-options.d.mts diff --git a/.dependency-cruiser.json b/.dependency-cruiser.json index ef75d3b00..01620e89f 100644 --- a/.dependency-cruiser.json +++ b/.dependency-cruiser.json @@ -229,7 +229,12 @@ "from": { "path": "^bin/" }, "to": { "path": "^src", - "pathNot": ["[.]schema[.]json$", "[.]d[.]ts$", "^src/report/"], + "pathNot": [ + "[.]schema[.]json$", + "[.]d[.]ts$", + "^src/report/", + "^src/config-utl/extract-depcruise-options.mjs$" + ], "reachable": false } }, diff --git a/doc/api.md b/doc/api.md index ba6e4ca35..a7f922bca 100644 --- a/doc/api.md +++ b/doc/api.md @@ -17,15 +17,17 @@ and between modules in folder. Here's an example that cruises all files in the `src` folder and prints the dependencies to stdout: ```typescript -import { cruise, IReporterOutput } from "dependency-cruiser"; +import { cruise, type IReporterOutput } from "dependency-cruiser"; const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE: string[] = ["src"]; try { - const cruiseResult: IReporterOutput = await cruise(ARRAY_OF_FILES_AND_DIRS_TO_CRUISE); -} catch(error) - - -console.dir(cruiseResult.output, { depth: 10 }); + const cruiseResult: IReporterOutput = await cruise( + ARRAY_OF_FILES_AND_DIRS_TO_CRUISE, + ); + console.dir(cruiseResult.output, { depth: 10 }); +} catch (pError) { + console.error(pError); +} ``` You might notice a few things when you do this @@ -52,11 +54,11 @@ const cruiseOptions: ICruiseOptions = { try { const cruiseResult: IReporterOutput = await cruise( ARRAY_OF_FILES_AND_DIRS_TO_CRUISE, - cruiseOptions + cruiseOptions, ); console.dir(cruiseResult.output, { depth: 10 }); -} catch (error) { - console.error(error); +} catch (pError) { + console.error(pError); } ``` @@ -86,51 +88,56 @@ try { const cruiseResult: IReporterOutput = await cruise( ARRAY_OF_FILES_AND_DIRS_TO_CRUISE, cruiseOptions, - webpackResolveOptions + webpackResolveOptions, ); console.dir(cruiseResult.output, { depth: 10 }); -} catch (error) { - console.error(error); +} catch (pError) { + console.error(pError); } ``` ### Utility functions ```typescript -import { cruise, ICruiseOptions, IReporterOutput } from "dependency-cruiser"; -import extractDepcruiseConfig from "dependency-cruiser/config-utl/extract-depcruise-config"; +import { + IResolveOptions, + cruise, + type ICruiseOptions, + type IReporterOutput, +} from "dependency-cruiser"; +import extractDepcruiseOptions from "dependency-cruiser/config-utl/extract-depcruise-options"; import extractTSConfig from "dependency-cruiser/config-utl/extract-ts-config"; import extractWebpackResolveConfig from "dependency-cruiser/config-utl/extract-webpack-resolve-config"; -import extractBabelConfig from "dependency-cruiser/config-utl/extract-babel-config"; +// import extractBabelConfig from "dependency-cruiser/config-utl/extract-babel-config"; try { - const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE = ["src"]; + const lArrayOfFilesAndDirectoriesToCruise = ["src"]; - const depcruiseConfig: ICruiseOptions = await extractDepcruiseConfig( - "./.dependency-cruiser.js" - ); - const webpackResolveConfig = await extractWebpackResolveConfig( - "./webpack.conf.js" + const depcruiseOptions: ICruiseOptions = await extractDepcruiseOptions( + "./.dependency-cruiser.json", ); + const lWebpackResolveConfig = (await extractWebpackResolveConfig( + "./webpack.config.js", + )) as IResolveOptions; const tsConfig = extractTSConfig("./tsconfig.json"); // const babelConfig = await extractBabelConfig("./babel.conf.json"); const cruiseResult: IReporterOutput = await cruise( - ARRAY_OF_FILES_AND_DIRS_TO_CRUISE, - depcruiseConfig, - webpackResolveConfig, + lArrayOfFilesAndDirectoriesToCruise, + depcruiseOptions, + lWebpackResolveConfig, // change since v13: in stead of passing the tsConfig directly, like so: // tsconfig // you now pass it into an object that also supports other types of // compiler options, like those for babel: { - tsConfig: tsConfig, + tsConfig, // babelConfig: babelConfig, - } + }, ); console.dir(cruiseResult.output, { depth: 10 }); -} catch (error) { - console.error(error); +} catch (pError) { + console.error(pError); } ``` diff --git a/package.json b/package.json index 8a1702051..cef19d5de 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,10 @@ "types": "./types/config-utl/extract-depcruise-config.d.mts", "import": "./src/config-utl/extract-depcruise-config/index.mjs" }, + "./config-utl/extract-depcruise-options": { + "types": "./types/config-utl/extract-depcruise-options.d.mts", + "import": "./src/config-utl/extract-depcruise-options.mjs" + }, "./config-utl/extract-ts-config": { "types": "./types/config-utl/extract-ts-config.d.mts", "import": "./src/config-utl/extract-ts-config.mjs" diff --git a/src/config-utl/extract-depcruise-config/index.mjs b/src/config-utl/extract-depcruise-config/index.mjs index 1c2822c45..830b0a9eb 100644 --- a/src/config-utl/extract-depcruise-config/index.mjs +++ b/src/config-utl/extract-depcruise-config/index.mjs @@ -47,7 +47,7 @@ async function processExtends(pReturnValue, pAlreadyVisited, pBaseDirectory) { * @param {string} pConfigFileName * @param {Set?} pAlreadyVisited * @param {string?} pBaseDirectory - * @return {import('../../../types/options.mjs').ICruiseOptions} dependency-cruiser options + * @return {import('../../../types/configuration.mjs').IConfiguration} dependency-cruiser options * @throws {Error} when the config is not valid (/ does not exist/ isn't readable) */ export default async function extractDepcruiseConfig( diff --git a/src/config-utl/extract-depcruise-config/read-config.mjs b/src/config-utl/extract-depcruise-config/read-config.mjs index 47a6afee8..3d4e2b60b 100644 --- a/src/config-utl/extract-depcruise-config/read-config.mjs +++ b/src/config-utl/extract-depcruise-config/read-config.mjs @@ -2,6 +2,11 @@ import { readFile } from "node:fs/promises"; import { extname } from "node:path"; import json5 from "json5"; +/** + * + * @param {string} pAbsolutePathToConfigFile + * @returns {Promise} + */ export default async function readConfig(pAbsolutePathToConfigFile) { if ( [".js", ".cjs", ".mjs", ""].includes(extname(pAbsolutePathToConfigFile)) diff --git a/src/config-utl/extract-depcruise-options.mjs b/src/config-utl/extract-depcruise-options.mjs new file mode 100644 index 000000000..f764a08c1 --- /dev/null +++ b/src/config-utl/extract-depcruise-options.mjs @@ -0,0 +1,28 @@ +import extractDepcruiseConfig from "./extract-depcruise-config/index.mjs"; + +/** + * + * @param {import('../../../types/configuration.mjs').IConfiguration}} pConfiguration + * @returns {import('../../../types/configuration.mjs').ICruiseOptions} + */ +function configuration2options(pConfiguration) { + /* c8 ignore next 1 */ + const lConfiguration = structuredClone(pConfiguration || {}); + const lReturnValue = structuredClone(lConfiguration?.options ?? {}); + + delete lConfiguration.options; + lReturnValue.ruleSet = structuredClone(lConfiguration); + lReturnValue.validate = Object.keys(lReturnValue.ruleSet).length > 0; + + return lReturnValue; +} + +/** + * + * @param {string} pConfigFileName + * @returns {Promise} + */ +export default async function extractDepcruiseOptions(pConfigFileName) { + const lReturnValue = await extractDepcruiseConfig(pConfigFileName); + return configuration2options(lReturnValue); +} diff --git a/test/config-utl/__mocks__/depcruiseconfig/empty.mjs b/test/config-utl/__mocks__/depcruiseconfig/empty.mjs new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/test/config-utl/__mocks__/depcruiseconfig/empty.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error-with-options.json b/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error-with-options.json new file mode 100644 index 000000000..cc1ffb09a --- /dev/null +++ b/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error-with-options.json @@ -0,0 +1,30 @@ +{ + "allowed": [ + { + "from": {}, + "to": {} + } + ], + "forbidden": [ + { + "name": "sub-not-allowed", + "severity": "error", + "from": {}, + "to": { + "path": "sub" + } + } + ], + "options": { + "includeOnly": ["src"], + "reporterOptions": { + "text": { + "highlightFocused": true + } + }, + "cache": { + "strategy": "metadata", + "compress": true + } + } +} diff --git a/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error.json b/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error.json new file mode 100644 index 000000000..2d84f4663 --- /dev/null +++ b/test/config-utl/__mocks__/depcruiseconfig/rules.sub-not-allowed-error.json @@ -0,0 +1,21 @@ +{ + "allowed": [ + { + "from": { + }, + "to": { + } + } + ], + "forbidden": [ + { + "name": "sub-not-allowed", + "severity": "error", + "from": { + }, + "to": { + "path": "sub" + } + } + ] +} diff --git a/test/config-utl/extract-depcruise-options.spec.mjs b/test/config-utl/extract-depcruise-options.spec.mjs new file mode 100644 index 000000000..b5a61f6de --- /dev/null +++ b/test/config-utl/extract-depcruise-options.spec.mjs @@ -0,0 +1,71 @@ +import { deepEqual } from "node:assert/strict"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import loadOptions from "#config-utl/extract-depcruise-options.mjs"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const mockDirectory = join(__dirname, "__mocks__", "depcruiseconfig"); + +describe("[I] config-utl/extract-depcruise-options", () => { + it("correctly converts an empty configuration to options", async () => { + const lFoundOptions = await loadOptions(join(mockDirectory, "empty.mjs")); + deepEqual(lFoundOptions, { + ruleSet: {}, + validate: false, + }); + }); + + it("correctly converts a configuration with a rule set to options", async () => { + const lFoundOptions = await loadOptions( + join(mockDirectory, "rules.sub-not-allowed-error.json"), + ); + deepEqual(lFoundOptions, { + ruleSet: { + allowed: [{ from: {}, to: {} }], + forbidden: [ + { + name: "sub-not-allowed", + severity: "error", + from: {}, + to: { + path: "sub", + }, + }, + ], + }, + validate: true, + }); + }); + + it("correctly converts a configuration with a rule set, & puts options in the right spot", async () => { + const lFoundOptions = await loadOptions( + join(mockDirectory, "rules.sub-not-allowed-error-with-options.json"), + ); + deepEqual(lFoundOptions, { + ruleSet: { + allowed: [{ from: {}, to: {} }], + forbidden: [ + { + name: "sub-not-allowed", + severity: "error", + from: {}, + to: { + path: "sub", + }, + }, + ], + }, + validate: true, + includeOnly: ["src"], + reporterOptions: { + text: { + highlightFocused: true, + }, + }, + cache: { + strategy: "metadata", + compress: true, + }, + }); + }); +}); diff --git a/types/config-utl/extract-depcruise-config.d.mts b/types/config-utl/extract-depcruise-config.d.mts index d75e9d23f..1fb906f68 100644 --- a/types/config-utl/extract-depcruise-config.d.mts +++ b/types/config-utl/extract-depcruise-config.d.mts @@ -1,25 +1,27 @@ -import type { ICruiseOptions } from "../options.mjs"; +import type { IConfiguration } from "../configuration.mjs"; /** * Reads the file with name `pConfigFileName` returns the parsed cruise - * options. + * configuration. If you're looking for the function to read a + * dependency-cruiser configuration file and put the result into the `cruise` + * function use `extractDepcruiseOptions` instead.s * * You can safely ignore the optional parameters. This should work (given * `.dependency-cruiser.js` exists and contains a valid dependency-cruiser * config) * * ```javascript - * const depcruiseConfig = extractDepcruiseConfig("./.dependency-cruiser.js") + * const depcruiseConfig = await extractDepcruiseConfig("./.dependency-cruiser.js") * ``` * * @param pConfigFileName * @param pAlreadyVisited * @param pBaseDirectory - * @return dependency-cruiser options + * @return dependency-cruiser configuration * @throws when the config is not valid (/ does not exist/ isn't readable) */ export default function extractDepcruiseConfig( pConfigFileName: string, pAlreadyVisited?: Set, - pBaseDirectory?: string -): Promise; + pBaseDirectory?: string, +): Promise; diff --git a/types/config-utl/extract-depcruise-options.d.mts b/types/config-utl/extract-depcruise-options.d.mts new file mode 100644 index 000000000..8bf364355 --- /dev/null +++ b/types/config-utl/extract-depcruise-options.d.mts @@ -0,0 +1,18 @@ +import type { ICruiseOptions } from "../options.mjs"; + +/** + * Reads the file with name `pConfigFileName` returns the parsed cruise + * options you can use as + * + * ```javascript + * const depcruiseOptions = await extractDepcruiseOptions("./.dependency-cruiser.js") + * const cruiseResult = await cruise(["./src"], depcruiseOptions); + * ``` + * + * @param pConfigFileName + * @return dependency-cruiser options + * @throws when the config is not valid (/ does not exist/ isn't readable) + */ +export default function extractDepcruiseOptions( + pConfigFileName: string, +): Promise; diff --git a/types/dependency-cruiser.d.mts b/types/dependency-cruiser.d.mts index 440b8891f..deb1ed52b 100644 --- a/types/dependency-cruiser.d.mts +++ b/types/dependency-cruiser.d.mts @@ -90,7 +90,7 @@ export interface ITranspileOptions { export function cruise( pFileAndDirectoryArray: string[], pCruiseOptions?: ICruiseOptions, - pResolveOptions?: IResolveOptions, + pResolveOptions?: Partial, pTranspileOptions?: ITranspileOptions, ): Promise;