The pre-commit hook is file-scoped (LEFTHOOK_FILES), so it can't naturally run the test suite. Pre-push is the right tier for it: - Runs after all commits are made but before the push leaves the machine, catching regressions that span multiple commits - ~3.5s including the tsc step (negligible vs the typical push round-trip to CI) - Offline and deterministic, same philosophy as pre-commit lefthook.yml: new pre-push section, sequential (parallel: false since there's only one command, but the explicit value documents the intent that this hook runs commands in order rather than racing). project-specs.md: updated the CHECK TIERS table to add the pre-push column with in it. Updated the rule-of-thumb list to include the pre-push tier. Updated the 'why' notes to explain why lives in pre-push rather than pre-commit (LEFTHOOK_FILES doesn't apply to the test runner).
638 lines
28 KiB
Markdown
638 lines
28 KiB
Markdown
# 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. 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.
|
||
- 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`.
|
||
- `typescript/method-signature-style` — method signatures
|
||
in `interface` are conventional TS ergonomics.
|
||
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`, `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.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
|
||
- 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`, `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 `<prefix>:<name>` script is implicitly aggregated by a
|
||
`<prefix>` 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. There is
|
||
**no** `npm run fix` aggregator by design: fixes should be
|
||
intentional, not batched. Run individually.
|
||
- `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.
|
||
|
||
### 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:knip`, `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. 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-*,@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.
|
||
- `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: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.
|
||
(There is no `fix` aggregator in the scripts; run the `fix:*` scripts
|
||
individually.)
|
||
|
||
### 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). ~500ms. 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` | CI build | CI publish |
|
||
| ----------------- | :--------: | :------: | :-------------: | :------: | :--------: |
|
||
| `check:tsc` | ✅ | ✅ | ✅ | ✅ | — |
|
||
| `check:oxlint` | ✅ | — | ✅ | ✅ | — |
|
||
| `check:oxfmt` | ✅ | — | ✅ | ✅ | — |
|
||
| `check:cspell` | ✅ | — | ✅ | ✅ | — |
|
||
| `test` | — | ✅ | — | ✅ | — |
|
||
| `check:knip` | — | — | ✅ | ✅ | — |
|
||
| `check:outdated` | — | — | ✅ | ✅ | — |
|
||
| `publish:publint` | — | — | — | — | ✅ |
|
||
| `publish:attw` | — | — | — | — | ✅ |
|
||
|
||
#### 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 (~500ms
|
||
for the current 6 tests). 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 <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:
|
||
|
||
```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<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
|
||
|
||
```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.
|