Files
tiny-pattern-ts/project-specs.md
T
tmu e45592ae00 🔧 Unify lefthook and package.json scripts via LEFTHOOK_FILES env var
lefthook and package.json had parallel command definitions for the
same tools (oxlint, oxfmt, cspell). Consolidate by making lefthook
call the npm scripts, with staged files passed via the
LEFTHOOK_FILES env var. The scripts use ${LEFTHOOK_FILES:-<default>}
so they default to the full project when invoked manually and to
the staged-files list when invoked from lefthook.

Changes:
- package.json#check:oxlint: `oxlint ${LEFTHOOK_FILES:-src}`
  (lints src/ manually; staged files from lefthook)
- package.json#check:oxfmt: `oxfmt --check ${LEFTHOOK_FILES:-src}`
- package.json#check:cspell: `cspell lint ${LEFTHOOK_FILES:-.}`
  (walks CWD manually; staged files from lefthook)
- package.json#check:tsc, check📦 unchanged (no file args)
- lefthook.yml: file-filtered hooks (oxlint, oxfmt, cspell) now use
  `sh -c 'LEFTHOOK_FILES="$0" npm run check:*' {staged_files}` to
  inject the staged-files list into the env. sort-package-json and
  typecheck call npm scripts directly (no file args).
- project-specs.md: document the unification pattern

Why sh -c + env var instead of the simpler 'npm run ... -- {staged_files}':
  'oxlint src file.ts' lints the whole src/ tree *plus* file.ts
  (oxlint doesn't dedupe paths). Setting LEFTHOOK_FILES as an env
  var (which lefthook's 'env:' config does not template) requires
  the sh -c wrapper, but it gives the right semantics: when the
  var is set, only the explicit files are checked; when unset,
  the default (src/ or .) is used.

Verified:
- 'npm run check:oxlint' (no env) lints all of src/
- 'LEFTHOOK_FILES=src/match.ts npm run check:oxlint' lints only that file
- 'npx lefthook run pre-commit' with a staged TS file: cspell output
  shows '1/1 src/match.ts' (only staged file, not whole project)
- All 5 hooks pass on a real staged change
- 'lefthook validate' reports 'All good'
- 'npm run check' exits 0
2026-09-03 18:44:19 +00:00

18 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). Single source of truth for the underlying commands is in package.json#scripts; the lefthook config only describes what to run on which files. Hooks that should run on staged files (oxlint, oxfmt, cspell) set the LEFTHOOK_FILES env var to the staged-files list via a sh -c wrapper, and the npm script uses ${LEFTHOOK_FILES:-<default>} to default to the full project when invoked manually:

    • pre-commit.commands.oxlint (glob *.{ts,tsx,js,jsx,mjs,cjs}): sh -c 'LEFTHOOK_FILES="$0" npm run check:oxlint' {staged_files}
    • pre-commit.commands.oxfmt (glob *.{ts,tsx,js,jsx,mjs,cjs}): sh -c 'LEFTHOOK_FILES="$0" npm run check:oxfmt' {staged_files}
    • pre-commit.commands.cspell: sh -c 'LEFTHOOK_FILES="$0" npm run check:cspell' {staged_files}
    • pre-commit.commands.sort-package-json: npm run check:package (no file args needed)
    • pre-commit.commands.typecheck: npm run check:tsc (no file args needed) 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 ${LEFTHOOK_FILES:-src} — lints src/ by default; when invoked from the lefthook pre-commit hook with LEFTHOOK_FILES set to the staged-files list, lints only those files. This is the single source of truth for the oxlint command and is shared between the manual npm run check and the pre-commit hook.
  • check:oxfmt: oxfmt --check ${LEFTHOOK_FILES:-src} — same pattern as check:oxlint (whole src/ by default, staged files from lefthook).
  • check:tsc: tsc --noEmit.
  • check:cspell: cspell lint ${LEFTHOOK_FILES:-.} — walks the project root by default; from lefthook, only the staged files.
  • 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.