From b1b1039eac6c50f603c8688c13e36cb2b6306198 Mon Sep 17 00:00:00 2001
From: Jordan Hall <jordan@libertyware.co.uk>
Date: Sun, 26 May 2024 23:51:11 +0100
Subject: [PATCH] feat(Bun): creation of bun library with Bun Test support

---
 e2e/bun-e2e/.eslintrc.json                    |  18 ++
 e2e/bun-e2e/jest.config.ts                    |  12 ++
 e2e/bun-e2e/project.json                      |  21 ++
 e2e/bun-e2e/src/bun.spec.ts                   |  65 ++++++
 e2e/bun-e2e/tsconfig.json                     |  10 +
 e2e/bun-e2e/tsconfig.spec.json                |  14 ++
 nx.json                                       |  12 +-
 package.json                                  |   2 +-
 packages/bun/.eslintrc.json                   |  32 +++
 packages/bun/README.md                        |  53 +++++
 packages/bun/executors.json                   |   9 +
 packages/bun/jest.config.ts                   |  10 +
 packages/bun/package.json                     |  13 ++
 packages/bun/project.json                     |  64 ++++++
 packages/bun/src/executors/.gitkeep           |   0
 .../bun/src/executors/test/executor.spec.ts   |  11 ++
 packages/bun/src/executors/test/executor.ts   | 159 +++++++++++++++
 packages/bun/src/executors/test/schema.d.ts   |  12 ++
 packages/bun/src/executors/test/schema.json   |  62 ++++++
 packages/bun/src/index.ts                     |   0
 packages/bun/src/utils/bun-cli.ts             | 185 ++++++++++++++++++
 packages/bun/tsconfig.json                    |  16 ++
 packages/bun/tsconfig.lib.json                |  10 +
 packages/bun/tsconfig.spec.json               |  14 ++
 tools/scripts/start-local-registry.ts         |  34 ++++
 tools/scripts/stop-local-registry.ts          |  10 +
 tsconfig.base.json                            |   1 +
 yarn.lock                                     |  34 ++++
 28 files changed, 881 insertions(+), 2 deletions(-)
 create mode 100644 e2e/bun-e2e/.eslintrc.json
 create mode 100644 e2e/bun-e2e/jest.config.ts
 create mode 100644 e2e/bun-e2e/project.json
 create mode 100644 e2e/bun-e2e/src/bun.spec.ts
 create mode 100644 e2e/bun-e2e/tsconfig.json
 create mode 100644 e2e/bun-e2e/tsconfig.spec.json
 create mode 100644 packages/bun/.eslintrc.json
 create mode 100644 packages/bun/README.md
 create mode 100644 packages/bun/executors.json
 create mode 100644 packages/bun/jest.config.ts
 create mode 100644 packages/bun/package.json
 create mode 100644 packages/bun/project.json
 create mode 100644 packages/bun/src/executors/.gitkeep
 create mode 100644 packages/bun/src/executors/test/executor.spec.ts
 create mode 100644 packages/bun/src/executors/test/executor.ts
 create mode 100644 packages/bun/src/executors/test/schema.d.ts
 create mode 100644 packages/bun/src/executors/test/schema.json
 create mode 100644 packages/bun/src/index.ts
 create mode 100644 packages/bun/src/utils/bun-cli.ts
 create mode 100644 packages/bun/tsconfig.json
 create mode 100644 packages/bun/tsconfig.lib.json
 create mode 100644 packages/bun/tsconfig.spec.json
 create mode 100644 tools/scripts/start-local-registry.ts
 create mode 100644 tools/scripts/stop-local-registry.ts

