Skip to content
stellaPublic

About

Shared configuration for stella packages

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Stella tooling

stella tooling

Shared TypeScript, oxlint, and Rust configuration for stella public packages.

This repo intentionally contains only portable tooling policy:

  • @stll/typescript-config: strict TypeScript config presets.
  • @stll/oxlint-config: general upstream oxlint rules, the shared stella-lowercase and no-raw-colors JS plugins, the shared toolchain policy (toolchain.json) and the stll-* commands that enforce and generate it.
  • @stll/oxlint-plugin: portable static-safety rules for any casts, unsafe type assertions, incomplete union-keyed records, raw DOM HTML sinks, and route-query waterfalls, plus Bun test hygiene and test-reliability rules.
  • @stll/time: named duration constants, calendar-date parsing and arithmetic, the calendar day in a time zone, and a side-effect-free Temporal.
  • @stll/test-kit: hermetic test primitives for Bun: a controllable clock, event-based waits, per-test temp directories and an outbound network guard.
  • rust/: source-of-truth Rust formatting, lint, Cargo alias and profile templates, which stll-rust-policy generates into consumers.
  • rust-lints/: stella_source_lint, a stable-Rust source checker (cargo stella-lint, built on syn) for stella-specific Rust rules.

Repo-specific stella rules stay in the consuming repo: domain authorization, i18n, generated native artifacts, benchmark exceptions, and package-specific ignores. Route-query conventions are shared here because they are the common TanStack Router + React Query contract.

Shared toolchain

@stll/oxlint-config/toolchain.json is the versioned source for shared tool versions. Install @stll/oxlint-config as a dev dependency, inherit its pins, and run bunx --no-install stll-toolchain-check from the repository root in CI. The CLI inspects tracked configuration files, prints path:line: [rule] message, and exits with status 1 on a mismatch. Tools absent from a repository do not introduce requirements.

Schema version 1 contains these fields:

Fields Contract
schemaVersion Supported policy schema version (1).
bun Minimum Bun; the repository pins one exact release at or above it everywhere.
oxlint, oxlint-tsgolint, @oxlint/plugins, @stll/oxlint-plugin, lefthook Exact dependency and catalog pins. Declared local workspace packages must resolve to the same version.
typescript The TypeScript release bun check implements; a typescript peer range must include it. Repositories allow their own TypeScript installs in stll-toolchain.json typescriptAllowlist.
node, python, rust Runtime selectors and version-manager pins. Node selects the policy major (26.x); static minor and patch selectors within that major are allowed. A Python minor pin preserves existing patch selectors within that release series; a patch policy requires the exact patch. Rust selects one exact stable release across toolchain files, commands, workflow selectors and images.
bunArchiveSha256, nodeArchiveSha256 SHA256 of the pinned Bun release and Node patch archives (amd64, arm64) that generated cloud setup verifies.
postgres, valkey, valkeyArtifacts Service pins for generated cloud setup: the PostgreSQL major, and the Valkey release with its per-architecture package URLs and SHA256.
consumerNode, consumerNpm, consumerPnpm, consumerTypescript Consumer policy that published engines and peers must support, independent of the development toolchain (see Published package contracts).
actions Approved action names mapped to full sha and version comment.
dependabot Shared schedule, cooldown and groups. Package and action ignores derive from the owned pins. actionGroups come first in every GitHub Actions entry, so first-party reusable workflows that move by commit arrive as one update.
formatter bun fmt is the only formatter. style is the one [fmt] style (line width, import groups) every Bun repository's root bunfig.toml repeats exactly; ignorePatterns lists the generated files every repository leaves to their generators. A repository adds only its own ignore patterns and, where it uses Tailwind, sortTailwindcss.

The formatter-policy rule makes bun fmt the only formatter. It fails on an oxfmt, Prettier, Biome or dprint dependency, catalog entry, script or command anywhere in the repository; on .oxfmtrc*, oxfmt.config.*, .prettierrc*, prettier.config.*, .prettierignore, biome.json and dprint.json files and on a prettier key in package.json; on a package script named fmt (it would shadow bun fmt); and on a changesets format other than false. In a Bun repository the root bunfig.toml holds the only formatter configuration in its [fmt] table: exactly the shared style from toolchain.json, the shared ignore patterns, and nothing repository-specific beyond extra ignores and sortTailwindcss; lefthook pre-commit runs bun fmt --staged, and a workflow runs bun fmt --check without path operands, so CI always checks the whole repository.

Configure oven-sh/setup-bun with bun-version-file: package.json and a matching packageManager. Configure setup-node/setup-python with node-version-file/python-version-file referencing tracked .node-version or .nvmrc / .python-version files; direct version inputs are forbidden. Node and Python image tags and mise/asdf entries inherit the same pins. The Node policy is 26.x: selectors must select major 26. Bare major, major wildcard, minor and stable patch selectors within that major are allowed; open ranges, other majors, prereleases and floating aliases fail. Release and publish workflows use exact patch version-file selectors for reproducible builds; this repository pins .node-version to 26.10.0. The series policy accepts that patch without weakening release validation. Runtime images may retain a full SHA256 digest alongside an exact stable patch tag: Node must be inside its policy series and match tracked root .node-version when present; Python must be a patch in its policy series; Bun must equal its policy version. Supported image variants remain allowed. Static image families outside Node, Python, and Bun are outside this runtime check; unresolved image names still fail. Digest validation is declaration-only: the guard enforces the exact policy tag and a full SHA256 digest, but does not verify that the digest belongs to the tag. Dependabot refreshes digests per repository; a shared tag-to-digest map would require a tooling release for every base-image digest bump across repositories, which costs more than it buys. GitHub JavaScript action host runtimes (runs.using) are platform-managed and do not select the project toolchain Node version.

