diff --git a/dist/chunks/cac.fSuRXrAx.js b/dist/chunks/cac.fSuRXrAx.js index 7b254fba15c5a1f7a0716aa22d943918dace605c..79971c50b0003a7f147264964471836c58e2649f 100644 --- a/dist/chunks/cac.fSuRXrAx.js +++ b/dist/chunks/cac.fSuRXrAx.js @@ -2318,7 +2318,12 @@ function parseCLI(argv, config = {}) { if (arrayArgs[0] !== "vitest") throw new Error(`Expected "vitest" as the first argument, received "${arrayArgs[0]}"`); arrayArgs[0] = "/index.js"; arrayArgs.unshift("node"); - let { args, options } = createCLI(config).parse(arrayArgs, { run: false }); + const cli = createCLI(config); + let { args, options } = cli.parse(arrayArgs, { run: false }); + // Validate without executing; help/version clear the matched command in parse(). + cli.matchedCommand?.checkUnknownOptions(); + cli.matchedCommand?.checkOptionValue(); + cli.matchedCommand?.checkRequiredArgs(); if (arrayArgs[2] === "watch" || arrayArgs[2] === "dev") options.watch = true; if (arrayArgs[2] === "run" && !options.watch) options.run = true; if (arrayArgs[2] === "related") { diff --git a/dist/chunks/index.D4dXTzh9.js b/dist/chunks/index.D4dXTzh9.js index 7f8ab40e591a03cffbb914d5bcd8f06702ae4619..70d5d27486c4c6a5561d5b6c7c39548cf9fcd1e5 100644 --- a/dist/chunks/index.D4dXTzh9.js +++ b/dist/chunks/index.D4dXTzh9.js @@ -16,6 +16,7 @@ import { Console } from 'node:console'; class BareModuleMocker { options; static pendingIds = []; + pendingMockResolution = Promise.resolve(); spyModule; primitives; registries = /* @__PURE__ */ new Map(); @@ -101,27 +102,35 @@ class BareModuleMocker { }; }); } - async resolveMocks() { - if (!BareModuleMocker.pendingIds.length) return; - const pendingIds = BareModuleMocker.pendingIds; - BareModuleMocker.pendingIds = []; - const resolveMock = async (mock) => { - return { - mock, - ...await this.resolveId(mock.id, mock.importer) + resolveMocks() { + // The queue empties before awaited resolution; every fetch must still join + // that pass before reading its registry, including calls with no new ids. + const pass = this.pendingMockResolution.then(() => this.resolvePendingMocks()); + this.pendingMockResolution = pass.catch(() => {}); + return pass; + } + async resolvePendingMocks() { + while (BareModuleMocker.pendingIds.length) { + const pendingIds = BareModuleMocker.pendingIds; + BareModuleMocker.pendingIds = []; + const resolveMock = async (mock) => { + return { + mock, + ...await this.resolveId(mock.id, mock.importer) + }; }; - }; - // group consecutive mocks of the same action type together, - // resolve in parallel inside each group, but run groups sequentially - // to preserve mock/unmock ordering - const groups = groupByConsecutiveAction(pendingIds); - for (const group of groups) { - // apply in queue order: two mocks of the same path can resolve out of - // order, and the last queued one must win - const resolvedGroup = await Promise.all(group.map(resolveMock)); - for (const { mock, id, url, external } of resolvedGroup) { - if (mock.action === "unmock") this.unmockPath(id); - if (mock.action === "mock") this.mockPath(mock.id, id, url, external, mock.type, mock.factory); + // group consecutive mocks of the same action type together, + // resolve in parallel inside each group, but run groups sequentially + // to preserve mock/unmock ordering + const groups = groupByConsecutiveAction(pendingIds); + for (const group of groups) { + // apply in queue order: two mocks of the same path can resolve out of + // order, and the last queued one must win + const resolvedGroup = await Promise.all(group.map(resolveMock)); + for (const { mock, id, url, external } of resolvedGroup) { + if (mock.action === "unmock") this.unmockPath(id); + if (mock.action === "mock") this.mockPath(mock.id, id, url, external, mock.type, mock.factory); + } } } } @@ -753,7 +762,7 @@ function startVitestModuleRunner(options) { const rawId = unwrapId(id); resolvingModules.add(rawId); try { - if (VitestMocker.pendingIds.length) await moduleRunner.mocker.resolveMocks(); + await moduleRunner.mocker.resolveMocks(); if (!isImportActual) { const resolvedMock = moduleRunner.mocker.getDependencyMockByUrl(id); if (resolvedMock?.type === "manual" || resolvedMock?.type === "redirect") return { diff --git a/dist/chunks/index.DzobfTyw.js b/dist/chunks/index.DzobfTyw.js index 08c3c116fbf8fe4685b45dee57f0f645b1270d81..7de72ebe6dc3449a6b9b380747495c6b4d8d955e 100644 --- a/dist/chunks/index.DzobfTyw.js +++ b/dist/chunks/index.DzobfTyw.js @@ -6367,7 +6367,9 @@ class ModuleFetcher { if ("code" in result) trace.setAttribute("vitest.fetched_module.code_length", result.code.length); } async getCachePath(environment, moduleGraphModule) { - if (!this.fsCacheEnabled) return null; + // VCS and configureVitest imports precede project/cache-key initialization. + // Use normal Vite transforms until persistent cache integrity is established. + if (!this.fsCacheEnabled || this.fsCache.lockfileHash === void 0) return null; const moduleId = moduleGraphModule.id; const memoryCacheKey = this.fsCache.getMemoryCachePath(environment, moduleId); // undefined means there is no key in memory @@ -7121,6 +7123,7 @@ class FileSystemModuleCache { rootCache; metadataFilePath; version = "1.0.0-beta.7"; + lockfileHash; fsCacheRoots = /* @__PURE__ */ new WeakMap(); fsEnvironmentHashMap = /* @__PURE__ */ new WeakMap(); fsCacheKeyGenerators = /* @__PURE__ */ new Set(); @@ -7150,6 +7153,9 @@ class FileSystemModuleCache { return false; } async clearCache(log = true) { + // Disk deletion also retires paths retained by live module graphs. + this.fsCacheRoots = /* @__PURE__ */ new WeakMap(); + this.vitest.clearAllCachePaths(); const fsCachePaths = [this.rootCache, ...this.vitest.projects.map((r) => r.config.fsModuleCachePath || this.rootCache)]; const uniquePaths = Array.from(new Set(fsCachePaths)); await Promise.all(uniquePaths.map((directory) => rm(directory, { @@ -7228,6 +7234,11 @@ class FileSystemModuleCache { invalidateAllCachePaths(environment) { debugFs?.(`the ${environment.name} environment cache is invalidated`); this.fsCacheKeys.get(environment)?.clear(); + for (const module of environment.moduleGraph.idToModuleMap.values()) { + for (const transform of [module.transformResult, module.invalidationState]) { + if (transform && typeof transform !== "string") delete transform.__vitestTmp; + } + } } getMemoryCachePath(environment, id) { const result = this.fsCacheKeys.get(environment)?.get(id); @@ -7285,7 +7296,7 @@ class FileSystemModuleCache { return value; }); // everything in the key that does not depend on the module, as one digest - environmentHash = hash("sha1", (process.env.NODE_ENV ?? "") + this.version + cacheConfig, "hex"); + environmentHash = hash("sha1", (process.env.NODE_ENV ?? "") + this.version + this.lockfileHash + cacheConfig, "hex"); this.fsEnvironmentHashMap.set(environment, environmentHash); } hashString += id + fileContent + environmentHash + coverageAffectsCache; @@ -7332,25 +7343,23 @@ class FileSystemModuleCache { // or a new version of vite/vitest is installed // for the same reason we also cache config file content, but that won't catch changes made in external plugins async ensureCacheIntegrity() { - if (![this.vitest.getRootProject(), ...this.vitest.projects].some((p) => p.config.fsModuleCache)) return; - const metadata = await this.readMetadata(); - const currentLockfileHash = getLockfileHash(this.vitest.vite.config.root); - // no metadata found, just store a new one, don't reset the cache - if (!metadata) { - debugFs?.(`fs metadata file was created with hash ${currentLockfileHash}`); - await this.writeMetadata(currentLockfileHash); - return; + if ([this.vitest.getRootProject(), ...this.vitest.projects].some((p) => p.config.fsModuleCache)) { + const metadata = await this.readMetadata(); + const currentLockfileHash = getLockfileHash(this.vitest.vite.config.root); + if (metadata?.lockfileHash !== currentLockfileHash) { + if (metadata) { + await this.clearCache(false); + this.vitest.vite.config.logger.info(`fs cache was cleared because lockfile has changed`, { + timestamp: true, + environment: y.yellow("[vitest]") + }); + debugFs?.(`fs cache was cleared because lockfile has changed`); + } + debugFs?.(`fs metadata file was created with hash ${currentLockfileHash}`); + await this.writeMetadata(currentLockfileHash); + } + this.lockfileHash = currentLockfileHash; } - // if lockfile didn't change, don't do anything - if (metadata.lockfileHash === currentLockfileHash) return; - // lockfile changed, let's clear all caches - await this.clearCache(false); - await this.writeMetadata(currentLockfileHash); - this.vitest.vite.config.logger.info(`fs cache was cleared because lockfile has changed`, { - timestamp: true, - environment: y.yellow("[vitest]") - }); - debugFs?.(`fs cache was cleared because lockfile has changed`); } } /** @@ -10683,6 +10692,7 @@ var RunnerState = /* @__PURE__ */ function(RunnerState) { RunnerState["STARTING"] = "starting"; RunnerState["STARTED"] = "started"; RunnerState["START_FAILURE"] = "start_failure"; + RunnerState["EXITED"] = "exited"; RunnerState["STOPPING"] = "stopping"; RunnerState["STOPPED"] = "stopped"; return RunnerState; @@ -10740,7 +10750,7 @@ class PoolRunner { }), { eventNames: ["onCancel"], post: (request) => { - if (this._state !== RunnerState.STOPPING && this._state !== RunnerState.STOPPED) this.postMessage(request); + if (this._state !== RunnerState.STOPPING && this._state !== RunnerState.STOPPED && this._state !== RunnerState.EXITED) this.postMessage(request); }, on: (callback) => this._eventEmitter.on("rpc", callback), timeout: -1 @@ -10758,7 +10768,7 @@ class PoolRunner { postMessage(message) { // Only send messages when runner is active (not fully stopped) // Allow sending during STOPPING state for the 'stop' message itself - if (this._state !== RunnerState.STOPPED) return this.worker.send(message); + if (this._state !== RunnerState.STOPPED && this._state !== RunnerState.EXITED) return this.worker.send(message); } startTracesSpan(name) { const traces = this._traces; @@ -10790,7 +10800,7 @@ class PoolRunner { // Wait for any ongoing operation to complete if (this._operationLock) await this._operationLock; if (this._state === RunnerState.STARTED || this._state === RunnerState.STARTING) return; - if (this._state === RunnerState.STOPPED) throw new Error("[vitest-pool-runner]: Cannot start a stopped runner"); + if (this._state === RunnerState.STOPPED || this._state === RunnerState.EXITED) throw new Error("[vitest-pool-runner]: Cannot start a stopped runner"); // Create operation lock to prevent concurrent start/stop this._operationLock = createDefer(); let startSpan; @@ -10830,14 +10840,14 @@ class PoolRunner { } }); await startPromise; - this._state = RunnerState.STARTED; + if (this._state !== RunnerState.EXITED) this._state = RunnerState.STARTED; // record how long it took to spawn this worker, load its bundle and set up the // environment, so the reporter can surface the cost of `isolate: true` const { state } = this.project.vitest; state.startupTime += performance.now() - startedAt; state.workersSpawned += 1; } catch (error) { - this._state = RunnerState.START_FAILURE; + if (this._state !== RunnerState.EXITED) this._state = RunnerState.START_FAILURE; startSpan?.recordException(error); throw error; } finally { @@ -10849,7 +10859,9 @@ class PoolRunner { async stop(options) { // Wait for any ongoing operation to complete if (this._operationLock) await this._operationLock; - if (this._state === RunnerState.STOPPED || this._state === RunnerState.STOPPING) return; + if (this._state === RunnerState.STOPPED) return; + if (this._state === RunnerState.STOPPING) return this.waitForTerminated(); + const exited = this._state === RunnerState.EXITED; this._otel?.span.setAttribute("vitest.worker.files", this._otel.files); if (this._state === RunnerState.IDLE) { this._otel?.span.end(); @@ -10872,12 +10884,13 @@ class PoolRunner { stopSpan.recordException(response.error); this.project.vitest.state.catchError(response.error, "Teardown Error"); } - resolve(); + // Only the responding transport can promise an explicit graceful exit. + resolve(response.willExit ? Promise.resolve().then(() => this.worker.waitForExit?.()) : undefined); this.off("message", onStop); } }; - // Don't wait for graceful exit's response when force exiting - if (options?.force) return onStop({ + // A dead worker cannot acknowledge stop; still join its transport cleanup. + if (options?.force || exited) return onStop({ type: "stopped", __vitest_worker_response__: true }); @@ -10887,21 +10900,21 @@ class PoolRunner { __vitest_worker_request__: true, otelCarrier: this.getOTELCarrier() }); - }), STOP_TIMEOUT).finally(() => { + }), STOP_TIMEOUT).finally(async () => { stopSpan.end(); + // Deadline/error paths must also terminate and join the worker. + await this._traces.$(`vitest.${this.worker.name}.stop`, { context: this._otel?.workerContext }, () => this.worker.stop()); }); - this._eventEmitter.removeAllListeners(); - this._offCancel(); - this._rpc.$close(/* @__PURE__ */ new Error("[vitest-pool-runner]: Pending methods while closing rpc")); - // Stop the worker process (this sets _fork/_thread to undefined) - // Worker's event listeners (error, message) are implicitly removed when worker terminates - await this._traces.$(`vitest.${this.worker.name}.stop`, { context: this._otel?.workerContext }, () => this.worker.stop()); this._state = RunnerState.STOPPED; } catch (error) { - // Ensure we transition to stopped state even on error + // Ensure failed graceful shutdown cannot report a successful test run. + this.project.vitest.state.catchError(error, "Teardown Error"); this._state = RunnerState.STOPPED; throw error; } finally { + this._eventEmitter.removeAllListeners(); + this._offCancel(); + this._rpc.$close(/* @__PURE__ */ new Error("[vitest-pool-runner]: Pending methods while closing rpc")); this._lastTestFiles = []; this._operationLock.resolve(); this._operationLock = null; @@ -10933,7 +10946,8 @@ class PoolRunner { const errorDetails = hasCode || signal ? `with ${hasCode ? `exit code ${code} ` : ""}${signal ? `signal ${signal} ` : ""}` : ""; const testFileDetails = this._lastTestFiles.length ? ` while running test file${this._lastTestFiles.length === 1 ? "" : "s"} ${this._lastTestFiles.join(", ")}` : ""; const error = /* @__PURE__ */ new Error(`Worker exited unexpectedly ${errorDetails}during ${this._state} state${testFileDetails}`); - this._state = RunnerState.STOPPED; + // Process exit is terminal for reuse, but stop still owns cleanup and settlement. + this._state = RunnerState.EXITED; this._eventEmitter.emit("error", error); }; waitForStart() { @@ -11041,10 +11055,22 @@ class ForksPoolWorker { this._fork.stderr.pipe(this.stderr, { end: false }); } } - async stop() { + waitForExit() { const fork = this.fork; + return new Promise((resolve, reject) => { + const onExit = (code, signal) => { + if (code === 0 && signal == null) resolve(); + else reject(new Error(`Worker exited during graceful shutdown (code: ${code}, signal: ${signal})`)); + }; + if (fork.exitCode != null || fork.signalCode != null) onExit(fork.exitCode, fork.signalCode); + else fork.once("exit", onExit); + }); + } + async stop() { + const fork = this._fork; + if (!fork) return; const waitForExit = new Promise((resolve) => { - if (fork.exitCode != null) resolve(); + if (fork.exitCode != null || fork.signalCode != null) resolve(); else fork.once("exit", resolve); }); /* @@ -11053,10 +11079,12 @@ class ForksPoolWorker { * - https://github.com/jestjs/jest/blob/25a8785584c9d54a05887001ee7f498d489a5441/packages/jest-worker/src/workers/ChildProcessWorker.ts#L463-L477 * - https://github.com/tinylibs/tinypool/blob/40b4b3eb926dabfbfd3d0a7e3d1222d4dd1c0d2d/src/runtime/process-worker.ts#L56 */ - const sigkillTimeout = setTimeout(() => fork.kill("SIGKILL"), SIGKILL_TIMEOUT); - fork.kill(); - await waitForExit; - clearTimeout(sigkillTimeout); + if (fork.exitCode == null && fork.signalCode == null) { + const sigkillTimeout = setTimeout(() => fork.kill("SIGKILL"), SIGKILL_TIMEOUT); + fork.kill(); + await waitForExit; + clearTimeout(sigkillTimeout); + } if (fork.stdout) { await streamFlushed(fork.stdout); fork.stdout.unpipe(this.stdout); @@ -11474,7 +11502,7 @@ class Pool { await resolver.promise.catch((error) => span?.recordException(error)).finally(() => span?.end()); const index = this.activeTasks.indexOf(activeTask); if (index !== -1) this.activeTasks.splice(index, 1); - if (!task.isolate && !runner.isTerminated && !isMemoryLimitReached && this.queue[0]?.task.isolate === false && isEqualRunner(runner, this.queue[0].task)) { + if (!task.isolate && runner.isStarted && !resolver.isRejected && !isMemoryLimitReached && this.queue[0]?.task.isolate === false && isEqualRunner(runner, this.queue[0].task)) { this.sharedRunners.push(runner); return this.schedule(); } @@ -11482,7 +11510,7 @@ class Pool { // Runner termination can also already start from task cancellation. if (!runner.isTerminated) { const id = setTimeout(() => this.logger.error(`[vitest-pool]: Timeout terminating ${task.worker} worker for test files ${formatFiles(task)}.`), this.options.teardownTimeout); - this.exitPromises.push(runner.stop({ force: resolver.isRejected }).then(() => clearTimeout(id)).catch((error) => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error))); + this.exitPromises.push(runner.stop({ force: resolver.isRejected }).finally(() => clearTimeout(id)).catch((error) => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error))); } this.freeWorkerId(poolId); } @@ -11724,6 +11752,8 @@ function createPool(ctx) { const groupResults = await Promise.allSettled(promises); results.push(...groupResults); } + // Join worker teardown before reporters and the CLI compute the run result. + await pool.close(); const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason); if (errors.length > 0) throw new AggregateError(errors, "Errors occurred while running tests. For more information, see serialized error."); } @@ -12002,6 +12032,7 @@ class TestProject { config; viteConfig; vite; + namePrefix; hash; /** @internal */ typechecker; /** @internal */ _resolver; @@ -12015,13 +12046,18 @@ class TestProject { typecheckFilesList = null; _globalSetups; _provided = {}; - constructor(vitest, server, viteConfig, projectConfig) { + constructor(vitest, server, viteConfig, projectConfig, namePrefix = "") { this.vitest = vitest; this.globalConfig = vitest.config; this.tmpDir = join(tmpdir(), nanoid()); this.vite = server; this.viteConfig = viteConfig; this.config = projectConfig; + // Container identity is fixed when the entry resolves and must survive sibling attachment. + Object.defineProperty(this, "namePrefix", { + value: namePrefix, + enumerable: true + }); this.hash = generateHash(this.config.root + this.config.name); this._provideObject(projectConfig.provide); } @@ -12304,8 +12340,8 @@ class TestProject { } } /** @internal */ - static _createBasicProject(vitest) { - const project = new TestProject(vitest, vitest.vite, vitest.viteConfig, vitest.config); + static _createBasicProject(vitest, namePrefix = "") { + const project = new TestProject(vitest, vitest.vite, vitest.viteConfig, vitest.config, namePrefix); project.runner = vitest.runner; project._resolver = vitest._resolver; project._fetcher = vitest._fetcher; @@ -12322,8 +12358,8 @@ class TestProject { * * @internal */ - static _spawnSibling(parent, config) { - const sibling = new TestProject(parent.vitest, parent.vite, parent.viteConfig, config); + static _spawnSibling(parent, config, namePrefix = parent.namePrefix) { + const sibling = new TestProject(parent.vitest, parent.vite, parent.viteConfig, config, namePrefix); sibling.runner = parent.runner; sibling._resolver = parent._resolver; sibling._fetcher = parent._fetcher; @@ -12439,7 +12475,8 @@ async function resolveProjectEntries(harness, globalViteConfig, globalConfig, de debug?.(`no projects declared, the root config is the only project`); baseEntries = [{ viteConfig: globalViteConfig, - projectConfig: globalConfig + projectConfig: globalConfig, + namePrefix: "" }]; } // Ensure project names are unique across declared projects (and any @@ -12542,6 +12579,28 @@ async function resolveDeclaredProjectEntries(context, definitions) { const fileProjects = [...configFiles, ...nonConfigDirectories]; const promises = []; projectConfigs.forEach((options, index) => { + if (typeof options.config === "string") { + const configRoot = parentConfig.root; + const configFile = resolve(configRoot, options.config); + const { config: _, root: optionRoot, test: testOptions, namePrefix, ...restOptions } = options; + if (namePrefix !== void 0 && typeof namePrefix !== "string") throw new TypeError("The file-backed project namePrefix must be a string."); + const customRoot = testOptions?.root ?? optionRoot; + const root = customRoot === void 0 ? dirname(configFile) : resolve(configRoot, customRoot); + const test = customRoot === void 0 ? testOptions : { + ...testOptions, + root + }; + debug?.(`project at ${configFile} resolves its own Vite config with root ${root}`); + promises.push(concurrent(() => resolveSingleProjectEntry(context, { + ...restOptions, + test, + root, + configFile, + namePrefix, + replayExecutedProject: namePrefix !== void 0 + }, configFile))); + return; + } const ownServerReason = getOwnServerReason(context, options); if (ownServerReason === void 0) { debug?.(`inline project ${inlineProjectLabel(options, index)} shares the Vite server of ${parentViteConfig.configFile ?? parentConfig.root}`); @@ -12571,10 +12630,12 @@ async function resolveDeclaredProjectEntries(context, definitions) { // resolved pair: the root (or the container) also runs as a regular project if (parentViteConfig.configFile === path) { debug?.(`project at ${path} is the declaring config itself, reusing its resolved config`); + const ancestors = context.ancestors.length > 1 ? context.ancestors.slice(0, -1) : void 0; promises.push(Promise.resolve({ viteConfig: parentViteConfig, projectConfig: parentConfig, - ancestors: context.ancestors.length > 1 ? context.ancestors.slice(0, -1) : void 0 + ancestors, + namePrefix: ancestors?.at(-1) ?? "" })); continue; } @@ -12607,7 +12668,7 @@ async function flattenContainerEntries(context, entries) { // inline projects cannot declare `projects`; the declaring config's own // entry (emitted when it references its own config file) is kept as-is — // its `projects` are the definitions currently being resolved - if (entry.inline || definitions === void 0 || entry.projectConfig === context.parentConfig) { + if (entry.replayExecutedProject || entry.inline || definitions === void 0 || entry.projectConfig === context.parentConfig) { result.push(entry); continue; } @@ -12819,18 +12880,21 @@ function resolveSharedServerEntry(context, options, index) { moduleRunnerOptions: parentConfig._moduleRunnerOptions } }); - mergedOptions.name = resolveProjectName(mergedOptions.name, index, context.ancestors.at(-1)); + const namePrefix = context.ancestors.at(-1) ?? ""; + mergedOptions.name = resolveProjectName(mergedOptions.name, index, namePrefix); return { viteConfig: parentViteConfig, projectConfig: resolveTestConfig(harness.logger, mergedOptions, parentViteConfig, parentConfig), inline: true, sharedServer: true, + namePrefix, ancestors: context.ancestors.length ? [...context.ancestors] : void 0 }; } async function resolveSingleProjectEntry(context, options, workspacePath) { const { harness, rootViteConfig, rootConfig, parentViteConfig, parentConfig, cliOverrides } = context; - const { configFile, ...restOptions } = options; + const { configFile, namePrefix: explicitNamePrefix, replayExecutedProject, ...restOptions } = options; + const namePrefix = explicitNamePrefix ?? context.ancestors.at(-1) ?? ""; const captures = {}; // only inline entries (keyed by their index) extend another config; // file-based projects own all of their values @@ -12864,7 +12928,7 @@ async function resolveSingleProjectEntry(context, options, workspacePath) { for (const key in parentViteConfig.env) projectViteConfig.env[key] ??= parentViteConfig.env[key]; const mergedOptions = projectViteConfig.test ?? {}; // resolved after `viteResolveConfig` so a plugin can still set `test.name` - mergedOptions.name = resolveProjectName(mergedOptions.name, workspacePath, context.ancestors.at(-1)); + mergedOptions.name = resolveProjectName(mergedOptions.name, workspacePath, namePrefix); const projectConfig = resolveTestConfig(harness.logger, mergedOptions, projectViteConfig, parentConfig); projectViteConfig.test = projectConfig; if (inheritsParentConfig && options.extends === void 0 && configFile) warnDuplicateInheritedPlugins(harness.logger, projectConfig, projectViteConfig, parentViteConfig, configFile, rootConfig.root); @@ -12882,6 +12946,8 @@ async function resolveSingleProjectEntry(context, options, workspacePath) { viteConfig: projectViteConfig, projectConfig, inline: isInlineEntry, + namePrefix, + replayExecutedProject, ancestors: context.ancestors.length ? [...context.ancestors] : void 0 }; } @@ -12974,6 +13040,7 @@ function expandBrowserInstancesInEntries(globalConfig, entries, names) { result.push({ viteConfig, projectConfig: clonedConfig, + namePrefix: entry.namePrefix, ancestors: entry.ancestors }); }); @@ -13035,6 +13102,7 @@ function expandBenchmarksInEntries(entries, names, benchmarkOnly) { result.push({ viteConfig: entry.viteConfig, projectConfig: benchmarkConfig, + namePrefix: entry.namePrefix, ancestors: entry.ancestors, sharedServer: entry.sharedServer }); @@ -13181,7 +13249,7 @@ function resolveDirectoryConfig(directory) { if (configFile) return resolve(directory, configFile); return null; } -function resolveProjectName(name, workspacePath, containerLabel) { +function resolveProjectName(name, workspacePath, namePrefix) { let { label, color } = typeof name === "string" ? { label: name } : { label: "", ...name @@ -13195,7 +13263,7 @@ function resolveProjectName(name, workspacePath, containerLabel) { } // Projects declared by a container config are namespaced by the container's // name: the "unit" project of an "app" container is named "app (unit)". - if (containerLabel) label = `${containerLabel} (${label})`; + if (namePrefix) label = `${namePrefix} (${label})`; return { label, color @@ -13225,7 +13293,7 @@ async function attachProjectsFromEntries(vitest, entries) { } const projects = []; for (const entry of entries) { - const { viteConfig, projectConfig, hidden } = entry; + const { viteConfig, projectConfig, hidden, namePrefix } = entry; const primary = primaryByViteConfig.get(viteConfig); if (primary) { if (hidden) continue; @@ -13238,14 +13306,14 @@ async function attachProjectsFromEntries(vitest, entries) { // a shared-server project reuses only the Vite server if (entry.sharedServer) { debug?.(`project ${projectLabel(projectConfig.name)} reuses the Vite server of ${projectLabel(primary.name)} with its own module runner`); - const project = new TestProject(vitest, primary.vite, viteConfig, projectConfig); + const project = new TestProject(vitest, primary.vite, viteConfig, projectConfig, namePrefix); project._sharedViteServer = true; project._initializeRunners(primary.vite); projects.push(project); continue; } debug?.(`project ${projectLabel(projectConfig.name)} shares the Vite server and module runner of ${projectLabel(primary.name)}`); - const sibling = TestProject._spawnSibling(primary, projectConfig); + const sibling = TestProject._spawnSibling(primary, projectConfig, namePrefix); // Browser-instance siblings share the primary's single (browser) Vite // server; each gets its own `ProjectBrowser` view onto it. if (primary._parentBrowser) sibling.browser = primary._parentBrowser.spawn(sibling); @@ -13258,7 +13326,7 @@ async function attachProjectsFromEntries(vitest, entries) { debug?.(`creating a Vite server for project ${projectLabel(projectConfig.name)}`); const children = childrenByViteConfig.get(viteConfig) ?? []; const { server, parent } = await createClusterServer(vitest, viteConfig, projectConfig, children); - const project = new TestProject(vitest, server, viteConfig, projectConfig); + const project = new TestProject(vitest, server, viteConfig, projectConfig, namePrefix); // a shared entry can create the container's server on first use, // but the server still belongs to the declaring config project._sharedViteServer = !!entry.sharedServer; @@ -20622,6 +20690,7 @@ class Vitest { // populate will merge all configs into every project, // we don't want that when just listing tags if (!resolved.listTags) populateProjectsTags(this.coreWorkspaceProject, this.projects); + await this._fsCache.ensureCacheIntegrity(); this.reporters = await createReporters(resolved.reporters, this); // API setup (watch mode only). Must run after the reporters array is built // above, since `setup()` appends the UI/API WebSocket reporter to it. For a @@ -20638,7 +20707,6 @@ class Vitest { setup(this, browserServer); } } - await this._fsCache.ensureCacheIntegrity(); await Promise.all([...this._onSetServer.map((fn) => fn()), this._traces.waitInit()]); } /** @internal */ @@ -20679,7 +20747,7 @@ class Vitest { if (this.coverageProvider?.onFileTransform) this.clearAllCachePaths(); } clearAllCachePaths() { - this.projects.forEach(({ vite }) => { + new Set([this.getRootProject(), ...this.projects]).forEach(({ vite }) => { Object.values(vite.environments).forEach((environment) => this._fsCache.invalidateAllCachePaths(environment)); }); } diff --git a/dist/chunks/index.m3L2HgmY.js b/dist/chunks/index.m3L2HgmY.js index dadbf7ade257975994efc67f2cd2a6b48c46fc1a..bb1a15b797f8dde8ddeb5a4da51688bc94828d03 100644 --- a/dist/chunks/index.m3L2HgmY.js +++ b/dist/chunks/index.m3L2HgmY.js @@ -4964,12 +4964,12 @@ function requireFakeTimersSrc () { return this.refed; }, refresh: function () { + clock.timerHeap.remove(timer); timer.callAt = clock.now + (parseInt(String(timer.delay)) || (clock.duringTick ? 1 : 0)); - clock.timerHeap.remove(timer); timer.order = uniqueTimerOrder++; setTimer(clock, timer); clock.timerHeap.push(timer); diff --git a/dist/chunks/init-forks.DgHqDQHC.js b/dist/chunks/init-forks.DgHqDQHC.js index b94d451557434b23c38737e23163ffed35d14257..cfe76caf7dd29928d737cb5bd17ce9f399086b65 100644 --- a/dist/chunks/init-forks.DgHqDQHC.js +++ b/dist/chunks/init-forks.DgHqDQHC.js @@ -13,7 +13,12 @@ processOn("error", onError); function workerInit(options) { const { runTests } = options; init({ - post: (v) => processSend(v), + // Flush the response, then exit explicitly while native profiler signal handlers + // are still installed. The parent joins actual exit within its stop deadline. + post: (v) => { + if (v.__vitest_worker_response__ && v.type === "stopped") return processSend({ ...v, willExit: true }, (error) => processExit(error ? 1 : process.exitCode)); + return processSend(v); + }, on: (cb) => processOn("message", cb), off: (cb) => processOff("message", cb), teardown: () => { diff --git a/dist/chunks/plugin.d.CN87HSxv.d.ts b/dist/chunks/plugin.d.CN87HSxv.d.ts index b31a948b64185b00819abf6820db864d95008082..8ea87c4e7588503a03b2d8f103ce8d26102ad4cd 100644 --- a/dist/chunks/plugin.d.CN87HSxv.d.ts +++ b/dist/chunks/plugin.d.CN87HSxv.d.ts @@ -1032,13 +1032,14 @@ declare class TestProject { config: ResolvedConfig; viteConfig: ResolvedConfig$1; vite: ViteDevServer; + readonly namePrefix: string; hash: string; private runner; private closingPromise; private typecheckFilesList; private _globalSetups?; private _provided; - constructor(vitest: Vitest, server: ViteDevServer, viteConfig: ResolvedConfig$1, projectConfig: ResolvedConfig); + constructor(vitest: Vitest, server: ViteDevServer, viteConfig: ResolvedConfig$1, projectConfig: ResolvedConfig, namePrefix?: string); /** * Provide a value to the test context. This value will be available to all tests with `inject`. */ @@ -2694,6 +2695,8 @@ interface PoolWorker { send: (message: WorkerRequest) => void; deserialize: (data: unknown) => unknown; start: () => Promise; + /** Observe exit when a stopped response declares willExit; reject abnormal termination. */ + waitForExit?: () => Promise; stop: () => Promise; /** * This is called on workers that already satisfy certain constraints: @@ -2759,6 +2762,8 @@ type WorkerResponse = { error?: unknown; } | { type: "stopped"; + /** The transport will exit itself after sending this response. */ + willExit?: true; error?: unknown; } | { type: "testfileFinished"; @@ -4432,6 +4437,7 @@ interface ResolvedConfig extends Omit, "project" | "config" interface ResolvedProjectEntry { viteConfig: ResolvedConfig$1; projectConfig: ResolvedConfig; + namePrefix: string; /** * When set, this entry exists only so browser-instance siblings can attach * to a parent that owns the Vite server and (later) the browser provider. @@ -4489,6 +4495,16 @@ interface UserWorkspaceConfig extends UserConfig$1 { type UserProjectConfigFn = (env: ConfigEnv) => UserWorkspaceConfig | Promise; type UserProjectConfigExport = UserWorkspaceConfig | Promise | UserProjectConfigFn; type TestProjectInlineConfiguration = (UserWorkspaceConfig & { + /** + * Load a file-backed project while allowing explicit project options such as `root`. + * Unlike `extends`, the file owns its complete project config and is evaluated once. + */ + config?: string; + /** + * Preserve the resolved container prefix when replaying an executed file project. + * When present, the file is treated as a standalone project. + */ + namePrefix?: string; /** * Relative path to the extendable config. All other options will be merged with this config. * If `true`, the project will inherit all options from the root config. diff --git a/dist/chunks/run.C5UmxDPh.js b/dist/chunks/run.C5UmxDPh.js index d397cb801d96ed4387adec3f8ef09123ddc636fe..f103cbe65ad64dceddb733bdd2e2a0347c42c04f 100644 --- a/dist/chunks/run.C5UmxDPh.js +++ b/dist/chunks/run.C5UmxDPh.js @@ -3782,14 +3782,17 @@ function throttle(fn, ms) { let pendingCall; return function call(...args) { const now = unixNow(); - if (now - last > ms) { + if (now - last >= ms) { last = now; clearTimeout(pendingCall); pendingCall = void 0; return fn.apply(this, args); } // Make sure fn is still called even if there are no further calls - pendingCall ??= setTimeout(call.bind(this), ms, ...args); + pendingCall ??= setTimeout(() => { + pendingCall = undefined; + call.apply(this, args); + }, ms); }; } // throttle based on summary reporter's DURATION_UPDATE_INTERVAL_MS diff --git a/dist/node.d.ts b/dist/node.d.ts index be99c57c695ce0524369adc790d76cefd6d843bc..4d434dab726c6279051c23e84a22abe2716f1add 100644 --- a/dist/node.d.ts +++ b/dist/node.d.ts @@ -425,6 +425,7 @@ declare class ForksPoolWorker implements PoolWorker { stop(): Promise; deserialize(data: unknown): unknown; private emitError; + waitForExit(): Promise; private get fork(); }