diff --git a/packages/vite/src/node/__tests__/inspect-context.test.ts b/packages/vite/src/node/__tests__/inspect-context.test.ts index 76ad1518..49485b66 100644 --- a/packages/vite/src/node/__tests__/inspect-context.test.ts +++ b/packages/vite/src/node/__tests__/inspect-context.test.ts @@ -1172,6 +1172,68 @@ describe('vite inspect context', () => { }) }) +describe('vite inspect unchanged results', () => { + it('stores a result that matches its input once and reads it back for every step', async () => { + const dir = mkdtempSync(join(tmpdir(), 'vite-inspect-unchanged-')) + const filename = join(dir, 'payloads.bin') + const { ctx, envCtx, vite } = await createFixture({ + store: { + filename, + }, + }) + const source = `const value = '${'s'.repeat(1024)}'` + const transformed = `export const value = '${'t'.repeat(1024)}'` + const record = () => { + envCtx.recordTransform('/src/same.ts', { + name: 'plugin-a', + result: transformed, + start: 0, + end: 1, + }, source, vite.config.plugins[1]) + // plugin-b returns its input untouched. + envCtx.recordTransform('/src/same.ts', { + name: 'plugin-b', + result: transformed, + start: 1, + end: 2, + }, transformed, vite.config.plugins[2]) + } + + try { + record() + await ctx.store.flush() + expect(statSync(filename).size).toBe(source.length + transformed.length) + await expect(envCtx.getModuleTransformInfo('/src/same.ts')).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: source }, + { name: 'plugin-a', result: transformed }, + { name: 'plugin-b', result: transformed }, + ], + }) + expect((await envCtx.getPluginDetails(2)).calls).toMatchObject([{ unchanged: true }]) + + // The shared payload is released once and its range is reused without corruption. + for (let cycle = 0; cycle < 3; cycle++) { + envCtx.invalidate('/src/same.ts') + record() + await ctx.store.flush() + expect(statSync(filename).size).toBe(source.length + transformed.length) + } + await expect(envCtx.getModuleTransformInfo('/src/same.ts')).resolves.toMatchObject({ + transforms: [ + { name: '__load__', result: source }, + { name: 'plugin-a', result: transformed }, + { name: 'plugin-b', result: transformed }, + ], + }) + } + finally { + await ctx.close() + rmSync(dir, { recursive: true, force: true }) + } + }) +}) + describe('vite inspect storage fallback', () => { it('keeps records in memory after an archive write fails', async () => { const dir = mkdtempSync(join(tmpdir(), 'vite-inspect-fallback-')) diff --git a/packages/vite/src/node/inspect/store/index.ts b/packages/vite/src/node/inspect/store/index.ts index 318b917c..f8599b5c 100644 --- a/packages/vite/src/node/inspect/store/index.ts +++ b/packages/vite/src/node/inspect/store/index.ts @@ -456,6 +456,17 @@ class PayloadViteInspectStore implements ViteInspectStore { const prepared: PreparedWrite[] = [] const pluginCallWrites: InspectPluginCallWrite[] = [] const sourceState = new Map() + // Latest code queued for each module in this batch, so a step that returns its input unchanged reuses that payload. + const latestCode = new Map() + const pushCode = (key: string, value: string | null | undefined): number => { + const latest = latestCode.get(key) + if (value != null && latest?.value === value) + return latest.index + const index = values.push(value) - 1 + if (value != null) + latestCode.set(key, { value, index }) + return index + } const hasSource = (scope: string, moduleId: string, transformRequest?: object): boolean => { const key = scopeKey(scope, moduleId) @@ -486,13 +497,16 @@ class PayloadViteInspectStore implements ViteInspectStore { if (write.operation === 'recordTransform') { const key = scopeKey(write.scope, write.moduleId) if (!hasSource(write.scope, write.moduleId, write.transformRequest)) { - item.sourceIndex = values.push(write.preTransformCode) - 1 + latestCode.delete(key) + item.sourceIndex = pushCode(key, write.preTransformCode) sourceState.set(key, { hasSource: true, transformRequest: write.transformRequest }) } - item.resultIndex = values.push(write.info.result) - 1 + item.resultIndex = pushCode(key, write.info.result) } else if (write.operation === 'recordLoad') { - item.resultIndex = values.push(write.info.result) - 1 + const key = scopeKey(write.scope, write.moduleId) + latestCode.delete(key) + item.resultIndex = pushCode(key, write.info.result) sourceState.set(scopeKey(write.scope, write.moduleId), { hasSource: write.info.result != null, transformRequest: write.transformRequest }) } prepared.push(item)