diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4aeda17fdac59..9cbd004522110 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,6 +28,9 @@ jobs: with: node-version: 24 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: Cache dependencies uses: actions/cache@v5 with: @@ -46,6 +49,11 @@ jobs: - run: npm install + # --no-install because a bare `bunx quartz` falls back to an unrelated + # package on npm instead of failing + - name: Ensure bunx resolves the local CLI + run: bunx --no-install quartz --help + - name: Install Quartz plugins run: npx quartz plugin install diff --git a/package-lock.json b/package-lock.json index cc63450c62269..4ce9617faf636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@jackyzha0/quartz", "version": "5.0.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@clack/prompts": "^0.11.0", diff --git a/package.json b/package.json index 8705f4e2680e2..21d17b876063a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test": "tsx --test", "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", "install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts", - "prebuild": "npm run install-plugins" + "prebuild": "npm run install-plugins", + "postinstall": "node quartz/bootstrap-postinstall.mjs" }, "engines": { "npm": ">=10.9.2", diff --git a/quartz/bootstrap-postinstall.mjs b/quartz/bootstrap-postinstall.mjs new file mode 100644 index 0000000000000..d0a4f4b85e7d6 --- /dev/null +++ b/quartz/bootstrap-postinstall.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +import { + chmodSync, + existsSync, + mkdirSync, + readlinkSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from "fs" +import path from "path" + +// npm links the `bin` entry into node_modules/.bin on install, bun does not +try { + const root = path.resolve(import.meta.dirname, "..") + const target = path.join(root, "quartz", "bootstrap-cli.mjs") + const binDir = path.join(root, "node_modules", ".bin") + const linkPath = path.join(binDir, "quartz") + const relativeTarget = path.relative(binDir, target) + + // as a dependency, npm links the bin itself, running here would only litter a node_modules into the package + if (!existsSync(path.join(root, "node_modules"))) { + process.exit(0) + } + + mkdirSync(binDir, { recursive: true }) + + if (process.platform === "win32") { + // windows ignores the shebang, so it needs the same shims npm writes + const posixTarget = relativeTarget.split(path.sep).join("/") + + // a symlink left by a wsl run would be followed and the shim written straight into the cli source file + try { + unlinkSync(linkPath) + } catch {} + + writeFileSync( + linkPath, + `#!/bin/sh\nexec node --no-deprecation "$(dirname "$0")/${posixTarget}" "$@"\n`, + ) + writeFileSync( + `${linkPath}.cmd`, + `@ECHO off\r\nnode --no-deprecation "%~dp0${relativeTarget}" %*\r\n`, + ) + writeFileSync( + `${linkPath}.ps1`, + `#!/usr/bin/env pwsh\nnode --no-deprecation "$PSScriptRoot/${posixTarget}" $args\nexit $LASTEXITCODE\n`, + ) + } else { + try { + // checked in as 755, but bunx won't run the cli if a setup drops the mode + chmodSync(target, 0o755) + } catch (err) { + console.warn(`[quartz] could not make ${target} executable: ${err.message}`) + } + + try { + symlinkSync(relativeTarget, linkPath) + } catch (err) { + if (err.code !== "EEXIST") { + throw err + } + // not necessarily a symlink, a windows checkout leaves a plain file here + let current = null + try { + current = readlinkSync(linkPath) + } catch {} + + if (current !== relativeTarget) { + unlinkSync(linkPath) + symlinkSync(relativeTarget, linkPath) + } + } + } +} catch (err) { + console.warn(`[quartz] could not link node_modules/.bin/quartz: ${err.message}`) +}