Node projects

A Zuke build living inside a Node monorepo has different defaults than one driving standalone release binaries: the tools it wraps are usually installed locally by npm/yarn/pnpm and hoisted to the repo root, not sitting on PATH. This page covers the pieces that make that setup natural — tool resolution, workspaces, reading a wrapper's API, provisioning npm-registry tools, and the zuke setup launcher.

Resolving from node_modules/.bin

By default a wrapper spawns the bare tool name and lets the OS find it on PATH. In a Node monorepo the tools are usually installed locally and hoisted to the repo root instead, so Zuke can resolve them npx-style — walking up from the working directory for node_modules/.bin/<tool> (the .cmd/.bat shims, launched through cmd /c, on Windows) and falling back to PATH on a miss. There are three ways to turn it on, most specific first:

  • Per call: .fromNodeModules() (or .fromPath() to force PATH) on any settings object.
  • Per wrapper: the JS-ecosystem wrappers default to node_modules-first, so in a workspace package whose binaries are hoisted you write no .toolPath() and no .fromNodeModules() at all.
  • Repo-wide: ZUKE_TOOL_RESOLUTION=node_modules (or path) flips every wrapper without touching call sites. A per-call .fromNodeModules()/.fromPath() still wins over the ambient value.
import { OxlintTasks } from "jsr:@zuke/oxlint";

// per call — resolve node_modules/.bin first (or .fromPath() to force PATH)
await OxlintTasks.lint((s) => s.fromNodeModules());
ZUKE_TOOL_RESOLUTION=node_modules   # every wrapper resolves node_modules/.bin first, repo-wide
ZUKE_TOOL_RESOLUTION=path           # every wrapper resolves PATH first, skipping node_modules

An explicit .toolPath(...) always wins over resolution — pins from toolchain() stay hermetic. When resolution walks past a node_modules directory that lacks the tool, ToolNotFoundError adds a hint to run npm ci or provision it via toolchain(). resolvedArgv() reports the argv a run will actually spawn (the resolved shim or the bare fallback) for diagnostics.

npm workspaces

NpmTasks.run covers workspaces both ways: .workspace("app") for one, and .workspaces() for every workspace (compose with .ifPresent() to skip those missing the script) — the two are mutually exclusive.

import { NpmTasks } from "jsr:@zuke/npm";

await NpmTasks.run((s) => s.script("build").workspace("app"));         // one workspace
await NpmTasks.run((s) => s.script("build").workspaces().ifPresent()); // every workspace, skipping those missing the script

Reading a wrapper's API — zuke doc

deno doc jsr:@zuke/deno run inside a Node repo drowns the output in @types/node resolution warnings, because Deno discovers the surrounding project's deno.json / node_modules. zuke doc <package> runs deno doc from an isolated throwaway directory, so type resolution starts clean and the API prints readably in place:

zuke doc core                      # jsr:@zuke/core
zuke doc @scope/pkg                # a scoped package
zuke doc deno --filter DenoTasks   # extra flags pass through to deno doc

A bare name resolves to jsr:@zuke/<name>; an explicit jsr:/npm:/https: specifier or a file path is used as-is. Extra flags after the package (like --filter) pass straight through to deno doc.

Provisioning npm-registry tools

Not every tool ships a release binary — many are published on the npm registry instead. toolchain((t) => t.npm({ name, version })) provisions one as a version-pinned, cached tool without an ambient npm ci, resolving into the same Map<name, AbsolutePath> as release tools — and it composes with node_modules resolution above, since an explicit pin from a toolchain always wins. The full treatment — installation layout, caching, and the bin option for a differing bin name — lives on Installing tools: npm-package tools.

setup collision guard

zuke setup writes a ./zuke launcher into the project. If a zuke/ directory already occupies that name — common in a Node monorepo with its own zuke/ package or workspace — setup stops with an actionable error instead of writing next to it. Pass --launcher-name <name> to write the launcher (and its .ps1 counterpart) under a different name:

deno run -A jsr:@zuke/cli setup --launcher-name zk   # write the launcher (and its .ps1) under a different name