A published package can test the minimum Node major in its engines.node support range in one declared workflow job. Add a scoped engineFloors entry to tracked root stll-toolchain.json:

{
  "engineFloors": [
    {
      "package": "packages/library",
      "workflow": ".github/workflows/ci.yml",
      "job": "node-floor"
    }
  ]
}

That job may use a literal exact stable patch in setup-node's node-version whose major matches the range's minimum major and whose release satisfies the range (for example, 20.10.0 for >=20.10.0). The approved action SHA, version comment and input validation also apply. Other jobs and runtime files follow the shared Node series. Missing packages or jobs, private packages, absent setup-node selectors and floor mismatches fail configuration validation. optOuts is optional when only engineFloors is declared.

The Rust stable pin is 1.99.0. Floating channels and off-pin Rust references fail the shared check. Cargo rust-version declarations remain minimum support versions and may be lower than the shared pin. Rust setup actions (dtolnay/rust-toolchain and actions-rust-lang/setup-rust-toolchain) require explicit exact toolchain inputs; every release in a comma-separated setup list must satisfy the pin or its declared exception.

Rust exceptions use one closed declaration in root stll-toolchain.json:

{
  "rustExceptions": [
    {
      "pin": "1.97.1",
      "reason": "Awaiting upstream support for the shared compiler",
      "tracking": "https://github.com/owner/repository/issues/123",
      "expires": "2026-11-11"
    }
  ]
}

Replace the example with a verified compatibility constraint and its tracking issue. Each exception permits only that exact release until its expiry date; expired exceptions follow the quarantine expiry scope below. Floating channels, duplicate pins, extra fields and rust-version opt-outs fail validation. The same exception applies to toolchain files, workflow Rust inputs and environments, Cargo/rustup commands and Rust images.

Hawk 0.1.11 is the current temporary Rust exception: its driver fails to build with 1.99.0 due to compiler-private API changes (E0061 and E0308). It uses 1.97.1 until tracking issue #99 is resolved, with the exception expiring on 2026-11-11. The stable source checker and all ordinary Rust work use the shared 1.99.0 pin. Consumer repositories that run Hawk need the same exact, tracked exception declaration.

engines.node and requires-python are support ranges: they must include the shared runtime. The Node support range must include every stable release in the policy major, so >=26, ^26 and ^24 || ^26 pass; ^24, <26, >=27 and ranges that omit part of major 26 fail. A minor Python pin requires support for every patch in that series. Python comparisons, compatible releases, wildcard exclusions, and comma intersections are supported for final releases; unsupported syntax produces a diagnostic.

Direct compiler invocations in scripts.typecheck must use bun check. Classification decodes shell tokens to find compiler calls; acceptance requires bun check after leading environment prefixes are removed. Delegated wrappers and workspace runners are not inferred from manifests.

Listed shared actions require their approved SHA and matching # vX comment. CI also checks the shared release policy's runtime selector action set against toolchain.json, deriving its immutable checkout ref from the existing release-policy workflow reference. Other remote actions and reusable workflows require a full SHA. Local actions are repository-owned. The checker resolves YAML aliases and TOML tool tables. The package guide is the canonical contract for checkout provenance, runtime version files and scoped dynamicSelectors. It documents current-source and delegated refs, reusable-workflow caller context, sparse-checkout requirements, declaration locators and fail-closed validation. Use scoped declarations for reviewed unresolved selectors rather than broader rule opt-outs; literal pins and local-action provenance remain enforced.

The named rules are bun-pins, package-pins, typescript-layout, node-engine, node-version, python-version, rust-version, runtime-manager, runtime-docker, runtime-workflow, action-pins, workflow-trust, dependabot-policy, type-aware-projects, formatter-policy, cloud-setup-drift, and quarantine-policy. Non-Rust rule exceptions require a tracked root stll-toolchain.json listing the rule and its reason:

{
  "optOuts": [{ "rule": "bun-pins", "reason": "This repository uses npm." }]
}

Each workspace answers for its own TypeScript install: it declares one only through its repository's typescriptAllowlist entry (see TypeScript toolchain), with no root counterpart. Peer ranges, exact versions included, declare support: a typescript range must include the policy release. Ranges do not count as installs.

JavaScript update roots include the repository root, declared workspace membership, and directories with their own bun.lock, package-lock.json, yarn.lock, or pnpm-lock.yaml. Independent manifests with dependencies, an explicit package manager, or dependency-bearing workspace members also create update roots. Metadata-only fixtures do not. Package-manager selection follows the nearest declaration or lock; workspace members share their owner's update entry. Bundler Gemfile/Gemfile.lock and Composer composer.json/composer.lock declare roots at their own directories.

