From e78f624cbbdace1ca908182bf2efa6fb63636ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 5 Sep 2026 00:21:56 +0200 Subject: [PATCH] :fire: Drop project-specs.md; absorb unique rationale into README project-specs.md was a parallel document that restated most of what was already in package.json, README.md, and the config files themselves. The only content that wasn't already captured elsewhere was the 'why' behind a handful of non-obvious tooling choices, which now lives in README.md as a new 'Tooling decisions' subsection. This eliminates the drift problem between the two docs (the source of drift in the previous commit) by having one source of truth for 'what' (the config files) and one source for 'why' (README). --- README.md | 16 ++ project-specs.md | 674 ----------------------------------------------- 2 files changed, 16 insertions(+), 674 deletions(-) delete mode 100644 project-specs.md diff --git a/README.md b/README.md index cd28922..d94e3da 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex). - **@arethetypeswrong/cli** (`attw`) — validates `.d.ts` declarations against multiple module-resolution scenarios. Runs on publish only with `--profile esm-only` (the package is intentionally ESM-only). - **lefthook** — git pre-commit hooks. +### Tooling decisions + +The choice and configuration of each tool above is the result of deliberate trade-offs, not defaults. The non-obvious ones: + +- **`tsconfig.json` extends `@tsconfig/strictest` + `@tsconfig/node26`**; `tsconfig.build.json` extends it to add the emit-only options (`declaration`, `sourceMap`, `outDir`, `target: es2024`, `rewriteRelativeImportExtensions: true`) and to exclude test files. This separation lets the editor and CI type-check from one config while the build emits from the other. +- **Source imports use `.ts` extensions** so `node --strip-types` resolves them at test time. `rewriteRelativeImportExtensions: true` in `tsconfig.build.json` rewrites them to `.js` in the emitted `dist/*` output, so consumers see conventional ESM imports. +- **Type-aware oxlint is enabled declaratively** via `options.typeAware: true` in `.oxlintrc.json` (powered by `oxlint-tsgolint`). The script commands stay clean — no CLI flag — and type-aware mode is a property of the config, not the invocation. +- **Source-level `oxlint-disable` directives** are used for known type-aware false positives (see `src/pattern.ts`, `src/match.ts`, `src/index.test.ts`). The disable lives next to the code it silences, not in `.oxlintrc.json`, so the trade-off is visible to anyone reading the source. +- **`knip --include dependencies,exports,files`** intentionally omits the `types` category, which produces systematic false positives for libraries whose exported types are part of the public API. The targeted scope keeps the signal high without config-file boilerplate. +- **`attw --profile esm-only`** is semantically correct: this package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug. +- **The `publish:` prefix has no local aggregator.** `publint` and `attw` validate the _publishable artifact_ (`dist/`), not the source, and require a fresh build. They run only in the CI `publish` job immediately before `npm publish` — there is intentionally no `npm run publish`. +- **`check:tsc` runs first** in the `npm run check` chain so a type error short-circuits the rest (faster feedback than letting oxlint/oxfmt run and then failing on tsc at the end). +- **`check:knip` and `check:outdated` are NOT in pre-commit** — knip scans the whole project (~4s, would noticeably slow the hook), and `check-outdated` queries the npm registry (~6.5s, network-dependent, advisory not correctness). Both run in `npm run check` and CI; pre-commit stays fast and offline. +- **The pre-commit hook sets the `LEFTHOOK_FILES` env var** to the staged-files list, and the affected scripts use `${LEFTHOOK_FILES:-}` to default to the whole project when invoked manually. This keeps `package.json#scripts` as the single source of truth for the underlying commands — `lefthook.yml` only describes _what to run on which files_. +- **`tslib` and `type-fest` are deliberately not used.** `tslib` is a runtime helper for old ES3/ES5 targets (the project targets ES2024); `type-fest` was never imported. knip caught both. + ### Requirements - Node.js >= 26 (engines field; pinned via `.node-version`). diff --git a/project-specs.md b/project-specs.md deleted file mode 100644 index cc338d9..0000000 --- a/project-specs.md +++ /dev/null @@ -1,674 +0,0 @@ -# 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 via `@tsconfig/strictest` - (extended from `tsconfig.json`). The project also extends - `@tsconfig/node26` for Node 26's `target`/`module`/`lib` defaults; - `tsconfig.json` is the type-check base (`noEmit: true`, includes - `src/`), and `tsconfig.build.json` extends it to add the emit-only - options (`declaration`, `sourceMap`, `outDir`, `target: es2024`, - `rewriteRelativeImportExtensions: true`) and to exclude test files - from emission. The `verbatimModuleSyntax: true` and - `allowImportingTsExtensions: true` overrides live in - `tsconfig.json` (the latter only works because the base has - `noEmit: true`). The strictest flags the project relies on (inherited - from `@tsconfig/strictest`) are: `strict`, `noImplicitAny`, - `noImplicitThis`, `alwaysStrict`, `strictNullChecks`, - `strictFunctionTypes`, `strictBindCallApply`, - `strictPropertyInitialization`, `noImplicitReturns`, - `noFallthroughCasesInSwitch`, `noUncheckedIndexedAccess`, - `noImplicitOverride`, `noUnusedLocals`, `noUnusedParameters`, - `forceConsistentCasingInFileNames`, `isolatedModules`, - `verbatimModuleSyntax`, plus the additional strictest flags - (`exactOptionalPropertyTypes`, `noPropertyAccessFromIndexSignature`, - `noUncheckedIndexedAccess` etc.) that `@tsconfig/strictest` enables - beyond a plain `strict: true`. -- **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. Type-aware rules are enabled via - `options.typeAware: true` in `.oxlintrc.json` (no CLI flag needed); - the linter then uses `oxlint-tsgolint` for rules that require type - information. -- **oxlint-tsgolint**: TypeScript-Go-backed type-aware linter for oxlint. - Activated by `options.typeAware: true` in `.oxlintrc.json`. Native - bindings installed as `optionalDependencies` per platform (same - pattern as oxlint). -- **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. - - `typescript/method-signature-style` — method signatures in - `interface` declarations are conventional TS ergonomics. - - Stylistic rules superseded by oxfmt (oxfmt is the canonical - formatter; these rules either conflict with its output or - duplicate features oxfmt already provides): - - `eslint/one-var` — oxfmt uses comma-joined `const` - declarations; oxlint wanted to split them. - - `import/group-exports` — oxfmt keeps separate `export` - statements as-is. - - `import/exports-last` — statement ordering is up to oxfmt. - - `eslint/sort-imports` — replaced by oxfmt's built-in - `sortImports` (enabled in `.oxfmtrc.json`). - - `import/consistent-type-specifier-style` — inline - `import { type X, Y }` is intentional for grouping. - - `unicorn/prefer-export-from` — conflicts with how the - barrel `src/index.ts` re-exports through `src/match.ts`. - 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`, `tsgolint`, `EDITMSG`, `typescriptteam`, - `gitmoji`, `dbaeumer`, `msvc`, `publint`, `attw`, `arethetypeswrong`, - `knip`) and a few library names (`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:-}` 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.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 (git hook manager) - - check-outdated (outdated dependency warnings) - - knip (unused dependency / export / file detection) - - publint (publish-time `package.json` validation) - - @arethetypeswrong/cli (attw, publish-time `.d.ts` validation) - - @tsconfig/strictest, @tsconfig/node26 (extended from in - `tsconfig.json`) - - 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) - - oxlint-tsgolint (type-aware linter backend, also with native - bindings as `optionalDependencies`) - - cspell (spell checking) - - typescript (TS 7) - - @types/node - - `tslib` and `type-fest` were removed (commit `ec98223`): knip - flagged them as unused, and they were not appropriate for an - ES2024-targeting ESM-only library (`tslib` is a runtime helper for - old ES3/ES5 targets; `type-fest` was never imported). - -## 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): - - `@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`, `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 - -### PREFIX CONVENTION - -Script names use a prefix that signals _when_ the script is intended -to run. A `:` script is implicitly aggregated by a -`` script (if one exists) and run by the corresponding -lefthook hook or CI step. Picking the right prefix documents the -script's intended lifecycle: - -- `check:*` — read-only verification. Aggregated by `npm run check` - (which runs all `check:*` scripts in order). Called from the - lefthook pre-commit hook on staged files, and from the CI build - job on the full project. Read-only; never modifies files. -- `fix:*` — mutating counterpart of a `check:*` script. Aggregated - by `npm run fix`. Use after `npm run check` to auto-resolve - issues; the diff is the review surface. -- `test:*` — test scripts. `npm run test` is the canonical entry - point (`check:tsc` + unit tests); `test:unit` skips the typecheck - for fast local iteration; `test:ci` adds c8 coverage and is the - CI variant. -- `publish:*` — runs only at publish time, in the CI `publish` job - (immediately before `npm publish`). There is **no** local - `npm run publish` script — publishing is CI-only by policy (see - §7). The `publish:` prefix still documents intent: this script - validates the _publishable artifact_ (e.g., `dist/`) rather than - the source. - -A new script should pick the prefix that matches its lifecycle, not -invent a new one. If no existing prefix fits, that is a signal the -script does not belong in the standard pipeline. - -### SETUP - -- `use:git-commit-message`: Set up commit message template (if needed). - -### TEST - -- `test`: Run `check:tsc` then `node --test --strip-types "src/**/*.test.ts"`. - The glob is required because Node 26 does not auto-discover test - files in a bare directory argument (`node --test src/` is - interpreted as a module path on Node 26+). -- `test:unit`: Run unit tests with `node --test --strip-types "src/**/*.test.ts"` - (no preceding typecheck). -- `test:ci`: Run tests in CI mode with c8 coverage (text + lcov + html - reporters), uploading `coverage/` as an artifact. The command is - `c8 --reporter=text --reporter=lcov --reporter=html node --test ---strip-types "src/**/*.test.ts"` (the glob is required on Node 26 - for the same reason `test` uses one — see below). - -### 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:tsc`, `check:oxlint`, - `check:oxfmt`, `check:cspell`, `check:knip`, `check:outdated`. - `check:tsc` is first so a type error short-circuits the rest - (faster feedback than letting oxlint/oxfmt run and then failing on - tsc at the end). -- `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. Type-aware rules are activated declaratively via - `options.typeAware: true` in `.oxlintrc.json` (not via a CLI flag), - so the script command stays clean. oxlint only understands JS/TS- - family languages, so config files (JSON/YAML/Markdown) are - intentionally outside its scope; they are checked only by oxfmt. - Source-level `oxlint-disable` directives are used to silence - type-aware false positives in the generic type machinery: 4× for - `typescript/no-unsafe-type-assertion` and 1× for - `typescript/no-unnecessary-type-parameters` in `src/pattern.ts` / - `src/match.ts` (legitimate type machinery that needs restructuring, - documented in the source); 1× file-level for - `typescript/no-floating-promises` in `src/index.test.ts` - (`expectTypeOf(...)` is a sync type-assertion library that - oxlint-tsgolint misidentifies). -- `check:oxfmt`: `oxfmt --check ${LEFTHOOK_FILES:-.}` — formats the - whole project (`.`) by default, including JS/TS, JSON/JSONC, - YAML, Markdown, MDX and other supported file types. From lefthook - pre-commit, the `LEFTHOOK_FILES` env var scopes to the staged - files matching `*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,yaml,yml,md,mdx}`. - Built-in `sortPackageJson: true` keeps `package.json` keys - alphabetized (replaces the former `sort-package-json` tool). - Built-in import sorting (enabled via `sortImports: true`) replaces - any external import-sort plugin. -- `check:tsc`: `tsc` (noEmit is set in `tsconfig.json`). -- `check:cspell`: `cspell lint ${LEFTHOOK_FILES:-.}` — walks the - project root by default; from lefthook, only the staged files. -- `check:outdated`: `check-outdated --ignore-pre-releases ---ignore-packages @oxfmt/binding-darwin-arm64,@oxfmt/binding-darwin-x64,@oxfmt/binding-linux-arm64-gnu,@oxfmt/binding-linux-arm64-musl,@oxfmt/binding-linux-x64-gnu,@oxfmt/binding-linux-x64-musl,@oxfmt/binding-win32-x64-msvc,@oxlint/binding-darwin-arm64,@oxlint/binding-darwin-x64,@oxlint/binding-linux-arm64-gnu,@oxlint/binding-linux-arm64-musl,@oxlint/binding-linux-x64-gnu,@oxlint/binding-linux-x64-musl,@oxlint/binding-win32-x64-msvc,@oxlint-tsgolint/darwin-arm64,@oxlint-tsgolint/darwin-x64,@oxlint-tsgolint/linux-arm64,@oxlint-tsgolint/linux-x64,@oxlint-tsgolint/win32-arm64,@oxlint-tsgolint/win32-x64`. The oxfmt, oxlint, and oxlint-tsgolint native bindings are declared as `optionalDependencies` so the correct one is selected per platform automatically; bindings for non-current platforms show as "not installed" and are explicitly ignored here. -- `check:knip`: `knip --include dependencies,exports,files` — finds - unused dependencies, value exports, and source files. The - scoped `--include` list intentionally omits the `types` - category, which produces systematic false positives for - libraries whose exported types (e.g. `Matcher`, `Pattern`) are - part of the public API and not consumed internally. The - targeted scope keeps the signal high (real unused-dep - detection) without config-file boilerplate. - -### FIX - -- `fix`: Run all fixes in order — `fix:oxlint`, then `fix:oxfmt`. - Use this when `npm run check` reports issues you want to - auto-resolve. The fix scripts write changes in place; review - the diff before committing. -- `fix:oxlint`: `oxlint --fix src`. -- `fix:oxfmt`: `oxfmt ${LEFTHOOK_FILES:-.}` — same scoping as - `check:oxfmt` (whole project by default, staged files from - lefthook). Writes changes in place. - -### PUBLISH - -- `publish:publint`: `publint` — runs the pack-and-lint flow against - the current project (uses `npm pack` to validate the actual - publishable artifact against `package.json`'s `files`, `exports`, - `main`, etc.). Requires a fresh `build` to have populated `dist/`. - Called by the CI `publish` job immediately before `npm publish` - (see §6). Not part of `npm run check` and not run on commit: - it validates publishing correctness, not source correctness. -- `publish:attw`: `attw . --pack --profile esm-only` — validates the - emitted `.d.ts` declarations against multiple TypeScript - module-resolution scenarios. Uses `--profile esm-only` because - this package is intentionally ESM-only (no CommonJS shim); CJS - resolution scenarios are explicitly out of scope by design, not - a bug. Called by the CI `publish` job alongside `publish:publint` - and `npm publish` (see §6). Not part of `npm run check` and not - run on commit: same rationale as `publish:publint`. - -### HOOKS - -- Lefthook runs the relevant `check:*` scripts on staged files in - parallel for pre-commit, and `npm test` on pre-push. See - `lefthook.yml`. - -### CHECK TIERS — what runs where and why - -The `check:*` scripts are split across three execution contexts -based on **speed**, **scope** (staged files vs whole project), and -**side effects** (network, registry queries). The split is a -deliberate design choice; the rule of thumb is: - -- **pre-commit hook**: fast, file-scoped, offline, deterministic. - Catches what _you just changed_ in under a second or two. -- **pre-push hook**: runs the unit test suite (`npm test`, - including a full `tsc` over the project). ~3.5s. Catches - runtime/logic bugs across the whole codebase before the push - leaves your machine. The `tsc` step is technically redundant - with pre-commit but validates against unstaged changes too. -- **`npm run check`**: full project audit. Slower, may query the - network, runs everything that's not in pre-commit. -- **CI build job**: authoritative. Runs `npm run check` and - `npm run test:ci` on every push and PR. The ground truth — - if CI passes, the codebase is clean. - -| Script | pre-commit | pre-push | `npm run check` | `npm run fix` | CI build | CI publish | -| ----------------- | :--------: | :------: | :-------------: | :-----------: | :------: | :--------: | -| `check:tsc` | ✅ | ✅ | ✅ | — | ✅ | — | -| `check:oxlint` | ✅ | — | ✅ | — | ✅ | — | -| `check:oxfmt` | ✅ | — | ✅ | — | ✅ | — | -| `check:cspell` | ✅ | — | ✅ | — | ✅ | — | -| `test` | — | ✅ | — | — | ✅ | — | -| `check:knip` | — | — | ✅ | — | ✅ | — | -| `check:outdated` | — | — | ✅ | — | ✅ | — | -| `fix:oxlint` | — | — | — | ✅ | — | — | -| `fix:oxfmt` | — | — | — | ✅ | — | — | -| `publish:publint` | — | — | — | — | — | ✅ | -| `publish:attw` | — | — | — | — | — | ✅ | - -(Each `test` row also runs `check:tsc` internally, so pre-push gets a -full `tsc` for free.) - -#### Why these splits? - -- **`check:tsc`, `check:oxlint`, `check:oxfmt`, `check:cspell`** - are in pre-commit because they are fast (~0.2–0.5s each), fully - offline, and naturally scope to staged files via the - `LEFTHOOK_FILES` env var convention. They give instant feedback - on what you typed. - -- **`test` (and the `tsc` it includes) is in pre-push** because it - runs the whole test suite across the whole project (~3.5s - for the current 6 tests, including a full `tsc`). The pre-commit - `LEFTHOOK_FILES` convention doesn't apply to the test runner, - so pre-commit isn't the right home. Pre-push is the natural - place: it runs after all commits are made but before the push - leaves the machine, catching regressions that span multiple - commits. - -- **`check:knip` is NOT in pre-commit or pre-push** because it - scans the whole project (not staged files) and takes ~4s. - Adding it would noticeably slow both hooks. It's still fast - enough to run locally before pushing, and CI catches it - regardless. - -- **`check:outdated` is NOT in pre-commit** because (a) it queries - the npm registry (~6.5s) which means network dependency in a - hook, (b) the result is advisory, not a pass/fail correctness - check, and (c) the same network call in CI or locally on demand - gives the same answer. It belongs in scheduled or on-demand - runs, not in a blocking hook. - -- **`publish:publint` and `publish:attw` are NOT in `check`** — - they belong to the `publish:` prefix (see above) because they - validate the _publishable artifact_ (`dist/`) and require a - fresh build. Running them on every commit would be wasteful - (and slow, `npm pack` is involved). They run in the CI - `publish` job immediately before `npm publish`. - -#### What to do before pushing - -Run `npm run check` locally. If you only trust pre-commit + CI, -know that anything `check:knip` or `check:outdated` would catch -will be caught by CI on the PR before merge (assuming branch -protection is configured to require CI to pass). - ---- - -## 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. - Runs the **full `check` chain** (all 6 `check:*` scripts) and - the test suite with coverage. This is the authoritative gate — - if it passes, the codebase is clean. See §5 ("CHECK TIERS") - for which checks run here vs. locally vs. pre-commit. - - `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 run publish:publint` (see - §5 for why this is `publish:` and not `check:`), - `npm run publish:attw`, and `npm publish --access public` - with `NODE_AUTH_TOKEN` from secrets. Runs the **publish-tier - checks** that validate the publishable artifact before - publishing to npm. - -## 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 ` - 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: - -```jsonc -"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` 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 - -```plaintext -# 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 - -```plaintext -# 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, 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). - -```json -{ - "[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`) - -```json -{ - "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 TypeScript 7 via the `typescriptteam.native-preview` - extension, with `oxc.oxc-vscode` for formatting and linting.