Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ bun.lockb
# -------------------------
.playwright-mcp/

# Personal mise overrides (mise.toml itself is checked in)
mise.local.toml
mise.*.local.toml

# -------------------------
# AI Agent Config (Generated)
# -------------------------
Expand Down
17 changes: 16 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ You’ll need the following installed:
- [Node.js](https://nodejs.org/en/download) (≥ 22.19.0)
- [Git](https://git-scm.com/downloads)
- [PNPM](https://pnpm.io/installation) (≥ 10.17.0)
- [Volta](https://docs.volta.sh/guide) or [NVM](https://github.com/nvm-sh/nvm) (we recommend Volta for automatic Node management)
- [mise](https://mise.jdx.dev), or [NVM](https://github.com/nvm-sh/nvm)
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (optional, for AI-assisted development)

> [!TIP]
> PNPM will automatically use the correct Node version when running scripts.
> If you prefer NVM: after installing it, simply run `nvm use` in the repo root.
> If you prefer mise: see [Using mise](#-using-mise-optional) — it manages pnpm for you too.

### ⬇️ Fork & Clone

Expand Down Expand Up @@ -73,6 +74,20 @@ pnpm build:packages
> If imports like `react` are not resolving, set your TS version to the workspace one:
> `CMD/CTRL + Shift + P` → `TypeScript: Select TypeScript Version` → _Use Workspace Version_.

### 🧰 Using mise (optional)

[mise](https://mise.jdx.dev) is an alternative to Volta/NVM that manages **both** Node and pnpm from the checked-in [`mise.toml`](./mise.toml). It is entirely optional — no repo script or CI job depends on it, and the version pins it reads (`.nvmrc` for Node, `packageManager` for pnpm) are the same ones every other contributor uses.

```sh
mise trust # once per clone; mise only reads configs you've trusted
mise install # provisions Node (from .nvmrc) and pnpm
mise run setup # pnpm install + pnpm build:packages
```

`mise.toml` also puts the workspace's `node_modules/.bin` on `PATH`, so `biome`, `turbo`, and `tsgo` can be run directly rather than through `pnpm exec`.

Everything else stays as documented below: use the `pnpm` scripts, not mise tasks. Personal additions — extra tools, environment variables — belong in a gitignored `mise.local.toml` or `.env.local`, never in the shared config.

### 🏗 Building & Development

To run the workspace in development mode:
Expand Down
56 changes: 56 additions & 0 deletions build/scripts/check-workspace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* 8. i18n locales — tag lists match locale files and generated stubs
* 9. Agent context — portable skill metadata, compatibility imports, and budgets
* 10. Internal records — organized design docs, frontmatter, and lifecycle status
* 11. mise tool pins — optional mise.toml agrees with the canonical version pins
*/
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
import { dirname, join, resolve, sep } from 'node:path';
Expand Down Expand Up @@ -818,6 +819,60 @@ function checkInternalRecords() {
return { ok: warnings.length === 0, warnings };
}

// ── Check 11: mise tool pins ────────────────────────────────────────────────

/** Returns the body of a top-level TOML table, or null when it is absent. */
function tomlTable(text, name) {
const lines = text.split(/\r?\n/);
const start = lines.findIndex((line) => line.trim() === `[${name}]`);
if (start === -1) return null;

const body = lines.slice(start + 1);
const end = body.findIndex((line) => /^\s*\[/.test(line));
return (end === -1 ? body : body.slice(0, end)).join('\n');
}

/**
* `mise.toml` is optional contributor convenience, so this check is a no-op
* without it. When present, its pnpm pin must match `packageManager` — mise
* users would otherwise silently run a different pnpm than CI. Node stays out
* of `[tools]` on purpose: mise reads `.nvmrc`/`.node-version`, keeping one
* Node pin shared with nvm, Volta, and `actions/setup-node`.
*/
function checkMiseToolPins() {
const warnings = [];
const misePath = join(ROOT, 'mise.toml');
if (!existsSync(misePath)) return { ok: true, warnings };

const miseText = readText(misePath);
const tools = tomlTable(miseText, 'tools');
const settings = tomlTable(miseText, 'settings');

const expected = readJson(join(ROOT, 'package.json')).packageManager?.match(/^pnpm@(.+)$/)?.[1];
const pinned = tools?.match(/^\s*pnpm\s*=\s*["']([^"']+)["']/m)?.[1];

if (!expected) {
warnings.push('package.json: "packageManager" must pin a pnpm version');
} else if (pinned !== expected) {
warnings.push(
`mise.toml: [tools] pnpm should be "${expected}" to match packageManager (got: ${pinned ?? 'missing'})`
);
}

if (tools && /^\s*node\s*=/m.test(tools)) {
warnings.push(
'mise.toml: drop the [tools] node pin — .nvmrc/.node-version is the single Node pin shared with nvm, Volta, and CI'
);
}

// Without the opt-in, mise ignores the Node version files and pins no Node.
if (!/idiomatic_version_file_enable_tools\s*=\s*\[[^\]]*["']node["']/.test(settings ?? '')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settings check matches comments

Medium Severity

The idiomatic-version-file check matches the assignment anywhere in the [settings] body, including comments. Commenting out idiomatic_version_file_enable_tools = ["node"] still satisfies the regex, so check:workspace can pass while mise users get no Node pin.

Suggested change
if (!/idiomatic_version_file_enable_tools\s*=\s*\[[^\]]*["']node["']/.test(settings ?? '')) {
if (!/^\s*idiomatic_version_file_enable_tools\s*=\s*\[[^\]]*["']node["']/m.test(settings ?? '')) {
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 286e914. Configure here.

warnings.push('mise.toml: [settings] idiomatic_version_file_enable_tools must include "node" so .nvmrc is honored');
}

return { ok: warnings.length === 0, warnings };
}

// ── Main ────────────────────────────────────────────────────────────────────

const checks = [
Expand All @@ -831,6 +886,7 @@ const checks = [
{ name: 'i18n locales', fn: checkI18nLocales },
{ name: 'Agent context', fn: checkAgentContext },
{ name: 'Internal records', fn: checkInternalRecords },
{ name: 'mise tool pins', fn: checkMiseToolPins },
];

let failed = 0;
Expand Down
30 changes: 30 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Optional convenience for contributors who use mise (https://mise.jdx.dev).
#
# mise is not required. `.node-version`/`.nvmrc` (Node) and `packageManager`
# (pnpm) stay the source of truth, so nvm, Volta, and CI never drift from this
# file. See "Using mise" in CONTRIBUTING.md.
min_version = "2025.1.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect mise min_version floor

Low Severity

min_version is set to 2025.1.0, but idiomatic_version_file_enable_tools only exists since mise 2025.4.6. Versions in between pass the floor, warn on an unknown field, and never get the loud failure this pin is meant to provide.

Suggested change
min_version = "2025.1.0"
min_version = "2025.4.6"
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 286e914. Configure here.


[settings]
# Resolve Node from `.nvmrc`/`.node-version` instead of duplicating the pin.
# Idiomatic version files are opt-in per project, and older mise releases spell
# this setting differently — `min_version` above turns that into a loud error
# rather than a silently unpinned Node.
idiomatic_version_file_enable_tools = ["node"]

[tools]
# Mirrors `packageManager` in package.json; `pnpm check:workspace` enforces it.
# Node is deliberately absent — see `[settings]` above.
pnpm = "11.17.0"

[env]
# Expose workspace binaries (biome, turbo, tsgo, prettier, tsx) directly, so
# they can be invoked without a `pnpm exec` prefix.
_.path = ["{{config_root}}/node_modules/.bin"]

# Personal, gitignored overrides. Skipped when the file does not exist.
_.file = { path = ".env.local", redact = true }

[tasks.setup]
description = "Install dependencies and build workspace packages"
run = ["pnpm install", "pnpm build:packages"]
Loading