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.