diff --git a/.cursor/rules/contributing.mdc b/.cursor/rules/contributing.mdc deleted file mode 100644 index 5c637e65..00000000 --- a/.cursor/rules/contributing.mdc +++ /dev/null @@ -1,242 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- - -# Wouter - Minimalist React Router - -## Project Overview - -Wouter is a **minimalist-friendly ~2.1KB React router** that focuses on performance, small bundle size, and simplicity. It provides hook-based routing for React and Preact applications. - -### Key Features - -- Tiny bundle size (2.1KB gzipped vs 18.7KB React Router) -- Hook-based API (`useLocation`, `useRoute`, `useParams`, etc.) -- Zero dependencies -- Supports React and Preact -- Optional top-level `` component -- Nested routing support -- SSR support -- Memory location for testing - -## Project Structure - -``` -wouter/ -├── packages/ -│ ├── wouter/ # Main React package -│ │ ├── src/ -│ │ │ ├── index.js # Main router implementation -│ │ │ ├── use-browser-location.js -│ │ │ ├── use-hash-location.js -│ │ │ ├── memory-location.js -│ │ │ ├── react-deps.js # React imports -│ │ │ └── paths.js # Path utilities -│ │ ├── test/ # Test files -│ │ ├── types/ # TypeScript definitions -│ │ └── esm/ # Built ES modules -│ └── wouter-preact/ # Preact package -├── assets/ # Documentation assets -└── .github/ # GitHub workflows -``` - -## Development Workflow - -### Prerequisites - -- Node.js v20.17.0+ or v22.9.0+ -- npm - -### Commands - -```bash -# Install dependencies -npm install - -# Run tests (watch mode) -npm test - -# Run tests (single run) -npm test -- --run - -# Run specific test file -npm test -- packages/wouter/test/router.test.tsx --run - -# Run tests with coverage -npm test -- --coverage - -# Build packages -npm run build - -# Type checking -npm run typecheck - -# Lint code -npm run lint -``` - -### Test Files Organization - -- `test/router.test.tsx` - Router component and memoization tests -- `test/use-location.test.tsx` - Location hook tests -- `test/use-route.test.tsx` - Route matching tests -- `test/link.test.tsx` - Link component tests -- `test/memory-location.test.ts` - Memory location for testing -- `test/nested-route.test.tsx` - Nested routing tests -- `test/ssr.test.tsx` - Server-side rendering tests - -## Key Development Principles - -### Performance First - -- **Bundle size matters**: Every byte counts. Use size-limit CI checks -- **Stable object references**: Prevent unnecessary re-renders with proper memoization -- **Minimize dependencies**: Zero external dependencies policy - -### Code Style - -- **Functional programming**: Prefer hooks over class components -- **Compact code**: Optimize for bundle size (may sacrifice readability) -- **React patterns**: Use `React.memo`, `useCallback`, `useMemo` appropriately - -### Testing Guidelines - -- **Comprehensive coverage**: Test all public APIs -- **Real-world scenarios**: Include complex use cases (like language switching) -- **Memory location**: Use for isolated testing -- **Memoization tests**: Verify stable object references - -## Core APIs - -### Hooks - -- `useLocation()` - Get/set current location -- `useRoute(pattern)` - Match current location against pattern -- `useParams()` - Extract route parameters -- `useSearch()` - Get search/query string -- `useSearchParams()` - Get/set URLSearchParams -- `useRouter()` - Access router configuration - -### Components - -- `` - Optional configuration wrapper -- `` - Conditional rendering based on path -- `` - Navigation component -- `` - Exclusive routing -- `` - Programmatic navigation - -### Location Hooks - -- `useBrowserLocation` - Browser history API -- `useHashLocation` - Hash-based routing -- `memoryLocation` - In-memory for testing - -## Performance Considerations - -### Router Memoization - -The Router component uses sophisticated memoization to prevent unnecessary re-renders: - -- Only creates new router objects when props actually change -- Preserves stable references for `React.memo` components -- Critical for performance in complex applications - -### Bundle Optimization - -- Use `// ... existing code ...` pattern in edits to minimize diffs -- Prefer shorter variable names in production builds -- Optimize for gzip/brotli compression - -## Common Patterns - -### Testing Router Components - -```javascript -import { memoryLocation } from "wouter/memory-location"; - -const { hook } = memoryLocation({ path: "/test/path" }); -render( - - - -); -``` - -### Custom Location Hooks - -```javascript -const useCustomLocation = () => { - const [location, setLocation] = useBrowserLocation(); - // Add custom logic here - return [location, setLocation]; -}; -``` - -### Nested Routing - -```javascript - - - - - -``` - -## Debugging Tips - -### Common Issues - -1. **Router memoization**: Check if unnecessary re-renders occur -2. **Base path conflicts**: Verify base path inheritance in nested routers -3. **Pattern matching**: Use regexparam syntax for route patterns -4. **SSR hydration**: Ensure client/server router configs match - -### Debugging Tools - -- React DevTools Profiler for render tracking -- Network tab for bundle size verification -- Console warnings for deprecated patterns - -## Contributing Guidelines - -### Before Making Changes - -1. Run full test suite: `npm test -- --run` -2. Check bundle size impact -3. Verify TypeScript definitions -4. Test with both React and Preact - -### Adding New Features - -1. Consider bundle size impact -2. Add comprehensive tests -3. Update TypeScript definitions -4. Document in README if public API -5. Maintain backward compatibility - -### Performance Changes - -1. Add before/after performance tests -2. Verify memoization behavior -3. Test with complex nested scenarios -4. Check for memory leaks in long-running apps - -## Architecture Notes - -### Router Context System - -- Default router created on-demand -- Context inheritance with override capabilities -- Memoization prevents cascade re-renders -- Base path accumulation in nested routers - -### Pattern Matching - -- Uses regexparam library internally -- Supports named parameters, wildcards, optional segments -- Regex patterns supported for complex matching -- Loose mode for nested routing - -This project prioritizes performance and minimalism while maintaining full feature parity with larger routing solutions. diff --git a/.cursor/rules/publishing.mdc b/.cursor/rules/publishing.mdc deleted file mode 100644 index dc89c544..00000000 --- a/.cursor/rules/publishing.mdc +++ /dev/null @@ -1,109 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Publishing Wouter Packages - -This document outlines the process for publishing new versions of the wouter packages to npm. - -## Prerequisites - -- Ensure you have npm publish permissions for both `wouter` and `wouter-preact` packages -- Have npm authentication set up (you'll need OTP access) -- All tests should be passing -- All changes should be committed to the repository - -## Publishing Process - -### 1. Version Bump - -Update the version in both package.json files: -- `packages/wouter/package.json` -- `packages/wouter-preact/package.json` - -For semantic versioning: -- **Patch** (x.x.X): Bug fixes, small improvements -- **Minor** (x.X.x): New features, backward compatible -- **Major** (X.x.x): Breaking changes - -### 2. Dry Run Validation - -Run dry publish commands to validate the packages before actual publishing: - -```bash -npm publish --dry-run -w wouter -npm publish --dry-run -w wouter-preact -``` - -This will show you: -- Package contents and file list -- Bundle size information -- Version confirmation -- Any potential issues - -### 3. Build Verification (Optional) - -Check the build artifacts in the `/esm/` folders to ensure the latest updates are properly built: -- `packages/wouter/esm/` -- `packages/wouter-preact/esm/` - -The `prepublishOnly` script automatically builds the packages, but it's good to verify. - -## ⚠️ Confirmation Required - -**ALWAYS ask for user confirmation before proceeding with the following steps:** - -### 4. Publish to npm - -Publish both packages to the npm registry: - -```bash -npm publish -w wouter -npm publish -w wouter-preact -``` - -**Note:** npm might fail with "EOTP" (Error One-Time Password) even when the OTP was entered correctly in the browser. This is a known npm issue - the packages are usually still published successfully despite the error message. - -### 5. Git Commit and Tag - -After successful publishing: - -1. **Commit the version changes:** - ```bash - git add packages/wouter/package.json packages/wouter-preact/package.json - git commit -m "Bump version to X.X.X" - ``` - -2. **Create a version tag:** - ```bash - git tag vX.X.X - ``` - -3. **Push to remote (optional):** - ```bash - git push origin main - git push origin vX.X.X - ``` - -## Troubleshooting - -### Common Issues - -- **Authentication errors**: Run `npm login` or use the browser authentication flow -- **OTP timeout**: Generate a fresh OTP code from your authenticator app -- **Permission denied**: Ensure you have publish permissions for both packages -- **Version conflicts**: Check if the version already exists on npm - -### Verification - -After publishing, verify the packages are available: -- Check [wouter on npm](mdc:https:/www.npmjs.com/package/wouter) -- Check [wouter-preact on npm](mdc:https:/www.npmjs.com/package/wouter-preact) -- Test installation: `npm install wouter@latest` - -## Package Information - -- **wouter**: Main React package (~22KB package size) -- **wouter-preact**: Preact-specific package (~22KB package size) -- Both packages maintain version parity and should be published together \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..8d23e85a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,117 @@ +name: Publish packages + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: Existing release tag (for example v3.12.0 or v3.12.0-next.0) + required: true + type: string + dry_run: + description: Validate and pack without publishing + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: npm-publish + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + steps: + - uses: actions/checkout@v6 + with: + ref: refs/tags/${{ inputs.tag || github.ref_name }} + fetch-depth: 0 + persist-credentials: false + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.2 + + # npm >= 11.5.1 supports OIDC. Bun handles installs and checks. + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - run: npm install --global npm@11.17.0 + + - name: Check release tag and package versions + id: release + run: | + git merge-base --is-ancestor HEAD origin/main + bun - <<'JS' + import { appendFileSync } from "node:fs"; + const tag = process.env.RELEASE_TAG; + const number = "(?:0|[1-9]\\d*)"; + const identifier = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][\\dA-Za-z-]*)"; + const pattern = new RegExp(`^v${number}\\.${number}\\.${number}(?:-${identifier}(?:\\.${identifier})*)?$`); + if (!pattern.test(tag)) throw new Error(`Invalid release tag: ${tag}`); + const version = tag.slice(1); + for (const name of ["wouter", "wouter-preact"]) { + const pkg = await Bun.file(`packages/${name}/package.json`).json(); + if (pkg.name !== name || pkg.version !== version || pkg.private) { + throw new Error(`${name} must be public and have version ${version}`); + } + } + appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ndist_tag=${version.includes("-") ? "next" : "latest"}\n`); + JS + + - run: bun install --frozen-lockfile + - run: bun test --coverage + + # Tests remove copied Preact sources, so prepare packages after testing. + - name: Prepare packages + run: | + bun run --cwd packages/wouter prepublishOnly + bun run --cwd packages/wouter-preact prepublishOnly + - run: bun run test-types + - run: bun run lint + - run: bun run size + + - name: Pack and inspect both packages + run: | + mkdir -p "$RUNNER_TEMP/packages" + npm pack --workspace wouter --workspace wouter-preact --ignore-scripts --json --pack-destination "$RUNNER_TEMP/packages" > "$RUNNER_TEMP/packages.json" + bun - <<'JS' + const packed = await Bun.file(`${process.env.RUNNER_TEMP}/packages.json`).json(); + if (packed.length !== 2) throw new Error("Expected both packages"); + for (const pkg of packed) { + const manifest = await Bun.file(`packages/${pkg.name}/package.json`).json(); + const files = new Set(pkg.files.map(file => file.path)); + const required = ["README.md", "src/react-deps.js", "src/paths.js", ...Object.values(manifest.exports).flatMap(entry => Object.values(entry))]; + for (const path of required) { + if (!files.has(path.replace(/^\.\//, ""))) throw new Error(`${pkg.name} is missing ${path}`); + } + } + JS + + - name: Publish missing versions to npm + if: ${{ !inputs.dry_run }} + env: + VERSION: ${{ steps.release.outputs.version }} + DIST_TAG: ${{ steps.release.outputs.dist_tag }} + run: | + for package in wouter wouter-preact; do + # Allow recovery when only one package was published on a previous run. + if npm view "$package@$VERSION" version --json > "$RUNNER_TEMP/version.json"; then + echo "$package@$VERSION already exists; skipping" + continue + elif ! jq -e '.error.code == "E404"' "$RUNNER_TEMP/version.json" > /dev/null; then + echo "::error::Could not check $package@$VERSION on npm" + exit 1 + fi + npm publish "$RUNNER_TEMP/packages/$package-$VERSION.tgz" --ignore-scripts --access public --tag "$DIST_TAG" + done diff --git a/AGENTS.md b/AGENTS.md index b8100b77..c9a7f579 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,111 +1,43 @@ ---- -description: Use Bun instead of Node.js, npm, pnpm, or vite. -globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" -alwaysApply: false ---- - -Default to using Bun instead of Node.js. - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; - -// import .css files directly and it works -import './index.css'; - -import { createRoot } from "react-dom/client"; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. +# Working on wouter + +Small, hook-based routing for React and Preact. Keep changes focused and avoid +adding runtime dependencies or unnecessary bytes. + +This file is the shared source of instructions for coding agents. Keep guidance +here or in linked Markdown documents, rather than editor-specific rule files. + +## Layout and commands + +- `packages/wouter/src/` is the shared JavaScript implementation; types live in + each package's `types/` directory. +- `wouter-preact` copies the shared sources and keeps its own `react-deps.js`. + Edit shared sources in `wouter`, and check both packages when changing APIs. +- Use Bun: `bun install --frozen-lockfile`, `bun test --coverage`, + `bun run test-types`, and `bun run lint`. +- Run `bun run --cwd packages/wouter-preact prepublishOnly` before + `bun run size`; the tests remove their temporary Preact source copies. +- Packages ship source files directly. There is no separate bundle build. +- For releases, follow [RELEASING.md](RELEASING.md). npm is used only for packing + and publishing through GitHub Actions with trusted publishing. + +## Development principles + +- Preserve backward compatibility. Update both packages' type definitions and the + README when changing a public API. +- Use hooks and functions; preserve stable references where callers rely on them + to avoid unnecessary renders. +- Add focused regression tests for behavior changes. Use `memoryLocation` for + isolated routing tests; check React, Preact, and SSR when relevant. +- Check bundle size for runtime changes. Support performance claims with a + before-and-after measurement. + +## Pull request descriptions + +- Start with the problem and the resulting behavior in 1–3 sentences. +- For a small fix, aim for 50–120 words. Add detail only when it helps review. +- Explain why; let the diff show which files and functions changed. +- Include an example or screenshot when it makes the change easier to understand. +- Mention material risks or compatibility changes, if any, and checks actually run. +- Skip template headings for short PRs, debugging diaries, repeated summaries, + agent attribution, and session links. +- Keep the title specific and the description accurate as the implementation changes. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3d..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 16ebc563..b71a1e9a 100644 --- a/README.md +++ b/README.md @@ -1094,7 +1094,7 @@ Wouter's motto is **"Minimalist-friendly"**. - `wouter-preact` reuses the same source except for `react-deps.js` (Preact-specific hooks) - Type definitions are duplicated between packages (not ideal, but works for now) -**Development:** Tests run directly from source files (no build required). Run `npm run test` for interactive mode or `npm run test -- --run` for a single run. Use `npm run build` to build the distributable package before publishing. +**Development:** Use `bun install --frozen-lockfile` and `bun test`. Tests run directly from source files; no build is required. See [AGENTS.md](AGENTS.md) for development commands and [RELEASING.md](RELEASING.md) for publishing both packages through GitHub Actions. ## Acknowledgements diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..a122921a --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,70 @@ +# Releasing wouter + +The [Publish packages workflow](https://github.com/molefrog/wouter/actions/workflows/release.yml) +publishes `wouter` and `wouter-preact` together. It runs tests with coverage, type +checks, lint, size checks, and package-content checks before publishing the exact +tarballs it inspected. Stable versions use `latest`; prereleases use `next`. + +## One-time npm setup + +Merge `.github/workflows/release.yml` into `main`. In the npm settings for **each** +package, add a GitHub Actions trusted publisher: + +| Setting | Value | +| --- | --- | +| Organization or user | `molefrog` | +| Repository | `wouter` | +| Workflow filename | `release.yml` | +| Environment name | Leave empty | +| Allowed actions | Enable direct publishing with `npm publish` | + +Configure both [wouter](https://www.npmjs.com/package/wouter/access) and +[wouter-preact](https://www.npmjs.com/package/wouter-preact/access). No `NPM_TOKEN` +secret is needed. npm uses GitHub OIDC and attaches provenance automatically. +See [npm's trusted publishing guide](https://docs.npmjs.com/trusted-publishers/). + +## Publish a version + +1. Set the same new version in `packages/wouter/package.json` and + `packages/wouter-preact/package.json`. Run `bun install --lockfile-only` to + update `bun.lock`, then commit and merge the version change into `main`. +2. Tag that commit and push the tag (replace the example version): + + ```sh + git switch main + git pull --ff-only + git tag v3.12.0 + git push origin v3.12.0 + ``` + +The tag push starts publishing automatically. A tag such as `v3.12.0-next.0` +publishes both packages under `next` without changing `latest`. The tag must match +both package versions and point to a commit on `main`. Build metadata (`+...`) +is not supported in release tags. + +To publish an existing tag manually, choose **Publish packages → Run workflow**, +leave the workflow branch as `main`, and enter the tag. Or use: + +```sh +gh workflow run release.yml --ref main -f tag=v3.12.0 +``` + +Select **Validate and pack without publishing** (or add `-F dry_run=true`) to +check an existing tag without publishing. A tag push always publishes. + +## Verify or retry + +Check the Actions run, then inspect both packages: + +```sh +npm view wouter@3.12.0 version dist.attestations +npm view wouter-preact@3.12.0 version dist.attestations +npm view wouter dist-tags +npm view wouter-preact dist-tags +``` + +npm cannot publish two packages atomically. If one succeeds and the other fails, +fix the reported issue and rerun the same tag. Existing versions are skipped; +their dist-tags are left unchanged. Registry errors other than a missing version +stop the run. Never move a released tag or try to overwrite a published version; +code changes require a new version.