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 forcePATH) 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(orpath) 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