diff --git a/e2e/bun-e2e/.eslintrc.json b/e2e/bun-e2e/.eslintrc.json
new file mode 100644
index 0000000000..9d9c0db55b
--- /dev/null
+++ b/e2e/bun-e2e/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/e2e/bun-e2e/jest.config.ts b/e2e/bun-e2e/jest.config.ts
new file mode 100644
index 0000000000..84f5d3b76a
--- /dev/null
+++ b/e2e/bun-e2e/jest.config.ts
@@ -0,0 +1,12 @@
+/* eslint-disable */
+export default {
+  displayName: 'bun-e2e',
+  preset: '../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
+  },
+  moduleFileExtensions: ['ts', 'js', 'html'],
+  coverageDirectory: '../../coverage/e2e/bun-e2e',
+  globalSetup: '../../tools/scripts/start-local-registry.ts',
+  globalTeardown: '../../tools/scripts/stop-local-registry.ts',
+};
diff --git a/e2e/bun-e2e/project.json b/e2e/bun-e2e/project.json
new file mode 100644
index 0000000000..0a4264270d
--- /dev/null
+++ b/e2e/bun-e2e/project.json
@@ -0,0 +1,21 @@
+{
+  "name": "bun-e2e",
+  "$schema": "../../node_modules/nx/schemas/project-schema.json",
+  "projectType": "application",
+  "sourceRoot": "e2e/bun-e2e/src",
+  "implicitDependencies": ["bun"],
+  "targets": {
+    "e2e": {
+      "executor": "@nx/jest:jest",
+      "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+      "options": {
+        "jestConfig": "e2e/bun-e2e/jest.config.ts",
+        "runInBand": true
+      },
+      "dependsOn": ["^build"]
+    },
+    "lint": {
+      "executor": "@nx/eslint:lint"
+    }
+  }
+}
diff --git a/e2e/bun-e2e/src/bun.spec.ts b/e2e/bun-e2e/src/bun.spec.ts
new file mode 100644
index 0000000000..5a933f2ebe
--- /dev/null
+++ b/e2e/bun-e2e/src/bun.spec.ts
@@ -0,0 +1,65 @@
+import { execSync } from 'child_process';
+import { mkdirSync, rmSync } from 'fs';
+import { dirname, join } from 'path';
+
+describe('bun', () => {
+  let projectDirectory: string;
+
+  beforeAll(() => {
+    projectDirectory = createTestProject();
+
+    // The plugin has been built and published to a local registry in the jest globalSetup
+    // Install the plugin built with the latest source code into the test repo
+    execSync(`npm install @nx/bun@e2e`, {
+      cwd: projectDirectory,
+      stdio: 'inherit',
+      env: process.env,
+    });
+  });
+
+  afterAll(() => {
+    // Cleanup the test project
+    rmSync(projectDirectory, {
+      recursive: true,
+      force: true,
+    });
+  });
+
+  it('should be installed', () => {
+    // npm ls will fail if the package is not installed properly
+    execSync('npm ls @nx/bun', {
+      cwd: projectDirectory,
+      stdio: 'inherit',
+    });
+  });
+});
+
+/**
+ * Creates a test project with create-nx-workspace and installs the plugin
+ * @returns The directory where the test project was created
+ */
+function createTestProject() {
+  const projectName = 'test-project';
+  const projectDirectory = join(process.cwd(), 'tmp', projectName);
+
+  // Ensure projectDirectory is empty
+  rmSync(projectDirectory, {
+    recursive: true,
+    force: true,
+  });
+  mkdirSync(dirname(projectDirectory), {
+    recursive: true,
+  });
+
+  execSync(
+    `npx --yes create-nx-workspace@latest ${projectName} --preset apps --nxCloud=skip --no-interactive`,
+    {
+      cwd: dirname(projectDirectory),
+      stdio: 'inherit',
+      env: process.env,
+    }
+  );
+  console.log(`Created test project in "${projectDirectory}"`);
+
+  return projectDirectory;
+}
diff --git a/e2e/bun-e2e/tsconfig.json b/e2e/bun-e2e/tsconfig.json
new file mode 100644
index 0000000000..b9c9d95376
--- /dev/null
+++ b/e2e/bun-e2e/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/e2e/bun-e2e/tsconfig.spec.json b/e2e/bun-e2e/tsconfig.spec.json
new file mode 100644
index 0000000000..9b2a121d11
--- /dev/null
+++ b/e2e/bun-e2e/tsconfig.spec.json
@@ -0,0 +1,14 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"]
+  },
+  "include": [
+    "jest.config.ts",
+    "src/**/*.test.ts",
+    "src/**/*.spec.ts",
+    "src/**/*.d.ts"
+  ]
+}
diff --git a/nx.json b/nx.json
index 97fb82ea1d..66eef3d328 100644
--- a/nx.json
+++ b/nx.json
@@ -41,8 +41,18 @@
     "@nx/eslint:lint": {
       "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
       "cache": true
+    },
+    "@nx/js:tsc": {
+      "cache": true,
+      "dependsOn": ["^build"],
+      "inputs": ["default", "^default"]
     }
   },
   "nxCloudAccessToken": "MDRmYzUxMmYtNTQwZi00MjZkLTg0ZTYtMzc5Y2RhOTE4YTc2fHJlYWQtd3JpdGU=",
