Contributing
What you need to know before opening a PR against
@socketregistry/packageurl-js. Dev setup, the test/coverage/lint
workflow, the parallel-session git rules, and the pre-PR checklist.
Who this is for #
First-time contributors and returning ones who want to refresh on the commands and conventions. Reading order: Setup → Making a change → Running checks → Opening the PR.
Setup #
Requirements:
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 22 | Built-in TypeScript stripping (--experimental-strip-types on 22, default on 23+). |
| pnpm | ≥ 11.0.0-rc.0 | Install via corepack enable pnpm or npm install -g pnpm. |
| git | ≥ 2.30 | Submodule support. |
Clone and install:
git clone https://github.com/SocketDev/socket-packageurl-js.git
cd socket-packageurl-js
pnpm install
pnpm install also:
- Sets up the
huskypre-commit hooks. - Initializes
upstream/meandersubmodule lazily on firstpnpm tour:build.
pnpm tour doctor reports which external tools the build may shell
out to are present vs missing on your machine. All listed tools are
optional; missing ones fall back to safe defaults.
Repo layout #
socket-packageurl-js/
├── src/ ← library source (see docs/architecture.md)
├── test/ ← functional tests (vitest)
├── scripts/ ← build/lint/test/release scripts
├── docs/ ← these docs
├── val/ ← Val Town comment backend (tour-specific)
├── upstream/meander/ ← submodule (tour generator)
├── .config/ ← tsconfig + vitest configs
├── .github/workflows/ ← CI (ci.yml, pages.yml, provenance.yml, ...)
├── .claude/ ← Claude Code config (agents, skills, hooks)
├── tour.json ← tour manifest
└── pages/ ← tour build output (gitignored)
You almost never edit upstream/meander/ (that's the submodule
we pin) or pages/ (build output).
Making a change #
1. Branch, worktree, or sibling clone #
This repo expects multiple concurrent Claude Code sessions may
be working on the same checkout. Plain git checkout -b inside the
main clone yanks the working tree out from under any other session.
Use one of:
- Worktree —
git worktree add -b my-task ../socket-packageurl-js-my-task main - Sibling clone — clone the repo elsewhere entirely
- Same clone — only if you are sure nobody else has a session going
See the PARALLEL CLAUDE SESSIONS section of CLAUDE.md for the
full doctrine.
2. Edit source #
Per CLAUDE.md:
- TypeScript strict mode; type imports must be in separate
import typelines. - No
nullexcept__proto__: nullor external-API requirement — useundefined. { __proto__: null, ...payload }for config/return/internal objects.- No dynamic imports (
await import(...)). - No
fetch()— usehttpJson/httpText/httpRequestfrom@socketsecurity/lib/http-request. - For file existence:
existsSyncfromnode:fs. For deletion:safeDelete/safeDeleteSyncfrom@socketsecurity/lib/fs— never reach directly forfs.rm/fs.unlink/fs.rmdir. - Default to NO comments in code. Only where the WHY is non-obvious to a senior engineer.
- Use the
Edittool for text changes; neversed/awk.
3. Write tests #
Two vitest configs:
| Config | When | File naming |
|---|---|---|
.config/vitest.config.mts |
Normal tests. Threads, shared memory. Fast. | *.test.mts |
.config/vitest.config.isolated.mts |
Tests that mock globals via vi.doMock, modify process.env, or process.chdir. Forks, full isolation. |
*.isolated.test.mts |
Test style in this repo: functional. Tests assert behavior via the public API — inputs → outputs. Tests never read source files and assert on their contents.
Example:
import { test, expect } from 'vitest'
import { PackageURL } from '../src/package-url.js'
test('parses npm scoped package', () => {
const purl = new PackageURL('pkg:npm/@scope/pkg@1.0.0')
expect(purl.namespace).toBe('@scope')
expect(purl.name).toBe('pkg')
expect(purl.version).toBe('1.0.0')
})
If you are adding a new ecosystem or URL parser, put tests under
test/purl-types/<name>.test.mts or
test/url-converter/<name>.test.mts.
Running checks #
The canonical pre-PR check is a single command:
pnpm check
It runs:
pnpm type— tsgo strict type-check.pnpm lint— oxlint across the tree.pnpm test— vitest (both configs).- Format verification.
Every step runs independently too:
| Command | What |
|---|---|
pnpm build |
Compile src/ → dist/ (esbuild). pnpm build --watch for dev. |
pnpm type |
Strict TypeScript check, no emit. |
pnpm lint |
oxlint. |
pnpm fix |
Auto-fix what's auto-fixable (formatter + lint autofixes). |
pnpm test |
Run vitest. pnpm test:unit path/to/file.test.mts for a single file. Never use -- before test paths — runs ALL tests. |
pnpm testu |
Update vitest snapshots (review the diff before committing). |
pnpm cover |
Coverage. Must stay at 100%. |
pnpm format |
Run oxfmt across the tree (writes fixes). |
pnpm format --check |
Verify formatting without writing. |
pnpm security |
AgentShield + zizmor security scan. |
Coverage — the 100% rule #
pnpm cover must report 100% line/branch/function coverage.
If you add a code path, add a test that exercises it. If you discover a corner case while reading, add a test for it even if the coverage number already says 100% (percentages can miss edge cases).
When coverage drops, the failure names the file and the missed
line/branch. Add a test or document an /* c8 ignore next */ with
a justification comment.
Snapshot tests #
pnpm testu regenerates snapshots. Never run this unless the
snapshot drift is intentional — otherwise you are deleting future-
you's safety net. Review the snapshot diff before committing.
The parallel-session git rules #
Forbidden in the primary checkout (the one another session may be editing):
git stash/git stash pop— shared store; another session can pop yours.git add -A/git add .— sweeps files belonging to other sessions.git checkout <branch>/git switch <branch>— yanks the working tree.git reset --hardagainst a non-HEAD ref — discards another session's commits.
Required for branch work:
git worktree add -b <task-branch> ../<repo>-<task> main
cd ../<repo>-<task>
# edit, commit, push from here
cd -
git worktree remove ../<repo>-<task>
Required for staging:
git add <specific-file> [<file>…]
Never -A / ..
For a quick WIP save: commit on a new branch from inside a worktree, not a stash.
Commit messages #
<type>(<scope>): <description>
<optional body>
<optional footer>
Types used in this repo:
feat— new featurefix— bug fixrefactor— restructure without behavior changedocs— documentation onlytest— tests onlychore— tooling, deps, repo configstyle— formatting / whitespace
Scopes are free-form but stable: purl-types/npm, url-converter,
tour, hardening. One commit = one concern. If the body
explains both a refactor and a fix, they should probably be two
commits.
Never add AI attribution ("Co-authored-by: Claude", etc.) to commit messages. Humans commit; tools assist.
Opening a PR #
After pnpm check passes and all commits are conventional:
gh pr create --title "<conventional title>" --body "…"
The PR body should:
- Summarize the change (1–3 bullets).
- Include a test plan as a markdown checklist.
- Link any related issue (
Fixes #123if applicable).
CI will run:
.github/workflows/ci.yml— fullpnpm checkin a clean env..github/workflows/pages.yml— if you touched tour sources, rebuilds the tour and deploys to GH Pages on merge to main..github/workflows/valtown.yml— if you touchedval/, deploys the comment backend after merge..github/workflows/provenance.yml— on tag, signs + publishes to npm with attestations.
Pre-PR checklist #
Copy into your PR description and tick off:
-
pnpm checkpasses locally. -
pnpm coverstill at 100%. - New tests for new behavior (functional style, public API).
- No
null, nofetch(), nofs.rm/unlink/rmdirdirect calls. - Commit messages are Conventional, no AI attribution.
- Documentation touched if user-facing behavior or API changed.
- For new ecosystem handlers: entry in
src/purl-types/, registered insrc/purl-type.ts, builder factory insrc/package-url-builder.ts, tests. -
CLAUDE.md+docs/*updated if conventions shifted.
Hazards #
Things that have caught contributors before:
- Forgetting to bump
upstream/meanderwhen the pin is stale.pnpm tour:build --refreshwill re-clone and rebuild. - Running
pnpm testuby accident — if you only wanted to run tests, usepnpm test.testuupdates snapshots. - Editing
dist/directly — it's a build artifact. Changes here are lost on next build. Editsrc/and rebuild. pnpm installside-effects — the install step runshuskyto set up git hooks. A barenpm installin this repo will skip that and PRs will push with unformatted files. Use pnpm..env.localaccidentally committed — it's gitignored; don'tgit add -A.minimumReleaseAgeexclusions — NEVER add packages tominimumReleaseAgeExcludein CI. Locally, ask before adding. The age threshold is a security control.