@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.
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