rust-version, node-engine, workflow-trust, type-aware-projects, cloud-setup-drift and quarantine-policy are mandatory: opt-outs cannot bypass them (Rust uses expiring rustExceptions instead).

Unknown rules, empty reasons, duplicate rules, and malformed configurations fail. Opt-outs apply repository-wide to the named rule, so keep them narrow.

.github/dependabot.yml is the shared reference. It declares each detected package ecosystem and update root (Bun roots use bun, npm/pnpm/Yarn use npm, and uv projects use uv), uses the policy schedule/groups and a five-day cooldown, and ignores tooling-owned npm and action pins. Workspace members share their update root; independent manifests need their own entry. pnpm-workspace.yaml owns pnpm catalogs and membership. Omitted packages includes only the root, matching the current pnpm contract; declare member patterns explicitly to keep membership stable across pnpm versions. The generator derives the reference from the same policy and ecosystem registry as the guard: run bun run generate:dependabot after policy changes. Consumers run bunx --no-install stll-dependabot from their repository root; the command reads tracked declarations and writes canonical .github/dependabot.yml (or updates an existing .yaml). stll-toolchain-check rejects missing coverage and policy drift. Use stll-dependabot --check in CI for read-only byte drift detection. Pass --config stll-dependabot.json to declare ecosystems and directories explicitly; the declaration cannot change shared schedule, cooldown, groups, or owned-tool ignores. Repository holds on other dependencies go in stll-toolchain.json dependabotIgnores, each with a reason; they cannot name or match a shared pin. See the package guide for the input format. The published @stll/oxlint-config/dependabot subpath exports generateDependabotConfig and parseToolchainPolicy for programmatic generation. Owned package pins follow release metadata. The changeset:version command first includes the policy package in the release plan whenever an owned workspace pin changes, then synchronizes pins through bun scripts/check-lockfile-workspace-versions.ts --write, which also refreshes cached workspace versions without regenerating the dependency graph.

Package quarantine

Before installing dependencies, run the CLI from the exact published @stll/oxlint-config release that includes stll-quarantine. Replace X.Y.Z with that release's exact version; bunx installs the pinned package when it is not already available:

bunx --bun --package @stll/oxlint-config@X.Y.Z stll-quarantine --sync
bun install

stll-quarantine --sync creates or updates the Bun quarantine preset. The preset keeps the five-day release-age gate for third-party packages and writes permanent exemptions for exact registry-backed @stll/ package names from declarations and the lockfile, including native platform packages. Wildcards and version selectors are rejected. Creation timestamps record audit history; they never expire.

Third-party exceptions require an inline exact UTC expiry:

[install]
minimumReleaseAge = 432_000
minimumReleaseAgeExcludes = [
  "@stll/oxlint-config", # quarantine-excluded-since: 2026-10-17T00:00:00.000Z
  "third-party-package", # quarantine-expires: 2026-10-20T00:00:00.000Z
]

stll-toolchain-check validates tracked root bunfig.toml and warns starting 48 hours before a temporary exception expires. Expired exceptions warn during the existing 24-hour removal grace period, then fail validation on default-branch pushes, schedules and dispatches. Pull requests and merge groups fail only when the change adds or edits the overdue entry (inherited ones warn; a shallow checkout fetches just the base commit to decide); local runs warn. The standalone stll-quarantine --check command performs the same policy check without writes; --prune removes expired third-party entries and retains first-party entries. Only --sync changes the generated first-party set. Unrelated Bun settings and temporary third-party entries remain repository inputs.

The shared scheduled quarantine workflow runs the same tooling-owned core from an immutable source checkout, before installing consumer dependencies. It opens or refreshes the existing removal branch, so each repository has one removal PR. Consumers retain the pinned stella/.github policy and scheduled-prune callers; adopt the workflow revision that delegates to this core.

Usage

Install the shared TypeScript and oxlint packages:

bun add -d @stll/typescript-config @stll/oxlint-config @stll/oxlint-plugin @oxlint/plugins oxlint oxlint-tsgolint

The shared defaults require oxlint 1.87.0 or newer and oxlint-tsgolint 7.0.2003 or newer. Pin the current versions from @stll/oxlint-config/toolchain.json; do not use the deprecated oxlint-tsgolint 0.x line.

TypeScript toolchain

Every repository that uses these configs follows one toolchain:

  • Type checking is bun check, Bun's built-in TypeScript 7 checker. Each package's typecheck script runs bun check --no-pretty --all --project=<tsconfig>; do not call tsc --noEmit or wrap tsc in a custom script.
  • No typescript package. bun check embeds the checker, oxlint-tsgolint embeds its own, and stll-toolchain-check resolves tsconfig projects with tsrs, so no manifest installs typescript or @typescript/native. A package that uses a tool which imports or peers on typescript keeps it only through its repository's own typescriptAllowlist (below); there is no shared list of such tools.
  • Declarations built with tsdown use dts: { generator: "oxc" }, which needs explicit types on exported declarations. A package that cannot provide them points tsdown at tsrs instead: dts: { generator: "tsgo", tsgo: { path: <tsrs bin> } }.
  • Compiler-API code does not import typescript. Programs, the type checker and tsconfig resolution use tsrs 0.18.0-ts7.1.0-dev.20260929 through its async API only (import { API } from "tsrs/unstable/async"; close the API with await api.close() on every path). The tsrs CLI accepts --noEmit, --pretty false, -p, --build --force, --listFilesOnly, --extendedDiagnostics and --declaration --emitDeclarationOnly --outDir; it rejects --showConfig, so read configs with api.parseConfigFile. Syntax-only reads (walking an AST, matching node kinds) use oxc-parser 0.155.0 (parseSync). Plain import scans that may ignore type-only imports use new Bun.Transpiler({ loader: "tsx" }).scanImports(text); otherwise read oxc-parser's module record. Pin both packages exactly.

