Repository navigation
feat: run the engine on Node through JSR npm-compat, off Deno globals - #161
Merged
Merged
Conversation
Port every `Deno.*` call in `src/` to a single runtime edge, `src/runtime.ts`, so JSR's npm-compatibility package executes on Node with no wrapper and no compiled binary. `npx jsr add @wazoo/workspace` already needed no Deno and Node 24 already loaded the whole module graph; the only blocker was `ReferenceError: Deno is not defined`. - `src/runtime.ts` is the only module permitted to import `node:*`. It uses `node:child_process`, `node:fs/promises`, `node:os` and `node:process`, and preserves `Deno.Command#output()` semantics that callers depend on: it resolves for every exit code, and rejects only when the binary cannot be spawned, which is how `createGitHubRepoWithFallback` reports a missing `gh`. - Host differences are normalised once, never at call sites: `isWindows()` (`Deno.build.os` says "windows", `process.platform` says "win32"), `cliArgs()` (`process.argv.slice(2)`, correct for `deno run`, a compiled binary, and `node <script>`), and exit via `process.exitCode` so piped `--json` output is never truncated by an immediate exit. - `@types/node` is the sole non-`@std` dependency: type-only, resolved through Deno's global cache, creating no `node_modules/` directory. - Fix the compiled binary's baked permissions. The code spawns `gh` (`add --create`) and `robocopy`/`rmdir`/`rm` (the ADR-0006 purge ladder) as well as `git`, so `--allow-run=git` alone would throw `PermissionDenied`. - Move to 0.8.0, which also delivers the already-merged but unpublished `sweep`/`purge` commands to users. - ADR-0007 records the decision, including the Deno sandbox regression for Node consumers; the README documents the no-Deno install. Verified: `deno task ci` 193 passed / 0 failed; a cold-cache `deno check src/ tests/` is green (the CI path, with no `node_modules`); and the runtime edge was exercised directly under Node — non-zero exit resolves, a missing binary rejects, and `cwd`/fs/argv/exit behaviour all hold. End-to-end proof on the published artifact is an `-rc` publish, pending approval. Closes #160 Closes #159
The previous commit removed every `Deno.*` call from `src/`, and the engine still could not run on Node: `@std/fs` — a dependency, not our code — calls the `Deno` global directly. Its `exists` and `walk` reach `Deno.stat`, `Deno.readDir` and `Deno.errors.NotFound` in the TypeScript source *and* in the JavaScript JSR ships to npm. Seven modules imported `exists` and one imported `walk`, so the CLI died at `ReferenceError: Deno is not defined` during manifest discovery, before any command could run. `deno test` could not catch this and did not: Deno defines the `Deno` global, so the suite stays green with the dependency in place. It surfaced only when the module graph was actually executed on Node. - `src/runtime.ts` now owns the two shapes `@std/fs` provided, as `pathExists` and `walkDirectoryTree`, so the runtime edge covers host access *and* filesystem traversal. `src/` no longer imports `@std/fs` at all. - `walkDirectoryTree` keeps `@std/fs/walk`'s observable semantics, because `wspace path` ranks what it is handed: the root is returned first, `maxDepth` counts the root as depth 0, `skip` patterns match the whole path rather than the basename, a skipped directory is neither returned nor descended into, and symlinks are never followed. - `pathExists` documents the one deliberate divergence: a path that cannot be stat'd reads as absent, where `exists` reports it as present. - Seven call sites move from `exists` to `pathExists`; `src/commands/path.ts` moves from `walk` to `walkDirectoryTree`. - `@std/fs` stays a *test* dependency (`exists`, `expandGlob`); JSR omits test-only dependencies from the published package. - ADR-0007 records the finding, and replaces its claim that an `-rc` publish is the only end-to-end proof, since a local run caught this one. Verified: `deno task ci` is 199 passed / 0 failed / 1 ignored (a symlink test skipped on Windows). Reproducing JSR's specifier rewrites locally and executing the resulting graph on Node 24 gives parity with the published 0.7.0 binary: `check --json` identical across all 39 entries, `path worlds-api` identical, `path wiki` identical including its ambiguous-match stderr and exit 1, and `sweep --dry-run --json` exits 0. `@jsr/std__fs` is unreachable from the graph, and the remaining `@std/path` and `@std/cli` dependencies are runtime-neutral (`std__path` guards with `global.process?.cwd ?? global.Deno?.cwd`, `std__cli` with `"Deno" in globalThis`). Refs #160
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Removes every
Deno.*global fromsrc/so JSR's npm-compatibility packageruns on Node, not just installs there.
npx jsr add @wazoo/workspacealready required no Deno, and Node 24 alreadyloaded the entire module graph (transpiled
.jsbeside the.ts,@/x.tsrewritten to correct relative paths,
jsr:@std/clirewritten to@jsr/std__cli,import.meta.mainhonoured). It then died atReferenceError: Deno is not defined. JSR transpiles but does not shim Denoglobals — that was the whole barrier.
Closes #160. Also closes #159: the version moves to 0.8.0, which is what makes
the already-merged
sweep/purgereachable at all (JSR is immutable, so theycould never ship from 0.7.0).
Two commits, because the first attempt was not enough
a6d4191portssrc/offDeno.*.0ab53effixes what that missed, and it isthe more interesting half: the engine still could not run on Node, because
@std/fsis Deno-only. Itsexistsandwalkcall theDenoglobaldirectly, in the TypeScript source and in the JavaScript JSR ships to npm.
Seven modules imported
existsand one importedwalk, so the CLI died atReferenceError: Deno is not definedduring manifest discovery, before anycommand could run.
deno testcould not catch this and did not: Deno defines theDenoglobal, so193 tests were green with the dependency in place. It appeared only when the
module graph was executed on Node — which is the entire point of the change, and
the reason the verification below is not "the suite passes".
Shape of the change
src/runtime.tsis the only module allowed to importnode:*(child_process,fs/promises,os,process). Deno implementsthose specifiers natively, so a single implementation serves both runtimes.
@std/fscannot.pathExistsandwalkDirectoryTreereplaceexistsandwalk.walkDirectoryTreekeeps@std/fs/walk's observable semantics on purpose, sincewspace pathrankswhat it is handed: the root is returned first,
maxDepthcounts the root asdepth 0,
skippatterns match the whole path rather than the basename, askipped directory is neither returned nor descended into, and symlinks are
never followed.
pathExistsdocuments its one divergence: a path that cannotbe stat'd reads as absent, where
existsreports it as present.runProcessresolves for every exit codeincluding non-zero, and rejects only when the binary cannot be spawned. That
rejection is load-bearing: it is how
createGitHubRepoWithFallbackrecognises"gh CLI is not available". Stdin is detached so
gitcannot hang on acredential prompt.
isWindows()for"windows"vs"win32";cliArgs()forprocess.argv.slice(2); exit viaprocess.exitCoderather thanprocess.exitso piped--jsonoutput is nottruncated.
@types/nodeis the only new dependency — type-only, resolved throughDeno's global cache, so no
node_modules/directory appears in the repo.deno task buildallowed only--allow-run=git, but the codealso spawns
ghandrobocopy/rmdir/rm. Sincedeno compilebakespermissions in, those paths would have thrown
PermissionDenied.docs/adr/0007-runtime-neutral-engine.mdrecords the decision, the sandboxregression, and the
@std/fsfinding;AGENTS.mdamends the "@std only"convention; the README documents the no-Deno install.
Verification
deno task cideno check src/ tests/(freshDENO_DIR, the CI path)node_modulescwd/fs/cliArgs/exit-code all correctcheck --jsonon Node vs the published 0.7.0 binarypath worlds-apion Node vs the binarypath wiki(ambiguous) on Node vs the binarysweep --dry-run --jsonon Node@jsr/std__fsin the staged graph@std/pathand@std/cliare runtime-neutral (std__pathguards withglobal.process?.cwd ?? global.Deno?.cwd,std__cliwith"Deno" in globalThis)The Node rows come from reproducing JSR's specifier rewrites locally (the
@/alias, and
jsr:@std/*→@jsr/std__*) and executing the resulting graph withnode --experimental-transform-types. The pre-existing Deno tests are untouchedand remain the behaviour-parity gate.
What I could not verify
Only JSR's server-side packaging. It builds the npm-compat tarball at publish
time, and
deno transpileoutput is not directly Node-loadable (it leavesjsr:specifiers), so the final link is a realnpx jsr add @wazoo/workspace@0.8.0followed bynode node_modules/@wazoo/workspace/src/cli.js check --jsonon the publishedartifact. The rewrites that publish applies were reproduced by hand for every
row above; the packaging step itself is the one part not exercised.
The merge that would prove it also publishes 0.8.0, and so is approval-gated.
Known and accepted
user's ambient authority. Documented in ADR-0007 rather than discovered later.
exports, notbin(README install command 'deno install -g jsr:@wazoo/workspace' produces a no-op; bin not honored for JSR packages #135), so this yields a runnableentrypoint, not a
wspacecommand onPATH.open and this does not preclude it.