-  "defaultBase": "main"
+  "defaultBase": "main",
+  "release": {
+    "version": {
+      "preVersionCommand": "yarn nx run-many -t build"
+    }
+  }
 }
diff --git a/package.json b/package.json
index 7184638528..b9a787b433 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "@rspack/plugin-minify": "^0.5.6",
     "@rspack/plugin-react-refresh": "^0.5.6",
     "@swc-node/register": "1.8.0",
+    "@types/bun": "^1.1.3",
     "@types/fs-extra": "^11.0.1",
     "@types/jest": "29.4.0",
     "@types/node": "18.16.9",
@@ -73,4 +74,3 @@
     "verdaccio": "^5.0.4"
   }
 }
-
diff --git a/packages/bun/.eslintrc.json b/packages/bun/.eslintrc.json
new file mode 100644
index 0000000000..6b4ec01943
--- /dev/null
+++ b/packages/bun/.eslintrc.json
@@ -0,0 +1,32 @@
+{
+  "extends": ["../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.json"],
+      "parser": "jsonc-eslint-parser",
+      "rules": {
+        "@nx/dependency-checks": "error"
+      }
+    },
+    {
+      "files": ["./package.json", "./executors.json"],
+      "parser": "jsonc-eslint-parser",
+      "rules": {
+        "@nx/nx-plugin-checks": "error"
+      }
+    }
+  ]
+}
diff --git a/packages/bun/README.md b/packages/bun/README.md
new file mode 100644
index 0000000000..e31ca3a19a
--- /dev/null
+++ b/packages/bun/README.md
@@ -0,0 +1,53 @@
+<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
+
+<hr>
+
+# Nx: Smart, Fast and Extensible Build System
+
+Nx is a next generation build system with first class monorepo support and powerful integrations.
+
+This package is a Bun plugin for Nx.
+
+## Getting Started
+
+Use `--preset=@nx/bun` when creating new workspace.
+
+e.g.
+
+```bash
+npx create-nx-workspace@latest bun-demo --preset=@nx/bun
+```
+
+Now, you can go into the `bun-demo` folder and start development.
+
+```bash
+cd bun-demo
+npm start
+```
+
+You can also run lint, test, and e2e scripts for the project.
+
+```bash
+npm run lint
+npm run test
+npm run e2e
+```
+
+## Existing workspaces
+
+You can add Bun to any existing Nx workspace.
+
+First, install the plugin:
+
+```bash
+npm install --save-dev @nx/bun
+```
+
+Then, run the `bun-project` generator:
+
+```bash
+npx nx g @nx/bun:bun-project --skipValidation
+```
+
+**Note:** The `--skipValidation` option allows you to overwrite existing build targets.
+
diff --git a/packages/bun/executors.json b/packages/bun/executors.json
new file mode 100644
index 0000000000..71f80a2e0f
--- /dev/null
+++ b/packages/bun/executors.json
@@ -0,0 +1,9 @@
+{
+  "executors": {
+    "test": {
+      "implementation": "./src/executors/test/executor",
+      "schema": "./src/executors/test/schema.json",
+      "description": "Test with Bun test"
+    }
+  }
+}
diff --git a/packages/bun/jest.config.ts b/packages/bun/jest.config.ts
new file mode 100644
index 0000000000..7f9bc910ac
--- /dev/null
+++ b/packages/bun/jest.config.ts
@@ -0,0 +1,10 @@
+/* eslint-disable */
+export default {
+  displayName: 'bun',
+  preset: '../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
+  },
+  moduleFileExtensions: ['ts', 'js', 'html'],
+  coverageDirectory: '../../coverage/packages/bun',
+};
diff --git a/packages/bun/package.json b/packages/bun/package.json
new file mode 100644
index 0000000000..4e0b2736e2
--- /dev/null
+++ b/packages/bun/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "@nx/bun",
+  "version": "0.0.1",
+  "dependencies": {
+    "@nx/js": "~19.1.0",
+    "@nx/devkit": "~19.1.0",
+    "tslib": "^2.3.0"
+  },
+  "type": "commonjs",
+  "main": "./src/index.js",
+  "typings": "./src/index.d.ts",
+  "executors": "./executors.json"
+}
diff --git a/packages/bun/project.json b/packages/bun/project.json
new file mode 100644
index 0000000000..965c828df9
--- /dev/null
+++ b/packages/bun/project.json
@@ -0,0 +1,64 @@
+{
+  "name": "bun",
+  "$schema": "../../node_modules/nx/schemas/project-schema.json",
+  "sourceRoot": "packages/bun/src",
+  "projectType": "library",
+  "release": {
+    "version": {
+      "generatorOptions": {
+        "packageRoot": "dist/{projectRoot}",
+        "currentVersionResolver": "git-tag"
+      }
+    }
+  },
+  "tags": [],
+  "targets": {
+    "build": {
+      "executor": "@nx/js:tsc",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/packages/bun",
+        "main": "packages/bun/src/index.ts",
+        "tsConfig": "packages/bun/tsconfig.lib.json",
+        "assets": [
+          "packages/bun/*.md",
+          {
+            "input": "./packages/bun/src",
+            "glob": "**/!(*.ts)",
+            "output": "./src"
+          },
+          {
+            "input": "./packages/bun/src",
+            "glob": "**/*.d.ts",
+            "output": "./src"
+          },
+          {
+            "input": "./packages/bun",
+            "glob": "generators.json",
+            "output": "."
+          },
+          {
+            "input": "./packages/bun",
+            "glob": "executors.json",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "nx-release-publish": {
+      "options": {
+        "packageRoot": "dist/{projectRoot}"
+      }
+    },
+    "lint": {
+      "executor": "@nx/eslint:lint"
+    },
+    "test": {
+      "executor": "@nx/jest:jest",
+      "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+      "options": {
+        "jestConfig": "packages/bun/jest.config.ts"
+      }
+    }
+  }
+}
diff --git a/packages/bun/src/executors/.gitkeep b/packages/bun/src/executors/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/bun/src/executors/test/executor.spec.ts b/packages/bun/src/executors/test/executor.spec.ts
new file mode 100644
index 0000000000..3c6c6317e7
--- /dev/null
+++ b/packages/bun/src/executors/test/executor.spec.ts
@@ -0,0 +1,11 @@
+import executor from './executor';
+import { TestExecutorSchema } from './schema';
+
+const options: TestExecutorSchema = {};
+
+describe('Test Executor', () => {
+  it('can run', async () => {
+    const output = await executor(options);
+    expect(output.success).toBe(true);
+  });
+});
diff --git a/packages/bun/src/executors/test/executor.ts b/packages/bun/src/executors/test/executor.ts
new file mode 100644
index 0000000000..84310ae74e
--- /dev/null
+++ b/packages/bun/src/executors/test/executor.ts
@@ -0,0 +1,159 @@
+import { ExecutorContext } from '@nx/devkit';
+import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
+
+import { ExecOptions, UnifiedChildProcess, assertBunAvailable, executeCliAsync, isBunSubprocess } from '../../utils/bun-cli';
+import { TestExecutorSchema } from './schema';
+import { ChildProcess } from 'child_process';
+
+interface TestExecutorNormalizedSchema extends TestExecutorSchema {
+  testDir: string;
+}
+
+interface ActiveTask {
+  id: string;
+  killed: boolean;
+  childProcess: null | UnifiedChildProcess;
+  start: () => Promise<void>;
+  stop: (signal: NodeJS.Signals) => Promise<void>;
+}
+
+const activeTasks: ActiveTask[] = [];
+
+export default async function* runExecutor(
+  options: TestExecutorSchema,
+  context: ExecutorContext
+) {
+  await assertBunAvailable();
+  const opts = normalizeOptions(options, context);
+  const args = createArgs(opts);
+
+  const task: ActiveTask = createTask(args, {});
+  activeTasks.push(task);
+
+  await task.start();
+
+  process.on('SIGTERM', () => stopAllActiveTasks('SIGTERM'));
+  process.on('SIGINT', () => stopAllActiveTasks('SIGINT'));
+  process.on('SIGHUP', () => stopAllActiveTasks('SIGHUP'));
+  process.on('uncaughtException', (err) => {
+    console.error('Caught exception:', err);
+    stopAllActiveTasks('SIGTERM').finally(() => process.exit(1));
+  });
+
+  yield* createAsyncIterable(async ({ next, done }) => {
+    if (isBunSubprocess(task.childProcess)) {
+      await task.childProcess?.exited;
+    } else {
+      await new Promise<void>((resolve) => {
+        (task.childProcess as ChildProcess)?.on('exit', () => {
+          resolve();
+        });
+      });
+    }
+
+    next({ success: !task.killed });
+    if (!options.watch) {
+      done();
+    }
+  });
+}
+
+function createTask(args: string[], options: ExecOptions): ActiveTask {
+  const id = crypto.randomUUID();
+  const childProcess: UnifiedChildProcess | null = null;
+
+  const start = async () => {
+    for await (const message of executeCliAsync(args, options)) {
+      handleMessage('stdout', message);
+    }
+  };
+
+  const stop = async (signal: NodeJS.Signals) => {
+    if (childProcess) {
+      childProcess.kill(signal)
+    }
+  };
+
+  const task: ActiveTask = {
+    id,
+    killed: false,
+    childProcess,
+    start,
+    stop,
+  };
+
+  return task;
+}
+
+function handleMessage(type: 'stdout' | 'stderr', message: string) {
+  if (process.send) {
+    process.send({ type, message });
+  } else {
+    if (type === 'stdout') {
+      console.log(message);
+    } else {
+      console.error(message);
+    }
+  }
+}
+
+function createArgs(options: TestExecutorNormalizedSchema) {
+  const args: string[] = ['test', `--cwd=${options.testDir}`];
+
+  if (options.smol) {
+    args.push('--smol');
+  }
+
+  if (options.config) {
+    args.push(`-c ${options.config}`);
+  }
+  if (options.tsConfig) {
+    args.push(`--tsconfig-override=${options.tsConfig}`);
+  }
+
+  if (typeof options.bail === 'boolean') {
+    args.push('--bail');
+  } else if (typeof options.bail === 'number') {
+    args.push(`--bail=${options.bail}`);
+  }
+  if (options.preload) {
+    args.push(`--preload=${options.preload}`);
+  }
+  if (options.timeout) {
+    args.push(`--timeout=${options.timeout}`);
+  }
+
+  if (options.rerunEach) {
+    args.push(`--rerun-each=${options.rerunEach}`);
+  }
+  if (options.watch) {
+    args.push('--watch');
+  }
+  return args;
+}
+
+function normalizeOptions(
+  options: TestExecutorSchema,
+  context: ExecutorContext
+): TestExecutorNormalizedSchema {
+  const projectConfig =
+    context.projectGraph?.nodes?.[context.projectName]?.data;
+
+  if (!projectConfig) {
+    throw new Error(
+      `Could not find project configuration for ${context.projectName} in executor context.`
+    );
+  }
+  return {
+    ...options,
+    testDir: projectConfig.sourceRoot || projectConfig.root,
+  };
+}
+
+async function stopAllActiveTasks(signal: NodeJS.Signals) {
+  for (const task of activeTasks) {
+    if (!task.killed) {
+      await task.stop(signal);
+    }
+  }
+}
diff --git a/packages/bun/src/executors/test/schema.d.ts b/packages/bun/src/executors/test/schema.d.ts
new file mode 100644
index 0000000000..011f335eb9
--- /dev/null
+++ b/packages/bun/src/executors/test/schema.d.ts
@@ -0,0 +1,12 @@
+export interface TestExecutorSchema {
+    bail?: boolean | number;
+    watch?: boolean;
+    preload?: string;
+    updateSnapshots?: boolean;
+    timeout?: number;
+    rerunEach?: number;
+    smol: boolean;
+    config?: string;
+    bun: boolean;
+    tsConfig?: string;
+}
\ No newline at end of file
diff --git a/packages/bun/src/executors/test/schema.json b/packages/bun/src/executors/test/schema.json
new file mode 100644
index 0000000000..5e98399943
--- /dev/null
+++ b/packages/bun/src/executors/test/schema.json
@@ -0,0 +1,62 @@
+{
+  "$schema": "https://json-schema.org/schema",
+  "version": 2,
+  "title": "Test executor",
+  "description": "",
+  "type": "object",
+  "properties": {
+    "bail": {
+      "description": "abort the test run early after a pre-determined number of test failures. By default Bun will run all tests and report all failures, but sometimes in CI environments it's preferable to terminate earlier to reduce CPU usage.",
+      "anyOf": [
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "number"
+        }
+      ],
+      "default": true
+    },
+    "watch": {
+      "type": "boolean",
+      "description": "to watch for changes and re-run tests."
+    },
+    "preload": {
+      "type": "string",
+      "description": "Bun test lifecycle hooks in separate files"
+    },
+    "timeout": {
+      "description": "specify a per-test timeout in milliseconds. If a test times out, it will be marked as failed. The default value is 5000.",
+      "type": "number",
+      "default": 5000
+    },
+    "rerun-each": {
+      "type": "number",
+      "description": "run each test multiple times. This is useful for detecting flaky or non-deterministic test failures."
+    },
+    "update-snapshots": {
+      "type": "boolean",
+      "description": "Update Bun snapshots.",
+      "default": false
+    },
+    "smol": {
+      "type": "boolean",
+      "description": "In memory-constrained environments, use the smol flag to reduce memory usage at a cost to performance.",
+      "default": false
+    },
+    "config": {
+      "type": "string",
+      "description": "Config file to load bun from (e.g. -c bunfig.toml"
+    },
+    "bun": {
+      "type": "boolean",
+      "description": "Force a script or package to use Bun.js instead of Node.js (via symlinking node)",
+      "default": false
+    },
+    "tsConfig": {
+      "type": "string",
+      "description": "Load tsconfig from path instead of cwd/tsconfig.json"
+    }
+  },
+  "required": []
+}
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/bun/src/utils/bun-cli.ts b/packages/bun/src/utils/bun-cli.ts
new file mode 100644
index 0000000000..b228ab3645
--- /dev/null
+++ b/packages/bun/src/utils/bun-cli.ts
@@ -0,0 +1,185 @@
+import { stripIndents } from '@nx/devkit';
+import { SpawnOptions, Subprocess } from 'bun';
+import { ChildProcess, spawn } from 'child_process';
+
+export const isBun = typeof Bun !== 'undefined';
+
+export type ExecOptions<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+> = BunExecOptions<In, Out, Err> | NodeExecOptions;
+
+type BunExecOptions<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+> = {
+  cwd?: string;
+  stdin?: In;
+  stdout?: Out;
+  stderr?: Err;
+};
+
+type NodeExecOptions = {
+  cwd?: string;
+  stdio?: 'inherit' | 'pipe';
+};
+
+export function isBunExecOptions<
+  In extends SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable
+>(
+  options: ExecOptions<In, Out, Err>
+): options is BunExecOptions<In, Out, Err> {
+  return isBun;
+}
+
+export function isNodeExecOptions(
+  options: ExecOptions
+): options is NodeExecOptions {
+  return !isBun;
+}
+
+export type UnifiedChildProcess<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+> = ChildProcess | Subprocess<In, Out, Err>;
+
+export function isBunSubprocess<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+>(process: UnifiedChildProcess): process is Subprocess<In, Out, Err> {
+  return isBun && 'exited' in process;
+}
+
+export async function assertBunAvailable(forceInstall = false) {
+  try {
+    if (isBun) {
+      Bun.spawnSync({ cmd: ['bun', '--version'] });
+      return true;
+    } else {
+      const { execSync } = await import('child_process');
+      execSync('bun --version');
+      return true;
+    }
+  } catch (e) {
+    if (forceInstall && !process.env.NX_DRY_RUN) {
+      const { execSync } = await import('child_process');
+      execSync(`curl -fsSL https://bun.sh/install | bash`);
+      return true;
+    } else if (forceInstall) {
+      throw new Error(
+        stripIndents`force install of bun is not supported in dry-run`
+      );
+    }
+    throw new Error(stripIndents`Unable to find Bun on your system.
+          Bun will need to be installed in order to run targets from nx-bun in this workspace.
+          You can learn how to install bun at https://bun.sh/docs/installation
+        `);
+  }
+}
+
+export async function* executeCliAsync<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+>(
+  args: string[],
+  options: ExecOptions<In, Out, Err> = {}
+): AsyncGenerator<string, void, void> {
+  if (isBun) {
+    if (isBunExecOptions(options)) {
+      const bunOptions: SpawnOptions.OptionsObject<In, Out, Err> = {
+        cwd: options.cwd || process.cwd(),
+        env: { ...process.env },
+        stdin: options.stdin || ('pipe' as In),
+        stdout: options.stdout || ('pipe' as Out),
+        stderr: options.stderr || ('pipe' as Err),
+        ipc(message) {
+          console.log(message);
+        },
+        serialization: "json",
+      };
+
+      const childProcess = Bun.spawn(['bun', ...args], bunOptions);
+
+      if (isBunSubprocess(childProcess)) {
+        if (childProcess.stdout && typeof childProcess.stdout !== 'number') {
+          const stdoutReader = childProcess.stdout.getReader();
+          while (true) {
+            const { value, done } = await stdoutReader.read();
+            if (done) break;
+            yield new TextDecoder().decode(value);
+          }
+        }
+
+        if (childProcess.stderr && typeof childProcess.stderr !== 'number') {
+          const stderrReader = childProcess.stderr.getReader();
+          while (true) {
+            const { value, done } = await stderrReader.read();
+            if (done) break;
+            yield new TextDecoder().decode(value);
+          }
+        }
+
+        await childProcess.exited;
+      }
+    }
+  } else {
+    if (isNodeExecOptions(options)) {
+      const childProcess = spawn('bun', args, {
+        cwd: options.cwd || process.cwd(),
+        env: { ...process.env },
+        windowsHide: true,
+        stdio: (options as NodeExecOptions).stdio || 'pipe',
+      });
+
+      if (childProcess.stdout) {
+        for await (const data of childProcess.stdout) {
+          yield data.toString();
+        }
+      }
+
+      if (childProcess.stderr) {
+        for await (const data of childProcess.stderr) {
+          yield data.toString();
+        }
+      }
+
+      let childProcessClosed = false;
+      childProcess.on('close', (code) => {
+        if (code !== 0) {
+          throw new Error(`child process exited with code ${code}`);
+        }
+        childProcessClosed = true;
+      });
+
+      while (!childProcessClosed) {
+        await new Promise((resolve) => setTimeout(resolve, 100)); // Simple polling mechanism
+      }
+    }
+  }
+}
+
+export async function executeCliWithLogging<
+  In extends SpawnOptions.Writable = SpawnOptions.Writable,
+  Out extends SpawnOptions.Readable = SpawnOptions.Readable,
+  Err extends SpawnOptions.Readable = SpawnOptions.Readable
+>(
+  args: string[],
+  options: ExecOptions<In, Out, Err> = {}
+): Promise<boolean> {
+  try {
+    for await (const message of executeCliAsync(args, options)) {
+      console.log(message);
+    }
+    return true;
+  } catch (error) {
+    console.error(error);
+    return false;
+  }
+}
diff --git a/packages/bun/tsconfig.json b/packages/bun/tsconfig.json
new file mode 100644
index 0000000000..19b9eece4d
--- /dev/null
+++ b/packages/bun/tsconfig.json
@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "commonjs"
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/packages/bun/tsconfig.lib.json b/packages/bun/tsconfig.lib.json
new file mode 100644
index 0000000000..e383430f89
--- /dev/null
+++ b/packages/bun/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "declaration": true,
+    "types": ["node", "bun"]
+  },
+  "include": ["src/**/*.ts"],
+  "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/packages/bun/tsconfig.spec.json b/packages/bun/tsconfig.spec.json
new file mode 100644
index 0000000000..fe467a45e5
--- /dev/null
+++ b/packages/bun/tsconfig.spec.json
@@ -0,0 +1,14 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node", "bun"]
+  },
+  "include": [
+    "jest.config.ts",
+    "src/**/*.test.ts",
+    "src/**/*.spec.ts",
+    "src/**/*.d.ts"
+  ]
+}
diff --git a/tools/scripts/start-local-registry.ts b/tools/scripts/start-local-registry.ts
new file mode 100644
index 0000000000..b7c2201d50
--- /dev/null
+++ b/tools/scripts/start-local-registry.ts
@@ -0,0 +1,34 @@
+/**
+ * This script starts a local registry for e2e testing purposes.
+ * It is meant to be called in jest's globalSetup.
+ */
+import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry';
+import { releasePublish, releaseVersion } from 'nx/release';
+
+export default async () => {
+  // local registry target to run
+  const localRegistryTarget = 'nx-labs:local-registry';
+  // storage folder for the local registry
+  const storage = './tmp/local-registry/storage';
+
+  global.stopLocalRegistry = await startLocalRegistry({
+    localRegistryTarget,
+    storage,
+    verbose: false,
+  });
+
+  await releaseVersion({
+    specifier: '0.0.0-e2e',
+    stageChanges: false,
+    gitCommit: false,
+    gitTag: false,
+    firstRelease: true,
+    generatorOptionsOverrides: {
+      skipLockFileUpdate: true,
+    },
+  });
+  await releasePublish({
+    tag: 'e2e',
+    firstRelease: true,
+  });
+};
diff --git a/tools/scripts/stop-local-registry.ts b/tools/scripts/stop-local-registry.ts
new file mode 100644
index 0000000000..31d5d347f7
--- /dev/null
+++ b/tools/scripts/stop-local-registry.ts
@@ -0,0 +1,10 @@
+/**
+ * This script stops the local registry for e2e testing purposes.
+ * It is meant to be called in jest's globalTeardown.
+ */
+
+export default () => {
+  if (global.stopLocalRegistry) {
+    global.stopLocalRegistry();
+  }
+};
diff --git a/tsconfig.base.json b/tsconfig.base.json
index d7a43d7953..b5496a95dc 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,6 +15,7 @@
     "skipDefaultLibCheck": true,
     "baseUrl": ".",
     "paths": {
+      "@nx/bun": ["packages/bun/src/index.ts"],
       "@nx/nx-ignore": ["packages/nx-ignore/src/index.ts"],
       "@nx/rspack": ["packages/rspack/src/index.ts"]
     }