stll-toolchain-check enforces this with the typescript-layout rule:

  • A manifest, catalog, override or resolution that declares typescript, @typescript/native or an alias of either (npm:typescript@…) fails, with a message pointing at bun check, tsrs and oxc-parser and showing the allowlist entry that would permit it.

  • A repository allows an install with an entry in typescriptAllowlist in its root stll-toolchain.json: the workspace directory (. for the root), the declared name, the exact spec it resolves to, the dependency in that same workspace that needs it, and a reason:

    {
      "typescriptAllowlist": [
        {
          "workspace": "apps/docs",
          "name": "typescript",
          "spec": "6.0.3",
          "dependency": "@astrojs/check",
          "reason": "astro check loads the TypeScript 6 language service."
        }
      ]
    }

    The workspace may declare the entry directly or through a catalog (catalog:, catalog:ts6); either way it must resolve to spec. A catalog may hold a TypeScript entry only at the spec of an allowed install that references it. Overrides and resolutions never declare TypeScript.

  • An entry fails once it is stale: its workspace no longer declares the name, or no longer depends on the named dependency. A different spec fails too.

  • A typecheck script that runs a compiler (tsc, tsgo, a launcher around them) fails; bun check with any arguments passes.

  • A peer range on typescript must include the TypeScript release bun check implements (typescript in toolchain.json).

When the root manifest declares oxlint-tsgolint, the same command lists the files each lint script covers and reads every tsconfig with tsrs; the consumer installs no compiler for it.

Use the library TypeScript preset:

{
  "extends": "@stll/typescript-config/library.json",
  "include": ["src"]
}

The package also exports base.json, node.json, bun.json, published.json (JavaScript disabled, isolated declarations enabled), and react-library.json (DOM libraries and the React JSX runtime). These profiles inherit errors for unreachable code and unused labels.

With the TypeScript 7 native compiler, opt into one checker per project by adding @stll/typescript-config/single-checker.json after the main profile in an extends array. This additive profile changes only the checker count:

{
  "extends": [
    "@stll/typescript-config/react-library.json",
    "@stll/typescript-config/single-checker.json"
  ],
  "include": ["src"]
}

checkers: 1 also applies to native checker consumers that read tsconfig, such as oxlint-tsgolint. This profile requires the native compiler; TypeScript 6 does not recognize checkers. A repository-recorded application measurement compared one checker at 82 seconds / 3.0 GB / 12.0 million instantiations with four at 87 seconds / 5.3 GB / 31.3 million instantiations. These results are specific to that project, not a benchmark of every consumer.

Import-extension rewriting, symlink preservation, incremental compilation, and tsBuildInfoFile placement remain explicit consumer options: they depend on emit requirements, module identity, and cache ownership. The shared base sets none of them.

Node scripts need an explicit TypeScript project when the library project's include does not cover them. Install @types/node as a direct development dependency of the consumer, then add scripts/tsconfig.json:

{
  "extends": "@stll/typescript-config/node.json",
  "include": [
    "**/*.js",
    "**/*.jsx",
    "**/*.mjs",
    "**/*.cjs",
    "**/*.ts",
    "**/*.tsx",
    "**/*.mts",
    "**/*.cts"
  ]
}

The existing Node preset selects Node ambient types; the local include assigns ownership of scripts to that project. Keep the library project unchanged. For scripts in .github/tools, add .github/tools/tsconfig.json with the same extends and all local include patterns shown above. A sibling scripts/tsconfig.json that includes ../.github/tools does not own those files: Oxlint discovers the nearest tsconfig.json; run the ordinary type-aware lint command from the repository root. Missing Node ambient types can otherwise appear as TS2591 on node:fs or require, followed by misleading any diagnostics on Node API results. Adding casts or changing conditionals does not repair resolution. Use the existing bun.json preset for Bun tests and benchmarks. Include root configuration files in their declared project as well. No additional preset, compiler overrides, or config import side effects are needed.

stll-toolchain-check enforces this ownership with type-aware-projects when the root manifest declares oxlint-tsgolint. It compares the files selected by the root lint command with configured root files. The search starts at the nearest discoverable tsconfig.json and its tracked reference graph, then searches ancestor configs and their references for unmatched files. An unreferenced sibling cannot claim ownership. Imported files do not become configured roots merely because the compiler loads them into a program; the pinned backend resolver checks root membership. Oxlint's --tsconfig flag changes import resolution; it does not select a type-aware ownership project. Only tracked JavaScript and TypeScript files selected by lint are checked; lint exclusions stay excluded. An unrelated, unreferenced config does not establish ownership. Oxlint supplies its file inventory and tsrs expands inherited config, JSONC, includes and excludes; this check does not run lint rules or type checking. Unsupported lint launchers or unreadable project configurations produce diagnostics rather than bypassing the check. Add the appropriate shared project declaration instead of suppressing the rule.

