Repository: sverweij/dependency-cruiser
Pull request #984 (2025-02-14 20:28:53)

feat(cli): adds init option for bun
## Description

- adds an option to the init config to (1) detect whether bun is likely used (2) add the bun builtins in the config.

## Motivation and Context

seems useful for those projects that use bun

## How Has This Been Tested?

- [x] green ci
- [x] additional non-regression tests


## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without changing functionality)
- [x] 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).


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=sverweij_dependency-cruiser&pullRequest=984) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=sverweij_dependency-cruiser&pullRequest=984&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=sverweij_dependency-cruiser&pullRequest=984&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=sverweij_dependency-cruiser&pullRequest=984)


codeclimate[bot]: Code Climate has analyzed commit be28e0db and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/984).

sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=sverweij_dependency-cruiser&pullRequest=984) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=sverweij_dependency-cruiser&pullRequest=984&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=sverweij_dependency-cruiser&pullRequest=984&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=sverweij_dependency-cruiser&pullRequest=984&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=sverweij_dependency-cruiser&pullRequest=984)


codeclimate[bot]: Code Climate has analyzed commit 4d7c0e52 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/984).


Patch:
diff --git a/doc/options-reference.md b/doc/options-reference.md
index 31b00858..b808a4e2 100644
--- a/doc/options-reference.md
+++ b/doc/options-reference.md
@@ -1499,6 +1499,9 @@ In contexts that are not nodejs this might be not (entirely) right:
 - when targeting a platform built on top of nodejs, like electron, you might
   want to specify the packages built into that platform as built-in/ core
   as well.
+- when targeting a platform that is compatible with nodejs, but extends it
+  (e.g. bun) - you might want to add the packages it adds. If `--init`, during
+  setup, detects you're using bun, it'll add the bun builtins with this option.
 
 To override the default behaviour you can pass a `builtInModules` object in
 the options section of your dependency-cruiser configuration. The object has two
