From b1b1039eac6c50f603c8688c13e36cb2b6306198 Mon Sep 17 00:00:00 2001 From: Jordan Hall 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: '/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 @@ +

Nx - Smart, Fast and Extensible Build System

+ +
+ +# 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: '/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; + stop: (signal: NodeJS.Signals) => Promise; +} + +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((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 | 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 +): options is BunExecOptions { + 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; + +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 { + 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 = {} +): AsyncGenerator { + if (isBun) { + if (isBunExecOptions(options)) { + const bunOptions: SpawnOptions.OptionsObject = { + 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 = {} +): Promise { + 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"