Script inventory includes automatic pre/post stages of literal package-manager run aliases and the root lint script. Declare the root packageManager when root hooks exist; workspace lint tasks inherit it. npm and Bun run those hooks; tracked project .npmrc ignore-scripts=true suppresses npm hooks. Pinned pnpm 12.9.1 defaults to running them and reads the boolean enablePrePostScripts from tracked pnpm-workspace.yaml; it skips a hook whose name already occurs in the main script body, matching that version's behavior. Yarn Berry (declared major 2, 3 or 4) does not run arbitrary pre/post hooks. Unknown manager flavors, unreadable configuration, nested workspace overrides, custom Yarn executables or plugins, and dynamic lifecycle settings produce diagnostics. Automatic hook bodies do not recursively invent further pre/post stages. Filtered Bun build preparation requires one exact workspace package name and no prebuild/postbuild hooks; unsupported hook scopes must be made explicit instead of being omitted from inventory.

Use the oxlint preset with local exceptions:

import { library } from "@stll/oxlint-config";

export default library({
  ignorePatterns: ["dist/", "npm/", "*.node"],
  overrides: [
    {
      files: ["scripts/**"],
      rules: {
        "no-console": "off",
      },
    },
  ],
});

Add the portable safety rules to an existing Oxlint config:

import { defineConfig } from "oxlint";

import {
  portableSafetyPluginSpecifiers,
  portableSafetyRules,
} from "@stll/oxlint-plugin";

export default defineConfig({
  jsPlugins: [...portableSafetyPluginSpecifiers],
  rules: { ...portableSafetyRules },
});

For TanStack Router + React Query applications, add the route-query guards to prevent cache misses and render-fetch waterfalls during navigation:

import { defineConfig } from "oxlint";

import {
  routeQueryPluginSpecifiers,
  routeQueryRules,
} from "@stll/oxlint-plugin";

export default defineConfig({
  jsPlugins: routeQueryPluginSpecifiers,
  rules: routeQueryRules,
});

require-loader-prefetch requires every statically attributable useSuspenseQuery in a route to be referenced by that route's loader. no-raw-route-query-client requires route freshness helpers instead of raw TanStack Query client calls and keeps pending components synchronous.

Test suites add bunTestHygieneRules (no focused, disabled or identically titled tests, no .resolves/.rejects matchers, no forced database drops) and testReliabilityRules (no wall clock, real timer waits, unseeded randomness, real network or checkout writes in tests, and no assertions on measured time or timer races) the same way, with bunTestHygienePluginSpecifiers and testReliabilityPluginSpecifiers. @stll/test-kit provides the replacements those rules point to.

When using no-unsafe-inner-html, disable the blanket react/no-danger rule. Its default sanitizerPolicy: "allowlist" permits static HTML and calls whose names match the shared HTML sanitizer allowlist. Set sanitizerPolicy: "provenance" to require a // safe-html: <provenance> comment for dynamic values, including calls to sanitizer-named functions.

Oxlint 1.80 replaced react/react-compiler with category-specific React Compiler rules. Enable the actionable categories explicitly with library({ react: { files: ["src/**/*.{ts,tsx}"] } }); select only React-owned paths, including .ts hooks. The base library() preset is framework-neutral; react/invariant and react/todo remain off because they report compiler internals rather than source defects. Consumers can suppress one precise category without hiding unrelated compiler diagnostics. A repo adopting these rules against an existing findings backlog should carve out temporary overrides entries per React-owned legacy path and fix forward. Each consumer React override must include the React plugin: oxlint resolves its rules against its own plugin list or the framework-neutral base list, not an earlier override.

import { library, libraryPlugins } from "@stll/oxlint-config";

export default library({
  react: { files: ["src/**/*.{ts,tsx}"] },
  overrides: [
    {
      files: ["src/legacy/**/*.{ts,tsx}"],
      plugins: [...libraryPlugins, "react"],
      rules: { "react/hooks": "off" },
    },
  ],
});

Include any additional consumer plugins in that override's plugins list too.

Use the helper directly as the root config when possible. That keeps root-level options, shared JS plugins, shared ignores, and local exceptions in one merged object.

If a repo needs the extends style used by other oxlint config packages, keep repo-specific ignores in the root config:

import { defineConfig } from "oxlint";

import stella from "@stll/oxlint-config";

export default defineConfig({
  extends: [stella],
  ignorePatterns: ["dist/", "npm/", "*.node"],
});

CommonJS repos can use require in oxlint.config.ts:

const { library } = require("@stll/oxlint-config");

module.exports = library();

Recommended scripts:

{
  "scripts": {
    "typecheck": "bun check --no-pretty --all --project=tsconfig.json",
    "lint": "bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware .",
    "lint:fix": "bun --bun oxlint -c oxlint.config.ts --type-aware --fix ."
  }
}

Generate Rust policy from the published CLI. Commit stll-rust-policy.json:

{ "kind": "single-crate" }
bunx --no-install stll-rust-policy
bunx --no-install stll-rust-policy --check

The generator owns Cargo lint/profile values, Cargo aliases, rustfmt and Clippy policy from rust/; --check reports drift without writing. Unrelated TOML values survive generation; comments and layout are canonicalized. See rust/README.md for workspace members and reasoned exceptions.

Install the stable source lint using an exact tooling commit, then run it after Clippy. Replace <sha> with the commit containing the adopted checker:

cargo install --git https://github.com/stella/tooling --rev <sha> --locked stella_source_lint
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo ci-stella-lint

Cloud setup

This repository adopts its own shared cloud setup with pinned Bun, Node, and Rust (Clippy and rustfmt). Provision a cloud environment with bash .agents/cloud-setup.sh install; run bash .agents/cloud-setup.sh start at each session start (the committed Claude SessionStart hook does this).

Releasing packages

Add a Changeset for every pull request that changes a published package. Choose the affected package and semantic bump with bun run changeset; use an empty Changeset when a release is intentionally unnecessary.

After the change lands on main, the shared release workflow maintains one Version Packages pull request. Merging that pull request updates package versions and changelogs, then .github/workflows/publish.yml builds the tarballs and delegates the hardened npm and GitHub release transaction to the versioned stella/.github contract. Package tags use the immutable <name>@<version> form.

Shared cloud setup

Declare cloud in tracked root stll-toolchain.json, then run bunx --no-install stll-cloud-setup to generate .agents/cloud-setup.sh and merge its remote-only Claude SessionStart hook into .claude/settings.json. Generation recreates its deleted tracked output files and requires unrelated tracked inputs to remain readable. The declaration requires install exactly bun install --frozen-lockfile or bun install --frozen-lockfile --ignore-scripts. Use the second when lifecycle scripts (for example postinstall code generation) are too heavy for cloud setup; tasks then run those scripts themselves when they need the output. Every setup phase logs its start and duration to stderr. services is any subset of postgres and valkey; omit it or use [] for runtimes and dependencies only. A canonical repository-relative envFile, such as .env.cloud, is required with services and rejected without services. Runtime-only scripts emit no environment file code. A tracked root .node-version must select an exact stable patch within the shared Node series. Root rust-toolchain.toml enables pinned Rust automatically; cloud.rust can also declare its toolchain, components, and targets. To provision another authorized compiler, explicitly select cloud.rust.additionalToolchains: ["1.97.1"]; each extra must match the shared pin or a valid rustExceptions entry. Extras use the minimal Rust profile. Exceptions alone authorize versions and install nothing; additional compilers do not install Hawk or its required components. Optional cloud.tools installs exact supported releases of cargo-nextest, wasm-bindgen-cli, and uv from verified prebuilt artifacts. The wasm-bindgen CLI pin must match the crate in Cargo.lock. Installed commands work in a fresh shell without sourcing runtime environment files. The mandatory cloud-setup-drift rule compares the committed script with the same generator and rejects missing, changed, or undeclared scripts and missing or conflicting startup hooks.

The script has explicit install and start commands. It targets Ubuntu 24.04 with root access or passwordless sudo; these are capability requirements, not assumptions about every cloud image. See the package guide for lifecycle wiring, service isolation, environment-file ownership, and host documentation.

Published package contracts

Run stll-publish-contract in PR CI. Every published root or declared workspace package commits publish-contract.json, recording engines, peer support ranges, resolved JavaScript targets, and entry points. stll-publish-contract --write records an explicit contract decision as a reviewable diff; it also validates consumer support and cannot accept development-only requirements.

The consumer policy pins Node 22.23.3, npm 12.2.0, pnpm 12.9.1, and TypeScript 6.0.3 independently of the development toolchain. Published engines and TypeScript peers must support the consumer versions; ranges may include newer versions. An absent engines.node is unrestricted, matching package-manager semantics; a declared range must contain the consumer Node version. Both guards share this rule. Bun runtime/compiler requirements are rejected. JSON and declaration-only packages record {"type":"types-only"} explicitly.

Build adapters use the installed configuration loaders and bind their behavior to tsdown 0.23.1, Vite 8.3.5, or @nuxt/module-builder 1.0.3 (unbuild 3.6.1 and mkdist 2.4.1). They record final transform targets, including separate Nuxt module and runtime entries. No syntax lowering records esnext. Only exact single build invocations (tsdown, vite build, or nuxt-module-build build) are supported. Shell composition, environment prefixes, launchers, directory changes, workspace filters, CLI overrides, and pre/post build lifecycle scripts fail. Vite accepts its version-bound default plugin pipeline and the default @vitejs/plugin-vue 6.0.8 factory; custom Vue compiler, template, script, and feature options are unsupported. Nuxt accepts the version-bound builder and owned target hook. Unapproved plugins, output transforms, and execution-order changes fail. The static guard validates the supported configuration, without parsing the emitted JavaScript syntax. Add a reviewed adapter when adopting another build tool.

Nuxt modules can set both emitted targets without replacing the builder's entries:

import { nuxtModuleTarget } from "@stll/oxlint-config/build-target";

export default { hooks: { "build:before": nuxtModuleTarget("es2022") } };

The Nuxt adapter accepts this owned hook from the same package version as the CLI; it captures the normalized configuration before cleanup or output writes. The built publish-contract CLI is also checked against a TypeScript configuration and relative TypeScript import on the package's minimum supported Node release.

