Start
Foundations
Search for a command to run or a page to navigate to.
One command wires up ESLint, Prettier and Stylelint with Simpson's rules. The only part you do by hand is authenticating the package feed.
The @strongtie packages live on the Simpson Azure Artifacts feed, not the public registry. Create a .npmrc in your project root to scope the @strongtie namespace — leave all other packages on registry.npmjs.org.
@strongtie:registry=https://pkgs.dev.azure.com/StrongTie/_packaging/Simpson/npm/registry/Scope the registry line to @strongtie:registry. Using a bare registry=… line would route all package installs through Azure Artifacts and break everything else.
Then add your credentials to ~/.npmrc — the full two-file setup (and why always-auth is no longer needed) is in Set up your .npmrc.
Ultracite installs ESLint, Prettier, and Stylelint, then writes the config files your project needs.
Don't install peer deps by hand before running init — Ultracite resolves the exact versions it needs. Installing a different major upfront will cause peer-conflict errors.
Install the three Simpson packages on top of what Ultracite set up.
Then update the three config files to re-export the Simpson presets.
import base from "@strongtie/eslint/base"
import react from "@strongtie/eslint/react"
import { defineConfig } from "eslint/config"
export default defineConfig([
{
ignores: [
"**/dist/",
"**/build/",
"**/.next/",
"**/.turbo/",
"**/node_modules/",
"**/*.config.ts",
"**/*.config.mjs",
"**/*.d.ts",
"**/*.json",
"components/ui/**/*",
],
},
// Base TypeScript config (auto-detects ./tsconfig.json)
...base(),
// React/JSX config
...react,
])export { default } from "@strongtie/prettier"export { default } from "@strongtie/stylelint"Both commands should exit 0. If they don't, see Troubleshooting — most failures are a missing peer dep or a tsconfig path issue.
The @strongtie/eslint preset inherits all of Ultracite's rules and makes targeted adjustments for Simpson's codebase.
@strongtie/no-static-inline-style errors on style props whose value is a plain object literal with only static values.
// ❌ Blocked — every value is a static literal
<div style={{ color: "red", margin: 0 }} />
// ✅ Allowed — at least one value is dynamic
<div style={{ color: dynamicColor }} />
<div style={{ width: `${size}px` }} />Use Tailwind for static styling. The rule allows dynamic style props where the value depends on runtime data.
In a monorepo each workspace runs ESLint against its own tsconfig.json. The base() factory in @strongtie/eslint/base auto-detects ./tsconfig.json relative to the config file — if your workspace's tsconfig is at a non-standard path, pass it explicitly:
import base from "@strongtie/eslint/base"
export default [
...base({ project: "./tsconfig.eslint.json" }),
]Root eslint.config.mjs files that span multiple packages should use ignores to exclude workspace-specific paths and let each workspace run its own config. Avoid a single root config that tries to cover all tsconfig.json files — TypeScript-aware rules need one tsconfig per lint invocation.
The react-refresh Vite plugin expects component files to export only React components. If a file exports a non-component value alongside a component, react-refresh warns and falls back to a full reload. The @strongtie/eslint/react config enables the react-refresh rule to catch this at lint time:
// ❌ Breaks fast refresh — non-component export alongside a component
export const CONFIG = { timeout: 5000 }
export function MyComponent() { ... }
// ✅ Move constants to a separate file
// constants.ts
export const CONFIG = { timeout: 5000 }
// MyComponent.tsx
export function MyComponent() { ... }The rule is set to "warn" (not "error") so it surfaces in development without blocking CI.