Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
837203c29e | ||
|
|
c7f566500e | ||
|
|
09c5b129bb | ||
|
|
b9fb054176 | ||
|
|
464789b385 | ||
|
|
49a78f3683 | ||
|
|
892aead383 | ||
|
|
682ecf1163 | ||
|
|
8b4722da26 | ||
|
|
da5ab79f9b | ||
|
|
3d0fe18d6a | ||
|
|
103cf04f8d | ||
|
|
7973bf0d0b | ||
|
|
8940537fc0 | ||
|
|
436b49e3be | ||
|
|
8924dd67d3 | ||
|
|
538272c222 | ||
|
|
e78f624cbb | ||
|
|
1a749b13d6 | ||
|
|
1fe3dbe28b | ||
|
|
8a8d57172c | ||
|
|
7e9401590e | ||
|
|
74e4538094 | ||
|
|
34e9b52ee8 | ||
|
|
2b3ef0f721 | ||
|
|
ec98223f8d |
No files matched your search
@@ -26,6 +26,21 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
# Advisory scans (dead code, dependency freshness). Non-blocking: surfaced on
|
||||
# the PR for visibility, but must never gate a merge — so continue-on-error and
|
||||
# intentionally NOT in `publish`'s `needs`.
|
||||
maintain:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run maintain
|
||||
|
||||
publish:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
src/
|
||||
.vscode/
|
||||
.editorconfig
|
||||
.oxfmtrc.json
|
||||
.oxlintrc.json
|
||||
.node-version
|
||||
cspell.json
|
||||
lefthook.yml
|
||||
commit-message-template
|
||||
@@ -0,0 +1,5 @@
|
||||
# Turn the `engines` field from a warning into a gate. By default a Node
|
||||
# version mismatch is only reported as a notice, so an install run under an
|
||||
# unpinned Node still succeeds and silently rewrites package-lock.json using
|
||||
# that older npm's resolution rules. Refuse the install instead.
|
||||
engine-strict=true
|
||||
+2
-5
@@ -22,11 +22,8 @@
|
||||
"unicorn/prefer-export-from": "off",
|
||||
"typescript/method-signature-style": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"es2024": true,
|
||||
"node": true
|
||||
},
|
||||
"options": { "typeAware": true },
|
||||
"env": { "builtin": true, "es2024": true, "node": true },
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts"],
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# AGENTS.md
|
||||
|
||||
Machine entry point for AI coding agents working in this repo. The authoritative
|
||||
guidance for humans lives in [CONTRIBUTING.md](./CONTRIBUTING.md) and
|
||||
[README.md](./README.md); this file only points at it and states the stable
|
||||
first-action facts. Do not restate evolving prose here — it will drift.
|
||||
|
||||
## First action
|
||||
|
||||
- Project: F#-style pattern matching for TypeScript/ESM. Node `>=26` (pinned via `.node-version`), ESM-only (no CommonJS shim).
|
||||
- **While iterating:** `npm run test` (`check:tsc` + the unit suite) for fast feedback on the files you changed.
|
||||
- **Definition of done — run this before you call the work finished:** `npm run verify`. If all green, commit. If red, look at the output, fix the root cause, and re-run.
|
||||
- **On commit:** write a good message (see [CONTRIBUTING.md § Commit messages](./CONTRIBUTING.md#commit-messages)). Lefthook's pre-commit hook already runs the fast, offline, staged-file checks — don't run them by hand. If the hook fails on style, `npm run fix`, restage, recommit.
|
||||
- **`npm run maintain` is NOT part of the feature loop.** Its scans are advisory, never a gate; run them only on an explicit maintenance / update-deps branch.
|
||||
|
||||
```sh
|
||||
npm run test # while iterating (fast feedback)
|
||||
npm run verify # definition of done: whole-project correctness, one shot
|
||||
```
|
||||
|
||||
## Never do
|
||||
|
||||
Don't silence the type system to force a green run. As an agent these are forbidden:
|
||||
|
||||
- `// @ts-nocheck`, `// @ts-ignore`, `// @ts-expect-error`
|
||||
- `// oxlint-disable` / `// oxlint-disable-next-line`
|
||||
- `as` casts used to push an expression through (type-aware oxlint already flags unsafe assertions)
|
||||
|
||||
Fix the root cause with the type system instead — narrowing, generics, `satisfies`, conditional / mapped types, utility types (`NonNullable`, `Exclude`, …). TypeScript can express it; that's the intended tool. The `oxlint-disable`-location rule in [CONTRIBUTING.md § Rules the tools don't enforce](./CONTRIBUTING.md#rules-the-tools-dont-enforce) is a **human** last-resort convention (so a reviewer can spot a deliberate suppression) — it is not permission for you to add one. If the types genuinely cannot express something, stop and surface the conflict (commit message / MR) rather than suppress it.
|
||||
|
||||
The same applies to the checks themselves: **never `git commit --no-verify`** (or otherwise skip a pre-commit / pre-push hook). The checks are fast and offline, so a redundant run is fine — bypassing a hook to get green is the identical anti-pattern. If a commit already skipped a hook, redo it through one: `git reset --soft HEAD~1 && git commit -C <skipped-sha>`.
|
||||
|
||||
Never start a long-lived / blocking process such as `npm run watch`. It runs until a human stops it with Ctrl-C, so in an agent turn it hangs forever and floods the context with continuous output. Reach for a one-shot command instead — `npm run test` (or `npm run check`) — to get feedback.
|
||||
|
||||
## Read these
|
||||
|
||||
- [CONTRIBUTING.md § Rules the tools don't enforce](./CONTRIBUTING.md#rules-the-tools-dont-enforce) — the constraints the linters don't catch; CI/review bounce these. **The most important section.**
|
||||
- [CONTRIBUTING.md § Testing discipline (type-driven)](./CONTRIBUTING.md#testing-discipline-type-driven) — write the `expectTypeOf` (type) before the `assert` (red); the types are the feature.
|
||||
- [CONTRIBUTING.md § Script prefix convention](./CONTRIBUTING.md#script-prefix-convention) — adding an `npm run` script? reuse an existing prefix or it doesn't belong.
|
||||
- [CONTRIBUTING.md § Commit messages](./CONTRIBUTING.md#commit-messages) — gitmoji + imperative + 50/72.
|
||||
- [CONTRIBUTING.md § Feedback tiers](./CONTRIBUTING.md#feedback-tiers) — what runs when and at what cost (`watch` / pre-commit / pre-push / `check` / `verify` / `fix` / `maintain` / CI).
|
||||
- [README.md § Tooling decisions](./README.md#tooling-decisions) — the rationale behind each tool choice; read before changing tooling.
|
||||
- [package.json `#scripts`](./package.json) — the source of truth for every command (the `LEFTHOOK_FILES` convention scopes them to staged files vs. the whole project).
|
||||
@@ -0,0 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
[Unreleased]: https://gitea.e1nsnull.de/tmu/tiny-pattern-ts
|
||||
@@ -0,0 +1,88 @@
|
||||
# Contributing
|
||||
|
||||
This document is for maintainers and contributors working on the project itself. End-user documentation is in [README.md](./README.md). The machine entry point for AI coding agents is [AGENTS.md](./AGENTS.md); keep this file as the prose home for the rules below so agents and humans don't diverge.
|
||||
|
||||
## Rules the tools don't enforce
|
||||
|
||||
CI and review will bounce these even though `npm run check` and the linters don't catch them. They're the high-frequency things a contributor (or an agent) reaches for by default:
|
||||
|
||||
- **Source imports use `.ts` extensions, never `.js`.** `node --strip-types` resolves the `.ts` form at test time; `rewriteRelativeImportExtensions` emits `.js` in `dist/`. "Pre-fixing" an import to `.js` breaks the inner loop. (rationale: README § Tooling decisions)
|
||||
- **A new `npm run` script must reuse an existing prefix** (`check:` / `fix:` / `test:` / `watch:` / `maintain:` / `publish:`). If none fits, that's a signal the script doesn't belong in the pipeline — not a reason to invent a new prefix. (see [Script prefix convention](#script-prefix-convention))
|
||||
- **`oxlint-disable` directives live in source, not `.oxlintrc.json`.** The trade-off must sit next to the code it silences. This is a _human_ last-resort convention; agents must not add these — see [AGENTS.md § Never do](./AGENTS.md#never-do). (rationale: README § Tooling decisions)
|
||||
- **Don't put slow / network / whole-project scans in `check` or pre-commit.** Advisory scans are not correctness gates; they belong under `maintain:`. (see [Feedback tiers](#feedback-tiers) and [Script prefix convention](#script-prefix-convention))
|
||||
- **There is no local `npm run publish`, and `publish:publint` / `publish:attw` don't go in `check`.** (see [Publishing workflow](#publishing-workflow))
|
||||
|
||||
## Commit messages
|
||||
|
||||
Gitmoji subject, imperative mood, 50/72 wrapping. The template is `commit-message-template`; run `npm run use:git-commit-message` once after cloning to register it as git's `commit.template`.
|
||||
|
||||
Examples from history: `:sparkles: Add watch tier with watch:test child`, `:recycle: Move type-aware config to .oxlintrc.json; use source-level disable directives`, `:memo: Restore unique maintainer content as CONTRIBUTING.md`. The body explains _what and why_, not _how_; link issues with `Resolves #...`.
|
||||
|
||||
## Script prefix convention
|
||||
|
||||
Script names in `package.json` 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; never modifies files. Aggregated by `npm run check`.
|
||||
- `fix:*` — mutating counterpart of a `check:*` script. Aggregated by `npm run fix`; the diff is the review surface.
|
||||
- `test:*` — test scripts. `test` is the canonical entry point (`check:tsc` + unit tests); `test:unit` skips the typecheck for fast local iteration; `test:ci` adds c8 coverage.
|
||||
- `watch:*` — long-running watchers for the manual inner dev loop. Aggregated by `watch`; currently a single child (`watch:test`), and a future `watch:oxlint` / `watch:tsc` would run concurrently under that umbrella.
|
||||
- `maintain:*` — advisory repo-maintenance scans: read-only, but whole-project and/or network-bound, so never a correctness gate. Aggregated by `npm run maintain`.
|
||||
- `publish:*` — validates the _publishable artifact_ (e.g. `dist/`) rather than the source, so it needs a fresh build. (see [Rules the tools don't enforce](#rules-the-tools-dont-enforce) and [Publishing workflow](#publishing-workflow))
|
||||
|
||||
A new script should pick the prefix that matches its lifecycle, not invent a new one. If no existing prefix fits, that's a signal the script doesn't belong in the standard pipeline.
|
||||
|
||||
Separately, some top-level scripts are **bare** (no prefix): the entry points that either run a single tool (`build`, `clean`) or aggregate a `prefix:*` family (`check`, `fix`, `test`, `watch`, `maintain`), plus `verify` — a cross-cutting convenience composing `check` + `test:unit` into one whole-project correctness gate. It deliberately uses `test:unit` rather than `test` because `check` already runs `check:tsc`, so the type checker runs exactly once. Bare commands are how you invoke a tier; the `prefix:*` scripts are what those tiers are made of.
|
||||
|
||||
## Feedback tiers
|
||||
|
||||
The tools are organized into a feedback ladder. Each tier catches different things at different costs; the rule of thumb is "earlier tiers fire more often, faster tiers catch less, slower tiers are more thorough":
|
||||
|
||||
| Tier | When | What it runs | Time |
|
||||
| -------------------------------- | ---------------------- | --------------------------------------------------------------------- | ----- |
|
||||
| `npm run watch` | manual | `watch:test` — re-runs tests on file save | ~0.1s |
|
||||
| Pre-commit (auto) | on stage | tsc + oxlint + oxfmt + cspell (staged files only) | ~1.3s |
|
||||
| Pre-push (auto) | on push | `npm test` (full tsc + unit tests) | ~3.5s |
|
||||
| `npm run check` | manual | Correctness gates: tsc + oxlint + oxfmt + cspell (whole project) | ~3s |
|
||||
| `npm run verify` | manual | Definition of done: `npm run check` + unit tests, one shot | ~6s |
|
||||
| `npm run fix` | manual | Auto-resolve fixable issues (lint, format) | ~3s |
|
||||
| `npm run maintain` | manual / CI (advisory) | `maintain:knip` + `maintain:outdated` (whole-project + network scans) | ~10s |
|
||||
| CI build (auto) | on push/PR | `npm run check` + `npm run test:ci` | ~30s+ |
|
||||
| CI maintain (auto, non-blocking) | on push/PR | `npm run maintain` — reports, never fails the build | ~10s |
|
||||
| CI publish (auto) | on tag | `publish:publint` + `publish:attw`, then `npm publish` | ~10s |
|
||||
|
||||
### Why these splits?
|
||||
|
||||
- **`watch:*` is a manual tier, not a hook.** The developer starts it on demand (it has to be killed with Ctrl-C) and it runs in a dedicated terminal pane. It sits as the earliest tier in the feedback ladder, catching failures the moment a file is saved — before staging, before commit.
|
||||
- **`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. The pre-commit `LEFTHOOK_FILES` convention doesn't apply to the test runner, so pre-commit isn't the right home. Pre-push runs after all commits are made but before the push leaves the machine, catching regressions that span multiple commits.
|
||||
|
||||
### Before pushing
|
||||
|
||||
Run `npm run verify` — the one-shot correctness gate in the table above. Run `npm run maintain` only on a maintenance / update-deps branch.
|
||||
|
||||
## Testing discipline (type-driven)
|
||||
|
||||
For this library the types _are_ the feature — narrowing, `exhaustive()` returns, the `Matcher<T>` contract — so a runtime-only test loop would verify the wrong thing. New behavior follows **type-driven development** (in Edwin Brady's sense): _treat the type as the plan for a program, and use the compiler and type checker as your assistant, guiding you to a complete program that satisfies the type_ ([idris-lang.org](https://www.idris-lang.org/)). Here that plan is the `expectTypeOf` assertion, written first. The loop is **type → red → green → refactor**:
|
||||
|
||||
1. **Type** — write the compile-time expectation first (`expectTypeOf(...).toEqualTypeOf<…>()`) and let `npm run check:tsc` fail on the _type_. The type error is the spec you want to hit before the runtime logic exists.
|
||||
2. **Red** — add the matching runtime assertion (`assert.*`) so `npm run test:unit` now fails on behavior.
|
||||
3. **Green** — implement in `src/*.ts` until both the type check and the test pass.
|
||||
4. **Refactor** — with the type system and the tests as the safety net, then `npm run verify` as the definition-of-done gate.
|
||||
|
||||
This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert.*` — keep them together. Type-first is also enforced structurally: `npm test` runs `check:tsc` before the test runner, so a wrong type can never be papered over by a passing assertion. Per [AGENTS.md § Never do](./AGENTS.md#never-do), reach green honestly — fix the types so both the type check and the runtime assertion pass, never suppress the ones you can't make pass.
|
||||
|
||||
## Publishing workflow
|
||||
|
||||
Publishing is CI-only by policy. Local `npm publish` is not supported.
|
||||
|
||||
1. Develop and merge PRs to `main`.
|
||||
2. CI runs `npm run check` + `npm run test:ci` on every push and PR — this is the authoritative gate.
|
||||
3. After all intended changes are on `main`, bump the version locally:
|
||||
```sh
|
||||
npm version <patch|minor|major>
|
||||
```
|
||||
4. Push the tag to the forge (Gitea):
|
||||
```sh
|
||||
git push --follow-tags origin main
|
||||
```
|
||||
5. The `publish` CI job runs on the tag: `build` → `publish:publint` → `publish:attw` → `npm publish --access public`. The publish-tier checks must pass before the artifact is published.
|
||||
@@ -6,20 +6,46 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
||||
|
||||
- **Build:** `npm run build`
|
||||
- **Test:** `npm run test`, `npm run test:ci`
|
||||
- **Watch:** `npm run watch`
|
||||
- **Checks:** `npm run check`, `npm run fix`
|
||||
- **Format/Fix:** `npm run fix:oxfmt`, `npm run fix:oxlint`
|
||||
- **Verify:** `npm run verify` — the definition of done
|
||||
- **Maintenance:** `npm run maintain` — advisory only
|
||||
- **Individual fixes:** `npm run fix:oxfmt`, `npm run fix:oxlint`
|
||||
|
||||
What each tier runs, when it fires and what it costs:
|
||||
[CONTRIBUTING.md § Feedback tiers](./CONTRIBUTING.md#feedback-tiers). How the
|
||||
`prefix:` in a script name is chosen:
|
||||
[§ Script prefix convention](./CONTRIBUTING.md#script-prefix-convention).
|
||||
|
||||
### Tooling
|
||||
|
||||
- **TypeScript 7** — type checker and build (`tsc`).
|
||||
- **node --test** + `--experimental-strip-types` — test runner (Node 22.6+, flag dropped on Node 24).
|
||||
- **c8** — code coverage for `test:ci`.
|
||||
- **oxlint** — Rust-based linter.
|
||||
- **oxlint** — Rust-based linter, with type-aware rules powered by **oxlint-tsgolint** (typescript-go).
|
||||
- **oxfmt** — Rust-based formatter (Prettier-compatible). Formats JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; built-in `package.json` key sorting replaces `sort-package-json`.
|
||||
- **cspell** — spell checking.
|
||||
- **publint** — validates `package.json` for ESM publishing correctness. Runs on publish only (in CI), not as part of `npm run check`.
|
||||
- **@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.
|
||||
- **knip** — finds unused dependencies, exports, and files.
|
||||
- **check-outdated** — reports dependencies behind the registry; it exits non-zero whenever _any_ dependency is outdated.
|
||||
- **publint** — validates `package.json` for ESM publishing correctness.
|
||||
- **@arethetypeswrong/cli** (`attw`) — validates `.d.ts` declarations against multiple module-resolution scenarios.
|
||||
- **lefthook** — git hooks.
|
||||
|
||||
Each tool's configuration trade-off is recorded in [Tooling decisions](#tooling-decisions); when it runs is in [CONTRIBUTING.md § Feedback tiers](./CONTRIBUTING.md#feedback-tiers).
|
||||
|
||||
### 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.
|
||||
- **`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).
|
||||
- **The pre-commit hook sets the `LEFTHOOK_FILES` env var** to the staged-files list, and the affected scripts use `${LEFTHOOK_FILES:-<default>}` 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
|
||||
|
||||
@@ -31,23 +57,9 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
||||
- TypeScript 7 is used via the `typescriptteam.native-preview` extension.
|
||||
- oxc extension provides oxlint squiggles and oxfmt format-on-save.
|
||||
|
||||
## Workflows
|
||||
## Contributing
|
||||
|
||||
- Version updates via `npm version`.
|
||||
- Publishing via GitHub Actions on tagged commits (see `.github/workflows/ci.yml`); the `publish` job runs `publish:publint` and `publish:attw` before `npm publish`.
|
||||
|
||||
## Script prefix convention
|
||||
|
||||
Script names follow a prefix convention that signals _when_ they run:
|
||||
|
||||
- `check:*` — read-only verification. Aggregated by `npm run check`. Used in pre-commit hooks and CI's build job.
|
||||
- `fix:*` — mutating counterpart of `check:*`. Run individually (no `npm run fix` aggregator by design — fixes should be intentional, not batched).
|
||||
- `test:*` — test scripts. `npm run test` runs the full suite; `test:unit` / `test:ci` are scope-specific variants.
|
||||
- `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.
|
||||
|
||||
## Contribution guidelines
|
||||
For maintainer and contributor docs — the script prefix convention, the feedback-tier system, the rules the tools don't enforce, and the publishing workflow — see [CONTRIBUTING.md](./CONTRIBUTING.md). AI coding agents: your entry point is [AGENTS.md](./AGENTS.md), which points back to CONTRIBUTING.md.
|
||||
|
||||
- Commit signing (GPG).
|
||||
- Set up commit message template: `npm run use:git-commit-message`.
|
||||
- See `commit-message-template`.
|
||||
- Type-only tests use `expect-type`'s `expectTypeOf(...)` inside `node --test` cases.
|
||||
+14
-3
@@ -2,8 +2,6 @@
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"tslib",
|
||||
"typefest",
|
||||
"lefthook",
|
||||
"oxlint",
|
||||
"oxfmt",
|
||||
@@ -18,7 +16,20 @@
|
||||
"msvc",
|
||||
"publint",
|
||||
"attw",
|
||||
"arethetypeswrong"
|
||||
"arethetypeswrong",
|
||||
"knip",
|
||||
"tsgolint",
|
||||
"gitea",
|
||||
"pubv",
|
||||
"knope",
|
||||
"runwisp",
|
||||
"glab",
|
||||
"postversion",
|
||||
"Zilla",
|
||||
"kacl",
|
||||
"bestikk",
|
||||
"silverwind",
|
||||
"idris"
|
||||
],
|
||||
"ignorePaths": ["dist", "node_modules", "coverage", "*.svg", ".gitignore"]
|
||||
}
|
||||
@@ -13,3 +13,9 @@ pre-commit:
|
||||
run: sh -c 'LEFTHOOK_FILES="$*" npm run check:cspell' sh {staged_files}
|
||||
typecheck:
|
||||
run: npm run check:tsc
|
||||
|
||||
pre-push:
|
||||
parallel: false
|
||||
commands:
|
||||
test:
|
||||
run: npm test
|
||||
Generated
+1435
-443
File diff suppressed because it is too large.
Load diff
+20
-23
@@ -10,13 +10,18 @@
|
||||
"pattern-matching",
|
||||
"typescript"
|
||||
],
|
||||
"homepage": "https://gitea.e1nsnull.de/tmu/tiny-pattern-ts#readme",
|
||||
"bugs": {
|
||||
"url": "https://gitea.e1nsnull.de/tmu/tiny-pattern-ts/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tmueller/tiny-pattern-ts.git"
|
||||
"url": "git+https://gitea.e1nsnull.de/tmu/tiny-pattern-ts.git"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
@@ -35,55 +40,47 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell && npm run check:outdated",
|
||||
"check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell",
|
||||
"check:cspell": "cspell lint ${LEFTHOOK_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",
|
||||
"check:oxfmt": "oxfmt --check ${LEFTHOOK_FILES:-.}",
|
||||
"check:oxlint": "oxlint ${LEFTHOOK_FILES:-src}",
|
||||
"check:tsc": "tsc",
|
||||
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
||||
"fix": "npm run fix:oxlint && npm run fix:oxfmt",
|
||||
"fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}",
|
||||
"fix:oxlint": "oxlint --fix src",
|
||||
"release": "./scripts/release.sh",
|
||||
"maintain": "npm run maintain:knip; npm run maintain:outdated",
|
||||
"maintain:knip": "knip --include dependencies,exports,files",
|
||||
"maintain:outdated": "check-outdated --ignore-pre-releases",
|
||||
"test": "npm run check:tsc && node --test --strip-types \"src/**/*.test.ts\"",
|
||||
"test:ci": "c8 --reporter=text --reporter=lcov --reporter=html node --test --strip-types \"src/**/*.test.ts\"",
|
||||
"test:unit": "node --test --strip-types \"src/**/*.test.ts\"",
|
||||
"verify": "npm run check && npm run test:unit",
|
||||
"watch": "npm run watch:test",
|
||||
"watch:test": "node --test --watch --strip-types \"src/**/*.test.ts\"",
|
||||
"publish:attw": "attw . --pack --profile esm-only",
|
||||
"publish:publint": "publint",
|
||||
"use:git-commit-message": "cp commit-message-template .git/COMMIT_EDITMSG || true"
|
||||
"use:git-commit-message": "git config commit.template commit-message-template"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.5",
|
||||
"@runwisp/pubv": "^1.5.1",
|
||||
"@tsconfig/node26": "^26.0.1",
|
||||
"@tsconfig/strictest": "^2.0.8",
|
||||
"@types/node": "^26.4.1",
|
||||
"c8": "^12.0.0",
|
||||
"check-outdated": "^3.0.0",
|
||||
"cspell": "^10.2.1",
|
||||
"cspell": "^10.2.2",
|
||||
"expect-type": "1.4.0",
|
||||
"knip": "^6.34.0",
|
||||
"lefthook": "^2.1.12",
|
||||
"oxfmt": "^0.66.0",
|
||||
"oxlint": "^1.81.0",
|
||||
"oxlint-tsgolint": "^7.0.2001",
|
||||
"publint": "^0.3.24",
|
||||
"tslib": "^2.8.1",
|
||||
"type-fest": "^5.9.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxfmt/binding-darwin-arm64": "^0.66.0",
|
||||
"@oxfmt/binding-darwin-x64": "^0.66.0",
|
||||
"@oxfmt/binding-linux-arm64-gnu": "^0.66.0",
|
||||
"@oxfmt/binding-linux-arm64-musl": "^0.66.0",
|
||||
"@oxfmt/binding-linux-x64-gnu": "^0.66.0",
|
||||
"@oxfmt/binding-linux-x64-musl": "^0.66.0",
|
||||
"@oxfmt/binding-win32-x64-msvc": "^0.66.0",
|
||||
"@oxlint/binding-darwin-arm64": "^1.81.0",
|
||||
"@oxlint/binding-darwin-x64": "^1.81.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "^1.81.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "^1.81.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "^1.81.0",
|
||||
"@oxlint/binding-linux-x64-musl": "^1.81.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "^1.81.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=26"
|
||||
},
|
||||
|
||||
@@ -1,529 +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. Inline in `tsconfig.json` (not
|
||||
via `@tsconfig/strictest`). The rule set is: `strict`, `noImplicitAny`,
|
||||
`noImplicitThis`, `alwaysStrict`, `strictNullChecks`, `strictFunctionTypes`,
|
||||
`strictBindCallApply`, `strictPropertyInitialization`, `noImplicitReturns`,
|
||||
`noFallthroughCasesInSwitch`, `noUncheckedIndexedAccess`, `noImplicitOverride`,
|
||||
`noUnusedLocals`, `noUnusedParameters`, `forceConsistentCasingInFileNames`,
|
||||
`isolatedModules`, `verbatimModuleSyntax`.
|
||||
- **EditorConfig**: Use `.editorconfig` from typescript-lib-starter-tiny.
|
||||
- **oxfmt**: Rust-based formatter, Prettier-compatible. Replaces Prettier.
|
||||
Config in `.oxfmtrc.json` (same shape as `.prettierrc`).
|
||||
- **oxlint**: Rust-based linter. Replaces ESLint. Config in `.oxlintrc.json`
|
||||
with `typescript`, `unicorn`, `oxc`, `import` plugins. Categories enabled
|
||||
as errors: `correctness`, `suspicious`, `restriction`. As warnings: `perf`,
|
||||
`style`. `nursery` is off.
|
||||
- **oxlint rules disabled by design** (in `.oxlintrc.json`):
|
||||
- `eslint/no-undefined` — we use `undefined` as the no-match sentinel.
|
||||
- `eslint/sort-keys` — handler/case order is semantic, not alphabetical.
|
||||
- `eslint/id-length` — `T`, `R`, `U`, `V` are standard TS generics.
|
||||
- `import/no-named-export` — false positive on library entry re-exports.
|
||||
- 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: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. oxlint only understands JS/TS-family languages, so config
|
||||
files (JSON/YAML/Markdown) are intentionally outside its scope;
|
||||
they are checked only by oxfmt.
|
||||
- `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.
|
||||
|
||||
### 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. See `lefthook.yml`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Repository & CI/CD
|
||||
|
||||
- **Repository**: Hosted on GitHub.
|
||||
- **Build Pipeline**: GitHub Actions (`.github/workflows/ci.yml`).
|
||||
- `build` job on push and pull_request to `main` and on
|
||||
`workflow_dispatch`. Steps: `actions/checkout@v4`,
|
||||
`actions/setup-node@v4` (with `node-version-file: .node-version`,
|
||||
`cache: npm`), `npm ci`, `npm run build`, `npm run check`,
|
||||
`npm run test:ci`, then upload `coverage/` as an artifact.
|
||||
- `publish` job: only on `refs/tags/*`, depends on `build`. Steps:
|
||||
`actions/checkout@v4`, `actions/setup-node@v4` (with
|
||||
`node-version-file: .node-version` and `registry-url`),
|
||||
`npm ci`, `npm run build`, `npm 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.
|
||||
|
||||
## 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.
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
# Release front-door. Run as `npm run release`.
|
||||
#
|
||||
# How we got here (short): we want hand-written Keep-a-Changelog notes, an
|
||||
# [Unreleased] -> "## [x.y.z] - DATE" graduation, and a tag that marks the exact
|
||||
# commit on main that gets published. No single tool did BOTH the [Unreleased]
|
||||
# graduation AND the package.json bump. So split by strength: `pubv` (tiny,
|
||||
# changelog-driven) owns preflight + the interactive major/minor/patch heuristic
|
||||
# + graduating/committing CHANGELOG.md (no tag, no push); `npm version` syncs
|
||||
# package.json + the lockfile; `--amend` folds them into pubv's single commit;
|
||||
# tag AFTER the amend (so the tag is never orphaned) and push.
|
||||
#
|
||||
# Rejected: the conventional-commits family (our history is gitmoji, not
|
||||
# Conventional; and we want hand-written notes); changesets/rtk (config + a
|
||||
# heavier version/publish flow that fights our CI-only publish); knope/kacl/
|
||||
# bestikk (changelog-only — don't bump package.json; plus 5yr/2yr/brand-new
|
||||
# maintenance); pubv alone (verified it never writes package.json). We also
|
||||
# tried `versions` (silverwind) — great Gitea support — but pairing it with a
|
||||
# hand-rolled promote became a ~180-line script we'd have to maintain, which is
|
||||
# exactly what this ~30-line version replaces.
|
||||
|
||||
CHANGELOG="CHANGELOG.md"
|
||||
|
||||
if ! command -v code >/dev/null 2>&1; then
|
||||
echo "Error: 'code' (VS Code CLI) not found; install it or remove the editor step." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running pubv..."
|
||||
pubv --no-tag --no-push --tag-prefix=none
|
||||
|
||||
echo "Opening ${CHANGELOG} in VS Code..."
|
||||
code --wait "${CHANGELOG}"
|
||||
|
||||
echo "Reading version from ${CHANGELOG}..."
|
||||
|
||||
VERSION=$(
|
||||
sed -nE 's/^## \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/p' "${CHANGELOG}" |
|
||||
head -n 1
|
||||
)
|
||||
|
||||
if [ -z "${VERSION}" ]; then
|
||||
echo "Error: Could not determine release version from ${CHANGELOG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release version: ${VERSION}"
|
||||
|
||||
echo "Updating package.json and package-lock.json..."
|
||||
npm version "${VERSION}" --no-git-tag-version
|
||||
|
||||
echo "Amending release commit..."
|
||||
git add package.json package-lock.json "${CHANGELOG}"
|
||||
git commit --amend -m ":bookmark: Release ${VERSION}"
|
||||
|
||||
echo "Creating tag ${VERSION}..."
|
||||
git tag "${VERSION}"
|
||||
|
||||
echo "Pushing release..."
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
echo "Release ${VERSION} completed."
|
||||
+4
-3
@@ -1,3 +1,4 @@
|
||||
/* oxlint-disable typescript/no-floating-promises -- expectTypeOf() is a sync type-assertion library that the type-aware linter misidentifies as a promise */
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "node:test";
|
||||
|
||||
@@ -27,7 +28,7 @@ test("P.type narrows to the typeof target", () => {
|
||||
});
|
||||
|
||||
test("exhaustive() returns the union of handler return types", () => {
|
||||
const result = match<"a" | "b">("a" as "a" | "b")
|
||||
const result = match<"a" | "b">("a")
|
||||
.with(P.literal("a"), () => 1 as const)
|
||||
.with(P.literal("b"), () => "two" as const)
|
||||
.exhaustive();
|
||||
@@ -37,7 +38,7 @@ test("exhaustive() returns the union of handler return types", () => {
|
||||
});
|
||||
|
||||
test("otherwise() falls back when no case matches", () => {
|
||||
const result = match<"x" | "y" | "z">("z" as "x" | "y" | "z")
|
||||
const result = match<"x" | "y" | "z">("z")
|
||||
.with(P.literal("x"), (v): string => `got ${v}`)
|
||||
.otherwise((v): string => `fallback ${v}`);
|
||||
assert.equal(result, "fallback z");
|
||||
@@ -46,7 +47,7 @@ test("otherwise() falls back when no case matches", () => {
|
||||
test("exhaustive throws when no case matches", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
match<"a" | "b" | "c">("c" as "a" | "b" | "c")
|
||||
match<"a" | "b" | "c">("c")
|
||||
.with(P.literal("a"), () => "A")
|
||||
.with(P.literal("b"), () => "B")
|
||||
.exhaustive(),
|
||||
|
||||
+4
-3
@@ -14,7 +14,7 @@ interface MatchBuilder<T, R> {
|
||||
const buildMatch = <T, R>(value: T, cases: Cases<R>): MatchBuilder<T, R> => {
|
||||
const apply = (): R | undefined => {
|
||||
for (const [matcher, handler] of cases) {
|
||||
if (matcher.matches(value as unknown)) {
|
||||
if (matcher.matches(value)) {
|
||||
return handler(value);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const buildMatch = <T, R>(value: T, cases: Cases<R>): MatchBuilder<T, R> => {
|
||||
): MatchBuilder<T, R | V> {
|
||||
const nextCases: Cases<R | V> = [
|
||||
...cases,
|
||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||
[pattern, handler as (value: unknown) => R | V],
|
||||
];
|
||||
return buildMatch(value, nextCases);
|
||||
@@ -43,8 +44,8 @@ const buildMatch = <T, R>(value: T, cases: Cases<R>): MatchBuilder<T, R> => {
|
||||
},
|
||||
otherwise(handler: (value: T) => R): R {
|
||||
for (const [matcher, run] of cases) {
|
||||
if (matcher.matches(value as unknown)) {
|
||||
return run(value) as R;
|
||||
if (matcher.matches(value)) {
|
||||
return run(value);
|
||||
}
|
||||
}
|
||||
return handler(value);
|
||||
|
||||
@@ -53,12 +53,16 @@ const typeMatcher = <T>(
|
||||
typeof expected === "object" &&
|
||||
expected !== null &&
|
||||
"matches" in expected,
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters
|
||||
keysMatch = <S extends object>(shape: S, candidate: object): boolean => {
|
||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||
for (const key of Object.keys(shape) as (keyof S)[]) {
|
||||
if (!(key in candidate)) {
|
||||
return false;
|
||||
}
|
||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||
const expected = shape[key],
|
||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||
actual = candidate[key as keyof object];
|
||||
if (isNestedMatcher(expected)) {
|
||||
if (!expected.matches(actual)) {
|
||||
@@ -78,6 +82,7 @@ const typeMatcher = <T>(
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
||||
const candidate = value as S;
|
||||
if (!keysMatch(shape, candidate)) {
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user