This repo is a pnpm workspace and uses Moon as the task runner.
- Repo tasks are run via Moon (not
package.jsonscripts). The only root script isprepare(Husky). pnpmis for dependency management only. Never use it as a prefix to run another Node binary (nopnpm exec, nopnpm <tool>, etc.). The only exception is inside Husky hooks (for example.husky/pre-commit).- Do not bypass pnpm's
minimumReleaseAgepolicy. When adding or updating dependencies, choose versions that satisfy the policy inpnpm-workspace.yaml. - Bun is BANNED in this repository. NEVER use it (use
tsxfor running TypeScript scripts).
Use Conventional Commits for both commit messages and PR titles: https://www.conventionalcommits.org/en/v1.0.0/
Format:
<type>(<scope>)!?: <description>
- The
scopeshould be a package name, minus any npm scope (like @jsx-email). Scope for@jsx-email/plugin-minifywould beplugin-minify- If the change is not tied to a project, use
chore(repo): .... repois reserved for repo-wide changes only and must not be combined with other scopes (avoidfeat(svc-api,repo): ...).- If the change affects multiple projects, list them in
scopeseparated by commas with no spaces (for examplefeat(svc-api,web-admin): ...).
- If the change is not tied to a project, use
- Add
!after the scope for breaking changes (for examplefeat(svc-api)!: ...). - Keep the description succinct and direct.
- If a Linear issue is associated, end the description with a period and append the issue ID(s).
- Single:
fix(svc-api): handle invalid tokens. JUR-21 - Multiple:
fix(svc-api): handle invalid tokens. JUR-21,JUR-22
- Single:
- In PR bodies, use
Resolves(notRefs) when preceding a Linear issue link (for exampleResolves https://linear.app/jurymax/issue/JUR-21).
- Push early and often to the branch you’re working on (especially in an ephemeral devbox), so your work is always backed up.
Prefer TypeScript over plain JavaScript for scripts written as code files, and prefer tsx for
running scripts written in TypeScript or JavaScript. This does not mean Bash scripts are
forbidden; use the best tool for the job.
"scripts" are defined as code files intended to be run standalone and typically located in a
scripts/ directory. "scripts" does not mean Moon commands or Bash commands.
Adhoc browser automation commands are forbidden. Do not run inline Playwright, Puppeteer, Chrome,
or other browser scripts with node -e, heredocs, shell one-liners, or temporary files outside the
repo. Browser automation must live in a committed TypeScript script under scripts/agents/ and be
run with tsx.
- The preview app lives in
apps/preview/app/src; keep app code there and mirror tests underapps/preview/app/test. - The packed preview is built by
apps/preview/scripts/pack-preview.ts, which concatenates an explicitsourceFileslist and strips local imports. When adding, moving, or importing preview source files that must run in the packed app, update that list and theimportHeaderin the pack script for any external imports/types those files need. - After changing preview source used by the packed app, run
./node_modules/.bin/moon run app-preview:packand the smoke test./node_modules/.bin/moon run smoke-v2:run. - Do not rely only on the Vite dev app checks for preview changes; smoke tests exercise the packed preview path used by the CLI.
Run checks before considering work complete.
moon run :lint
moon run :typecheck
moon run :testIf moon isn’t available on your PATH, run it via ./node_modules/.bin/moon (for example ./node_modules/.bin/moon run :lint).
There is no repo-wide Moon format task. Format changed files directly with ./node_modules/.bin/oxfmt <files...> before the final check pass.
moon repo:build.all --cache off is the canonical task to use for building (compiling) all packages.
Do not run :compile tasks directly. :build tasks can be called on projects outside of packages.
If a test fails because a workspace package entry cannot be resolved, check whether that package needs to be built first. Fix the Moon dependency graph; do not remove or weaken the test.
- Workspace config lives in
.moon/workspace.yml. - Shared tasks live in
.moon/tasks.ymland are inherited by all projects. - Add a project
moon.ymlonly when you need to override or extend inherited tasks. - Moon docs: https://moonrepo.dev/docs/config
- Workspace packages are declared in
pnpm-workspace.yaml, with a single lockfile atpnpm-lock.yaml. - Tooling that’s shared across the repo should live in the root
package.json. - Project
package.jsonfiles should only include what that project needs. - Use
workspace:for internal package dependencies. - Prefer the pnpm
catalog:protocol for shared third-party versions.
Husky is enabled via the root prepare script. The pre-commit hook runs
lint-staged, which runs oxlint and oxfmt on staged files.
- Keep source files to ~200 lines max
- If a file is pushing past ~200 lines, split it by concern (extract helpers/types, or move discrete features into separate modules).
- Test files and
.astrocomponents are exempt from this limit. - Markdown, MDX and CSS files are exempt
- Constant variable names should use
camelCase(for exampleconst myVar = null, notconst MY_VAR = null) - Prefer a
helpersdirectory/files overutils - Never use a
libdirectory for app code - Filenames are
kebab-casefor files you control - Prefer arrow functions
const f = () => {}vsfunction f {}where scope ofthisis not a concern - Parent-path imports outside of the current/affected project directory are forbidden
- Tests live in a
test/directory at the same level assrc/. - Tests should never exist within
src/. - All new files, bug fixes, and new features must include unit and integration test coverage.
- This is a requirement for a PR to be merged.
- The directory structure under
test/should mirrorsrc/.- If
src/server.tshas tests created for it, that test file should betest/server.test.ts. - If
src/routes/myroute.tshas tests created for it, that test file should betest/routes/myroute.test.ts.
- If
- Snapshots should live within
test/.snapshots/.- Each subdirectory should contain its own snapshots.
- If
test/routes/myroute.test.tsproduces snapshots, those snapshots should live attest/routes/.snapshots/.
- If
vitest.config.tssupports configuring a custom snapshot directory location.
- Each subdirectory should contain its own snapshots.
- Avoid classes unless state is essential and must be encapsulated.
- Avoid throwing a generic
Errorfor expected failures.- Prefer built-in errors when they accurately describe the failure (for example
TypeError,RangeError, orAssertionError). - Otherwise throw an app-specific error that subclasses
BaseError(never throwBaseErrordirectly). BaseErrormust beabstractand must accept acodein its constructor.- Error codes are string values with an
ERR_prefix (uppercase, underscore-delimited), for exampleconst errAnExample = 'ERR_AN_EXAMPLE'.
- Prefer built-in errors when they accurately describe the failure (for example
- Use ESM modules with TypeScript.
- Prefer workspace import paths (for example
@jsx-email/core) over deep relative hops.
- Use Valibot schemas for runtime validation of untrusted input (request bodies/params, env/config, external API payloads).
- Use the schema as the source of truth for object shape typing via
InferInput/InferOutput. - For object shapes, prefer
object(...); usestrictObject(...)when unknown keys should be rejected. - Prefer direct Valibot imports (for example
import { object, string } from 'valibot') instead ofimport * as v from 'valibot'. - Reuse schemas: if create and update payloads share a shape, use one schema (prefer
{ name }). Only split when the shapes diverge, and extend from the shared base. - Prefer Valibot schemas for validation where validation is necessary and derive types from the schema (
InferInput/InferOutput) instead of duplicating shapes in TypeScript.
- Prefer DRY (Do Not Repeat Yourself).
- Prefer KISS (Keep It Simple Stupid).
- Don’t over-optimize error handling or validation
- Don’t be over-protective; use sane, reasonable data protection and error checking/throwing.