diff --git a/docs/errors/VDT0004.md b/docs/errors/VDT0004.md new file mode 100644 index 000000000..7a58c78dd --- /dev/null +++ b/docs/errors/VDT0004.md @@ -0,0 +1,26 @@ +--- +outline: deep +--- + +# VDT0004: Inspect Storage Fallback + +## Message + +> Vite inspect could not write the `{archive}` archive to disk and now keeps new records in memory. + +## Cause + +The Vite inspect collector writes transform payloads and plugin-call records to archives in the Vite cache directory. When a write fails, for example because the disk is full or the directory became unwritable, the collector keeps the failed batch and every later record in memory. Every transform stays available in the Vite UI, and memory use grows until the dev server restarts. + +## Example + +Filling up the disk that holds `node_modules/.vite/devtools/inspect` while the dev server is transforming modules triggers this warning. + +## Fix + +Check that the Vite cache directory is writable and has free disk space, then restart the Vite dev server to resume writing to disk. + +## Source + +- [`packages/vite/src/node/inspect/store/payload.ts`](https://github.com/vitejs/devtools/blob/main/packages/vite/src/node/inspect/store/payload.ts) — the payload archive emits this when a batched write fails and it switches to in-memory payloads. +- [`packages/vite/src/node/inspect/store/plugin-calls.ts`](https://github.com/vitejs/devtools/blob/main/packages/vite/src/node/inspect/store/plugin-calls.ts) — the plugin-call archive emits this when a segment write fails and it switches to in-memory segments. diff --git a/docs/errors/index.md b/docs/errors/index.md index fb0409b9d..888566601 100644 --- a/docs/errors/index.md +++ b/docs/errors/index.md @@ -60,6 +60,7 @@ Emitted by `@vitejs/devtools-vite`. | [VDT0001](./VDT0001) | error | Inspect Context Unavailable | | [VDT0002](./VDT0002) | error | Inspect Target Not Found | | [VDT0003](./VDT0003) | error | Inspect Storage Error | +| [VDT0004](./VDT0004) | warn | Inspect Storage Fallback | ## Vitest DevTools (VTDT) diff --git a/packages/vite/src/node/__tests__/inspect-context.test.ts b/packages/vite/src/node/__tests__/inspect-context.test.ts index 6917b4c12..76ad15182 100644 --- a/packages/vite/src/node/__tests__/inspect-context.test.ts +++ b/packages/vite/src/node/__tests__/inspect-context.test.ts @@ -2,6 +2,7 @@ import type { Environment, Plugin, ResolvedConfig } from 'vite' import type { ViteInspectStoreOptions } from '../inspect/store' import type { ViteInspectPluginCallInfo } from '../inspect/types' import { existsSync, mkdtempSync, rmSync, statSync, truncateSync } from 'node:fs' +import { open } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -410,19 +411,11 @@ describe('vite inspect context', () => { const envCtx = vite.getEnvContext(env) const result = `export const value = '${'café'.repeat(4096)}'` const source = `const value = '${'source'.repeat(4096)}'` - const sourcemaps = { - version: 3, - sources: ['/src/main.ts'], - sourcesContent: [source], - mappings: 'AAAA;'.repeat(4096), - } - envCtx.recordTransform('/src/main.ts', { name: 'plugin-a', result, start: 0, end: 1, - sourcemaps, }, source) expect(await envCtx.getModulesList()).toHaveLength(1) await expect(envCtx.getModuleTransformInfo('/src/main.ts')).resolves.toEqual({ @@ -433,7 +426,6 @@ describe('vite inspect context', () => { result: source, start: 0, end: 0, - sourcemaps, }, { name: 'plugin-a', @@ -441,7 +433,6 @@ describe('vite inspect context', () => { result, start: 0, end: 1, - sourcemaps, }, ], }) @@ -467,32 +458,6 @@ describe('vite inspect context', () => { rmSync(dir, { recursive: true, force: true }) }) - it('serializes each sourcemap once before writing its payload', async () => { - const { ctx, envCtx, vite } = await createFixture() - const toJSON = vi.fn(() => ({ - version: 3, - mappings: 'AAAA', - })) - - envCtx.recordTransform('/src/serialized.ts', { - name: 'plugin-a', - result: 'export const serialized = true', - start: 0, - end: 1, - sourcemaps: { toJSON }, - }, 'const serialized = true', vite.config.plugins[1]) - - await ctx.store.flush() - expect(toJSON).toHaveBeenCalledOnce() - await expect(envCtx.getModuleTransformInfo('/src/serialized.ts')).resolves.toMatchObject({ - transforms: [ - { sourcemaps: { version: 3, mappings: 'AAAA' } }, - { sourcemaps: { version: 3, mappings: 'AAAA' } }, - ], - }) - expect(toJSON).toHaveBeenCalledOnce() - }) - it('reuses released file payload ranges after invalidation', async () => { const dir = mkdtempSync(join(tmpdir(), 'vite-inspect-reuse-')) const filename = join(dir, 'payloads.bin') @@ -507,10 +472,6 @@ describe('vite inspect context', () => { result: `export const value = '${'result'.repeat(4096)}'`, start: 0, end: 1, - sourcemaps: { - version: 3, - mappings: 'AAAA;'.repeat(4096), - }, }, `const value = '${'source'.repeat(4096)}'`, vite.config.plugins[1]) await ctx.store.flush() } @@ -706,11 +667,11 @@ describe('vite inspect context', () => { hijackPlugin(plugin, ctx) await ctx.close() - await expect((plugin.transform as any).call( + expect((plugin.transform as any).call( { environment: env }, 'export const value = 1', '/src/module.ts', - )).resolves.toBe('export const value = 1\n// transformed') + )).toBe('export const value = 1\n// transformed') }) it('persists an oversized record without truncating it', async () => { @@ -795,21 +756,17 @@ describe('vite inspect context', () => { maxBatchItems: 1, }, }) - let sourcemapRef: WeakRef | undefined + let infoRef: WeakRef | undefined await (async () => { - const sourcemaps = { - version: 3, - mappings: 'x'.repeat(8 * 1024 * 1024), - } - sourcemapRef = new WeakRef(sourcemaps) - await envCtx.recordTransform('/src/retained.ts', { + const info = { name: 'plugin-a', - result: 'export const retained = true', + result: `export const retained = '${'x'.repeat(8 * 1024 * 1024)}'`, start: 0, end: 1, - sourcemaps, - }, 'const retained = true', vite.config.plugins[1]!) + } + infoRef = new WeakRef(info) + await envCtx.recordTransform('/src/retained.ts', info, 'const retained = true', vite.config.plugins[1]!) })() await ctx.store.flush() @@ -818,7 +775,7 @@ describe('vite inspect context', () => { await new Promise(resolve => setImmediate(resolve)) } - expect(sourcemapRef?.deref()).toBeUndefined() + expect(infoRef?.deref()).toBeUndefined() expect(ctx.store.getStats()).toMatchObject({ queuedItems: 0, inFlightItems: 0, @@ -1214,3 +1171,118 @@ describe('vite inspect context', () => { } }) }) + +describe('vite inspect storage fallback', () => { + it('keeps records in memory after an archive write fails', async () => { + const dir = mkdtempSync(join(tmpdir(), 'vite-inspect-fallback-')) + const handle = await open(join(dir, 'probe.bin'), 'w+') + const fileHandlePrototype = Object.getPrototypeOf(handle) + await handle.close() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const { ctx, envCtx, vite } = await createFixture({ + store: { + filename: join(dir, 'payloads.bin'), + }, + }) + const pluginA = vite.config.plugins[1]! + const record = (index: number) => { + envCtx.recordTransform(`/src/module-${index}.ts`, { + name: 'plugin-a', + result: `export const value = ${index}`, + start: index, + end: index + 1, + }, `const value = ${index}`, pluginA) + } + + try { + record(0) + await ctx.store.flush() + + const writev = vi.spyOn(fileHandlePrototype, 'writev').mockRejectedValue(new Error('ENOSPC')) + record(1) + record(2) + await ctx.store.flush() + writev.mockRestore() + + for (let index = 0; index < 3; index++) { + await expect(envCtx.getModuleTransformInfo(`/src/module-${index}.ts`)).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: `const value = ${index}` }, + { name: 'plugin-a', result: `export const value = ${index}` }, + ], + }) + } + expect((await envCtx.getPluginDetails(1)).calls).toHaveLength(3) + const warnings = warn.mock.calls.map(args => args.map(String).join(' ')).filter(message => message.includes('VDT0004')) + expect(warnings).toHaveLength(2) + + // Released fallback payloads must not leak into reused ranges. + envCtx.invalidate('/src/module-1.ts') + record(3) + await ctx.store.flush() + await expect(envCtx.getModuleTransformInfo('/src/module-3.ts')).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: 'const value = 3' }, + { name: 'plugin-a', result: 'export const value = 3' }, + ], + }) + } + finally { + warn.mockRestore() + await ctx.close() + rmSync(dir, { recursive: true, force: true }) + } + }) +}) + +describe('vite inspect hook timing', () => { + it('keeps sync hooks sync and still records them', async () => { + const { ctx, env, envCtx, vite } = await createFixture() + const plugin = vite.config.plugins[1]! as Plugin & { + transform: NonNullable + } + plugin.transform = code => `${code}\n// sync` + hijackPlugin(plugin, ctx) + + const result = (plugin.transform as any).call({ environment: env }, 'const value = 1', '/src/sync.ts') + expect(result).toBe('const value = 1\n// sync') + + await ctx.store.flush() + await expect(envCtx.getModuleTransformInfo('/src/sync.ts')).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: 'const value = 1' }, + { name: 'plugin-a', result: 'const value = 1\n// sync' }, + ], + }) + }) + + it('records async hooks and rethrows sync errors', async () => { + const { ctx, env, envCtx, vite } = await createFixture() + const asyncPlugin = vite.config.plugins[1]! as Plugin & { + transform: NonNullable + } + asyncPlugin.transform = async code => `${code}\n// async` + hijackPlugin(asyncPlugin, ctx) + const failingPlugin = vite.config.plugins[2]! as Plugin & { + transform: NonNullable + } + failingPlugin.transform = () => { + throw new Error('sync failure') + } + hijackPlugin(failingPlugin, ctx) + + const pending = (asyncPlugin.transform as any).call({ environment: env }, 'const value = 1', '/src/async.ts') + expect(pending).toBeInstanceOf(Promise) + await expect(pending).resolves.toBe('const value = 1\n// async') + expect(() => (failingPlugin.transform as any).call({ environment: env }, 'const value = 1', '/src/async.ts')).toThrow('sync failure') + + await ctx.store.flush() + await expect(envCtx.getModuleTransformInfo('/src/async.ts')).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: 'const value = 1' }, + { name: 'plugin-a', result: 'const value = 1\n// async' }, + { name: 'plugin-b', result: '[Error]' }, + ], + }) + }) +}) diff --git a/packages/vite/src/node/__tests__/inspect-server.test.ts b/packages/vite/src/node/__tests__/inspect-server.test.ts index 697b5620c..9f60d3ca9 100644 --- a/packages/vite/src/node/__tests__/inspect-server.test.ts +++ b/packages/vite/src/node/__tests__/inspect-server.test.ts @@ -449,8 +449,8 @@ describe('vite inspect server invalidation', () => { const source = 'export const value = 1' writeFileSync(id, source) let fail = true - let now = Date.now() - const clock = vi.spyOn(Date, 'now').mockImplementation(() => now) + let now = performance.now() + const clock = vi.spyOn(performance, 'now').mockImplementation(() => now) const declined: Plugin = { name: 'test:declined-file-load', enforce: 'pre', diff --git a/packages/vite/src/node/diagnostics.ts b/packages/vite/src/node/diagnostics.ts index efa4f4872..1666d0b03 100644 --- a/packages/vite/src/node/diagnostics.ts +++ b/packages/vite/src/node/diagnostics.ts @@ -14,5 +14,9 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({ why: (p: { operation: string }) => `Vite inspect storage failed while ${p.operation}.`, fix: 'Restart the Vite dev server. If the error persists, clear the Vite cache directory and reinstall dependencies.', }, + VDT0004: { + why: (p: { archive: string }) => `Vite inspect could not write the ${p.archive} archive to disk and now keeps new records in memory.`, + fix: 'Check that the Vite cache directory is writable and has free disk space, then restart the Vite dev server to resume writing to disk.', + }, }, }) diff --git a/packages/vite/src/node/inspect/hijack.ts b/packages/vite/src/node/inspect/hijack.ts index fec2c7ad3..8cbea1ce9 100644 --- a/packages/vite/src/node/inspect/hijack.ts +++ b/packages/vite/src/node/inspect/hijack.ts @@ -47,6 +47,46 @@ function hijackHook(plugin: PluginWithOrder, name: 'transform' | 'load' | 'resol } } +type HookSettled = (resultValue: any, error: unknown, start: number, end: number) => void + +// Epoch milliseconds with sub-millisecond precision, comparable with `Date.now()`. +function now(): number { + return performance.timeOrigin + performance.now() +} + +function isThenable(value: unknown): value is PromiseLike { + return value != null && typeof (value as PromiseLike).then === 'function' +} + +// Keep sync hooks sync: only async results pay for a promise chain. +function measureHook(fn: (...args: any[]) => any, context: any, args: any[], settled: HookSettled): any { + const start = now() + let resultValue: any + try { + resultValue = fn.apply(context, args) + } + catch (error) { + settled(undefined, error, start, now()) + throw error + } + + if (!isThenable(resultValue)) { + settled(resultValue, undefined, start, now()) + return resultValue + } + + return Promise.resolve(resultValue).then( + (value) => { + settled(value, undefined, start, now()) + return value + }, + (error) => { + settled(undefined, error, start, now()) + throw error + }, + ) +} + const hijackedPlugins = new WeakSet() export function hijackPlugin(plugin: Plugin, ctx: ViteInspectContext): void { @@ -54,154 +94,107 @@ export function hijackPlugin(plugin: Plugin, ctx: ViteInspectContext): void { return hijackedPlugins.add(plugin) - hijackHook(plugin, 'transform', async (fn, context, args, order) => { + hijackHook(plugin, 'transform', (fn, context, args, order) => { const code = args[0] as string const id = args[1] as string const transformRequest = trackTransformRequestId(id) const envContext = ctx.filter(id) ? ctx.getEnvContext(context?.environment) : undefined - let resultValue: any - let error: unknown - const start = Date.now() + if (!envContext) + return fn.apply(context, args) - try { - resultValue = await fn.apply(context, args) - } - catch (err) { - error = err - } + return measureHook(fn, context, args, (resultValue, error, start, end) => { + if (isTransformRequestStale(transformRequest)) + return - const end = Date.now() - const result = error - ? '[Error]' - : typeof resultValue === 'string' - ? resultValue - : resultValue?.code?.toString() + const result = error + ? '[Error]' + : typeof resultValue === 'string' + ? resultValue + : resultValue?.code?.toString() - if (envContext && !isTransformRequestStale(transformRequest)) { - const sourcemaps = typeof resultValue === 'string' ? null : resultValue?.map envContext.recordTransform(id, { name: plugin.name, result, start, end, order, - sourcemaps, error: error ? parseError(error) : undefined, }, code, plugin) - } - - if (error) - throw error - return resultValue + }) }) - hijackHook(plugin, 'load', async (fn, context, args) => { + hijackHook(plugin, 'load', (fn, context, args) => { const id = args[0] as string const transformRequest = trackTransformRequestId(id) const envContext = ctx.getEnvContext(context?.environment) - let resultValue: any - let error: unknown - const start = Date.now() - - try { - resultValue = await fn.apply(context, args) - } - catch (err) { - error = err - } - - const end = Date.now() - const result = error - ? '[Error]' - : typeof resultValue === 'string' - ? resultValue - : resultValue?.code - const sourcemaps = typeof resultValue === 'string' ? null : resultValue?.map - - const info = { - name: plugin.name, - result, - start, - end, - sourcemaps, - error: error ? parseError(error) : undefined, - } - - if (result != null && envContext && !isTransformRequestStale(transformRequest)) { - envContext.recordLoad(id, { - ...info, - result, - }, plugin) - } - else if (envContext && !isTransformRequestStale(transformRequest)) { - envContext.recordLoadCall(id, { - name: plugin.name, - start, - end, - error: error ? parseError(error) : undefined, - }, plugin) - } - - if (error) - throw error - return resultValue + if (!envContext) + return fn.apply(context, args) + + return measureHook(fn, context, args, (resultValue, error, start, end) => { + if (isTransformRequestStale(transformRequest)) + return + + const result = error + ? '[Error]' + : typeof resultValue === 'string' + ? resultValue + : resultValue?.code + + if (result != null) { + envContext.recordLoad(id, { + name: plugin.name, + result, + start, + end, + error: error ? parseError(error) : undefined, + }, plugin) + } + else { + envContext.recordLoadCall(id, { + name: plugin.name, + start, + end, + error: error ? parseError(error) : undefined, + }, plugin) + } + }) }) - hijackHook(plugin, 'resolveId', async (fn, context, args) => { + hijackHook(plugin, 'resolveId', (fn, context, args) => { const id = args[0] as string const transformRequest = trackTransformRequestId(id) const envContext = ctx.filter(id) ? ctx.getEnvContext(context?.environment) : undefined - let resultValue: any - let error: unknown - const start = Date.now() - - try { - resultValue = await fn.apply(context, args) - } - catch (err) { - error = err - } - - const end = Date.now() - if (!envContext) { - if (error) - throw error - return resultValue - } - - if (isTransformRequestStale(transformRequest)) { - if (error) - throw error - return resultValue - } - - const result = error - ? stringifyError(error) - : typeof resultValue === 'object' - ? resultValue?.id - : resultValue - - if (result) { - envContext.recordResolveId(id, { - name: plugin.name, - result, - start, - end, - error, - }, plugin) - } - else { - envContext.recordResolveIdCall(id, { - name: plugin.name, - result, - start, - end, - error, - }, plugin) - } - - if (error) - throw error - return resultValue + if (!envContext) + return fn.apply(context, args) + + return measureHook(fn, context, args, (resultValue, error, start, end) => { + if (isTransformRequestStale(transformRequest)) + return + + const result = error + ? stringifyError(error) + : typeof resultValue === 'object' + ? resultValue?.id + : resultValue + + if (result) { + envContext.recordResolveId(id, { + name: plugin.name, + result, + start, + end, + error, + }, plugin) + } + else { + envContext.recordResolveIdCall(id, { + name: plugin.name, + result, + start, + end, + error, + }, plugin) + } + }) }) } diff --git a/packages/vite/src/node/inspect/store/index.ts b/packages/vite/src/node/inspect/store/index.ts index 44d7780b0..318b917cf 100644 --- a/packages/vite/src/node/inspect/store/index.ts +++ b/packages/vite/src/node/inspect/store/index.ts @@ -49,7 +49,6 @@ interface PreparedWrite { write: Exclude resultIndex?: number sourceIndex?: number - sourcemapIndex?: number } export type { @@ -491,11 +490,9 @@ class PayloadViteInspectStore implements ViteInspectStore { sourceState.set(key, { hasSource: true, transformRequest: write.transformRequest }) } item.resultIndex = values.push(write.info.result) - 1 - item.sourcemapIndex = pushSerializedPayload(values, write.info.sourcemaps) } else if (write.operation === 'recordLoad') { item.resultIndex = values.push(write.info.result) - 1 - item.sourcemapIndex = pushSerializedPayload(values, write.info.sourcemaps) sourceState.set(scopeKey(write.scope, write.moduleId), { hasSource: write.info.result != null, transformRequest: write.transformRequest }) } prepared.push(item) @@ -519,14 +516,12 @@ class PayloadViteInspectStore implements ViteInspectStore { write, rangeAt(ranges, prepared.resultIndex), rangeAt(ranges, prepared.sourceIndex), - rangeAt(ranges, prepared.sourcemapIndex), ) } else if (write.operation === 'recordLoad') { this.applyLoad( write, rangeAt(ranges, prepared.resultIndex), - rangeAt(ranges, prepared.sourcemapIndex), ) } else if (write.operation === 'recordPluginCall' && write.info.type === 'load') { @@ -568,7 +563,6 @@ class PayloadViteInspectStore implements ViteInspectStore { write: Extract, result: ViteInspectPayloadRange | undefined, source: ViteInspectPayloadRange | undefined, - sourcemaps: ViteInspectPayloadRange | undefined, ): void { const data = this.getScope(write.scope) let transforms = data.transforms.get(write.moduleId) @@ -596,17 +590,15 @@ class PayloadViteInspectStore implements ViteInspectStore { resultSize: source?.length ?? 0, start: write.info.start, end: write.info.start, - sourcemaps, }) transforms.invokeCount += 1 } - transforms.items.push(toStoredTransformInfo(write.info, result, sourcemaps)) + transforms.items.push(toStoredTransformInfo(write.info, result)) } private applyLoad( write: Extract, result: ViteInspectPayloadRange | undefined, - sourcemaps: ViteInspectPayloadRange | undefined, ): void { const data = this.getScope(write.scope) this.commitDeclinedLoadMetrics(data, write.publicModuleId, write.transformRequest) @@ -617,7 +609,7 @@ class PayloadViteInspectStore implements ViteInspectStore { transformRequest: write.transformRequest, publicModuleId: write.publicModuleId, invokeCount: (existing?.invokeCount ?? 0) + 1, - items: [toStoredTransformInfo(write.info, result, sourcemaps)], + items: [toStoredTransformInfo(write.info, result)], }) } @@ -709,10 +701,7 @@ class PayloadViteInspectStore implements ViteInspectStore { } private async restoreTransformInfo(info: StoredViteInspectTransformInfo): Promise { - const [result, sourcemaps] = await Promise.all([ - info.result ? this.payloads.read(info.result) : undefined, - info.sourcemaps ? this.payloads.read(info.sourcemaps) : undefined, - ]) + const result = info.result ? await this.payloads.read(info.result) : undefined return { name: info.name, plugin_id: info.pluginId, @@ -720,7 +709,6 @@ class PayloadViteInspectStore implements ViteInspectStore { start: info.start, end: info.end, order: info.order, - sourcemaps: sourcemaps == null ? undefined : parsePayload(sourcemaps), error: info.error, } } @@ -853,10 +841,8 @@ function matchesInvalid( function collectPayloadRanges(items: StoredViteInspectTransformInfo[]): ViteInspectPayloadRange[] { const ranges = new Map() for (const item of items) { - for (const range of [item.result, item.sourcemaps]) { - if (range) - ranges.set(`${range.offset}:${range.length}`, range) - } + if (item.result) + ranges.set(`${item.result.offset}:${item.result.length}`, item.result) } return Array.from(ranges.values()) } @@ -864,7 +850,6 @@ function collectPayloadRanges(items: StoredViteInspectTransformInfo[]): ViteInsp function toStoredTransformInfo( info: ViteInspectTransformInfo, result: ViteInspectPayloadRange | undefined, - sourcemaps: ViteInspectPayloadRange | undefined, ): StoredViteInspectTransformInfo { return { name: info.name, @@ -875,41 +860,10 @@ function toStoredTransformInfo( start: info.start, end: info.end, order: info.order, - sourcemaps, error: info.error, } } -function pushSerializedPayload( - values: Array, - value: unknown, -): number | undefined { - const serialized = serializePayload(value) - if (serialized == null) - return undefined - return values.push(serialized) - 1 -} - -function serializePayload(value: unknown): string | undefined { - if (value == null) - return undefined - try { - return JSON.stringify(value) - } - catch { - return JSON.stringify(String(value)) - } -} - -function parsePayload(value: string): unknown { - try { - return JSON.parse(value) - } - catch { - return undefined - } -} - function rangeAt( ranges: Array, index: number | undefined, @@ -941,12 +895,10 @@ function estimateWriteBytes(write: QueuedWrite): number { return pluginCallBytes + estimateStringBytes(write.info.result) + estimateStringBytes(write.preTransformCode) - + estimateTopLevelSourcemapBytes(write.info.sourcemaps) } if (write.operation === 'recordLoad') { return pluginCallBytes + estimateStringBytes(write.info.result) - + estimateTopLevelSourcemapBytes(write.info.sourcemaps) } return pluginCallBytes } @@ -961,17 +913,6 @@ function estimateStringBytes(value: unknown): number { return typeof value === 'string' ? value.length * 2 : 0 } -function estimateTopLevelSourcemapBytes(value: unknown): number { - if (value == null) - return 0 - if (typeof value === 'string') - return value.length * 2 - if (typeof value !== 'object') - return 0 - const mappings = (value as { mappings?: unknown }).mappings - return estimateStringBytes(mappings) -} - function reportStorageError(operation: string, error: unknown): void { diagnostics.VDT0003({ operation, cause: toError(error) }, { method: 'error' }) } diff --git a/packages/vite/src/node/inspect/store/payload.ts b/packages/vite/src/node/inspect/store/payload.ts index d1a6bea3f..4fd2f1b52 100644 --- a/packages/vite/src/node/inspect/store/payload.ts +++ b/packages/vite/src/node/inspect/store/payload.ts @@ -56,6 +56,8 @@ class MemoryInspectPayloadStore implements InspectPayloadStore { class FileInspectPayloadStore implements InspectPayloadStore { private nextOffset = 0 private freeRanges: ViteInspectPayloadRange[] = [] + // Payloads keyed by offset once the archive file can no longer be written. + private fallback?: Map constructor(private readonly file: FileHandle) {} @@ -77,7 +79,20 @@ class FileInspectPayloadStore implements InspectPayloadStore { if (buffer.length > 0) writes.push({ buffer, range }) } - await this.writeAllocatedPayloads(writes) + + if (!this.fallback) { + try { + await this.writeAllocatedPayloads(writes) + return ranges + } + catch (error) { + // Transform data can't be reproduced, so keep it in memory instead of dropping it. + this.fallback = new Map() + diagnostics.VDT0004({ archive: 'payload', cause: error instanceof Error ? error : new Error(String(error)) }) + } + } + for (const { buffer, range } of writes) + this.fallback.set(range.offset, buffer) return ranges } @@ -85,6 +100,10 @@ class FileInspectPayloadStore implements InspectPayloadStore { if (range.length === 0) return '' + const fallback = this.fallback?.get(range.offset) + if (fallback?.length === range.length) + return fallback.toString() + const buffer = Buffer.allocUnsafe(range.length) let readOffset = 0 while (readOffset < buffer.length) { @@ -106,10 +125,13 @@ class FileInspectPayloadStore implements InspectPayloadStore { async close(): Promise { this.freeRanges.length = 0 + this.fallback?.clear() await this.file.close() } reclaim(ranges: ViteInspectPayloadRange[]): void { + for (const range of ranges) + this.fallback?.delete(range.offset) this.freeRanges.push(...ranges.filter(range => range.length > 0)) this.mergeFreeRanges() } diff --git a/packages/vite/src/node/inspect/store/plugin-calls.ts b/packages/vite/src/node/inspect/store/plugin-calls.ts index 501e9bfb8..8ee9891d1 100644 --- a/packages/vite/src/node/inspect/store/plugin-calls.ts +++ b/packages/vite/src/node/inspect/store/plugin-calls.ts @@ -463,9 +463,38 @@ class BinaryInspectPluginCallStore implements InspectPluginCallStore { } class FilePluginCallStorage implements PluginCallStorage { + // Segments at or after `start` live in memory once the archive file can no longer be written. + private fallback?: { start: number, storage: MemoryPluginCallStorage } + constructor(private readonly file: FileHandle) {} async write(input: Buffer[], startPosition: number): Promise { + if (!this.fallback) { + try { + await this.writeFile(input, startPosition) + return + } + catch (error) { + // The archive is append-only, so every later position belongs to the fallback. + this.fallback = { start: startPosition, storage: new MemoryPluginCallStorage() } + diagnostics.VDT0004({ archive: 'plugin call', cause: error instanceof Error ? error : new Error(String(error)) }) + } + } + await this.fallback.storage.write(input, startPosition - this.fallback.start) + } + + async read(position: number, length: number): Promise { + if (this.fallback && position >= this.fallback.start) + return this.fallback.storage.read(position - this.fallback.start, length) + return this.readFile(position, length) + } + + async close(): Promise { + await this.fallback?.storage.close() + await this.file.close() + } + + private async writeFile(input: Buffer[], startPosition: number): Promise { let buffers = input let position = startPosition while (buffers.length > 0) { @@ -480,7 +509,7 @@ class FilePluginCallStorage implements PluginCallStorage { } } - async read(position: number, length: number): Promise { + private async readFile(position: number, length: number): Promise { const buffer = Buffer.allocUnsafe(length) let offset = 0 while (offset < buffer.length) { @@ -496,10 +525,6 @@ class FilePluginCallStorage implements PluginCallStorage { } return buffer.subarray(0, offset) } - - async close(): Promise { - await this.file.close() - } } class MemoryPluginCallStorage implements PluginCallStorage { diff --git a/packages/vite/src/node/inspect/store/types.ts b/packages/vite/src/node/inspect/store/types.ts index e66e452a9..8936e0f9b 100644 --- a/packages/vite/src/node/inspect/store/types.ts +++ b/packages/vite/src/node/inspect/store/types.ts @@ -36,7 +36,6 @@ export interface StoredViteInspectTransformInfo { start: number end: number order?: string - sourcemaps?: ViteInspectPayloadRange error?: ViteInspectErrorInfo } diff --git a/packages/vite/src/node/inspect/types.ts b/packages/vite/src/node/inspect/types.ts index 5c05a9d6a..9cde12575 100644 --- a/packages/vite/src/node/inspect/types.ts +++ b/packages/vite/src/node/inspect/types.ts @@ -16,7 +16,6 @@ export interface ViteInspectTransformInfo { start: number end: number order?: string - sourcemaps?: unknown error?: ViteInspectErrorInfo } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-vite/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-vite/index.snapshot.d.ts index 525a47c28..ca3e81464 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-vite/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-vite/index.snapshot.d.ts @@ -87,7 +87,6 @@ export interface ViteInspectTransformInfo { start: number; end: number; order?: string; - sourcemaps?: unknown; error?: ViteInspectErrorInfo; } // #endregion