@happyvertical/smrt-vitest
Vitest plugin for s-m-r-t projects -- required for all s-m-r-t tests. Auto-generates manifests, loads cross-package class metadata, and provides transaction-isolated test database utilities.
Installation
pnpm add -D @happyvertical/smrt-vitest
Usage
Required Plugin Setup
Every s-m-r-t project must include smrtVitestPlugin() in vitest.config.ts:
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
export default defineConfig({
plugins: [smrtVitestPlugin()],
});
Without this plugin, tests fail with "No field metadata found" or "unregistered class" errors.
Plugin Options
| Option | Type | Default | Description |
|---|---|---|---|
generateManifest | boolean | true | Auto-generate manifest at startup |
include | string[] | ['src/**/*.ts'] | Source patterns to scan |
exclude | string[] | ['**/*.d.ts', ...] | Patterns to exclude |
packages | string[] | [] | Additional packages beyond auto-discovered |
verbose | boolean | false | Emit per-package manifest-registration log lines. Also on with SMRT_VERBOSE=true or DEBUG containing smrt, regardless of this option. A one-line per-process summary (Loaded manifests from N/M packages) always logs once even when this is false. |
root | string | process.cwd() | Root directory |
setupFile | string | package setup entry | Override the setup file injected into Vitest projects |
aliasFilter | (entry) => boolean | keep all | Drop auto-generated workspace alias entries (receives the raw string find and replacement) |
Vite 8 (rolldown/oxc) Normalization
Vite 8 replaced esbuild with rolldown/oxc, which changed three behaviors that break s-m-r-t projects. The plugin normalizes all three so consuming apps don't need per-repo workarounds (evidence: anytown.ai#707, willgriffin.dev#220):
-
esbuild.tsconfigRawis ignored — legacy@smrt()decorators reach the bundle untransformed. The plugin injectsoxc.decorator = { legacy: true, emitDecoratorMetadata: true }(plus the matchingoxc.tsconfig.compilerOptionsmirror). -
oxc elides type-position side-effect imports by default — test files usually sit outside the tsconfig
include, so a repo-wideverbatimModuleSyntaxnever reaches them and side-effect model imports (s-m-r-t object registration) are silently dropped. The plugin injectsoxc.typescript = { onlyRemoveTypeImports: true }. -
Rolldown prefix-matches string alias
finds — a bare workspace alias like@org/pkg→src/index.tsmangles unaliased subpath imports (@org/pkg/sub→src/index.ts/sub). Workspace aliases are emitted as anchored exact-match RegExps, so unaliased subpaths fall through to the package exports map; usealiasFilterto drop entries entirely.Breaking change for direct
getWorkspaceViteAliases()consumers: each entry'sfindis now an anchoredRegExp, not astring(the returned array is still ordered most-specific first, and the new secondoptionsparameter is optional). Code that usedfindas a string — e.g. aMapkey or an equality filter — should match withentry.find.test('<specifier>')instead. Filters passed viaaliasFilter(or the helper'soptions.filter) are unaffected: they receive the raw stringfindbefore anchoring. Plugin-only consumers need no changes.
All defaults are override-able: any oxc field you set in your own config is
never injected (explicit consumer values always win, and sibling fields still
deep-merge), and oxc: false suppresses injection entirely. The oxc keys
are inert on esbuild-based vite ≤ 7.
Note: the plugin only reaches configs that include it (vitest configs and any
vite config listing it in plugins). An app's separate build-only
vite.config.ts without the plugin still needs its own oxc.decorator
settings on vite 8.
Watch Mode Note
The manifest is generated once at vitest startup. Restart vitest after adding new @smrt() classes or fields.
Test Manifest Snapshot
Test startup scans test-inclusive sources (inline *.test.ts fixture classes
need manifest metadata) and writes the result to a per-run snapshot,
.smrt/test-runs/<run>/manifest.json, never to .smrt/manifest.json,
dist/manifest.json, or their paired smrt-knowledge.json (#4128). Those
belong to production builds, so a build or typecheck running alongside a test
suite can neither replace the suite's manifest nor be replaced by it. Every
manifest read inside the run — worker registration, the runtime manifest
loader, createIsolatedTestDbFromManifest() — resolves the snapshot first;
tests that inspect generated metadata read getTestManifestPath(). The
snapshot directory is removed when the run exits, and directories left by
ended runs are pruned at the next startup.
Pool and Isolation
Measured on a 333-file, 3,259-test unit suite, 16 cores (#2897, follow-up to #2893):
| mode | wall | CPU | result |
|---|---|---|---|
pool: 'forks', isolated (default) | 22.9 s | 274 s | all pass |
pool: 'threads', isolated | 19.4 s | 234 s | all pass |
pool: 'forks', isolate: false | 12.1 s | 93 s | 7 fail in 1 file (leaked module mock from another file) |
pool: 'threads'is supported and came in ~15% cheaper on this suite. Try it per project -- native drivers can behave differently under threads vs. forks.isolate: falseis supported by this plugin's own setup: the registration guard insrc/setup.tsis self-healing by design (it re-registers afterObjectRegistry.clear(), which is why that guard exists -- #2750). Turning isolation off is a consumer decision, though: it removes the leak guard between test files, so a failing file under it is signaling a real cross-file leak to fix, not a plugin bug. Recommended for unit projects only, never for integration projects.- The per-file fixed cost this plugin pays is process + module-graph bootstrap
(roughly half a second of CPU per file), not manifest registration itself
(50-90 ms). Precomputing registration would save little against that fixed cost; see
the measurements in #2893. Lazy-loading
@happyvertical/aiin core (a separate, isolation-preserving change) is the bigger lever for consumers with the ai package on their dependency graph.
API
Plugin
| Export | Description |
|---|---|
smrtVitestPlugin(options?) | Vite plugin -- generates manifest and loads cross-package classes |
setupSmrtManifests(options?) | Imperative alternative for non-Vite setups (e.g., globalSetup files) |
Test Database Utilities
| Export | Description |
|---|---|
createIsolatedTestDbFromManifest(options?) | Create DB from local + explicitly requested registered dependency manifest objects, with FK ordering and STI dedup (recommended) |
createIsolatedTestDb(options?) | Create DB with raw DDL schema and transaction isolation |
createTestDb(prefix?) | Create DB with cleanup function (no transaction isolation) |
getTestDbConfig(prefix?) | Get DB config for current environment |
getInMemoryDbConfig() | Get in-memory SQLite config |
getTestAdapter() | Detect adapter: 'postgres' or 'sqlite' |
getAdapterDisplayName() | Human-readable adapter name for test labels |
isPostgresAvailable() | Check if DATABASE_URL is set |
DB adapter auto-detection: DATABASE_URL set -> PostgreSQL; otherwise -> SQLite temp files.
Transaction Isolation Example
import { createIsolatedTestDb } from '@happyvertical/smrt-vitest';
let db, cleanup;
beforeEach(async () => {
({ db, cleanup } = await createIsolatedTestDb({
schema: `CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL)`
}));
});
afterEach(async () => {
await cleanup(); // Rolls back transaction
});
it('should insert and query', async () => {
await db.insert('users', { id: '1', name: 'Alice' });
const user = await db.get('users', { id: '1' });
expect(user?.name).toBe('Alice');
});
Types
IsolatedTestDbOptions, IsolatedTestDbResult, ManifestTestDbOptions, TestDbAdapter, TestDbConfig, TransactionHandle
Dependencies
@happyvertical/smrt-core-- manifest builder, object registry@happyvertical/sql-- database connections and transactionsvitest(peer) -- Vite test framework
License
MIT