Skip to content

fix: allow running quartz via bunx - #2523

Open
NotMoondev wants to merge 4 commits into
jackyzha0:v5from
NotMoondev:fix/bunx-support
Open

fix: allow running quartz via bunx#2523
NotMoondev wants to merge 4 commits into
jackyzha0:v5from
NotMoondev:fix/bunx-support

Conversation

@NotMoondev

Copy link
Copy Markdown

Problem:
npm links a package's own bin entries into node_modules/.bin automatically, which is why npx quartz works right after cloning the repo. Bun doesn't do that, so bunx quartz fails with could not determine executable to run for package quartz.

Solution:
Added a small postinstall script that creates the node_modules/.bin/quartz symlink by hand, so it works regardless of which package manager did the install.

Only tested on macOS, so it's Unix only for now. Windows would need a .cmd shim, happy to add that if someone can test it.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
quartz ✅ Ready (View Log) Visit Preview 4afc5ec

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a postinstall bootstrap step so the quartz CLI is available via bunx quartz (and other runners that rely on node_modules/.bin) even when the package manager doesn’t auto-link bin entries (as Bun currently doesn’t).

Changes:

  • Add quartz/bootstrap-postinstall.mjs to create/refresh node_modules/.bin/quartz pointing at quartz/bootstrap-cli.mjs.
  • Wire the script into npm lifecycle via postinstall in package.json.
  • Update package-lock.json to reflect the presence of an install script.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
quartz/bootstrap-postinstall.mjs Implements the postinstall symlink/bootstrap logic for node_modules/.bin/quartz.
package.json Adds a postinstall hook to run the bootstrap script after installs.
package-lock.json Records that the package has an install script (hasInstallScript).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread quartz/bootstrap-postinstall.mjs Outdated
Comment on lines +14 to +19
const binDir = path.join(root, "node_modules", ".bin")
mkdirSync(binDir, { recursive: true })

// only tested on mac, someone on windows will have to add a .cmd shim for this
const linkPath = path.join(binDir, "quartz")
const relativeTarget = path.relative(binDir, target)
Comment on lines +25 to +27
} catch (err) {
console.warn(`[quartz] could not link node_modules/.bin/quartz: ${err.message}`)
}
@NotMoondev

Copy link
Copy Markdown
Author

Pushed an update which should make the code more robust and fix the issues mentioned by copilot

  • The script is wrapped in a try/catch now and exits 0 on failure. It's only a convenience shim, so it shouldn't be able to break npm install. That should onlx happen on older node versions where import.meta.dirname is undefined and path.resolve throws at the top level.
  • If .bin/quartz already exists I readlink it and only recreate when the target differs. Before a stale link would stay broken and the script still reported success.
  • chmod failures warn instead of being swallowed.
  • Windows exits early

Retested on mac with node 26, Windows still untested as I don't have a machine available :/

@saberzero1

Copy link
Copy Markdown
Collaborator

Pushed an update which should make the code more robust and fix the issues mentioned by copilot

  • The script is wrapped in a try/catch now and exits 0 on failure. It's only a convenience shim, so it shouldn't be able to break npm install. That should onlx happen on older node versions where import.meta.dirname is undefined and path.resolve throws at the top level.
  • If .bin/quartz already exists I readlink it and only recreate when the target differs. Before a stale link would stay broken and the script still reported success.
  • chmod failures warn instead of being swallowed.
  • Windows exits early

Retested on mac with node 26, Windows still untested as I don't have a machine available :/

The Quartz CI tests run on all three platforms. Tests targeting Windows can be verified that way.

@NotMoondev

Copy link
Copy Markdown
Author

Got hold of a Windows machine and did a fresh install of the fork there:

  • Windows doesn't exit early anymore! The script writes the same three shims npm generates (quartz, quartz.cmd, quartz.ps1), since the shebang is useless there.
  • Added a bun setup step to CI plus a bunx --no-install quartz --help smoke test. Bun wasn't installed at all and the jobs only ever called npx, so nothing verified bunx anywhere. --no-install because a bare bunx quartz falls back to an unrelated quartz package on npm instead of failing...

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (3)

quartz/bootstrap-postinstall.mjs:2

  • mkdirSync(binDir, { recursive: true }) will create node_modules/.bin (and potentially the entire node_modules directory) under the package root whenever postinstall runs. If Quartz is installed as a dependency via a git/file reference (where the package directory typically does not already contain its own node_modules), this can create a nested node_modules/ inside node_modules/@jackyzha0/quartz, which is usually undesirable. Consider bailing out unless root/node_modules already exists (i.e. only run in a repo checkout / root install).
import { chmodSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs"

quartz/bootstrap-postinstall.mjs:48

  • In the EEXIST case, readlinkSync(linkPath) will throw if linkPath exists but is not a symlink (e.g. a regular file created by another tool). That exception currently escapes the inner catch, so the script won't replace the incorrect entry. Handle the non-symlink case by wrapping readlinkSync in its own try/catch and recreating the link when it fails.
    try {
      symlinkSync(relativeTarget, linkPath)
    } catch (err) {
      if (err.code !== "EEXIST") {
        throw err
      }
      if (readlinkSync(linkPath) !== relativeTarget) {
        unlinkSync(linkPath)
        symlinkSync(relativeTarget, linkPath)
      }
    }

.github/workflows/ci.yaml:55

  • The new CI check runs bunx after npm install, but npm install already creates node_modules/.bin/quartz, so this step likely would have passed even before the new postinstall script. To actually validate the behavior being fixed (Bun not creating the shim), consider deleting the shim(s) and re-running quartz/bootstrap-postinstall.mjs before invoking bunx.
      - 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

@NotMoondev

Copy link
Copy Markdown
Author

First two comments by copilot are valid and i fixed both:

  • postinstall bails out if root/node_modules doesn't exist. Reproduced it with a file: dep and yeah, it created a node_modules inside the package for nothing.
  • readlinkSync has its own try/catch now and the entry gets replaced if it's not a symlink. Not really an edge case, the Windows branch writes a plain file there.

ALSO, I found a worse error in my own code while messing around with that. The Windows branch wrote to .bin/quartz, which on a shared checkout is the symlink the Linux run left behind. It follows it, so the shim ended up inside bootstrap-cli.mjs and the cli called itself in a loop :/ Gets unlinked before writing now.

Third comment I don't think is right though. npm only links bins of dependencies, not the root packag's own, which is basically why this PR exists. Checked with a small package with a bin field and one dep, after npm install --ignore-scripts only the dep shows up in .bin, and bunx --no-install fails there without the shim, npx works either way because it falls back to the local package.json.
Lmk if I'm missing something tho.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants