|
| 1 | +# npm → Deno per-repo migration recipe |
| 2 | + |
| 3 | +Canonical procedure for migrating a hyperpolymath estate repository from |
| 4 | +`package.json` + npm/Node to `deno.json` + Deno. |
| 5 | + |
| 6 | +Policy: `docs/JS-RUNTIME-POLICY.adoc` (Deno > Bun > pnpm > npm). |
| 7 | +Campaign tracker: hyperpolymath/standards#253. |
| 8 | +Rule enforcement: hypatia `cicd_rules/nodejs_detected` + `npx_or_npm_run_in_ci`. |
| 9 | + |
| 10 | +## 0. Decide which class the repo is in |
| 11 | + |
| 12 | +Before touching anything, decide which of the migration classes applies. |
| 13 | +Each class has a different end-state. |
| 14 | + |
| 15 | +| Class | Signal | End-state | |
| 16 | +|---|---|---| |
| 17 | +| **A. Pure-Deno port** | Repo's `package.json` only lists dev-only Node-compatible tools (`typescript`, `vitest`, build helpers). No host contract requires Node. | Delete `package.json` + `package-lock.json`. Author `deno.json`. CI workflows swap to `deno test`/`deno task`. | |
| 18 | +| **B. npm wrapper via Deno** | Repo wraps an npm-published tool that does not yet have a Deno-native fork (e.g., `rescript`, `vite`, `tailwindcss`). | Keep dependency expressed as `npm:pkg@semver` inside `deno.json`'s `imports`. Tasks call `deno run -A --node-modules-dir=auto npm:pkg`. No `package.json`. | |
| 19 | +| **C. Carve-out** | One of the six classes in `cicd_rules/nodejs_detected` `path_allow_prefixes` (VSCode extension, bootstrap shim, upstream fork, archived, vendored, example/fixture). | **Skip migration.** File stays on npm; no PR. | |
| 20 | + |
| 21 | +A given repo with multiple `package.json` files can split across classes — handle each manifest on its own merit. |
| 22 | + |
| 23 | +## 1. Inventory the current `package.json` |
| 24 | + |
| 25 | +```bash |
| 26 | +# Capture starting point. |
| 27 | +cat package.json |
| 28 | +ls -la package-lock.json bun.lockb yarn.lock pnpm-lock.yaml 2>/dev/null |
| 29 | +``` |
| 30 | + |
| 31 | +Record: |
| 32 | + |
| 33 | +- Direct deps (`dependencies` + `devDependencies`). |
| 34 | +- Scripts (`scripts.*`). |
| 35 | +- `engines.node`, `engines.npm` — note for replacement by `engines.deno`. |
| 36 | +- `private`, `type`, `exports` — preserved as needed. |
| 37 | + |
| 38 | +## 2. Author `deno.json` from the canonical template |
| 39 | + |
| 40 | +Copy `deno.json` from this directory. Adjust: |
| 41 | + |
| 42 | +- `name` — `@hyperpolymath/<repo-name>`. |
| 43 | +- `version` — preserve from `package.json`. |
| 44 | +- `license` — `MPL-2.0-or-later` (estate default) unless repo policy differs. |
| 45 | +- `compilerOptions` — preserve `strict` and friends from `tsconfig.json` if present. |
| 46 | +- `imports` — populate from `dependencies`: |
| 47 | + - Deno-native: `"@std/": "https://deno.land/std@0.224.0/"` (and similar). |
| 48 | + - JSR: `"@scope/pkg": "jsr:@scope/pkg@^1.2.3"`. |
| 49 | + - npm fallback: `"pkg": "npm:pkg@^1.2.3"` (Class B only). |
| 50 | +- `tasks` — port from `scripts`: |
| 51 | + - `"build": "rescript"` → `"build": "deno run -A --node-modules-dir=auto npm:rescript"`. |
| 52 | + - `"test": "vitest"` → `"test": "deno test -A src/"` (port tests to Deno test API where reachable; if not yet portable, `"test": "deno run -A --node-modules-dir=auto npm:vitest"`). |
| 53 | +- `nodeModulesDir`: |
| 54 | + - Default `"none"` (Class A — pure Deno). |
| 55 | + - Set to `"auto"` only when an npm package's lifecycle requires it (Class B; rescript and most ESM-shipped npm packages are fine without it). |
| 56 | + |
| 57 | +## 3. Delete the npm scaffolding |
| 58 | + |
| 59 | +```bash |
| 60 | +git rm package.json package-lock.json |
| 61 | +# Also remove bun.lockb / yarn.lock / pnpm-lock.yaml / .npmrc if present. |
| 62 | +git rm -f bun.lockb yarn.lock pnpm-lock.yaml .npmrc 2>/dev/null || true |
| 63 | + |
| 64 | +# node_modules/ should already be in .gitignore. |
| 65 | +rm -rf node_modules |
| 66 | +``` |
| 67 | + |
| 68 | +## 4. Update `.gitignore` |
| 69 | + |
| 70 | +Confirm these entries are present (RSR canonical template propagates them — see `docs/JS-RUNTIME-POLICY.adoc §Canonical .gitignore Entries`): |
| 71 | + |
| 72 | +``` |
| 73 | +# npm-avoidant (standards#67): estate JS-runtime policy is Deno>Bun>pnpm>npm. |
| 74 | +package-lock.json |
| 75 | +**/package-lock.json |
| 76 | +node_modules/ |
| 77 | +**/node_modules/ |
| 78 | +bun.lockb |
| 79 | +yarn.lock |
| 80 | +pnpm-lock.yaml |
| 81 | +``` |
| 82 | + |
| 83 | +## 5. Migrate CI workflows |
| 84 | + |
| 85 | +Search for any of these and replace: |
| 86 | + |
| 87 | +| Before | After | |
| 88 | +|---|---| |
| 89 | +| `actions/setup-node@<sha>` | `denoland/setup-deno@<sha>` (or remove if no JS step remains) | |
| 90 | +| `npm ci` / `npm install` | `deno cache <entrypoint>` (often unnecessary — Deno caches at first run) | |
| 91 | +| `npm test` / `npm run test` | `deno task test` (or `deno test -A src/`) | |
| 92 | +| `npx <tool>` | `deno run -A --node-modules-dir=auto npm:<tool>` (Class B) or Deno-native equivalent (Class A) | |
| 93 | + |
| 94 | +Note: hypatia `cicd_rules/npx_or_npm_run_in_ci` blocks `npx` and `npm run` in CI run-blocks (added 2026-05-28). Don't leave any. |
| 95 | + |
| 96 | +## 6. Verify locally |
| 97 | + |
| 98 | +```bash |
| 99 | +deno check src/ |
| 100 | +deno lint src/ |
| 101 | +deno fmt --check src/ |
| 102 | +deno test -A src/ |
| 103 | +``` |
| 104 | + |
| 105 | +Class B (npm wrapper) — exercise the wrapped tool end-to-end: |
| 106 | + |
| 107 | +```bash |
| 108 | +deno task build |
| 109 | +deno task test |
| 110 | +``` |
| 111 | + |
| 112 | +## 7. Commit pattern |
| 113 | + |
| 114 | +```bash |
| 115 | +git add deno.json .gitignore .github/workflows/ |
| 116 | +git rm package.json package-lock.json |
| 117 | +git commit -m "feat(deno): migrate <repo> npm → Deno (standards#253) |
| 118 | +
|
| 119 | +<class-specific summary: deleted N npm manifests, added deno.json with |
| 120 | +M imports, ported K tasks, CI now runs on deno test> |
| 121 | +
|
| 122 | +Class: A | B (per docs/migrations/npm-to-deno-template/MIGRATION.md) |
| 123 | +Carry-forward: <any blocked Deno-native equivalent, e.g. \"vitest not |
| 124 | +portable until deno-test gets <feature>\">" |
| 125 | +``` |
| 126 | + |
| 127 | +## 8. PR + auto-merge |
| 128 | + |
| 129 | +Per estate convention: auto-merge with squash. |
| 130 | + |
| 131 | +```bash |
| 132 | +gh pr create --title "feat(deno): <repo> npm → Deno (standards#253)" \ |
| 133 | + --body "<paste commit body + cross-ref campaign>" |
| 134 | +gh pr merge --auto --squash --delete-branch |
| 135 | +``` |
| 136 | + |
| 137 | +## Carry-forward patterns observed in oikos Phase 5 + 5 follow-ups (memory) |
| 138 | + |
| 139 | +- **ReScript wrapping** (canonical Class B): `deno run -A --node-modules-dir=auto npm:rescript@^12.0.0`. `--allow-scripts=npm:rescript` when the install lifecycle requires it. |
| 140 | +- **Tailwind / vite / esbuild** — same pattern as rescript: `npm:<tool>@<semver>`, `--node-modules-dir=auto`. |
| 141 | +- **`type: "module"` repos** — Deno is ESM-native, no extra step. |
| 142 | +- **`exports` field** — preserve in `deno.json` if the package is published; otherwise drop. |
| 143 | + |
| 144 | +## Anti-patterns |
| 145 | + |
| 146 | +- ❌ Don't keep `package.json` "for tooling only" — `deno.json` covers fmt/lint/test/tasks. |
| 147 | +- ❌ Don't fall back to `npm:` specifiers when a JSR or Deno-native equivalent exists (use `deno info <pkg>` to check). |
| 148 | +- ❌ Don't commit `node_modules/` even on Class B — `--node-modules-dir=auto` regenerates at run-time. |
| 149 | +- ❌ Don't add `"engines": {"node": "..."}` to `deno.json` — Deno doesn't honour it and it signals the repo isn't fully migrated. |
| 150 | + |
| 151 | +## When migration is blocked |
| 152 | + |
| 153 | +If a `package.json` cannot be removed (host-required, Node-only library, npm publish target), the path goes in the hypatia rule's `path_allow_prefixes` instead. See `standards/.claude/CLAUDE.md §npm Exemptions (Approved)` for the canonical exemption table. |
0 commit comments