diff --git a/.changeset/stable-rollup-chunk-ids.md b/.changeset/stable-rollup-chunk-ids.md new file mode 100644 index 0000000000..ac143fcb2a --- /dev/null +++ b/.changeset/stable-rollup-chunk-ids.md @@ -0,0 +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. 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/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..17540430b2 100644 --- a/packages/rollup-plugin/test/vite8.test.mjs +++ b/packages/rollup-plugin/test/vite8.test.mjs @@ -71,3 +71,47 @@ 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), { 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. + await fs.writeFile(path.join(root, 'src.ts'), 'console.log("app changed")') + const changed = await buildEntryFiles('dist-c') + assert.notDeepEqual(changed, first) +})