diff --git a/src/cli/init-config/build-config.mjs b/src/cli/init-config/build-config.mjs
index ec74de4a..12680722 100644
--- a/src/cli/init-config/build-config.mjs
+++ b/src/cli/init-config/build-config.mjs
@@ -160,6 +160,36 @@ function buildMainFieldsAttribute(pInitOptions) {
       mainFields: ["main", "types", "typings"],`;
 }
 
+/**
+ * @param {IInitConfig} pInitOptions
+ * @returns {string}
+ */
+function buildBuiltInModulesAttribute(pInitOptions) {
+  if (pInitOptions.usesBun) {
+    return `
+    /* List of built-in modules to use on top of the ones node declares.
+
+       See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#builtinmodules-influencing-what-to-consider-built-in--core-modules
+       for details
+    */
+    builtInModules: { 
+      add: [
+        "bun",
+        "bun:ffi",
+        "bun:jsc",
+        "bun:sqlite",
+        "bun:test",
+        "bun:wrap",
+        "detect-libc",
+        "undici",
+        "ws"
+      ]
+    },
+`;
+  }
+  return "";
+}
+
 /**
  * Creates a .dependency-cruiser config with a set of basic validations
  * to the current directory.
@@ -203,6 +233,10 @@ export default function buildConfig(pInitOptions) {
       "{{babelConfigAttribute}}",
       buildBabelConfigAttribute(pInitOptions),
     )
+    .replace(
+      "{{builtInModulesAttribute}}",
+      buildBuiltInModulesAttribute(pInitOptions),
+    )
     .replace("{{extensionsAttribute}}", buildExtensionsAttribute(pInitOptions))
     .replace("{{mainFieldsAttribute}}", buildMainFieldsAttribute(pInitOptions))
     .replace("{{version}}", pInitOptions.version)
diff --git a/src/cli/init-config/config-template.mjs b/src/cli/init-config/config-template.mjs
index 67ede9dd..baf4b406 100644
--- a/src/cli/init-config/config-template.mjs
+++ b/src/cli/init-config/config-template.mjs
@@ -314,8 +314,7 @@ module.exports = {
          Only works when the 'exportsFields' array is non-empty.
       */
       conditionNames: ["import", "require", "node", "default", "types"],
-      /*
-         The extensions, by default are the same as the ones dependency-cruiser
+      /* The extensions, by default are the same as the ones dependency-cruiser
          can access (run \`npx depcruise --info\` to see which ones that are in
          _your_ environment). If that list is larger than you need you can pass
          the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
@@ -324,26 +323,25 @@ module.exports = {
       {{extensionsAttribute}}
       /* What to consider a 'main' field in package.json */
       {{mainFieldsAttribute}}
-      /*
-         A list of alias fields in package.jsons
+      /* A list of alias fields in package.jsons
+        
          See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and
          the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields)
-         documentation 
+         documentation.
          
          Defaults to an empty array (= don't use alias fields).
        */
       // aliasFields: ["browser"],
     },
 
-    /*
-      skipAnalysisNotInRules will make dependency-cruiser execute 
-      analysis strictly necessary for checking the rule set only. 
+    /* skipAnalysisNotInRules will make dependency-cruiser execute 
+       analysis strictly necessary for checking the rule set only. 
 
-      See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
-      for details
+       See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
+       for details
      */
     skipAnalysisNotInRules: true,
-
+    {{builtInModulesAttribute}}
     reporterOptions: {
       dot: {
         /* pattern of modules that can be consolidated in the detailed
@@ -369,9 +367,9 @@ module.exports = {
       },
       archi: {
         /* pattern of modules that can be consolidated in the high level
-          graphical dependency graph. If you use the high level graphical
-          dependency graph reporter (\`archi\`) you probably want to tweak
-          this collapsePattern to your situation.
+           graphical dependency graph. If you use the high level graphical
+           dependency graph reporter (\`archi\`) you probably want to tweak
+           this collapsePattern to your situation.
         */
         collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)',
 
diff --git a/src/cli/init-config/environment-helpers.mjs b/src/cli/init-config/environment-helpers.mjs
index 2450cb4e..00913fc2 100644
--- a/src/cli/init-config/environment-helpers.mjs
+++ b/src/cli/init-config/environment-helpers.mjs
@@ -99,6 +99,30 @@ export function isLikelyMonoRepo(pFolderNames = getFolderNames(process.cwd())) {
   return pFolderNames.includes("packages");
 }
 
+function getPackageManager(pManifest) {
+  let lReturnValue = "";
+  try {
+    const lManifest = pManifest ?? readManifest();
+    if (Object.hasOwn(lManifest, "packageManager")) {
+      lReturnValue = lManifest.packageManager;
+    }
+  } catch (pError) {
+    // silently ignore - we'll return the empty string anyway then
+  }
+  return lReturnValue;
+}
+
+/**
+ * @param {Record<string,any>} pManifest
+ * @returns {boolean}
+ */
+export function likelyUsesBun(pManifest) {
+  const bunIsPackageManager = getPackageManager(pManifest).startsWith("bun");
+  return (
+    bunIsPackageManager || fileExists("bun.lockb") || fileExists("bunfig.toml")
+  );
+}
+
 export function hasTestsWithinSource(pTestLocations, pSourceLocations) {
   return pTestLocations.every((pTestLocation) =>
     pSourceLocations.includes(pTestLocation),
diff --git a/src/cli/init-config/get-user-input.mjs b/src/cli/init-config/get-user-input.mjs
index f4545a3c..2a98bec5 100644
--- a/src/cli/init-config/get-user-input.mjs
+++ b/src/cli/init-config/get-user-input.mjs
@@ -16,6 +16,7 @@ import {
   getBabelConfigCandidates,
   hasWebpackConfigCandidates,
   getWebpackConfigCandidates,
+  likelyUsesBun,
 } from "./environment-helpers.mjs";
 import { validateLocation } from "./validators.mjs";
 import { isAvailable as tscIsAvailable } from "#extract/tsc/parse.mjs";
@@ -146,6 +147,12 @@ const QUESTIONS = [
     message: "Full path to your webpack config:",
     choices: getWebpackConfigCandidates().map(toPromptChoice),
   },
+  {
+    name: "usesBun",
+    type: () => (likelyUsesBun() ? "confirm" : false),
+    message: "Looks like you're using bun. Detect its builtins?",
+    initial: true,
+  },
 ];
 
 /**
diff --git a/src/cli/init-config/index.mjs b/src/cli/init-config/index.mjs
index edde9a3d..3d96ef7c 100644
--- a/src/cli/init-config/index.mjs
+++ b/src/cli/init-config/index.mjs
@@ -16,6 +16,7 @@ import {
   getDefaultConfigFileName,
   hasJSConfigCandidates,
   getJSConfigCandidates,
+  likelyUsesBun,
   isTypeModule,
 } from "./environment-helpers.mjs";
 import { writeRunScriptsToManifest } from "./write-run-scripts-to-manifest.mjs";
@@ -50,6 +51,7 @@ function getOneShotConfig(pOneShotConfigId) {
     useBabelConfig: hasBabelConfigCandidates(),
     babelConfig: getBabelConfigCandidates().shift(),
     specifyResolutionExtensions: true,
+    usesBun: likelyUsesBun(),
   };
   /** @type {Map<OneShotConfigIDType, IPartialInitConfig>} */
   const lOneShotConfigs = new Map([
diff --git a/src/cli/init-config/types.d.ts b/src/cli/init-config/types.d.ts
index 52200f94..a1d76811 100644
--- a/src/cli/init-config/types.d.ts
+++ b/src/cli/init-config/types.d.ts
@@ -100,6 +100,10 @@ export interface IInitConfig {
    * (Formatted) date this config is created
    */
   date: string;
+  /**
+   * bun is used as package manager cq runtime
+   */
+  usesBun: boolean;
 }
 
 export type IPartialInitConfig = Partial<IInitConfig>;


Test patch:
diff --git a/test/cli/init-config/__fixtures__/bun-config/bunfig.toml b/test/cli/init-config/__fixtures__/bun-config/bunfig.toml
new file mode 100644
index 00000000..e69de29b
diff --git a/test/cli/init-config/__fixtures__/bun-lockfile/bun.lockb b/test/cli/init-config/__fixtures__/bun-lockfile/bun.lockb
new file mode 100644
index 00000000..e69de29b
diff --git a/test/cli/init-config/build-config.spec.mjs b/test/cli/init-config/build-config.spec.mjs
index 7075fed6..fd1054ce 100644
--- a/test/cli/init-config/build-config.spec.mjs
+++ b/test/cli/init-config/build-config.spec.mjs
@@ -10,13 +10,13 @@ import buildConfig from "#cli/init-config/build-config.mjs";
 import normalizeInitOptions from "#cli/init-config/normalize-init-options.mjs";
 
 const ajv = new Ajv();
+const RANDOM_TMP_FILENAME_LENGTH = 10;
 
 const createConfigNormalized = async (pInitOptions) => {
   const lConfigAsString = buildConfig(normalizeInitOptions(pInitOptions));
-  const lFileNameLength = 10;
   let lTemporaryFileName = join(
     tmpdir(),
-    `${randomBytes(lFileNameLength).toString("hex")}.cjs`,
+    `${randomBytes(RANDOM_TMP_FILENAME_LENGTH).toString("hex")}.cjs`,
   );
   writeFileSync(lTemporaryFileName, lConfigAsString, "utf8");
   const lConfigAsModule = await import(`file:///${lTemporaryFileName}`);
@@ -127,4 +127,22 @@ describe("[I] cli/init-config/build-config", () => {
       "typings",
     ]);
   });
