Files
tiny-pattern-ts/project-specs.md
T
tmu 45b6c141e8 🔧 Migrate lefthook config to v2 schema
The previous config was a hybrid of v1 (jobs: array) and v1
(top-level commands: block) that no longer validates under
lefthook 2.x. lefthook 2.0.0's schema has top-level 'commands:'
set to 'false'; the v1 jobs: array form still works, but the
orphan named-commands block at the top was silently invalid
(caught by 'lefthook validate').

Migrate to the v2-native 'commands:' (named) form under the hook:

  pre-commit:
    parallel: true
    commands:
      oxlint:
        glob: ...
        run: npx oxlint {staged_files}
      ...

Changes:
- Add 'min_version: 2.0.0' to declare the v2 schema explicitly
- Replace pre-commit.jobs: array with pre-commit.commands: (named)
- Inline the glob on each command (was a top-level property on each
  job in v1)
- Remove the orphan top-level 'commands:' block — it was never
  referenced by any hook, and v2 forbids it
- Update project-specs.md: drop the dangling reference to the
  lefthook 'commands:' block (it was the dead top-level one);
  describe the v2 commands: form

Verified:
- 'lefthook validate' reports 'All good'
- 'lefthook run pre-commit' executes all 5 hooks (oxlint, oxfmt,
  sort-package-json, typecheck, cspell); globs correctly skip hooks
  on non-matching files (e.g. lefthook.yml alone) and run them on
  TS/JS changes
- 'npm run check' exits 0
- 'lefthook dump' shows the normalized v2 config
2026-09-03 18:12:01 +00:00

17 KiB

Project Specifications for TypeScript NPM Module

  • name of package: tiny-pattern-ts

0. References

typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starter-tiny/

