Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .ast-grep/rules/no-file-comment.yml

This file was deleted.

12 changes: 0 additions & 12 deletions .ast-grep/rules/no-function-declaration.yml

This file was deleted.

8 changes: 0 additions & 8 deletions .ast-grep/rules/no-inline-export.yml

This file was deleted.

106 changes: 0 additions & 106 deletions .ast-grep/rules/strip-braces.yml

This file was deleted.

43 changes: 29 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ pnpm build && pnpm lint && pnpm test

All three must pass with zero errors.

Run tooling through `pnpm <script>`, never by invoking a binary in
`node_modules/.bin` directly — the scripts rely on pnpm putting that
directory on `PATH` and on `scriptShell: bash` from
`pnpm-workspace.yaml`. When adding a script, call the binary by bare
name (`biome check .`); do not wrap it in `pnpm` or `pnpm exec`, which
re-resolves it through a Windows `.CMD` shim that AppLocker-managed
hosts block. On Windows, drive the toolchain from Git Bash.
## Toolchain

Lint, format, and environment checks come from
[ts-canon](https://github.com/SynthLuvr/ts-canon) — one devDependency
that bundles biome, oxlint (tsgolint), the ast-grep rules,
convert-to-arrow, jscpd, and the pandoc/peer-deps/audit helpers.

- Run tooling through `pnpm <script>` (`pnpm lint`, `pnpm format`), not
by invoking binaries in `node_modules/.bin` directly.
- `ts-canon lint` / `ts-canon format` accept path arguments and `--fast`
(skips `pnpm audit` and jscpd).
- `pnpm exec ts-canon doctor` diagnoses toolchain/environment problems.
- Markdown files must be byte-identical to `pandoc --eol=lf -t gfm`
output; fix drift with `pnpm format`.
- Tool, rule, and preset changes belong in ts-canon — bump its version
in `package.json` to pick them up. Do not add per-step tool scripts
back here.

## Coding Conventions (Enforced)

Expand Down Expand Up @@ -80,7 +90,7 @@ export { foo };

``` ts
// ❌ Wrong
if (done)
if (done) {
return;
}

Expand All @@ -106,15 +116,20 @@ If the linter complains about formatting, run:
pnpm format
```

This runs four steps in order: 1. `convert-to-arrow` — rewrites
`function` declarations to arrow consts 2. `strip-braces` — removes
unnecessary braces from single-statement blocks 3. `biome format` —
formats all files 4. `biome check` — applies lint auto-fixes
This runs five steps in order (via `ts-canon format`):

1. `convert-to-arrow` — rewrites `function` declarations to arrow
consts
2. strip-braces — removes unnecessary braces from single-statement
blocks
3. `biome format` — formats all files
4. `biome check` — applies lint auto-fixes
5. `pandoc` — normalizes markdown to GFM

## Project Structure

- Source code lives in `src/`
- Tests live in `src/tests/` (filenames end in `.test.ts`)
- TypeScript is type-check only (`noEmit: true`) — use `tsx` for dev
execution
- TypeScript is type-check only (`noEmit: true`) — bundled tooling runs
TypeScript sources via tsx inside ts-canon
- ESM only (`"type": "module"`)
114 changes: 61 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@ A minimal TypeScript project template with a complete build, format,
lint, and test toolchain. The code does nothing useful — it’s a starting
point for new projects.

The lint/format toolchain lives in
[ts-canon](https://github.com/SynthLuvr/ts-canon): one devDependency
that bundles biome, oxlint (+ tsgolint), ast-grep rules,
convert-to-arrow, jscpd, and the pandoc/peer-dep/audit helpers, and
ships the canonical biome, tsconfig, and vitest presets. This repo keeps
only `typescript` and the vitest packages as direct devDependencies.

## Tech Stack

| Tool | Purpose |
|----|----|
| [pnpm](https://pnpm.io) | Package manager |
| [ts-canon](https://github.com/SynthLuvr/ts-canon) | The shared lint/format/doctor toolchain |
| [TypeScript](https://www.typescriptlang.org) | Type checking (`tsc --noEmit`) |
| [Biome](https://biomejs.dev) | Primary formatter and linter |
| [oxlint](https://oxc.rs/docs/guide/usage/linter) | Secondary type-aware linter |
| [ast-grep](https://ast-grep.github.io) | Structural lint/format rules |
| [convert-to-arrow](https://github.com/chimurai/convert-to-arrow) | Codemod: `function` → arrow consts |
| [Vitest](https://vitest.dev) | Test runner (unit + integration) |
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) | Coverage provider + 80% threshold |
| [jscpd](https://github.com/kucherenko/jscpd) | Code-duplication detection |
| [tsx](https://github.com/privatenumber/tsx) | Dev-time TypeScript execution |
| [npm-run-all2](https://github.com/bcomnes/npm-run-all2) | Orchestrates multi-step scripts |
| [pandoc](https://pandoc.org) | Markdown formatter (GFM) |
| [pandoc](https://pandoc.org) | Markdown formatter (GFM); system dependency |

Bundled inside ts-canon: Biome (primary formatter/linter), oxlint
(secondary type-aware linter), ast-grep (structural rules),
convert-to-arrow (`function` → arrow codemod), and jscpd
(code-duplication detection).

## Prerequisites

- [Node.js](https://nodejs.org) 26 and [pnpm](https://pnpm.io) (enforced
via `engines`; `.node-version` pins the major for fnm/nvm/asdf)
- [pandoc](https://pandoc.org) ≥ 3.10 — required by `pnpm lint:md` /
`pnpm format:md`
- [pandoc](https://pandoc.org) ≥ 3.10 — required by the markdown
lint/format steps; `pnpm exec ts-canon doctor` verifies it
- `bash` on `PATH` — pnpm runs scripts through it (see [Windows
notes](#windows-notes)); Git for Windows provides it

Expand All @@ -40,18 +46,15 @@ On Windows, pnpm installs three shims per binary in `node_modules/.bin`:
a POSIX shell script, a `.CMD` batch file, and a `.ps1`. Managed Windows
hosts commonly enforce an AppLocker script policy that blocks `.cmd` and
`.ps1` files under the user profile, which takes out every `.CMD` shim —
and `cmd.exe` is the shell pnpm reaches for by default. Three settings
keep the toolchain off that path:

- `scriptShell: bash` in `pnpm-workspace.yaml` — scripts run under bash,
which resolves the extensionless POSIX shims instead of the `.CMD`
ones. On Linux and macOS this is what already happens.
- Scripts call binaries by bare name (`biome check .`) rather than
`pnpm biome check .`. pnpm already puts `node_modules/.bin` on `PATH`;
routing back through `pnpm exec` re-resolves the binary to its `.CMD`
shim, bypassing the shell entirely.
- `scripts/oxlint.mts` and `scripts/peer-deps.mts` resolve tsgolint and
pnpm to real paths, so neither a shim nor a shell gets involved.
and `cmd.exe` is the shell pnpm reaches for by default.

ts-canon sidesteps this: it resolves every bundled tool from
`node_modules` by absolute path — the tool’s real entry point, read from
its package `bin` field — and spawns it directly, never through a shell
or a shim. pnpm itself is spawned via `npm_execpath`, and tsgolint is
handed to oxlint via `OXLINT_TSGOLINT_PATH`. `scriptShell: bash` in
`pnpm-workspace.yaml` keeps the remaining `pnpm <script>` invocations
off the `.CMD` shims.

pnpm itself is a shim too, so on such a host drive the toolchain from
Git Bash rather than PowerShell.
Expand All @@ -74,32 +77,38 @@ pnpm test # run unit tests (coverage gated)

### Lint

The `lint` script runs all linters in sequence via `npm-run-all`:
`pnpm lint` runs `ts-canon lint`, which runs every check in order and
fails fast on the first non-zero step:

1. Biome check (format + lint + import order)
2. oxlint with type-aware rules (tsgolint), warnings denied
3. ast-grep: no inline exports, no function declarations, no leading
file comments
4. pandoc: markdown must be GFM-formatted
5. `pnpm peers check` (skipped without a lockfile)
6. `pnpm audit --prod` (skipped with `--fast`)
7. jscpd: code duplication, 5% threshold (skipped with `--fast`)

| Script | Description |
|------------------------|-------------------------------------------|
| `pnpm lint` | Run all lint steps |
| `pnpm lint:biome` | Biome check: format + lint + import order |
| `pnpm lint:oxlint` | oxlint with type-aware rules (tsgolint) |
| `pnpm lint:exports` | ast-grep: no inline exports |
| `pnpm lint:functions` | ast-grep: no function declarations |
| `pnpm lint:md` | pandoc: Markdown must be GFM-formatted |
| `pnpm lint:peer-deps` | pnpm: no peer dependency conflicts |
| `pnpm lint:audit` | pnpm audit: production dependency vulns |
| `pnpm lint:duplicates` | jscpd: code duplication (5% threshold) |
Both `lint` and `format` accept path arguments, e.g.
`pnpm lint -- packages/foo`, and `--fast` skips the slow audit/jscpd
steps.

### Format

The `format` script runs all formatters in sequence:
`pnpm format` runs `ts-canon format`, which runs every formatter in
order (each step’s output is the next step’s input):

| Script | Description |
|----|----|
| `pnpm format` | Run all format steps |
| `pnpm format:arrows` | `convert-to-arrow` — rewrite `function` to arrow consts |
| `pnpm format:braces` | ast-grep strip single-statement braces |
| `pnpm format:biome` | Biome format with auto-fix |
| `pnpm format:check` | Biome check (lint + format auto-fix) |
| `pnpm format:md` | pandoc: reformat Markdown to canonical GFM |
1. `convert-to-arrow` — rewrite `function` declarations to arrow consts
2. ast-grep strip single-statement braces
3. Biome format
4. Biome check (lint + format auto-fix)
5. pandoc — reformat markdown to canonical GFM

### Doctor

`pnpm exec ts-canon doctor` verifies the environment: pandoc (≥ 3.10),
node (≥ 24), pnpm, the bundled tools, and the project-local
`typescript`.

### Test

Expand All @@ -122,25 +131,24 @@ These are **enforced** by the toolchain, not just preferences:
- **Single-statement brace stripping** — `if`/`for`/`while` with one
body line drop braces (ast-grep rule)
- **Double quotes**, 2-space indent, 80-char width, trailing commas,
semicolons (Biome)
semicolons (Biome preset)
- **ESM only** (`"type": "module"`)
- **Markdown via pandoc** — all `.md` formatted with `pandoc -t gfm`
(`lint:md`/`format:md`)

## Project Structure

├── .ast-grep/rules/ # Structural lint/format rules
├── .github/workflows/ # CI
├── scripts/
│ ├── oxlint.mts # oxlint runner (resolves tsgolint)
│ ├── pandoc-md.mts # Markdown lint/format via pandoc
│ └── peer-deps.mts # Peer dependency check
├── src/
│ ├── index.ts # Trivial module (replace with your code)
│ └── tests/ # Unit and integration tests
├── biome.json # Biome formatter + linter config
├── biome.json # Extends ts-canon/presets/biome.preset.json
├── .node-version # Node major, for fnm/nvm/asdf
├── .oxlintrc.json # oxlint type-aware rules
├── pnpm-workspace.yaml # pnpm settings (incl. scriptShell)
├── tsconfig.json # TypeScript config
└── vitest.config.ts # Test config
├── sgconfig.yml # Points ast-grep at ts-canon's rules
├── tsconfig.json # Extends ts-canon/presets/tsconfig.base.json
└── vitest.config.ts # Test config (mirrors ts-canon's preset)

Rule/config changes happen in ts-canon, not here: bump the ts-canon
version and the whole toolchain moves together. A local `biome.json`
that `extends` the preset can override anything repo-specific.
Loading