+
+  it("generates a builtins property with additional bun builtins when we use bun", async () => {
+    const lResult = await createConfigNormalized({ usesBun: true });
+
+    ajv.validate(configurationSchema, lResult);
+    ok(lResult.hasOwnProperty("options"));
+    deepEqual(lResult.options.builtInModules.add, [
+      "bun",
+      "bun:ffi",
+      "bun:jsc",
+      "bun:sqlite",
+      "bun:test",
+      "bun:wrap",
+      "detect-libc",
+      "undici",
+      "ws",
+    ]);
+  });
 });
diff --git a/test/cli/init-config/environment-helpers.spec.mjs b/test/cli/init-config/environment-helpers.spec.mjs
index 49e7e4a6..683ea1f8 100644
--- a/test/cli/init-config/environment-helpers.spec.mjs
+++ b/test/cli/init-config/environment-helpers.spec.mjs
@@ -3,6 +3,7 @@ import {
   isLikelyMonoRepo,
   hasTestsWithinSource,
   getFolderCandidates,
+  likelyUsesBun,
 } from "#cli/init-config/environment-helpers.mjs";
 
 describe("[U] cli/init-config/environment-helpers - isLikelyMonoRepo", () => {
@@ -59,3 +60,26 @@ describe("[U] cli/init-config/environment-helpers - getFolderCandidates", () =>
     deepEqual(getFolderCandidates(lCandidates)(lRealFolders), ["src"]);
   });
 });
+
+describe("[U] cli/init-config/environment-helpers - likelyUsesBun ", () => {
+  const WORKINGDIR = process.cwd();
+
+  afterEach("tear down", () => {
+    process.chdir(WORKINGDIR);
+  });
+  it("returns false when there's no sign of bun", () => {
+    equal(likelyUsesBun({}), false);
+  });
+
+  it("returns true when bun is the package manager", () => {
+    equal(likelyUsesBun({ packageManager: "bun@4.8.1" }), true);
+  });
+  it("returns true when there's a bun lockfile (bun.lockb)", () => {
+    process.chdir("test/cli/init-config/__fixtures__/bun-lockfile");
+    equal(likelyUsesBun({}), true);
+  });
+  it("returns true when there's a bun config file (bunfig.toml)", () => {
+    process.chdir("test/cli/init-config/__fixtures__/bun-config");
+    equal(likelyUsesBun({}), true);
+  });
+});