The static guard follows the packer declared in the tracked release workflow. With pnpm, it models publishConfig overrides for exports, main, module, types, typings, bin, and typesVersions. npm does not apply these overrides, so differing values fail; move the published entry into the source manifest. access and registry are allowed publication metadata. Other overrides, including engines, are rejected. Real npm and pnpm pack tests compare the modeled contract with each tarball manifest.

Packed-artifact checks run nightly, while the static contract check runs per PR. The reusable consumer job declares its exact scope in stll-toolchain.json:

{
  "consumerChecks": [
    {
      "workflow": ".github/workflows/consumer-compat.yml",
      "job": "consumer",
      "packages": ["packages/library"],
      "toolingVersion": "<installed tooling version>",
      "fixturePath": "tests/consumer"
    }
  ]
}

The declared workflow requires valid five-field cron with bounded numeric values, month/day names, lists, ranges, and positive steps; manual dispatch is optional. Day of week is 0–6 (SUN–SAT), matching the GitHub Actions schedule contract; numeric 7 is unsupported. The combined schedules must cover every weekday with unrestricted month and date fields, so weekly or seasonal schedules cannot satisfy the daily contract. Public packages require a semver-valid version. Published workspace packages under a vendor directory are unsupported; the contract CLI reports them rather than omitting them. bundleDependencies and bundledDependencies are unsupported because artifact staging does not install a bundled dependency tree; remove these fields before declaring consumer checks. The reusable consumer job must be stand-alone and unconditional (no needs, if, or strategy), so a job condition cannot skip its scheduled execution. That job calls package-consumer-compat.yml at the approved shared policy SHA with packages as the same JSON array and consumer-node equal to the consumer policy pin. Required tooling-version and fixture-path inputs equal the declared exact toolingVersion and repository-relative fixturePath; its consumer-compat.json must be tracked, parse with the runner’s fixture schema, and select exactly the declared package set. Replace <installed tooling version> with the exact installed @stll/oxlint-config release that provides the consumer runner; the guard binds the declaration to that package version. Every named package and its transitive workspace closure must be tracked, published, and have valid manifests. Each selected package must support that Node version. Unknown jobs, stale entries, undeclared consumer calls, and inconsistent inputs fail. This declaration applies to consumer checks; engineFloors retains its separate minimum-supported-major contract.

stll-consumer-compat --packages '["packages/library"]' --consumer-node 22.23.3 --fixture-path tests/consumer tests final tarballs in isolated projects with both npm and pnpm. The repository builds once with its development toolchain first. The runner reads the exact packer version and pack flags from a single tracked release workflow and packs directly with that manager. Missing, ambiguous, or unsupported pack configuration fails. Both consumers install the same release artifacts; the runner does not rewrite workspace or catalog protocols through a different manager. Fixture installs disable scripts, reject reserved runtime/manager bins throughout nested lifecycle paths, then approve only directly declared dependencies at their resolved lock identities. npm rebuilds those identities; pnpm runs its built-in pending rebuild. Both use the pinned consumer runtime before typecheck, build and smoke. Undeclared transitive build scripts receive no approval. Catalog entries that resolve to a local workspace package are unsupported: use workspace: for that dependency. Registry catalog declarations are staged intact; the release packer must resolve publication protocols. pnpm workspace owners require an adjacent tracked package.json; manifestless owners are unsupported. Public package names must be canonical lowercase, URL-safe npm names (at most 214 characters), with no traversal components. Workspace alias specifiers are unsupported: use the package name as the dependency key. The runner verifies the official Node archive checksum and exact runtime/package-manager versions, uses the oldest published React satisfying the package peer range and, when ReactDOM is required, its compatible renderer peer range, typechecks with the consumer TypeScript, and runs each fixture's build and usage smoke. Both publication packers and fixture commands use the provisioned consumer Node with isolated package-manager settings. Consumer command PATH contains only pinned wrappers, fixture binaries, and /usr/bin:/bin; it never inherits development tool directories. Installed binaries named node, npm, npx, or pnpm are rejected before fixture build and smoke commands.

The fixture directory contains consumer-compat.json:

{
  "packages": [
    {
      "package": "packages/library",
      "fixture": "library",
      "kind": "node",
      "build": ["npm", "run", "build"],
      "smoke": ["npm", "run", "smoke"]
    }
  ]
}

The selected package set must exactly match the fixture declaration set. Each fixture is a standalone project with its own source, package manifest, and TypeScript configuration. The package peers determine its fixture kind: a React peer requires react and a render smoke; other packages require node and an import with a representative call. A mismatching declaration fails. Dependency bindings, runtime tools, and caches belong to the runner. A nightly failure must open or update one issue in the consuming repository and use its existing failure notification.

Toolchain changes and typecheck probes

stll-toolchain-changed --since <git-ref> compares committed snapshots at the reference and HEAD. Fetch the reference and its history before running it. It prints one JSON object with changed, tools, and status; the detector exits successfully even when history is unreadable, reporting changed: true and status: "unreadable". Use changed to force the full CI depth on both pull requests and merge-queue runs. An unreadable snapshot never skips checks.

