@happyvertical/smrt-vitest 0.57.46 → 0.57.47
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 +56 -2
- package/README.md +13 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +75 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +459 -71
- package/dist/index.js.map +1 -1
- package/dist/test-db.d.ts +19 -5
- package/dist/test-db.d.ts.map +1 -1
- package/dist/test-db.js +37 -13
- package/dist/test-db.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* external package classes aren't registered in the test manifest.
|
|
7
7
|
*
|
|
8
8
|
* Uses ManifestManager for unified manifest loading, which properly handles
|
|
9
|
-
* the manifest priority order:
|
|
9
|
+
* the manifest priority order: this run's test manifest snapshot
|
|
10
|
+
* (`.smrt/test-runs/<run>/manifest.json`, #4128) -> .smrt/manifest.json ->
|
|
11
|
+
* dist/manifest.json
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```typescript
|
|
@@ -25,9 +27,11 @@
|
|
|
25
27
|
*
|
|
26
28
|
* @packageDocumentation
|
|
27
29
|
*/
|
|
28
|
-
import {
|
|
30
|
+
import { randomBytes } from 'node:crypto';
|
|
31
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
29
32
|
import { createRequire } from 'node:module';
|
|
30
|
-
import {
|
|
33
|
+
import { hostname } from 'node:os';
|
|
34
|
+
import { dirname, join, resolve } from 'node:path';
|
|
31
35
|
import { fileURLToPath } from 'node:url';
|
|
32
36
|
import { configDefaults } from 'vitest/config';
|
|
33
37
|
import { isVerbose, shouldLogManifestSummaryOnce } from './log.js';
|
|
@@ -118,6 +122,130 @@ export function setManifestRegistrationOptionsForRoot(root, options) {
|
|
|
118
122
|
byRoot[key] = options;
|
|
119
123
|
process.env[SMRT_VITEST_SETUP_OPTIONS_ENV_KEY] = JSON.stringify(byRoot);
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Environment variable carrying the active test manifest path per project
|
|
127
|
+
* root. Mirrors `SMRT_TEST_MANIFEST_PATHS_ENV_KEY` in
|
|
128
|
+
* `@happyvertical/smrt-core/manifest` (`test-artifacts.ts`), which owns the
|
|
129
|
+
* read side; kept as a literal here so {@link smrtVitestPlugin}'s `config()`
|
|
130
|
+
* hook can publish it synchronously, before Vitest forks any pool worker
|
|
131
|
+
* (see {@link SMRT_VITEST_SETUP_OPTIONS_ENV_KEY}). Parity with core is
|
|
132
|
+
* asserted in `src/__tests__/test-run-manifest-isolation.test.ts`.
|
|
133
|
+
*/
|
|
134
|
+
export const SMRT_TEST_MANIFEST_PATHS_ENV_KEY = '__SMRT_TEST_MANIFEST_PATHS__';
|
|
135
|
+
/**
|
|
136
|
+
* Directory, relative to a project root, holding one subdirectory per test
|
|
137
|
+
* run's manifest snapshot (#4128).
|
|
138
|
+
*/
|
|
139
|
+
export const SMRT_TEST_RUNS_DIR = '.smrt/test-runs';
|
|
140
|
+
/** A run directory whose owner is unknown or remote is pruned after this. */
|
|
141
|
+
const STALE_TEST_RUN_MS = 24 * 60 * 60 * 1000;
|
|
142
|
+
const testRunManifestPaths = new Map();
|
|
143
|
+
function isProcessAlive(pid) {
|
|
144
|
+
try {
|
|
145
|
+
process.kill(pid, 0);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// EPERM: the process exists but belongs to someone else.
|
|
150
|
+
return error.code === 'EPERM';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Remove snapshot directories whose run has ended. A run recorded on this
|
|
155
|
+
* host is removed only once its process is gone, however long it has been
|
|
156
|
+
* running (a watch or `--ui` session can outlive a day). A run whose owner
|
|
157
|
+
* cannot be probed — no readable `owner.json`, or another host or container
|
|
158
|
+
* sharing the checkout — is removed once it is older than a day.
|
|
159
|
+
*/
|
|
160
|
+
function pruneEndedTestRuns(root, keep) {
|
|
161
|
+
const runsDir = join(root, SMRT_TEST_RUNS_DIR);
|
|
162
|
+
let entries;
|
|
163
|
+
try {
|
|
164
|
+
entries = readdirSync(runsDir);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const thisHost = hostname();
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const runDir = join(runsDir, entry);
|
|
172
|
+
if (runDir === keep)
|
|
173
|
+
continue;
|
|
174
|
+
let owner;
|
|
175
|
+
try {
|
|
176
|
+
owner = JSON.parse(readFileSync(join(runDir, 'owner.json'), 'utf8'));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
owner = undefined;
|
|
180
|
+
}
|
|
181
|
+
let ageMs;
|
|
182
|
+
try {
|
|
183
|
+
ageMs = Date.now() - (owner?.startedAt ?? statSync(runDir).mtimeMs);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const probeable = owner?.hostname === thisHost && typeof owner.pid === 'number';
|
|
189
|
+
const ended = probeable
|
|
190
|
+
? owner?.pid !== process.pid && !isProcessAlive(owner?.pid)
|
|
191
|
+
: ageMs > STALE_TEST_RUN_MS;
|
|
192
|
+
if (ended) {
|
|
193
|
+
rmSync(runDir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The manifest snapshot path owned by this process's test run for `root`
|
|
199
|
+
* (#4128): `<root>/.smrt/test-runs/<pid>-<time>-<random>/manifest.json`.
|
|
200
|
+
*
|
|
201
|
+
* Test generation scans test-inclusive sources, so it must not share a path
|
|
202
|
+
* with production generation: `.smrt/manifest.json` is rewritten by every
|
|
203
|
+
* `smrtPlugin()` build (and by a typecheck whose Turbo graph rebuilds the
|
|
204
|
+
* package), which used to replace the test manifest while forked test
|
|
205
|
+
* workers were still reading it. Each run instead owns a directory nothing
|
|
206
|
+
* else writes; it is stable for the life of this process, so every plugin
|
|
207
|
+
* instance and config restart for the same root shares it, and removed when
|
|
208
|
+
* the process exits.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveTestRunManifestPath(root) {
|
|
211
|
+
const key = normalizeRootKey(resolve(root));
|
|
212
|
+
const existing = testRunManifestPaths.get(key);
|
|
213
|
+
if (existing)
|
|
214
|
+
return existing;
|
|
215
|
+
const runId = `${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
216
|
+
const manifestPath = join(key, SMRT_TEST_RUNS_DIR, runId, 'manifest.json');
|
|
217
|
+
testRunManifestPaths.set(key, manifestPath);
|
|
218
|
+
const runDir = dirname(manifestPath);
|
|
219
|
+
process.once('exit', () => {
|
|
220
|
+
rmSync(runDir, { recursive: true, force: true });
|
|
221
|
+
});
|
|
222
|
+
return manifestPath;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Publish `manifestPath` as the active test manifest for `root`, merged into
|
|
226
|
+
* {@link SMRT_TEST_MANIFEST_PATHS_ENV_KEY}. Keys match core's reader: the
|
|
227
|
+
* root as given, trimmed of trailing separators, and realpath'd.
|
|
228
|
+
*/
|
|
229
|
+
function registerTestRunManifestPath(root, manifestPath) {
|
|
230
|
+
let byRoot = {};
|
|
231
|
+
const raw = process.env[SMRT_TEST_MANIFEST_PATHS_ENV_KEY];
|
|
232
|
+
if (raw) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
236
|
+
byRoot = parsed;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// A malformed prior value is replaced.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const trimmed = root.replace(/[/\\]+$/, '') || root;
|
|
244
|
+
for (const key of new Set([root, trimmed, normalizeRootKey(root)])) {
|
|
245
|
+
byRoot[key] = manifestPath;
|
|
246
|
+
}
|
|
247
|
+
process.env[SMRT_TEST_MANIFEST_PATHS_ENV_KEY] = JSON.stringify(byRoot);
|
|
248
|
+
}
|
|
121
249
|
function resolveDefaultSetupFile() {
|
|
122
250
|
const sourceSetupPath = fileURLToPath(new URL('./setup.ts', import.meta.url));
|
|
123
251
|
if (existsSync(sourceSetupPath)) {
|
|
@@ -195,6 +323,147 @@ function readWorkspacePackageRoots(root) {
|
|
|
195
323
|
}
|
|
196
324
|
return packageRoots;
|
|
197
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* The runtime target an export condition object resolves to for a Node test
|
|
328
|
+
* run: `import`, then `default`, then `svelte` (nested condition objects are
|
|
329
|
+
* followed). `types` and `browser` are never runtime targets here.
|
|
330
|
+
*/
|
|
331
|
+
function exportRuntimeTarget(value) {
|
|
332
|
+
if (typeof value === 'string')
|
|
333
|
+
return value;
|
|
334
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
335
|
+
return null;
|
|
336
|
+
const conditions = value;
|
|
337
|
+
for (const condition of ['import', 'default', 'svelte']) {
|
|
338
|
+
const target = exportRuntimeTarget(conditions[condition]);
|
|
339
|
+
if (target)
|
|
340
|
+
return target;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Source file a built export target was compiled from (#2954): `./dist/<p>.js`
|
|
346
|
+
* maps to `src/<p>.ts`, `src/<p>.svelte.ts`, `src/<p>/index.ts`, or, for
|
|
347
|
+
* `svelte-package` layouts, the same under `src/lib/`. Returns `null` for
|
|
348
|
+
* targets outside `dist/` or with no matching source file.
|
|
349
|
+
*/
|
|
350
|
+
function sourceForExportTarget(packageRoot, target) {
|
|
351
|
+
const match = /^\.\/dist\/(.+?)\.(?:m?js)$/.exec(target);
|
|
352
|
+
if (!match)
|
|
353
|
+
return null;
|
|
354
|
+
const built = match[1];
|
|
355
|
+
for (const base of ['src', 'src/lib']) {
|
|
356
|
+
for (const candidate of [
|
|
357
|
+
`${base}/${built}.ts`,
|
|
358
|
+
`${base}/${built}.svelte.ts`,
|
|
359
|
+
`${base}/${built}/index.ts`,
|
|
360
|
+
]) {
|
|
361
|
+
const path = join(packageRoot, candidate);
|
|
362
|
+
if (existsSync(path))
|
|
363
|
+
return path;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Every exact (non-pattern) JavaScript subpath a workspace package exports,
|
|
370
|
+
* with the source file it is built from, or `null` when no source file can be
|
|
371
|
+
* derived. Read from the package's own `exports` map, so a newly exported
|
|
372
|
+
* subpath is covered without editing this file (#2954).
|
|
373
|
+
*/
|
|
374
|
+
export function getWorkspaceExportSources(packageRoot) {
|
|
375
|
+
let exportsMap;
|
|
376
|
+
try {
|
|
377
|
+
exportsMap = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')).exports;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
if (!exportsMap || typeof exportsMap !== 'object')
|
|
383
|
+
return [];
|
|
384
|
+
const entries = [];
|
|
385
|
+
for (const [subpath, value] of Object.entries(exportsMap)) {
|
|
386
|
+
if (!subpath.startsWith('.') || subpath.includes('*'))
|
|
387
|
+
continue;
|
|
388
|
+
const target = exportRuntimeTarget(value);
|
|
389
|
+
if (!target || !/\.m?js$/.test(target))
|
|
390
|
+
continue;
|
|
391
|
+
entries.push({
|
|
392
|
+
subpath,
|
|
393
|
+
target,
|
|
394
|
+
source: sourceForExportTarget(packageRoot, target),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return entries;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The concrete runtime target (`./dist/...`) a workspace package's exports
|
|
401
|
+
* map gives `subpath`, exact or pattern, or `null` when it exports none.
|
|
402
|
+
*/
|
|
403
|
+
export function workspaceExportTarget(packageRoot, subpath) {
|
|
404
|
+
let exportsMap;
|
|
405
|
+
try {
|
|
406
|
+
exportsMap = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')).exports;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (!exportsMap || typeof exportsMap !== 'object')
|
|
412
|
+
return null;
|
|
413
|
+
const entries = exportsMap;
|
|
414
|
+
if (subpath in entries)
|
|
415
|
+
return exportRuntimeTarget(entries[subpath]);
|
|
416
|
+
// Node picks the pattern with the longest prefix before `*`.
|
|
417
|
+
const patterns = Object.entries(entries)
|
|
418
|
+
.filter(([pattern]) => pattern.includes('*'))
|
|
419
|
+
.sort(([left], [right]) => right.indexOf('*') - left.indexOf('*'));
|
|
420
|
+
for (const [pattern, value] of patterns) {
|
|
421
|
+
const star = pattern.indexOf('*');
|
|
422
|
+
if (star < 0)
|
|
423
|
+
continue;
|
|
424
|
+
const prefix = pattern.slice(0, star);
|
|
425
|
+
const suffix = pattern.slice(star + 1);
|
|
426
|
+
if (!subpath.startsWith(prefix) ||
|
|
427
|
+
!subpath.endsWith(suffix) ||
|
|
428
|
+
subpath.length < prefix.length + suffix.length) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const target = exportRuntimeTarget(value);
|
|
432
|
+
if (!target?.includes('*'))
|
|
433
|
+
continue;
|
|
434
|
+
return target
|
|
435
|
+
.split('*')
|
|
436
|
+
.join(subpath.slice(prefix.length, subpath.length - suffix.length));
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Source file for the concrete `subpath` a consumer imports from a workspace
|
|
442
|
+
* package — exact or pattern export (`"./utils/*": "./dist/utils/*"`) — for
|
|
443
|
+
* the per-import resolution in {@link smrtVitestPlugin} (#2954). Exact
|
|
444
|
+
* subpaths are normally aliased statically; pattern subpaths can only be
|
|
445
|
+
* mapped per import. A JavaScript target maps like an exact export; another
|
|
446
|
+
* asset (CSS, JSON) maps to the same relative path under `src/` or
|
|
447
|
+
* `src/lib/`. Returns `null` when nothing is exported there or no source
|
|
448
|
+
* file exists.
|
|
449
|
+
*/
|
|
450
|
+
export function resolveWorkspaceExportSource(packageRoot, subpath) {
|
|
451
|
+
const concrete = workspaceExportTarget(packageRoot, subpath);
|
|
452
|
+
if (!concrete)
|
|
453
|
+
return null;
|
|
454
|
+
const jsSource = sourceForExportTarget(packageRoot, concrete);
|
|
455
|
+
if (jsSource)
|
|
456
|
+
return jsSource;
|
|
457
|
+
const asset = /^\.\/dist\/(.+)$/.exec(concrete);
|
|
458
|
+
if (asset && !/\.m?js$/.test(concrete)) {
|
|
459
|
+
for (const base of ['src', 'src/lib']) {
|
|
460
|
+
const path = join(packageRoot, base, asset[1]);
|
|
461
|
+
if (existsSync(path))
|
|
462
|
+
return path;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
198
467
|
function addAliasIfPresent(aliases, find, replacement) {
|
|
199
468
|
if (aliases.some((entry) => entry.find === find) ||
|
|
200
469
|
!existsSync(replacement)) {
|
|
@@ -318,6 +587,21 @@ export function getWorkspaceViteAliases(root = process.cwd(), options = {}) {
|
|
|
318
587
|
addAliasIfPresent(aliases, '@happyvertical/smrt-svelte/cookbook', join(packageRoot, 'src/components/cookbook/index.ts'));
|
|
319
588
|
}
|
|
320
589
|
}
|
|
590
|
+
// Every remaining exported subpath resolves to the source it is built from
|
|
591
|
+
// (#2954). The hand-written entries above win (`addAliasIfPresent` keeps the
|
|
592
|
+
// first entry per specifier): they cover entries whose source layout the
|
|
593
|
+
// derivation cannot infer. Without an alias a consumer test resolves the
|
|
594
|
+
// subpath through the exports map to the sibling's last BUILT dist — stale,
|
|
595
|
+
// or missing in a fresh worktree — while resolving the package root to
|
|
596
|
+
// source, which also loads two copies of shared modules.
|
|
597
|
+
for (const [packageName, packageRoot] of packageRoots.entries()) {
|
|
598
|
+
for (const { subpath, source } of getWorkspaceExportSources(packageRoot)) {
|
|
599
|
+
if (!source)
|
|
600
|
+
continue;
|
|
601
|
+
const specifier = subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`;
|
|
602
|
+
addAliasIfPresent(aliases, specifier, source);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
321
605
|
const { filter } = options;
|
|
322
606
|
const kept = filter ? aliases.filter((entry) => filter(entry)) : aliases;
|
|
323
607
|
return kept
|
|
@@ -330,6 +614,68 @@ export function getWorkspaceViteAliases(root = process.cwd(), options = {}) {
|
|
|
330
614
|
replacement,
|
|
331
615
|
}));
|
|
332
616
|
}
|
|
617
|
+
/** Forward-slash form of a filesystem path, for prefix comparisons. */
|
|
618
|
+
function toPosix(path) {
|
|
619
|
+
return path.replace(/\\/g, '/');
|
|
620
|
+
}
|
|
621
|
+
/** The package name a bare or subpath specifier belongs to. */
|
|
622
|
+
function packageNameOf(specifier) {
|
|
623
|
+
const parts = specifier.split('/');
|
|
624
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Detects a workspace package specifier resolved to a sibling's BUILT output
|
|
628
|
+
* (#2954).
|
|
629
|
+
*
|
|
630
|
+
* Every exported subpath of a workspace package is aliased to source (see
|
|
631
|
+
* {@link getWorkspaceViteAliases}), so a module loaded from another
|
|
632
|
+
* package's `dist/` means a specifier escaped the aliases: the test would
|
|
633
|
+
* run the sibling's last build — stale, or missing in a fresh worktree — and
|
|
634
|
+
* could pass against code that no longer exists. Returns the failure message
|
|
635
|
+
* for such an id, or `null`. Exempt: the package under test (its own build
|
|
636
|
+
* output), and packages the consumer deliberately resolves to built entries,
|
|
637
|
+
* by dropping their generated aliases through `aliasFilter` or by aliasing
|
|
638
|
+
* one of their specifiers to a `dist/` file itself.
|
|
639
|
+
*/
|
|
640
|
+
export function createWorkspaceDistGuard(root, exemptPackages = new Set()) {
|
|
641
|
+
const ownRoot = normalizeRootKey(resolve(root));
|
|
642
|
+
const distRoots = [];
|
|
643
|
+
for (const [name, packageRoot] of readWorkspacePackageRoots(root)) {
|
|
644
|
+
const real = normalizeRootKey(packageRoot);
|
|
645
|
+
if (real === ownRoot || exemptPackages.has(name))
|
|
646
|
+
continue;
|
|
647
|
+
distRoots.push({ name, distRoot: `${toPosix(real)}/dist/` });
|
|
648
|
+
}
|
|
649
|
+
return (id) => {
|
|
650
|
+
const file = toPosix(id.split('?')[0] ?? id);
|
|
651
|
+
const owner = distRoots.find(({ distRoot }) => file.startsWith(distRoot));
|
|
652
|
+
if (!owner)
|
|
653
|
+
return null;
|
|
654
|
+
return (`[smrt-vitest] ${file} is the built output of workspace package ${owner.name}. ` +
|
|
655
|
+
'A consumer test must run its source, not a possibly stale or missing dist (#2954). ' +
|
|
656
|
+
`Export the subpath from ${owner.name}'s package.json with a source file the ` +
|
|
657
|
+
'workspace aliases can derive (dist/<p>.js <- src/<p>.ts, src/<p>/index.ts, src/lib/...), ' +
|
|
658
|
+
'or resolve published entries deliberately via aliasFilter.');
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/** Workspace packages whose `dist/` a consumer config deliberately targets. */
|
|
662
|
+
function packagesWithDeliberateDistEntries(root, droppedSpecifiers, userAliases) {
|
|
663
|
+
const exempt = new Set();
|
|
664
|
+
for (const specifier of droppedSpecifiers) {
|
|
665
|
+
exempt.add(packageNameOf(specifier));
|
|
666
|
+
}
|
|
667
|
+
const roots = readWorkspacePackageRoots(root);
|
|
668
|
+
for (const { replacement } of userAliases) {
|
|
669
|
+
for (const [name, packageRoot] of roots) {
|
|
670
|
+
const target = typeof replacement === 'string' ? toPosix(replacement) : '';
|
|
671
|
+
if (target.startsWith(`${toPosix(packageRoot)}/dist/`) ||
|
|
672
|
+
target.startsWith(`${toPosix(normalizeRootKey(packageRoot))}/dist/`)) {
|
|
673
|
+
exempt.add(name);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return exempt;
|
|
678
|
+
}
|
|
333
679
|
function normalizeAliasEntries(alias) {
|
|
334
680
|
if (Array.isArray(alias)) {
|
|
335
681
|
return alias.filter((entry) => Boolean(entry) &&
|
|
@@ -466,17 +812,6 @@ async function importSmrtCoreManifestModule() {
|
|
|
466
812
|
return await importWorkspaceSourceModule(fallbackHref);
|
|
467
813
|
}
|
|
468
814
|
}
|
|
469
|
-
async function importSmrtCoreKnowledgeModule() {
|
|
470
|
-
const specifier = '@happyvertical/smrt-core/knowledge';
|
|
471
|
-
try {
|
|
472
|
-
return await import(specifier);
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
const fallbackHref = new URL('../../core/src/knowledge.ts', import.meta.url)
|
|
476
|
-
.href;
|
|
477
|
-
return await importWorkspaceSourceModule(fallbackHref);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
815
|
async function importDiscoverBaseClassesModule() {
|
|
481
816
|
const specifier = '@happyvertical/smrt-core/manifest/discover-base-classes';
|
|
482
817
|
try {
|
|
@@ -491,8 +826,12 @@ async function importDiscoverBaseClassesModule() {
|
|
|
491
826
|
* Load manifest from a package using ManifestManager
|
|
492
827
|
*
|
|
493
828
|
* This properly handles the manifest priority order:
|
|
494
|
-
* 1. .smrt/manifest.json (
|
|
829
|
+
* 1. .smrt/manifest.json (local build manifest)
|
|
495
830
|
* 2. dist/manifest.json (production manifest)
|
|
831
|
+
* `loadLocal()` would prefer an active test snapshot registered for the
|
|
832
|
+
* dependency's own root; only that root's own plugin registers one, so a
|
|
833
|
+
* dependency is read from its snapshot only when it runs in the same
|
|
834
|
+
* process or a child of its own test run.
|
|
496
835
|
*/
|
|
497
836
|
async function loadAndRegisterManifest(packageName, verbose) {
|
|
498
837
|
try {
|
|
@@ -616,12 +955,19 @@ async function loadAndRegisterLocalManifest(root, verbose) {
|
|
|
616
955
|
}
|
|
617
956
|
}
|
|
618
957
|
/**
|
|
619
|
-
* Generate
|
|
958
|
+
* Generate this run's test manifest snapshot using ManifestBuilder.
|
|
620
959
|
*
|
|
621
960
|
* This ensures the manifest is always fresh after adding new classes/fields.
|
|
622
|
-
* The ~1-2s overhead is minimal compared to test execution time.
|
|
961
|
+
* The ~1-2s overhead is minimal compared to test execution time. The scan is
|
|
962
|
+
* test-inclusive (inline `*.test.ts` fixture classes need metadata, #2760),
|
|
963
|
+
* so it is written only to `manifestPath` — the run's own snapshot
|
|
964
|
+
* (`outputMode: 'explicit'`) — never to `.smrt/manifest.json`,
|
|
965
|
+
* `dist/manifest.json`, or their paired `smrt-knowledge.json`, which belong
|
|
966
|
+
* to production generation (#4128). A production pair therefore stays
|
|
967
|
+
* byte-identical across a test run, so knowledge freshness (#3205) holds
|
|
968
|
+
* without regenerating it from test output.
|
|
623
969
|
*/
|
|
624
|
-
async function generateLocalManifest(root, options, verbose,
|
|
970
|
+
async function generateLocalManifest(root, options, verbose, manifestPath) {
|
|
625
971
|
try {
|
|
626
972
|
console.log('[smrt-vitest] Generating test manifest...');
|
|
627
973
|
const { ManifestBuilder } = await importSmrtCoreManifestModule();
|
|
@@ -631,6 +977,15 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
|
|
|
631
977
|
if (verbose) {
|
|
632
978
|
console.log(`[smrt-vitest] Discovered ${baseClasses.length} base classes (including ${baseClasses.length - 3} from external packages)`);
|
|
633
979
|
}
|
|
980
|
+
const runDir = dirname(manifestPath);
|
|
981
|
+
pruneEndedTestRuns(root, runDir);
|
|
982
|
+
mkdirSync(runDir, { recursive: true });
|
|
983
|
+
const owner = {
|
|
984
|
+
pid: process.pid,
|
|
985
|
+
hostname: hostname(),
|
|
986
|
+
startedAt: Date.now(),
|
|
987
|
+
};
|
|
988
|
+
writeFileSync(join(runDir, 'owner.json'), JSON.stringify(owner));
|
|
634
989
|
const builder = new ManifestBuilder(root);
|
|
635
990
|
const manifest = await builder.generate({
|
|
636
991
|
// File discovery
|
|
@@ -648,10 +1003,10 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
|
|
|
648
1003
|
includeExternalBaseClasses: true,
|
|
649
1004
|
includePrivateMethods: false,
|
|
650
1005
|
includeStaticMethods: true,
|
|
651
|
-
// Output configuration -
|
|
652
|
-
outputMode: '
|
|
1006
|
+
// Output configuration - only this run's snapshot (#4128)
|
|
1007
|
+
outputMode: 'explicit',
|
|
653
1008
|
artifactPurpose: 'test',
|
|
654
|
-
outputDir:
|
|
1009
|
+
outputDir: runDir,
|
|
655
1010
|
outputName: 'manifest.json',
|
|
656
1011
|
generateTypeStub: false,
|
|
657
1012
|
// Metadata
|
|
@@ -659,7 +1014,6 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
|
|
|
659
1014
|
moduleType: 'smrt',
|
|
660
1015
|
});
|
|
661
1016
|
const objectCount = Object.keys(manifest.objects).length;
|
|
662
|
-
await refreshTestKnowledgeArtifact(root, manifest, producerApi);
|
|
663
1017
|
console.log(`[smrt-vitest] ✓ Generated manifest with ${objectCount} object(s)`);
|
|
664
1018
|
return true;
|
|
665
1019
|
}
|
|
@@ -668,50 +1022,6 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
|
|
|
668
1022
|
return false;
|
|
669
1023
|
}
|
|
670
1024
|
}
|
|
671
|
-
async function refreshTestKnowledgeArtifact(root, manifest, producerApi) {
|
|
672
|
-
const { AGENT_SURFACE_HASH_PREFIX, buildDomainKnowledgeManifest, publishAtomicArtifact, resolveFileKnowledgeConfig, } = await importSmrtCoreKnowledgeModule();
|
|
673
|
-
const manifestPath = join(root, '.smrt/manifest.json');
|
|
674
|
-
const knowledgePath = join(root, '.smrt/smrt-knowledge.json');
|
|
675
|
-
let priorKnowledge;
|
|
676
|
-
try {
|
|
677
|
-
priorKnowledge = JSON.parse(readFileSync(knowledgePath, 'utf8'));
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
// A missing or malformed prior artifact is replaced by current generation.
|
|
681
|
-
}
|
|
682
|
-
const config = producerApi
|
|
683
|
-
? await producerApi.resolveKnowledgeConfig(manifest)
|
|
684
|
-
: await resolveFileKnowledgeConfig(root, manifest.packageName);
|
|
685
|
-
if (config.enabled === false) {
|
|
686
|
-
if (existsSync(knowledgePath))
|
|
687
|
-
unlinkSync(knowledgePath);
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
const agentSurface = producerApi
|
|
691
|
-
? await producerApi.resolveKnowledgeAgentSurface()
|
|
692
|
-
: undefined;
|
|
693
|
-
const knowledge = buildDomainKnowledgeManifest({
|
|
694
|
-
manifest,
|
|
695
|
-
rootDir: root,
|
|
696
|
-
manifestPath,
|
|
697
|
-
config,
|
|
698
|
-
agentSurface,
|
|
699
|
-
});
|
|
700
|
-
const priorSurfaceHashes = Object.entries(priorKnowledge?.sourceHashes ?? {}).filter(([key]) => key.startsWith(AGENT_SURFACE_HASH_PREFIX));
|
|
701
|
-
if (!agentSurface &&
|
|
702
|
-
priorKnowledge?.agentSurface &&
|
|
703
|
-
priorSurfaceHashes.length) {
|
|
704
|
-
// Without the producer API we cannot rescan declarations. Preserve the
|
|
705
|
-
// last surface *and its old source hashes*: rebuilding hashes from files
|
|
706
|
-
// under a stale declaration would incorrectly certify it as fresh.
|
|
707
|
-
knowledge.agentSurface = priorKnowledge.agentSurface;
|
|
708
|
-
Object.assign(knowledge.sourceHashes, Object.fromEntries(priorSurfaceHashes));
|
|
709
|
-
}
|
|
710
|
-
publishAtomicArtifact({
|
|
711
|
-
path: knowledgePath,
|
|
712
|
-
content: JSON.stringify(knowledge, null, 2),
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
1025
|
/**
|
|
716
1026
|
* Build the `oxc` transform defaults injected into the consumer config.
|
|
717
1027
|
*
|
|
@@ -835,12 +1145,42 @@ export function smrtVitestPlugin(options = {}) {
|
|
|
835
1145
|
const { packages = [], verbose: verboseOption = false, root = process.cwd(), generateManifest = true, setupFile = resolveDefaultSetupFile(), aliasFilter, } = options;
|
|
836
1146
|
const verbose = isVerbose(verboseOption);
|
|
837
1147
|
let manifestsLoaded = false;
|
|
1148
|
+
const testRunManifestPath = resolveTestRunManifestPath(root);
|
|
838
1149
|
const setupFileId = setupFile;
|
|
839
1150
|
const workspaceAliases = getWorkspaceViteAliases(root, {
|
|
840
1151
|
filter: aliasFilter,
|
|
841
1152
|
});
|
|
1153
|
+
const keptAliasSources = new Set(workspaceAliases.map((entry) => entry.find.source));
|
|
1154
|
+
// Specifiers the consumer's aliasFilter removed: those packages resolve
|
|
1155
|
+
// through their exports map to built entries on purpose.
|
|
1156
|
+
const droppedSpecifiers = aliasFilter
|
|
1157
|
+
? getWorkspaceViteAliases(root)
|
|
1158
|
+
.filter((entry) => !keptAliasSources.has(entry.find.source))
|
|
1159
|
+
.map((entry) => entry.find.source.replace(/^\^|\$$/g, '').replace(/\\(.)/g, '$1'))
|
|
1160
|
+
: [];
|
|
1161
|
+
let distGuard;
|
|
1162
|
+
let distExempt = new Set();
|
|
1163
|
+
const workspacePackages = [...readWorkspacePackageRoots(root).entries()];
|
|
1164
|
+
const matchWorkspacePackageSpecifier = (source) => {
|
|
1165
|
+
for (const [name, packageRoot] of workspacePackages) {
|
|
1166
|
+
if (source === name)
|
|
1167
|
+
return { name, root: packageRoot, subpath: '.' };
|
|
1168
|
+
if (source.startsWith(`${name}/`)) {
|
|
1169
|
+
return {
|
|
1170
|
+
name,
|
|
1171
|
+
root: packageRoot,
|
|
1172
|
+
subpath: `.${source.slice(name.length)}`,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
};
|
|
842
1178
|
const ensureSetupFiles = (value) => {
|
|
843
|
-
|
|
1179
|
+
// Vitest resolves `setupFiles` specifiers itself, outside `resolve.alias`,
|
|
1180
|
+
// so a workspace specifier such as `@happyvertical/smrt-vitest/svelte-setup`
|
|
1181
|
+
// would load the sibling's built dist (#2954). Map it to its source.
|
|
1182
|
+
const setupFiles = (Array.isArray(value) ? [...value] : value ? [value] : []).map((entry) => workspaceAliases.find((alias) => alias.find.test(entry))?.replacement ??
|
|
1183
|
+
entry);
|
|
844
1184
|
if (!setupFiles.includes(setupFileId)) {
|
|
845
1185
|
setupFiles.push(setupFileId);
|
|
846
1186
|
}
|
|
@@ -880,6 +1220,43 @@ export function smrtVitestPlugin(options = {}) {
|
|
|
880
1220
|
};
|
|
881
1221
|
return {
|
|
882
1222
|
name: 'smrt-vitest',
|
|
1223
|
+
enforce: 'pre',
|
|
1224
|
+
// A workspace specifier the static aliases did not rewrite — a pattern
|
|
1225
|
+
// export subpath, or one with no derivable source — is resolved here
|
|
1226
|
+
// (#2954). Pattern subpaths map to their source; anything still resolving
|
|
1227
|
+
// to a sibling's built JavaScript fails the test, so a stale or missing
|
|
1228
|
+
// dist cannot make a consumer suite pass. `enforce: 'pre'` places this
|
|
1229
|
+
// after Vite's alias step (aliased specifiers never reach it) but before
|
|
1230
|
+
// Vite's own resolver, which would otherwise resolve the exports map to
|
|
1231
|
+
// dist first. Code that deliberately imports a dist file by path (e.g. a
|
|
1232
|
+
// runtime loader under test) is not a specifier resolution and is left
|
|
1233
|
+
// alone.
|
|
1234
|
+
async resolveId(source, importer, options) {
|
|
1235
|
+
const workspace = distGuard
|
|
1236
|
+
? matchWorkspacePackageSpecifier(source)
|
|
1237
|
+
: null;
|
|
1238
|
+
// Packages the consumer resolves to published entries on purpose keep
|
|
1239
|
+
// their exports-map resolution.
|
|
1240
|
+
if (!distGuard || !workspace || distExempt.has(workspace.name)) {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (workspace.subpath !== '.') {
|
|
1244
|
+
const patternSource = resolveWorkspaceExportSource(workspace.root, workspace.subpath);
|
|
1245
|
+
if (patternSource)
|
|
1246
|
+
return patternSource;
|
|
1247
|
+
}
|
|
1248
|
+
// The exports map decides where Node and Vite would take the
|
|
1249
|
+
// specifier; a built JavaScript target fails whether or not that file
|
|
1250
|
+
// currently exists (stale or missing dist alike).
|
|
1251
|
+
const target = workspaceExportTarget(workspace.root, workspace.subpath);
|
|
1252
|
+
if (target && /\.m?js$/.test(target)) {
|
|
1253
|
+
const failure = distGuard(join(normalizeRootKey(workspace.root), target));
|
|
1254
|
+
if (failure) {
|
|
1255
|
+
throw new Error(`${failure} (specifier: ${source})`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return null;
|
|
1259
|
+
},
|
|
883
1260
|
config(userConfig) {
|
|
884
1261
|
// Propagate manifest-registration options to every worker process this
|
|
885
1262
|
// early — before Vitest spawns any pool worker — so ./setup.ts can
|
|
@@ -919,6 +1296,13 @@ export function smrtVitestPlugin(options = {}) {
|
|
|
919
1296
|
// logging; a project needing distinct isolation in that shape should
|
|
920
1297
|
// pass an explicit, distinct `root` to `smrtVitestPlugin()`.
|
|
921
1298
|
setManifestRegistrationOptionsForRoot(root, { packages, root, verbose });
|
|
1299
|
+
// Publish this run's test manifest snapshot path just as early, for
|
|
1300
|
+
// the same reason: workers forked before `configResolved()` must
|
|
1301
|
+
// still read the run's own snapshot, never the production
|
|
1302
|
+
// `.smrt/manifest.json` a concurrent build may rewrite (#4128).
|
|
1303
|
+
if (generateManifest) {
|
|
1304
|
+
registerTestRunManifestPath(root, testRunManifestPath);
|
|
1305
|
+
}
|
|
922
1306
|
const rootRetry = userConfig.test?.retry;
|
|
923
1307
|
// Build tools may copy co-located tests into both output directories.
|
|
924
1308
|
// Always retain Vitest defaults and consumer exclusions as well.
|
|
@@ -929,6 +1313,8 @@ export function smrtVitestPlugin(options = {}) {
|
|
|
929
1313
|
? userConfig.resolve
|
|
930
1314
|
: undefined;
|
|
931
1315
|
const alias = normalizeAliasEntries(resolveConfig?.alias);
|
|
1316
|
+
distExempt = packagesWithDeliberateDistEntries(root, droppedSpecifiers, alias);
|
|
1317
|
+
distGuard = createWorkspaceDistGuard(root, distExempt);
|
|
932
1318
|
return {
|
|
933
1319
|
// Vite 8 (rolldown/oxc) defaults — legacy @smrt() decorators and
|
|
934
1320
|
// type-import elision (see resolveOxcDefaults). Fields the consumer
|
|
@@ -956,14 +1342,16 @@ export function smrtVitestPlugin(options = {}) {
|
|
|
956
1342
|
};
|
|
957
1343
|
},
|
|
958
1344
|
// Run during config resolution to ensure manifests are loaded before tests
|
|
959
|
-
async configResolved(
|
|
1345
|
+
async configResolved() {
|
|
960
1346
|
if (manifestsLoaded)
|
|
961
1347
|
return;
|
|
962
1348
|
// Step 1: Generate local manifest if enabled (default: true)
|
|
963
1349
|
// This ensures manifest is always fresh after adding new classes/fields
|
|
964
1350
|
if (generateManifest) {
|
|
965
|
-
|
|
966
|
-
|
|
1351
|
+
// `config()` normally published this already; a caller driving
|
|
1352
|
+
// `configResolved()` alone (tests, custom harnesses) still gets it.
|
|
1353
|
+
registerTestRunManifestPath(root, testRunManifestPath);
|
|
1354
|
+
await generateLocalManifest(root, options, verbose, testRunManifestPath);
|
|
967
1355
|
}
|
|
968
1356
|
// Step 2: Load the local manifest so late-imported local classes are
|
|
969
1357
|
// available to schema preparation before the first DB call.
|
|
@@ -1086,5 +1474,5 @@ export async function setupSmrtManifests(options = {}) {
|
|
|
1086
1474
|
}
|
|
1087
1475
|
export default smrtVitestPlugin;
|
|
1088
1476
|
// Export test database utilities
|
|
1089
|
-
export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, isPostgresAvailable, } from './test-db.js';
|
|
1477
|
+
export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, getTestManifestPath, isPostgresAvailable, } from './test-db.js';
|
|
1090
1478
|
//# sourceMappingURL=index.js.map
|