@happyvertical/smrt-vitest 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +109 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +103 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/a11y.d.ts +19 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/a11y.js +39 -0
- package/dist/a11y.js.map +1 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +718 -0
- package/dist/index.js.map +1 -0
- package/dist/setup.d.ts +22 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +209 -0
- package/dist/setup.js.map +1 -0
- package/dist/svelte-setup.d.ts +2 -0
- package/dist/svelte-setup.d.ts.map +1 -0
- package/dist/svelte-setup.js +46 -0
- package/dist/svelte-setup.js.map +1 -0
- package/dist/svelte.d.ts +21 -0
- package/dist/svelte.d.ts.map +1 -0
- package/dist/svelte.js +21 -0
- package/dist/svelte.js.map +1 -0
- package/dist/test-db.d.ts +381 -0
- package/dist/test-db.d.ts.map +1 -0
- package/dist/test-db.js +887 -0
- package/dist/test-db.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +88 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMRT Vitest Plugin
|
|
3
|
+
*
|
|
4
|
+
* Automatically loads manifests from SMRT peer dependencies before tests run.
|
|
5
|
+
* This solves Issue #583 where cross-package integration tests fail because
|
|
6
|
+
* external package classes aren't registered in the test manifest.
|
|
7
|
+
*
|
|
8
|
+
* Uses ManifestManager for unified manifest loading, which properly handles
|
|
9
|
+
* the manifest priority order: .smrt/manifest.json (test) -> dist/manifest.json (production)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // vitest.config.ts
|
|
14
|
+
* import { defineConfig } from 'vitest/config';
|
|
15
|
+
* import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
|
|
16
|
+
*
|
|
17
|
+
* export default defineConfig({
|
|
18
|
+
* plugins: [smrtVitestPlugin()],
|
|
19
|
+
* test: {
|
|
20
|
+
* globals: true,
|
|
21
|
+
* environment: 'node',
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
function resolveDefaultSetupFile() {
|
|
33
|
+
const sourceSetupPath = fileURLToPath(new URL('./setup.ts', import.meta.url));
|
|
34
|
+
if (existsSync(sourceSetupPath)) {
|
|
35
|
+
return sourceSetupPath;
|
|
36
|
+
}
|
|
37
|
+
const distSetupPath = fileURLToPath(new URL('./setup.js', import.meta.url));
|
|
38
|
+
if (existsSync(distSetupPath)) {
|
|
39
|
+
return distSetupPath;
|
|
40
|
+
}
|
|
41
|
+
return '@happyvertical/smrt-vitest/setup';
|
|
42
|
+
}
|
|
43
|
+
function findWorkspaceRoot(startDir) {
|
|
44
|
+
let current = startDir;
|
|
45
|
+
while (true) {
|
|
46
|
+
if (existsSync(join(current, 'pnpm-workspace.yaml'))) {
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(current);
|
|
50
|
+
if (parent === current) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
current = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getWorkspaceSourceTsconfigPath(startDir = process.cwd()) {
|
|
57
|
+
const workspaceRoot = findWorkspaceRoot(startDir);
|
|
58
|
+
if (!workspaceRoot) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const tsconfigPath = join(workspaceRoot, 'tsconfig.package-build.json');
|
|
62
|
+
return existsSync(tsconfigPath) ? tsconfigPath : null;
|
|
63
|
+
}
|
|
64
|
+
async function importWorkspaceSourceModule(href) {
|
|
65
|
+
const { register } = await import('tsx/esm/api');
|
|
66
|
+
const tsconfigPath = getWorkspaceSourceTsconfigPath();
|
|
67
|
+
const unregister = register(tsconfigPath ? { tsconfig: tsconfigPath } : undefined);
|
|
68
|
+
try {
|
|
69
|
+
return (await import(href));
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await unregister();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function readWorkspacePackageRoots(root) {
|
|
76
|
+
const workspaceRoot = findWorkspaceRoot(root);
|
|
77
|
+
if (!workspaceRoot) {
|
|
78
|
+
return new Map();
|
|
79
|
+
}
|
|
80
|
+
const packagesDir = join(workspaceRoot, 'packages');
|
|
81
|
+
if (!existsSync(packagesDir)) {
|
|
82
|
+
return new Map();
|
|
83
|
+
}
|
|
84
|
+
const packageRoots = new Map();
|
|
85
|
+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
86
|
+
if (!entry.isDirectory()) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const packageRoot = join(packagesDir, entry.name);
|
|
90
|
+
const packageJsonPath = join(packageRoot, 'package.json');
|
|
91
|
+
if (!existsSync(packageJsonPath)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
96
|
+
if (typeof packageJson.name === 'string') {
|
|
97
|
+
packageRoots.set(packageJson.name, packageRoot);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Ignore invalid package manifests in the workspace scan.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return packageRoots;
|
|
105
|
+
}
|
|
106
|
+
function addAliasIfPresent(aliases, find, replacement) {
|
|
107
|
+
if (aliases.some((entry) => entry.find === find) ||
|
|
108
|
+
!existsSync(replacement)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (existsSync(replacement)) {
|
|
112
|
+
aliases.push({ find, replacement });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function getWorkspaceViteAliases(root = process.cwd()) {
|
|
116
|
+
const packageRoots = readWorkspacePackageRoots(root);
|
|
117
|
+
const aliases = [];
|
|
118
|
+
for (const [packageName, packageRoot] of packageRoots.entries()) {
|
|
119
|
+
addAliasIfPresent(aliases, packageName, join(packageRoot, 'src/index.ts'));
|
|
120
|
+
addAliasIfPresent(aliases, `${packageName}/svelte`, join(packageRoot, 'src/svelte/index.ts'));
|
|
121
|
+
addAliasIfPresent(aliases, `${packageName}/sveltekit`, join(packageRoot, 'src/sveltekit/index.ts'));
|
|
122
|
+
addAliasIfPresent(aliases, `${packageName}/ui`, join(packageRoot, 'src/ui.ts'));
|
|
123
|
+
addAliasIfPresent(aliases, `${packageName}/routes`, join(packageRoot, 'src/route-module.ts'));
|
|
124
|
+
addAliasIfPresent(aliases, `${packageName}/playground`, join(packageRoot, 'src/playground.ts'));
|
|
125
|
+
addAliasIfPresent(aliases, `${packageName}/playground`, join(packageRoot, 'src/svelte/playground.ts'));
|
|
126
|
+
addAliasIfPresent(aliases, `${packageName}/manifest`, join(packageRoot, 'src/manifest/index.ts'));
|
|
127
|
+
addAliasIfPresent(aliases, `${packageName}/manifest.json`, join(packageRoot, 'src/manifest/manifest.json'));
|
|
128
|
+
// smrt-chat exposes an internal trusted agent-runtime subpath (S5 #1392).
|
|
129
|
+
addAliasIfPresent(aliases, `${packageName}/internal/agent-runtime`, join(packageRoot, 'src/internal/agent-runtime.ts'));
|
|
130
|
+
if (packageName === '@happyvertical/smrt-core') {
|
|
131
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/testing', join(packageRoot, 'src/testing.ts'));
|
|
132
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/scanner', join(packageRoot, 'src/scanner/index.ts'));
|
|
133
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/vite-plugin', join(packageRoot, 'src/vite-plugin/index.ts'));
|
|
134
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/vite-plugin', join(packageRoot, 'src/vite-plugin.ts'));
|
|
135
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/consumer-plugin', join(packageRoot, 'src/consumer-plugin/index.ts'));
|
|
136
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/consumer-plugin', join(packageRoot, 'src/consumer-plugin.ts'));
|
|
137
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/manifest', join(packageRoot, 'src/manifest/index.ts'));
|
|
138
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/manifest/discover-base-classes', join(packageRoot, 'src/manifest/discover-base-classes.ts'));
|
|
139
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/schema/utils', join(packageRoot, 'src/schema/utils.ts'));
|
|
140
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/utils', join(packageRoot, 'src/utils.ts'));
|
|
141
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/utils/import-workspace-module', join(packageRoot, 'src/utils/import-workspace-module.ts'));
|
|
142
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/migrations', join(packageRoot, 'src/migrations.ts'));
|
|
143
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/runtime', join(packageRoot, 'src/runtime.ts'));
|
|
144
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/registry', join(packageRoot, 'src/registry.ts'));
|
|
145
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators', join(packageRoot, 'src/generators.ts'));
|
|
146
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/cli', join(packageRoot, 'src/generators/cli.ts'));
|
|
147
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/mcp', join(packageRoot, 'src/generators/mcp.ts'));
|
|
148
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/rest', join(packageRoot, 'src/generators/rest.ts'));
|
|
149
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/prebuild', join(packageRoot, 'src/prebuild.ts'));
|
|
150
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-core/decorators', join(packageRoot, 'src/decorators/index.ts'));
|
|
151
|
+
}
|
|
152
|
+
if (packageName === '@happyvertical/smrt-vitest') {
|
|
153
|
+
// Shared Svelte component-test harness (S11 #1416). Flat `src/*.ts` files,
|
|
154
|
+
// so the generic `/svelte` → `src/svelte/index.ts` convention misses them;
|
|
155
|
+
// map the exact subpaths. The length-desc sort below makes these win over
|
|
156
|
+
// the bare-package root alias.
|
|
157
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/svelte', join(packageRoot, 'src/svelte.ts'));
|
|
158
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/svelte-setup', join(packageRoot, 'src/svelte-setup.ts'));
|
|
159
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/a11y', join(packageRoot, 'src/a11y.ts'));
|
|
160
|
+
}
|
|
161
|
+
if (packageName === '@happyvertical/smrt-ui') {
|
|
162
|
+
// smrt-ui holds the domain-agnostic UI leaf (primitives, feedback,
|
|
163
|
+
// layout, calendar, chat, registry, theme system, i18n client). These
|
|
164
|
+
// subpaths map to nested source dirs, so the generic `${packageName}/ui`
|
|
165
|
+
// → `src/ui.ts` convention misses them; alias the exact source files.
|
|
166
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/ui', join(packageRoot, 'src/components/ui/index.ts'));
|
|
167
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/feedback', join(packageRoot, 'src/components/feedback/index.ts'));
|
|
168
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/layout', join(packageRoot, 'src/components/layout/index.ts'));
|
|
169
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/calendar', join(packageRoot, 'src/components/calendar/index.ts'));
|
|
170
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/chat', join(packageRoot, 'src/components/chat/index.ts'));
|
|
171
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/registry', join(packageRoot, 'src/registry/index.ts'));
|
|
172
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/theme', join(packageRoot, 'src/theme/index.ts'));
|
|
173
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/themes', join(packageRoot, 'src/themes/index.ts'));
|
|
174
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/i18n', join(packageRoot, 'src/i18n/index.ts'));
|
|
175
|
+
// Test-support harness (a11y helper + setup) moved here with the leaf;
|
|
176
|
+
// smrt-svelte's surviving component tests import it cross-package.
|
|
177
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/test-support/a11y', join(packageRoot, 'src/test-support/a11y.ts'));
|
|
178
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/test-support/setup', join(packageRoot, 'src/test-support/setup.ts'));
|
|
179
|
+
// utils ships nested helpers consumed by smrt-svelte's surviving forms.
|
|
180
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/utils/forms/formatters.js', join(packageRoot, 'src/utils/forms/formatters.ts'));
|
|
181
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-ui/utils/import-optional.js', join(packageRoot, 'src/utils/import-optional.ts'));
|
|
182
|
+
}
|
|
183
|
+
if (packageName === '@happyvertical/smrt-svelte') {
|
|
184
|
+
// The domain-agnostic UI leaf subpaths moved to @happyvertical/smrt-ui
|
|
185
|
+
// (#1582). smrt-svelte keeps the Node-only server i18n resolver here.
|
|
186
|
+
addAliasIfPresent(aliases, '@happyvertical/smrt-svelte/i18n/server', join(packageRoot, 'src/i18n/server.ts'));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return aliases.sort((left, right) => right.find.length - left.find.length);
|
|
190
|
+
}
|
|
191
|
+
function normalizeAliasEntries(alias) {
|
|
192
|
+
if (Array.isArray(alias)) {
|
|
193
|
+
return alias.filter((entry) => Boolean(entry) &&
|
|
194
|
+
typeof entry === 'object' &&
|
|
195
|
+
'find' in entry &&
|
|
196
|
+
'replacement' in entry);
|
|
197
|
+
}
|
|
198
|
+
if (alias && typeof alias === 'object') {
|
|
199
|
+
return Object.entries(alias).map(([find, replacement]) => ({
|
|
200
|
+
find,
|
|
201
|
+
replacement: String(replacement),
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Discover SMRT packages from package.json dependencies
|
|
208
|
+
*/
|
|
209
|
+
function discoverSmrtPackages(root, additionalPackages = []) {
|
|
210
|
+
const packageJsonPath = join(root, 'package.json');
|
|
211
|
+
if (!existsSync(packageJsonPath)) {
|
|
212
|
+
console.warn('[smrt-vitest] No package.json found at', root);
|
|
213
|
+
return additionalPackages;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
217
|
+
const allDeps = {
|
|
218
|
+
...packageJson.dependencies,
|
|
219
|
+
...packageJson.peerDependencies,
|
|
220
|
+
...packageJson.devDependencies,
|
|
221
|
+
};
|
|
222
|
+
// Find all @happyvertical/smrt-* packages (except smrt-vitest itself)
|
|
223
|
+
const smrtPackages = Object.keys(allDeps).filter((pkg) => pkg.startsWith('@happyvertical/smrt-') &&
|
|
224
|
+
pkg !== '@happyvertical/smrt-vitest');
|
|
225
|
+
// Combine with additional packages, removing duplicates
|
|
226
|
+
const allPackages = [...new Set([...smrtPackages, ...additionalPackages])];
|
|
227
|
+
return allPackages;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
console.error('[smrt-vitest] Failed to read package.json:', error);
|
|
231
|
+
return additionalPackages;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Find the root directory of a package
|
|
236
|
+
* Tries require.resolve first, then falls back to node_modules lookup
|
|
237
|
+
*/
|
|
238
|
+
function findPackageRoot(packageName) {
|
|
239
|
+
const require = createRequire(`${process.cwd()}/package.json`);
|
|
240
|
+
// Method 1: Try require.resolve to find package entry, then walk up to package.json
|
|
241
|
+
try {
|
|
242
|
+
const pkgMainPath = require.resolve(packageName);
|
|
243
|
+
let dir = dirname(pkgMainPath);
|
|
244
|
+
for (let i = 0; i < 10; i++) {
|
|
245
|
+
const pkgJsonPath = join(dir, 'package.json');
|
|
246
|
+
if (existsSync(pkgJsonPath)) {
|
|
247
|
+
try {
|
|
248
|
+
const content = readFileSync(pkgJsonPath, 'utf-8');
|
|
249
|
+
const json = JSON.parse(content);
|
|
250
|
+
if (json.name === packageName) {
|
|
251
|
+
return dir;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Keep walking up
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const parent = dirname(dir);
|
|
259
|
+
if (parent === dir)
|
|
260
|
+
break;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Fall through to Method 2
|
|
266
|
+
}
|
|
267
|
+
// Method 2: Direct node_modules lookup (for file: protocol linked packages)
|
|
268
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules', packageName);
|
|
269
|
+
const pkgJsonPath = join(nodeModulesPath, 'package.json');
|
|
270
|
+
if (existsSync(pkgJsonPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const content = readFileSync(pkgJsonPath, 'utf-8');
|
|
273
|
+
const json = JSON.parse(content);
|
|
274
|
+
if (json.name === packageName) {
|
|
275
|
+
return nodeModulesPath;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Fall through
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Method 3: Workspace package (sibling in monorepo)
|
|
283
|
+
const packageShortName = packageName.split('/').pop() || '';
|
|
284
|
+
const packageWithoutScope = packageShortName.replace(/^smrt-/, '');
|
|
285
|
+
const workspacePaths = [
|
|
286
|
+
join(process.cwd(), '..', packageWithoutScope),
|
|
287
|
+
join(process.cwd(), '..', packageShortName),
|
|
288
|
+
];
|
|
289
|
+
for (const workspacePath of workspacePaths) {
|
|
290
|
+
const workspacePkgPath = join(workspacePath, 'package.json');
|
|
291
|
+
if (existsSync(workspacePkgPath)) {
|
|
292
|
+
try {
|
|
293
|
+
const content = readFileSync(workspacePkgPath, 'utf-8');
|
|
294
|
+
const json = JSON.parse(content);
|
|
295
|
+
if (json.name === packageName) {
|
|
296
|
+
return workspacePath;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Keep trying
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
async function importSmrtCoreModule() {
|
|
307
|
+
const specifier = '@happyvertical/smrt-core';
|
|
308
|
+
try {
|
|
309
|
+
return await import(specifier);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
const fallbackHref = new URL('../../core/src/index.ts', import.meta.url)
|
|
313
|
+
.href;
|
|
314
|
+
return await importWorkspaceSourceModule(fallbackHref);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function importSmrtCoreManifestModule() {
|
|
318
|
+
const specifier = '@happyvertical/smrt-core/manifest';
|
|
319
|
+
try {
|
|
320
|
+
return await import(specifier);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
const fallbackHref = new URL('../../core/src/manifest/index.ts', import.meta.url).href;
|
|
324
|
+
return await importWorkspaceSourceModule(fallbackHref);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function importDiscoverBaseClassesModule() {
|
|
328
|
+
const specifier = '@happyvertical/smrt-core/manifest/discover-base-classes';
|
|
329
|
+
try {
|
|
330
|
+
return await import(specifier);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
const fallbackHref = new URL('../../core/src/manifest/discover-base-classes.ts', import.meta.url).href;
|
|
334
|
+
return await importWorkspaceSourceModule(fallbackHref);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Load manifest from a package using ManifestManager
|
|
339
|
+
*
|
|
340
|
+
* This properly handles the manifest priority order:
|
|
341
|
+
* 1. .smrt/manifest.json (test/dev manifest with all classes)
|
|
342
|
+
* 2. dist/manifest.json (production manifest)
|
|
343
|
+
*/
|
|
344
|
+
async function loadAndRegisterManifest(packageName, verbose) {
|
|
345
|
+
try {
|
|
346
|
+
const { ObjectRegistry } = await importSmrtCoreModule();
|
|
347
|
+
const { ManifestManager } = await importSmrtCoreManifestModule();
|
|
348
|
+
// Find the package root directory
|
|
349
|
+
const packageRoot = findPackageRoot(packageName);
|
|
350
|
+
if (!packageRoot) {
|
|
351
|
+
if (verbose) {
|
|
352
|
+
console.log(`[smrt-vitest] Could not find package root for ${packageName}`);
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
// Use ManifestManager to load manifest with proper priority
|
|
357
|
+
// (.smrt/manifest.json -> dist/manifest.json)
|
|
358
|
+
const manager = new ManifestManager(packageRoot);
|
|
359
|
+
const manifest = manager.loadLocal();
|
|
360
|
+
if (!manifest) {
|
|
361
|
+
if (verbose) {
|
|
362
|
+
console.log(`[smrt-vitest] No manifest found for ${packageName}`);
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const registered = registerManifestObjects(ObjectRegistry, manifest, manifest.packageName || packageName);
|
|
367
|
+
if (verbose || registered > 0) {
|
|
368
|
+
console.log(`[smrt-vitest] Loaded ${registered} classes from ${packageName}`);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (verbose) {
|
|
374
|
+
console.error(`[smrt-vitest] Failed to load manifest from ${packageName}:`, error);
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function registerManifestObjects(ObjectRegistry, manifest, packageName) {
|
|
380
|
+
if (!manifest?.objects) {
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
let registered = 0;
|
|
384
|
+
for (const [name, objectDef] of Object.entries(manifest.objects)) {
|
|
385
|
+
if (!ObjectRegistry.hasClass(name)) {
|
|
386
|
+
ObjectRegistry.registerFromManifest(name, objectDef, packageName);
|
|
387
|
+
registered++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return registered;
|
|
391
|
+
}
|
|
392
|
+
async function loadAndRegisterLocalManifest(root, verbose) {
|
|
393
|
+
try {
|
|
394
|
+
const { ObjectRegistry } = await importSmrtCoreModule();
|
|
395
|
+
const { ManifestManager } = await importSmrtCoreManifestModule();
|
|
396
|
+
const manager = new ManifestManager(root);
|
|
397
|
+
const manifest = manager.loadLocal();
|
|
398
|
+
if (!manifest) {
|
|
399
|
+
if (verbose) {
|
|
400
|
+
console.log('[smrt-vitest] No local manifest found');
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
const registered = registerManifestObjects(ObjectRegistry, manifest, manifest.packageName);
|
|
405
|
+
if (verbose || registered > 0) {
|
|
406
|
+
console.log(`[smrt-vitest] Loaded ${registered} classes from local manifest`);
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (verbose) {
|
|
412
|
+
console.error('[smrt-vitest] Failed to load local manifest:', error);
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Generate local manifest using ManifestBuilder
|
|
419
|
+
*
|
|
420
|
+
* This ensures the manifest is always fresh after adding new classes/fields.
|
|
421
|
+
* The ~1-2s overhead is minimal compared to test execution time.
|
|
422
|
+
*/
|
|
423
|
+
async function generateLocalManifest(_root, options, verbose) {
|
|
424
|
+
try {
|
|
425
|
+
console.log('[smrt-vitest] Generating test manifest...');
|
|
426
|
+
const { ManifestBuilder } = await importSmrtCoreManifestModule();
|
|
427
|
+
const { discoverBaseClasses } = await importDiscoverBaseClassesModule();
|
|
428
|
+
// Discover base classes from external SMRT packages
|
|
429
|
+
const baseClasses = await discoverBaseClasses();
|
|
430
|
+
if (verbose) {
|
|
431
|
+
console.log(`[smrt-vitest] Discovered ${baseClasses.length} base classes (including ${baseClasses.length - 3} from external packages)`);
|
|
432
|
+
}
|
|
433
|
+
const builder = new ManifestBuilder();
|
|
434
|
+
const manifest = await builder.generate({
|
|
435
|
+
// File discovery
|
|
436
|
+
include: options.include || ['src/**/*.ts'],
|
|
437
|
+
exclude: options.exclude || [
|
|
438
|
+
'**/*.d.ts',
|
|
439
|
+
'**/node_modules/**',
|
|
440
|
+
'**/dist/**',
|
|
441
|
+
],
|
|
442
|
+
// Scanner configuration
|
|
443
|
+
baseClasses,
|
|
444
|
+
followImports: true,
|
|
445
|
+
loadViteConfig: true,
|
|
446
|
+
discoverExternalPackages: true,
|
|
447
|
+
includeExternalBaseClasses: true,
|
|
448
|
+
includePrivateMethods: false,
|
|
449
|
+
includeStaticMethods: true,
|
|
450
|
+
// Output configuration - write to .smrt directory (ManifestManager default)
|
|
451
|
+
outputDir: '.smrt',
|
|
452
|
+
outputName: 'manifest.json',
|
|
453
|
+
generateTypeStub: false,
|
|
454
|
+
// Metadata
|
|
455
|
+
injectPackageInfo: true,
|
|
456
|
+
moduleType: 'smrt',
|
|
457
|
+
});
|
|
458
|
+
const objectCount = Object.keys(manifest.objects).length;
|
|
459
|
+
console.log(`[smrt-vitest] ✓ Generated manifest with ${objectCount} object(s)`);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.error('[smrt-vitest] Failed to generate manifest:', error);
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Resolve the per-test `retry` injected into the vitest config.
|
|
469
|
+
*
|
|
470
|
+
* Precedence: `SMRT_VITEST_RETRY` (a digits-only, non-negative integer) wins;
|
|
471
|
+
* otherwise an explicit consumer value is preserved as-is — including the object
|
|
472
|
+
* form (`{ count, delay }`) — so options aren't dropped; otherwise the default is
|
|
473
|
+
* 2 retries in CI (`process.env.CI`) and 0 everywhere else, so local runs surface
|
|
474
|
+
* flaky tests immediately while the shared cross-package CI job tolerates rare
|
|
475
|
+
* transient timing flakes.
|
|
476
|
+
*/
|
|
477
|
+
function resolveRetry(explicitRetry) {
|
|
478
|
+
const override = process.env.SMRT_VITEST_RETRY;
|
|
479
|
+
// Digits-only so "2x"/"2.5"/"-1" fall through to the explicit/default value
|
|
480
|
+
// rather than being silently coerced by parseInt.
|
|
481
|
+
if (override != null && /^\d+$/.test(override)) {
|
|
482
|
+
return Number.parseInt(override, 10);
|
|
483
|
+
}
|
|
484
|
+
if (explicitRetry != null) {
|
|
485
|
+
return explicitRetry;
|
|
486
|
+
}
|
|
487
|
+
return process.env.CI ? 2 : 0;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Create the SMRT Vitest plugin
|
|
491
|
+
*
|
|
492
|
+
* This plugin automatically generates and loads manifests before tests run,
|
|
493
|
+
* enabling cross-package integration tests without needing to run `smrt test` first.
|
|
494
|
+
*
|
|
495
|
+
* @param options - Plugin configuration options
|
|
496
|
+
* @returns Vitest plugin
|
|
497
|
+
*
|
|
498
|
+
* @example Basic usage
|
|
499
|
+
* ```typescript
|
|
500
|
+
* import { defineConfig } from 'vitest/config';
|
|
501
|
+
* import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
|
|
502
|
+
*
|
|
503
|
+
* export default defineConfig({
|
|
504
|
+
* plugins: [smrtVitestPlugin()],
|
|
505
|
+
* });
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* @example With additional packages
|
|
509
|
+
* ```typescript
|
|
510
|
+
* import { defineConfig } from 'vitest/config';
|
|
511
|
+
* import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
|
|
512
|
+
*
|
|
513
|
+
* export default defineConfig({
|
|
514
|
+
* plugins: [
|
|
515
|
+
* smrtVitestPlugin({
|
|
516
|
+
* packages: ['@my-org/custom-smrt-package'],
|
|
517
|
+
* verbose: true,
|
|
518
|
+
* }),
|
|
519
|
+
* ],
|
|
520
|
+
* });
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* @example Disable auto-generation (use pre-built manifest)
|
|
524
|
+
* ```typescript
|
|
525
|
+
* export default defineConfig({
|
|
526
|
+
* plugins: [
|
|
527
|
+
* smrtVitestPlugin({
|
|
528
|
+
* generateManifest: false, // Use existing manifest only
|
|
529
|
+
* }),
|
|
530
|
+
* ],
|
|
531
|
+
* });
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
export function smrtVitestPlugin(options = {}) {
|
|
535
|
+
const { packages = [], verbose = false, root = process.cwd(), generateManifest = true, setupFile = resolveDefaultSetupFile(), } = options;
|
|
536
|
+
let manifestsLoaded = false;
|
|
537
|
+
const setupFileId = setupFile;
|
|
538
|
+
const workspaceAliases = getWorkspaceViteAliases(root);
|
|
539
|
+
const ensureSetupFiles = (value) => {
|
|
540
|
+
const setupFiles = Array.isArray(value) ? [...value] : value ? [value] : [];
|
|
541
|
+
if (!setupFiles.includes(setupFileId)) {
|
|
542
|
+
setupFiles.push(setupFileId);
|
|
543
|
+
}
|
|
544
|
+
return setupFiles;
|
|
545
|
+
};
|
|
546
|
+
const applyTestDefaultsToProjects = (projects, rootRetry) => {
|
|
547
|
+
projects?.forEach((project) => {
|
|
548
|
+
if (!project || typeof project !== 'object' || !('test' in project)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const projectConfig = project;
|
|
552
|
+
projectConfig.test = {
|
|
553
|
+
...projectConfig.test,
|
|
554
|
+
// Vitest does NOT inherit the root `test.retry` into per-project configs,
|
|
555
|
+
// so apply it here, falling back to the root retry (then the CI default)
|
|
556
|
+
// when the project has none. resolveRetry preserves an explicit
|
|
557
|
+
// per-project value unless SMRT_VITEST_RETRY forces one.
|
|
558
|
+
retry: resolveRetry(projectConfig.test?.retry ?? rootRetry),
|
|
559
|
+
setupFiles: ensureSetupFiles(projectConfig.test?.setupFiles),
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
name: 'smrt-vitest',
|
|
565
|
+
config(userConfig) {
|
|
566
|
+
const rootRetry = userConfig.test?.retry;
|
|
567
|
+
applyTestDefaultsToProjects(userConfig.test?.projects, rootRetry);
|
|
568
|
+
const setupFiles = ensureSetupFiles(userConfig.test?.setupFiles);
|
|
569
|
+
const resolveConfig = userConfig.resolve && typeof userConfig.resolve === 'object'
|
|
570
|
+
? userConfig.resolve
|
|
571
|
+
: undefined;
|
|
572
|
+
const alias = normalizeAliasEntries(resolveConfig?.alias);
|
|
573
|
+
return {
|
|
574
|
+
resolve: {
|
|
575
|
+
alias: [...workspaceAliases, ...alias],
|
|
576
|
+
},
|
|
577
|
+
test: {
|
|
578
|
+
setupFiles,
|
|
579
|
+
// Re-run a failed test before failing the run, in CI only. Several
|
|
580
|
+
// packages have rare, CI-environment-specific timing flakes that pass
|
|
581
|
+
// on re-run (observed: every flaky "Test Packages" failure went green
|
|
582
|
+
// on rerun, on a different package each time, none reproducible
|
|
583
|
+
// locally). Retry keeps the shared cross-package CI job reliable
|
|
584
|
+
// WITHOUT masking real failures — a deterministic failure still fails
|
|
585
|
+
// all attempts, and vitest reports retried tests as "flaky" so they
|
|
586
|
+
// stay visible. An explicit `test.retry` in the consumer config is
|
|
587
|
+
// preserved (including the object form); override with
|
|
588
|
+
// SMRT_VITEST_RETRY=<n>.
|
|
589
|
+
retry: resolveRetry(rootRetry),
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
// Run during config resolution to ensure manifests are loaded before tests
|
|
594
|
+
async configResolved() {
|
|
595
|
+
if (manifestsLoaded)
|
|
596
|
+
return;
|
|
597
|
+
// Step 1: Generate local manifest if enabled (default: true)
|
|
598
|
+
// This ensures manifest is always fresh after adding new classes/fields
|
|
599
|
+
if (generateManifest) {
|
|
600
|
+
await generateLocalManifest(root, options, verbose);
|
|
601
|
+
}
|
|
602
|
+
// Step 2: Load the local manifest so late-imported local classes are
|
|
603
|
+
// available to schema preparation before the first DB call.
|
|
604
|
+
await loadAndRegisterLocalManifest(root, verbose);
|
|
605
|
+
// Step 3: Discover and load manifests from SMRT peer dependencies
|
|
606
|
+
const smrtPackages = discoverSmrtPackages(root, packages);
|
|
607
|
+
if (smrtPackages.length === 0) {
|
|
608
|
+
if (verbose) {
|
|
609
|
+
console.log('[smrt-vitest] No SMRT packages found to load');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
if (verbose) {
|
|
614
|
+
console.log(`[smrt-vitest] Discovered ${smrtPackages.length} SMRT packages:`, smrtPackages);
|
|
615
|
+
}
|
|
616
|
+
// Load manifests from all discovered packages
|
|
617
|
+
const results = await Promise.all(smrtPackages.map((pkg) => loadAndRegisterManifest(pkg, verbose)));
|
|
618
|
+
const successCount = results.filter(Boolean).length;
|
|
619
|
+
console.log(`[smrt-vitest] Loaded manifests from ${successCount}/${smrtPackages.length} packages`);
|
|
620
|
+
}
|
|
621
|
+
// Step 4: Validate local manifest is loaded
|
|
622
|
+
try {
|
|
623
|
+
const { ManifestManager } = await importSmrtCoreManifestModule();
|
|
624
|
+
const manager = new ManifestManager(root);
|
|
625
|
+
const localManifest = manager.loadLocal();
|
|
626
|
+
if (localManifest) {
|
|
627
|
+
console.log(`[smrt-vitest] ✓ Local manifest: ${Object.keys(localManifest.objects).length} objects`);
|
|
628
|
+
}
|
|
629
|
+
else if (!generateManifest) {
|
|
630
|
+
// Only show warning if auto-generation is disabled
|
|
631
|
+
// (if enabled and still missing, generateLocalManifest already logged an error)
|
|
632
|
+
const devPath = manager.getOutputPath('dev');
|
|
633
|
+
const buildPath = manager.getOutputPath('build');
|
|
634
|
+
console.warn(`
|
|
635
|
+
╔═══════════════════════════════════════════════════════════════════════╗
|
|
636
|
+
║ [smrt-vitest] WARNING: No local manifest found ║
|
|
637
|
+
╠═══════════════════════════════════════════════════════════════════════╣
|
|
638
|
+
║ Tests may fail with "No field metadata found" errors. ║
|
|
639
|
+
║ ║
|
|
640
|
+
║ Checked locations: ║
|
|
641
|
+
║ • ${devPath.padEnd(55)}║
|
|
642
|
+
║ • ${buildPath.padEnd(55)}║
|
|
643
|
+
║ ║
|
|
644
|
+
║ To fix, either: ║
|
|
645
|
+
║ • Enable generateManifest: true in plugin options (default) ║
|
|
646
|
+
║ • Run: smrt generate:test ║
|
|
647
|
+
║ • Run: npm run build (if manifest is part of build) ║
|
|
648
|
+
╚═══════════════════════════════════════════════════════════════════════╝
|
|
649
|
+
`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (verbose) {
|
|
654
|
+
console.warn('[smrt-vitest] Could not validate local manifest:', error);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
manifestsLoaded = true;
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Discover and register SMRT manifests from peer dependencies.
|
|
663
|
+
*
|
|
664
|
+
* An imperative alternative to {@link smrtVitestPlugin} for environments
|
|
665
|
+
* where a Vite plugin is not available (e.g., a plain `globalSetup` file or
|
|
666
|
+
* a custom test runner bootstrap).
|
|
667
|
+
*
|
|
668
|
+
* The function reads `package.json` in the working directory, finds all
|
|
669
|
+
* `@happyvertical/smrt-*` dependencies, locates their manifest files, and
|
|
670
|
+
* registers every class in the global `ObjectRegistry`. It does **not**
|
|
671
|
+
* generate a new manifest — use `smrtVitestPlugin()` with
|
|
672
|
+
* `generateManifest: true` (the default) if auto-generation is needed.
|
|
673
|
+
*
|
|
674
|
+
* @param options - Same options accepted by {@link smrtVitestPlugin}.
|
|
675
|
+
* Relevant fields: `packages`, `verbose`, `root`.
|
|
676
|
+
* @returns A promise that resolves once all manifests have been loaded.
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* // vitest.config.ts
|
|
681
|
+
* import { defineConfig } from 'vitest/config';
|
|
682
|
+
*
|
|
683
|
+
* export default defineConfig({
|
|
684
|
+
* test: {
|
|
685
|
+
* globalSetup: ['@happyvertical/smrt-vitest/setup'],
|
|
686
|
+
* },
|
|
687
|
+
* });
|
|
688
|
+
* ```
|
|
689
|
+
*
|
|
690
|
+
* @example Calling directly in a custom bootstrap
|
|
691
|
+
* ```typescript
|
|
692
|
+
* import { setupSmrtManifests } from '@happyvertical/smrt-vitest';
|
|
693
|
+
*
|
|
694
|
+
* await setupSmrtManifests({ verbose: true });
|
|
695
|
+
* ```
|
|
696
|
+
*
|
|
697
|
+
* @see {@link smrtVitestPlugin} for the recommended Vite-plugin approach that
|
|
698
|
+
* also handles manifest generation.
|
|
699
|
+
*/
|
|
700
|
+
export async function setupSmrtManifests(options = {}) {
|
|
701
|
+
const { packages = [], verbose = false, root = process.cwd() } = options;
|
|
702
|
+
await loadAndRegisterLocalManifest(root, verbose);
|
|
703
|
+
const smrtPackages = discoverSmrtPackages(root, packages);
|
|
704
|
+
if (smrtPackages.length === 0) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (verbose) {
|
|
708
|
+
console.log(`[smrt-vitest] Discovered ${smrtPackages.length} SMRT packages:`, smrtPackages);
|
|
709
|
+
}
|
|
710
|
+
// Load manifests from all discovered packages
|
|
711
|
+
const results = await Promise.all(smrtPackages.map((pkg) => loadAndRegisterManifest(pkg, verbose)));
|
|
712
|
+
const successCount = results.filter(Boolean).length;
|
|
713
|
+
console.log(`[smrt-vitest] Loaded manifests from ${successCount}/${smrtPackages.length} packages`);
|
|
714
|
+
}
|
|
715
|
+
export default smrtVitestPlugin;
|
|
716
|
+
// Export test database utilities
|
|
717
|
+
export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, isPostgresAvailable, } from './test-db.js';
|
|
718
|
+
//# sourceMappingURL=index.js.map
|