diff --git a/yarn.lock b/yarn.lock
index 36d66ab98e..ac659bd335 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3638,6 +3638,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/bun@^1.1.3":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@types/bun/-/bun-1.1.3.tgz#729b88160a0a9b89b950c935ba454f314aeefc14"
+  integrity sha512-i+mVz8C/lx+RprDR6Mr402iE1kmajgJPnmSfJ/NvU85sGGXSylYZ/6yc+XhVLr2E/t8o6HmjwV0evtnUOR0CFA==
+  dependencies:
+    bun-types "1.1.9"
+
 "@types/cacheable-request@^6.0.1", "@types/cacheable-request@^6.0.2":
   version "6.0.3"
   resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183"
@@ -3795,6 +3802,13 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.9.tgz#e79416d778a8714597342bb87efb5a6e914f7a73"
   integrity sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==
 
+"@types/node@~20.12.8":
+  version "20.12.12"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050"
+  integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==
+  dependencies:
+    undici-types "~5.26.4"
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -3888,6 +3902,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/ws@~8.5.10":
+  version "8.5.10"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787"
+  integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==
+  dependencies:
+    "@types/node" "*"
+
 "@types/yargs-parser@*":
   version "21.0.0"
   resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@@ -4897,6 +4918,14 @@ builtins@^5.0.0:
   dependencies:
     semver "^7.0.0"
 
+bun-types@1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.1.9.tgz#5a3728c91babe7d0a64283b07f03a58e4ea3b0ba"
+  integrity sha512-3YuLiH4Ne/ghk7K6mHiaqCqKOMrtB0Z5p1WAskHSVgi0iMZgsARV4yGkbfi565YsStvUq6GXTWB3ga7M8cznkA==
+  dependencies:
+    "@types/node" "~20.12.8"
+    "@types/ws" "~8.5.10"
+
 bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -11076,6 +11105,11 @@ unbox-primitive@^1.0.2:
     has-symbols "^1.0.3"
     which-boxed-primitive "^1.0.2"
 
+undici-types@~5.26.4:
+  version "5.26.5"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+  integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
 unicode-canonical-property-names-ecmascript@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"