Skip to content

Commit

Permalink
fix(config-util): adds utility that extracts dependency-cruiser optio…
Browse files Browse the repository at this point in the history
…ns 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] 📖

  - My change doesn't require a documentation update, or ...
  - it _does_ and I have updated it

- [x] ⚖️
- 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).
  • Loading branch information
sverweij authored May 9, 2024
1 parent 7e74a4e commit 54a7f42
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 37 deletions.
7 changes: 6 additions & 1 deletion .dependency-cruiser.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
},
Expand Down
63 changes: 35 additions & 28 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
```

Expand Down Expand Up @@ -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);
}
```
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/config-utl/extract-depcruise-config/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions src/config-utl/extract-depcruise-config/read-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { readFile } from "node:fs/promises";
import { extname } from "node:path";
import json5 from "json5";

/**
*
* @param {string} pAbsolutePathToConfigFile
* @returns {Promise<import('../../../types/configuration.mjs').IConfiguration>}
*/
export default async function readConfig(pAbsolutePathToConfigFile) {
if (
[".js", ".cjs", ".mjs", ""].includes(extname(pAbsolutePathToConfigFile))
Expand Down
28 changes: 28 additions & 0 deletions src/config-utl/extract-depcruise-options.mjs
Original file line number Diff line number Diff line change
@@ -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<import('../../../types/configuration.mjs').ICruiseOptions>}
*/
export default async function extractDepcruiseOptions(pConfigFileName) {
const lReturnValue = await extractDepcruiseConfig(pConfigFileName);
return configuration2options(lReturnValue);
}
1 change: 1 addition & 0 deletions test/config-utl/__mocks__/depcruiseconfig/empty.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"allowed": [
{
"from": {
},
"to": {
}
}
],
"forbidden": [
{
"name": "sub-not-allowed",
"severity": "error",
"from": {
},
"to": {
"path": "sub"
}
}
]
}
71 changes: 71 additions & 0 deletions test/config-utl/extract-depcruise-options.spec.mjs
Original file line number Diff line number Diff line change
@@ -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,
},
});
});
});
14 changes: 8 additions & 6 deletions types/config-utl/extract-depcruise-config.d.mts
Original file line number Diff line number Diff line change
@@ -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<string>,
pBaseDirectory?: string
): Promise<ICruiseOptions>;
pBaseDirectory?: string,
): Promise<IConfiguration>;
18 changes: 18 additions & 0 deletions types/config-utl/extract-depcruise-options.d.mts
Original file line number Diff line number Diff line change
@@ -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<ICruiseOptions>;
2 changes: 1 addition & 1 deletion types/dependency-cruiser.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export interface ITranspileOptions {
export function cruise(
pFileAndDirectoryArray: string[],
pCruiseOptions?: ICruiseOptions,
pResolveOptions?: IResolveOptions,
pResolveOptions?: Partial<IResolveOptions>,
pTranspileOptions?: ITranspileOptions,
): Promise<IReporterOutput>;

Expand Down

0 comments on commit 54a7f42

Please sign in to comment.