Run repository-installed tools only after the workflow's existing trust decision. The mandatory workflow-trust rule applies to PR workflows declaring a trust-check job. Repository execution requires a direct dependency and a condition requiring its trusted == 'true' output for every PR trigger. Before that boundary, commands must match a small metadata allowlist: shell output and string comparisons, bounded Git rev-parse/merge-base queries, and GET-only GitHub API reads. File readers, directory listings and file predicates require the job trust gate, including workspace and symlink paths. The trust job's if/elif comparisons and writes to GITHUB_OUTPUT use the same checks. Job containers and nonempty services require the job-level trust gate because they start before steps and supply executable code. A step condition cannot authorize their startup. Digest pinning alone does not approve an image for pre-trust execution; the guard has no implicit image approval catalog. Before trust, step actions must use a full SHA and match the explicit bootstrap catalog: actions/checkout and oven-sh/setup-bun, with bounded selector inputs. Bun installation requires an explicit literal version; version-file and path inputs require the job trust gate, including repository paths that may be symlinks. Checkout path and submodule traversal inputs also require trust. actions/setup-node requires trust because it also invokes package-manager commands from the checkout directory. Other actions and unsupported inputs require the job-level gate; a step condition cannot authorize them. Every job-level reusable workflow call requires the job-level trust condition, including pinned calls without inputs.

Interpreters, sourced files, executable paths, package managers (including metadata and script-disabled install modes), launchers, custom shells, and unknown commands or syntax require trust. Git history/diff commands and working-tree, index and fetch operations require trust; they are outside the revision-query allowlist. Unquoted pathname expansion also requires trust. Unsupported options and query programs also require the gate. For example, gate the entire detector job, including dependency installation:

check-changes:
  needs: [trust-check]
  if: >-
    always() && !cancelled()
    && ((github.event_name != 'pull_request'
         && github.event_name != 'pull_request_target')
        || needs.trust-check.outputs.trusted == 'true')
  runs-on: ubuntu-latest
  steps:
    # Check out, set up Bun, install dependencies, then run the shared detector.
    - run: bunx --no-install stll-toolchain-changed --since "$BASE_REF"

Actions interpolation is checked before shell quoting or comments. Only the bounded identity expressions github.sha, github.run_id, github.run_attempt and github.repository may appear directly in pre-trust scripts. Other contexts, functions and matrix values require environment indirection or the trust gate; the guard does not infer that an expression is safe from its shell quotes. Pass contributor-controlled metadata through a supported data environment key (PR_TITLE, HEAD_REF, ISSUE_BODY or COMMENT_BODY) and quote its expansion:

- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "$PR_TITLE"

The trust job must classify each PR trigger used by the workflow. Main/manual jobs remain runnable when the PR-only trust job is skipped. Job or step conditions may prove that execution is limited to non-PR events; event string comparisons follow Actions case-insensitive equality. The guard proves only supported static metadata commands and boolean conditions; unknown commands and conditions cannot authorize execution. It does not validate the trust job's authorization policy. Workflows with no declared trust job retain their existing fork-CI policy.

The comparison includes Bun declarations, Node selectors and support ranges, resolved TypeScript compiler versions (including workspace and compatibility aliases), oxlint, oxlint-tsgolint, and shared tooling pins. Lockfile resolutions determine installed package versions; manifest ranges alone do not. Resolution locations and importer bindings are retained: swapping compiler versions between workspaces cannot look unchanged. Compiler specifier changes report typescript even when the resolved release version is unchanged. Unknown lock shapes and ambiguous resolutions report an unreadable snapshot. Supported text locks include Bun JSONC (schema 0/1), pnpm YAML (5.4/6/9), npm package-lock/ shrinkwrap (1/2/3), and bounded Yarn Classic/Berry shapes; a binary-only Bun lock cannot be compared without executing Bun. Workflow/action and Docker definition edits conservatively force full CI. Unrelated application dependency changes do not report a compiler change. Setup-action runtime selectors and their tracked version-file contents must identify an exact stable release. Floating selectors, ranges, expressions, empty values, or omitted runtime versions report an unreadable snapshot even when their committed text is unchanged.

On pull requests, run the repository's ordinary Bun typecheck and the probe. The shared rust-napi-ci.yml workflow runs both when called with typecheck: true; elsewhere, run:

stll-typecheck-probe --project packages/api/tsconfig.json --seed-dir packages/api/src -- bun run typecheck

The probe writes an exclusive temporary TypeScript file in the declared project, requires the command to fail with TS2322 attributed to that file, removes it, and requires the clean command to pass. Use --seed-dir to select a repository-relative directory inside the project; the default is the config directory. The config must include the seed directory; excluded seeds fail instead of producing a vacuous pass. Commands are argv, not shell expressions. Cleanup also runs on failure and handled termination. Repeat the probe for independently checked projects as needed.

Workflow, composite-action, Docker, mise, and tool-version declarations are conservatively fingerprinted as shared changes, including referenced tracked setup-action version files (Node, Bun, Python, Go, Java and other runtimes). Rust toolchain-file selectors and conventional runtime files are included; cache-dependency-path is not a runtime selector. Compose and Kubernetes image definitions are conservatively fingerprinted alongside Dockerfiles. Catalog and unresolved declaration changes also report a change; an unclassifiable reference reports an unreadable snapshot. This may report a change for unrelated edits to these declaration files. Installed compiler comparisons use the owning lockfile resolutions.

About

Shared configuration for stella packages

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages