From ab875cdd6c442602271c3bde21f52275cbd7fcb6 Mon Sep 17 00:00:00 2001 From: Cypher-Aura-19 Date: Sat, 29 Aug 2026 15:18:09 +0500 Subject: [PATCH 1/2] fix(rollup-plugin): use stable chunk ids Use the content-derived createStableChunkId in the default symbol-set renderChunk path so identical builds keep the same chunk id and the same [hash] file names instead of renaming every chunk on every build. Co-Authored-By: Claude --- .changeset/stable-rollup-chunk-ids.md | 5 +++ packages/rollup-plugin/src/index.spec.ts | 8 +++-- packages/rollup-plugin/src/index.ts | 5 +-- packages/rollup-plugin/test/vite8.test.mjs | 41 ++++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 .changeset/stable-rollup-chunk-ids.md diff --git a/.changeset/stable-rollup-chunk-ids.md b/.changeset/stable-rollup-chunk-ids.md new file mode 100644 index 0000000000..2bb2d50ca5 --- /dev/null +++ b/.changeset/stable-rollup-chunk-ids.md @@ -0,0 +1,5 @@ +--- +'@posthog/rollup-plugin': patch +--- + +The default (symbol-set) release mode now derives chunk ids from chunk content instead of a random id per build, so identical builds keep the same chunk id and the same content-hashed `[hash]` file names instead of renaming every chunk on every build. diff --git a/packages/rollup-plugin/src/index.spec.ts b/packages/rollup-plugin/src/index.spec.ts index c354a2a3a1..1365424777 100644 --- a/packages/rollup-plugin/src/index.spec.ts +++ b/packages/rollup-plugin/src/index.spec.ts @@ -94,12 +94,14 @@ describe('posthogRollupPlugin', () => { expect(determineChunkIdFromSource(result!.code)).toBe(commentId) }) - it('mints a fresh chunk id per injection', () => { + it('derives the chunk id from content, so identical code keeps its id across rebuilds', () => { const plugin = testPlugin(options) const first = plugin.renderChunk.handler(code, { fileName: 'a.js' }) - const second = plugin.renderChunk.handler(code, { fileName: 'b.js' }) + const rebuilt = testPlugin(options).renderChunk.handler(code, { fileName: 'b.js' }) + const other = plugin.renderChunk.handler(`${code}more();`, { fileName: 'c.js' }) - expect(determineChunkIdFromSource(first!.code)).not.toBe(determineChunkIdFromSource(second!.code)) + expect(determineChunkIdFromSource(rebuilt!.code)).toBe(determineChunkIdFromSource(first!.code)) + expect(determineChunkIdFromSource(other!.code)).not.toBe(determineChunkIdFromSource(first!.code)) }) it('does not re-inject already injected code', () => { diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index eee693d19b..b758e06799 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -4,7 +4,6 @@ import { resolveConfig, runSourcemapCli, resolveReleaseId, - createChunkId, createStableChunkId, createChunkIdSnippet, createChunkIdComment, @@ -118,7 +117,9 @@ export default function posthogRollupPlugin(userOptions: PostHogRollupPluginOpti } if (!eventReleaseMode) { - const chunkId = createChunkId() + // Content-addressed so an unchanged chunk keeps its id — and therefore its + // emitted [hash] file name — across rebuilds. + const chunkId = createStableChunkId(code) rememberChunkId(chunk.fileName, chunkId) return injectChunkId(code, chunkId) } diff --git a/packages/rollup-plugin/test/vite8.test.mjs b/packages/rollup-plugin/test/vite8.test.mjs index 19dacf213a..302396e89d 100644 --- a/packages/rollup-plugin/test/vite8.test.mjs +++ b/packages/rollup-plugin/test/vite8.test.mjs @@ -71,3 +71,44 @@ process.stdin.on('end', () => { 'the runtime snippet and upload comment should carry the same chunk id' ) }) + +test('keeps [hash] file names identical across identical builds', async (t) => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'posthog-rollup-plugin-vite-')) + t.after(() => fs.rm(root, { recursive: true, force: true })) + + const cliPath = path.join(root, 'posthog-cli.mjs') + await fs.writeFile(cliPath, '#!/usr/bin/env node\n') + await fs.chmod(cliPath, 0o755) + await fs.writeFile(path.join(root, 'index.html'), '') + await fs.writeFile(path.join(root, 'src.ts'), 'console.log("app")') + + const buildEntryFiles = async (outDir) => { + await build({ + configFile: false, + root, + logLevel: 'silent', + plugins: [ + posthogRollupPlugin({ + personalApiKey: 'phx_test', + projectId: '1', + cliBinaryPath: cliPath, + sourcemaps: { deleteAfterUpload: false }, + }), + ], + build: { outDir, minify: 'esbuild' }, + }) + + return (await fs.readdir(path.join(root, outDir))).filter((fileName) => fileName.endsWith('.js')).sort() + } + + // The injected chunk id is content-addressed, so identical input must emit the same [hash] file + // names — a random id in renderChunk would rename every chunk on every build. + const first = await buildEntryFiles('dist-a') + const second = await buildEntryFiles('dist-b') + assert.deepEqual(first, second) + + // Changed input still gets a different chunk id and therefore a different hash. + await fs.writeFile(path.join(root, 'src.ts'), 'console.log("app changed")') + const changed = await buildEntryFiles('dist-c') + assert.notDeepEqual(changed, first) +}) From b396894cfce819ac0e75d6a359f06ddfddbeac3b Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 31 Aug 2026 13:28:35 +0200 Subject: [PATCH 2/2] fix(rollup-plugin): handle stable ids across releases --- .changeset/stable-rollup-chunk-ids.md | 3 ++- packages/plugin-utils/src/cli.spec.ts | 6 ++++++ packages/plugin-utils/src/cli.ts | 5 +++++ packages/rollup-plugin/test/vite8.test.mjs | 5 ++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.changeset/stable-rollup-chunk-ids.md b/.changeset/stable-rollup-chunk-ids.md index 2bb2d50ca5..ac143fcb2a 100644 --- a/.changeset/stable-rollup-chunk-ids.md +++ b/.changeset/stable-rollup-chunk-ids.md @@ -1,5 +1,6 @@ --- +'@posthog/plugin-utils': patch '@posthog/rollup-plugin': patch --- -The default (symbol-set) release mode now derives chunk ids from chunk content instead of a random id per build, so identical builds keep the same chunk id and the same content-hashed `[hash]` file names instead of renaming every chunk on every build. +The default (symbol-set) release mode now derives chunk ids from chunk content instead of a random id per build, so identical builds keep the same chunk id and the same content-hashed `[hash]` file names instead of renaming every chunk on every build. Symbol-set uploads now replace the previous release binding when a stable chunk id is reused by a later release. diff --git a/packages/plugin-utils/src/cli.spec.ts b/packages/plugin-utils/src/cli.spec.ts index 8363a135dc..4ca2468ccd 100644 --- a/packages/plugin-utils/src/cli.spec.ts +++ b/packages/plugin-utils/src/cli.spec.ts @@ -47,6 +47,12 @@ describe('buildSourcemapCliArgs', () => { expect(args).not.toContain('--delete-after') }) + it('forces symbol-set uploads so stable chunk ids can move to a new release', () => { + const args = buildSourcemapCliArgs(config, { stdin: true }, 'upload') + + expect(args).toContain('--force') + }) + it.each([ { releaseMode: 'symbol-set' as const, expected: false }, { releaseMode: 'event' as const, expected: true }, diff --git a/packages/plugin-utils/src/cli.ts b/packages/plugin-utils/src/cli.ts index df8f50bfcb..ab2cc61532 100644 --- a/packages/plugin-utils/src/cli.ts +++ b/packages/plugin-utils/src/cli.ts @@ -52,6 +52,11 @@ export function buildSourcemapCliArgs( // predating the flag. if (config.sourcemaps.releaseMode === 'event') { args.push('--release-mode', 'event') + } else if (command === 'upload') { + // Bundler-injected symbol-set ids are content-addressed so identical builds keep stable + // file names. A later release reuses the id but changes the release metadata in the + // upload, so allow the CLI to replace the previous release-bound symbol set. + args.push('--force') } // On `upload` the caller owns map deletion: `--delete-after` also rewrites diff --git a/packages/rollup-plugin/test/vite8.test.mjs b/packages/rollup-plugin/test/vite8.test.mjs index 302396e89d..17540430b2 100644 --- a/packages/rollup-plugin/test/vite8.test.mjs +++ b/packages/rollup-plugin/test/vite8.test.mjs @@ -98,13 +98,16 @@ test('keeps [hash] file names identical across identical builds', async (t) => { build: { outDir, minify: 'esbuild' }, }) - return (await fs.readdir(path.join(root, outDir))).filter((fileName) => fileName.endsWith('.js')).sort() + return (await fs.readdir(path.join(root, outDir), { recursive: true })) + .filter((fileName) => fileName.endsWith('.js')) + .sort() } // The injected chunk id is content-addressed, so identical input must emit the same [hash] file // names — a random id in renderChunk would rename every chunk on every build. const first = await buildEntryFiles('dist-a') const second = await buildEntryFiles('dist-b') + assert.ok(first.length > 0, 'the test must discover Vite output nested under assets/') assert.deepEqual(first, second) // Changed input still gets a different chunk id and therefore a different hash.