1. Development Environment

  • TypeScript: Use strictest practical rules. Inline in tsconfig.json (not via @tsconfig/strictest). The rule set is: strict, noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitReturns, noFallthroughCasesInSwitch, noUncheckedIndexedAccess, noImplicitOverride, noUnusedLocals, noUnusedParameters, forceConsistentCasingInFileNames, isolatedModules, verbatimModuleSyntax.

  • EditorConfig: Use .editorconfig from typescript-lib-starter-tiny.

  • oxfmt: Rust-based formatter, Prettier-compatible. Replaces Prettier. Config in .oxfmtrc.json (same shape as .prettierrc).

  • oxlint: Rust-based linter. Replaces ESLint. Config in .oxlintrc.json with typescript, unicorn, oxc, import plugins. Categories enabled as errors: correctness, suspicious, restriction. As warnings: perf, style. nursery is off.

  • oxlint rules disabled by design (in .oxlintrc.json):

    • eslint/no-undefined — we use undefined as the no-match sentinel.
    • eslint/sort-keys — handler/case order is semantic, not alphabetical.
    • eslint/id-length — T, R, U, V are standard TS generics.
    • import/no-named-export — false positive on library entry re-exports. For *.test.ts files additionally: no-unused-expressions (for expectTypeOf(...) calls), no-empty-file (we have a single import per test file in some cases), import/no-nodejs-modules (we use node:test/node:assert/node:fs intentionally), eslint/no-magic-numbers (literals in tests are fine).
  • Import sorting: built into oxfmt (no separate plugin).

  • cspell: Basic spelling configuration. Words dictionary in cspell.json covers tooling names (oxlint, oxfmt, oxc, nodenext, oxfmtrc, oxlintrc, sortpackagerc, EDITMSG, typescriptteam, gitmoji, dbaeumer, msvc) and a few library names (tslib, typefest, lefthook).

  • Lefthook: Pre-commit checks (run in parallel, lefthook v2 schema):

    • min_version: 2.0.0
    • pre-commit.commands.oxlint — npx oxlint {staged_files} on staged *.{ts,tsx,js,jsx,mjs,cjs} files
    • pre-commit.commands.oxfmt — npx oxfmt --check {staged_files} on staged *.{ts,tsx,js,jsx,mjs,cjs} files
    • pre-commit.commands.cspell — npx cspell {staged_files}
    • pre-commit.commands.sort-package-json — npx sort-package-json --check
    • pre-commit.commands.typecheck — npx tsc --noEmit outdated is intentionally NOT a pre-commit check (it can flag upstream patch releases that aren't actionable locally); it runs in CI and via the explicit lefthook run outdated command.
  • Additional Dev Dependencies:

    • lefthook
    • sort-package-json
    • check-outdated
    • c8 (coverage for node --test)
    • expect-type (type-level assertions in tests)
    • oxfmt, oxlint (with their native bindings as optionalDependencies so the correct binding is selected per platform automatically)
    • typescript (TS 7)
    • @types/node
    • tslib (available for future runtime helper imports)
    • type-fest (utility types)

2. Build & Test

  • Build Tool: TypeScript 7 (tsc) — no bundler, no Vite. The build is a plain tsc -p tsconfig.build.json invocation that emits ESM JavaScript and .d.ts declarations to dist/. ESM only, no CJS.
  • Testing: Node's built-in node --test with --strip-types (Node 22.6+, unflagged on Node 24/26). Test files are co-located with source as *.test.ts. No Vitest.
  • TypeScript Build Output: dist directory (set in tsconfig.build.json#outDir; tsconfig.json keeps outDir for editor tooling but excludes tests from emission via tsconfig.build.json#exclude).
  • Import extensions: Source uses .ts extensions in imports (e.g. from "./match.ts") so node --strip-types resolves them at test time. TypeScript's rewriteRelativeImportExtensions (in tsconfig.build.json) rewrites these to .js in the emitted dist/* output, so consumers see conventional ESM imports.
  • TypeScript Compiler Options Notes:
    • allowImportingTsExtensions: true is set in the root tsconfig.json (works because the root config has noEmit: true).
    • rewriteRelativeImportExtensions: true is set in tsconfig.build.json to rewrite .ts to .js on emit.
    • module: nodenext, moduleResolution: nodenext for ESM-first Node packages.
    • verbatimModuleSyntax: true enforces explicit import type.
    • target: es2024, lib: ["es2024"] (Node 26 supports all ES2024 features natively).
  • Additional Dev Dependencies (for types and tests):
    • tslib — available for runtime helper imports; not currently used by source (kept for future use per the original spec).
    • type-fest — utility types; not currently used by source (kept for future use per the original spec).
    • @types/node — types for node:test, node:assert, node:fs.
  • Node Version: >=26 (engines field). Pinned via .node-version for fnm/nvm/volta/mise auto-switching and CI (actions/setup-node@v4 with node-version-file: .node-version).

3. Project Structure & Files

  • .gitignore: Ignore dist, node_modules, coverage, and other common files.
  • .node-version: Single line containing the Node major version (currently 26). Used by version managers and CI.
  • .npmignore: Ignore node_modules/, coverage/, *.log, *.tsbuildinfo, src/, .vscode/, .editorconfig, .oxfmtrc.json, .oxlintrc.json, .node-version, cspell.json, lefthook.yml, .sortpackagerc.json, commit-message-template. (Note: dist/ is included via package.json#files, not by absence from .npmignore.)
  • .oxfmtrc.json: oxfmt configuration (Prettier-shaped).
  • .oxlintrc.json: oxlint configuration.
  • .oxlintrc.json + .oxfmtrc.json replace the old .eslintrc.cjs and .prettierrc.
  • LICENSE: MIT.
  • README.md: Scaffolded.
  • commit-message-template: From typescript-lib-starter-tiny.
  • Target Environments: Node 26 LTS only (no browser target; this is a pure Node library, no DOM, no DOM lib in tsconfig).
  • No React, No CJS, ESM only.

4. Automation & Quality

  • Version Automation: Use standard npm version for versioning.
  • Unused Dependency Check: Use check-outdated (devDep, runs in CI and via the explicit lefthook run outdated command).
  • No commitlint, no conventional commits. Commits use gitmoji prefixes (e.g. :sparkles:, :wrench:, :bug:, :fire:, :white_check_mark:, :tada:) for at-a-glance categorization.

5. Scripts

SETUP

  • use:git-commit-message: Set up commit message template (if needed).

TEST

  • test: Run tsc --noEmit then node --test --strip-types src/.
  • test:unit: Run unit tests with node --test --strip-types src/ (no preceding typecheck).
  • test:ci: Run tests in CI mode with c8 coverage (text + lcov + html reporters), uploading coverage/ as an artifact.

BUILD

  • build: Build the project using tsc -p tsconfig.build.json (emits dist/*.js + dist/*.d.ts + sourcemaps, with .ts imports rewritten to .js).

CLEAN

  • clean: Remove dist/ via node -e "fs.rmSync('dist', {recursive:true, force:true})". (Replaces clean:build from the original spec — same effect, no rimraf dep needed.)

CHECK

  • check: Run all checks in order — check:oxlint, check:oxfmt, check:tsc, check:cspell, check:package, check:outdated.
  • check:oxlint: oxlint src.
  • check:oxfmt: oxfmt --check src.
  • check:tsc: tsc --noEmit.
  • check:cspell: cspell ..
  • check:package: sort-package-json --check.
  • check:outdated: check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-*,@oxlint/binding-*. The oxc native bindings are declared as optionalDependencies so the correct one is selected per platform automatically; the *-platform bindings show as "not installed" on the current platform and are explicitly ignored here.

FIX

  • fix:oxlint: oxlint --fix src.
  • fix:oxfmt: oxfmt src.
  • fix:package: sort-package-json --write. (There is no fix aggregator in the scripts; run the fix:* scripts individually.)

HOOKS

  • Lefthook runs the relevant check:* scripts on staged files in parallel for pre-commit. See lefthook.yml.

6. Repository & CI/CD

  • Repository: Hosted on GitHub.
  • Build Pipeline: GitHub Actions (.github/workflows/ci.yml).
    • build job on push and pull_request to main and on workflow_dispatch. Steps: actions/checkout@v4, actions/setup-node@v4 (with node-version-file: .node-version, cache: npm), npm ci, npm run build, npm run check, npm run test:ci, then upload coverage/ as an artifact.
    • publish job: only on refs/tags/*, depends on build. Steps: actions/checkout@v4, actions/setup-node@v4 (with node-version-file: .node-version and registry-url), npm ci, npm run build, npm publish --access public with NODE_AUTH_TOKEN from secrets.

7. Versioning & Publishing

  • Version Update: Use npm version to bump version after merging to main and before publishing.
  • Publishing to npm: Only publish from CI on tagged commits.
  • Recommended Workflow:
    1. Develop and merge PRs to main
    2. Run all checks via CI
    3. Bump version with npm version <patch|minor|major>
    4. Push tag to GitHub
    5. CI builds and publishes to npm on tag

8. NPM Keywords

  • pattern-matching
  • pattern
  • match
  • algebraic-data-types
  • adt
  • typescript

The library is for pattern matching (not regex), similar to F#'s pattern matching, for TypeScript/ESM environments.

9. Code Coverage

  • Tool: c8 (V8-native coverage, no instrumentation step).
  • Configuration: c8 has no project config; the report shape is pinned in the test:ci script:
"test:ci": "c8 --reporter=text --reporter=lcov --reporter=html node --test --strip-types src/"
  • Reporters: text summary, HTML, and lcov (matching the original spec).
  • CI: coverage/ is uploaded as a workflow artifact via actions/upload-artifact@v4 (see .github/workflows/ci.yml).
  • Optional: Coverage thresholds can be added in c8 config when the library surface stabilizes.

10. Source Structure & Tree Shaking

  • Source Directory: All source code resides in src/ and is exported via src/index.ts.
  • Configuration:
    • sideEffects: false in package.json (set).
    • ESM-only exports; package.json#exports field maps . to {"types": "./dist/index.d.ts", "import": "./dist/index.js"}.
    • Avoid top-level side effects in modules.
    • Explicit re-exports in index.ts for best results.
  • Source structure (current):
    • src/index.ts — public barrel.
    • src/match.ts — match(value).with(...).exhaustive() / .otherwise(...) builder.
    • src/pattern.ts — P.literal, P.type, P.when, P.any, P.shape constructors and the Matcher<T> interface.
    • src/index.test.ts — runtime + type-level tests using node --test + expect-type.
  • The human will implement the source code.

11. Included Templates from typescript-lib-starter-tiny

.editorconfig

# Editor configuration, see http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
quote_type = double

[*.md]
max_line_length = 0
trim_trailing_whitespace = false

[COMMIT_EDITMSG]
max_line_length = 0

commit-message-template

# If applied, this commit will... (Max 50 char)


# Explain why this change is being made (Max 72 Char) [WHAT and WHY vs HOW]


# Provide links or keys to any relevant tickets, articles or other resources
Resolves #...

# --- COMMIT END ---
# Remember to
#    Use the imperative mood in the subject line
#    Capitalize the subject line
#    Do not end the subject line with a period
#    Separate subject from body with a blank line
#    Use the body to explain what and why vs. how
#    Can use multiple lines with "-" for bullet points in body

README.md Structure (scaffolded)

  • Project title and description
  • Development
    • Build: npm run build
    • Test: npm run test, npm run test:ci
    • Checks: npm run check, npm run fix:oxfmt, npm run fix:oxlint
  • Tooling section listing TypeScript 7, node --test, c8, oxlint, oxfmt, cspell, sort-package-json, lefthook
  • Requirements: Node.js >= 26
  • VSCode integration
    • Debugging
    • Running tests
    • oxc.oxc-vscode provides oxlint and oxfmt in-editor
  • Workflows
    • Version updates via npm version
    • Publishing via GitHub Actions on tagged commits
  • Contribution guidelines
    • Commit signing (GPG)
    • How to set up commit message template (npm run use:git-commit-message)
    • Reference to commit-message-template
    • Type-level tests use expect-type's expectTypeOf(...) inside node --test cases

12. Project Initialization & Commit Strategy

  • Start by initializing git with a main branch.
  • Initial commit: empty README + LICENSE.
  • Create a feature branch: feature/setup.
  • For each technology or tool added (and its configuration), create a separate commit:
    • Prepend each commit message with a matching gitmoji (e.g. :sparkles: for new features, :wrench: for config, :bug: for fixes, :fire: for removals, :white_check_mark: for tests, :tada: for initial commit).
    • Example commit messages used in this project:
      • :tada: Initial commit with empty README
      • :wrench: Track .vscode/settings.json for workspace settings
      • :construction_worker: Added GitHub Actions workflow for CI/CD
      • :test_tube: Added Vitest configuration with coverage (later removed)
      • :sparkles: Scaffolded src/index.ts entry point for library code
      • :wrench: Replace Vite/Vitest with TypeScript 7 and node --test
      • :wrench: Replace ESLint and Prettier with oxlint and oxfmt
      • :fire: Remove Vite scaffold leftovers
      • :sparkles: Add initial pattern-matching API
      • :white_check_mark: Add expect-type for type-level tests
      • :wrench: Declare Node 26 as the supported runtime
      • :bug: Use .ts extensions in imports for node --strip-types
      • :wrench: Remove Prettier from editor formatter config
  • Each commit should include only the relevant files and configuration for that technology/tool. This approach ensures a clean, understandable project history and makes it easy to review or revert specific setup steps.

13. Changelog Automation

  • Not currently configured. The intended workflow, when adopted, is a changesets-driven release process: feature PRs include a changeset, which gets consumed by a release workflow, producing a CHANGELOG.md and a version bump on merge to main.
  • The changelog should be updated as part of the release process.

14. Publishing Public

  • npm publishing is configured to be public by default.
  • publishConfig: { "access": "public" } is set in package.json.
  • The CI/CD pipeline publishes with --access public on tagged commits.

15. VSCode Integration

  • .vscode/settings.json uses oxc.oxc-vscode as the default formatter for [typescript], [javascript], [json], [jsonc], [markdown], [mdx], and [yaml] (oxfmt under the hood).
{
    "typescript.tsdk": "node_modules/typescript/lib",
    "js/ts.tsdk.path": "node_modules/typescript/lib",
    "[typescript]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[javascript]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[json]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[jsonc]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[markdown]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[mdx]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "[yaml]": {
        "editor.defaultFormatter": "oxc.oxc-vscode",
        "editor.formatOnSave": true
    },
    "editor.defaultFormatter": "oxc.oxc-vscode",
    "editor.formatOnSave": true
}
  • .vscode/extensions.json recommends:
    • oxc.oxc-vscode (oxlint + oxfmt, replaces eslint/prettier/vitest)
    • streetsidesoftware.code-spell-checker (cspell)
    • typescriptteam.native-preview (TypeScript 7 nightly support; replaces the older ms-vscode.vscode-typescript-next)
{
    "recommendations": [
        "oxc.oxc-vscode",
        "streetsidesoftware.code-spell-checker",
        "typescriptteam.native-preview"
    ]
}
  • .vscode/tasks.json is not currently provided; common tasks (build, test, lint, typecheck, format, spell, check:outdated) are run via the npm scripts in package.json from the integrated terminal.
  • VSCode uses the workspace TypeScript version via typescript.tsdk (and the explicit js/ts.tsdk.path), with oxc.oxc-vscode for formatting and linting.