From 211ae0840ba2fb8d1150f5388c29112c27279d95 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 21 Jul 2026 14:57:42 -0700 Subject: [PATCH] chore(toolbar): drop lowlight all-grammar re-export and posthog-typed typings from the bundle graph Resolve 'lowlight' in the toolbar build to a pared module exposing only createLowlight and common. The package index re-exports `all`, which pulls all 192 highlight.js grammars (~1.2 MB of source) into the bundle graph while only the 37 common grammars are used. Shim lib/posthog-typed (~200 KB of generated event typings) with a runtime-equivalent capture/captureRaw proxy. A parity test guards against the generated module growing runtime surface the shim lacks. Toolbar input graph: 13.78 MiB -> 12.45 MiB (978 -> 822 files). Boundary edges, eager output size, and CSP checks are unchanged. Generated-By: PostHog Code Task-Id: 40b722de-ba04-412c-96f4-35888826b7e9 --- frontend/src/toolbar/shims/posthogTyped.ts | 36 ++++++++++++++++++++++ frontend/src/toolbar/shims/shims.test.ts | 22 +++++++++++++ frontend/toolbar-config.mjs | 25 +++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 frontend/src/toolbar/shims/posthogTyped.ts diff --git a/frontend/src/toolbar/shims/posthogTyped.ts b/frontend/src/toolbar/shims/posthogTyped.ts new file mode 100644 index 000000000000..56d59333b551 --- /dev/null +++ b/frontend/src/toolbar/shims/posthogTyped.ts @@ -0,0 +1,36 @@ +import type { CaptureOptions, CaptureResult, Properties } from 'posthog-js' +import originalPostHog from 'posthog-js' + +// Toolbar shim — lib/posthog-typed is ~200 KB of generated event typings wrapped around a +// small runtime. This mirrors that runtime exactly (capture/captureRaw delegating to the +// posthog-js singleton, everything else proxied through) without the typings. +const enhanced: Record = { + capture: ( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined => originalPostHog.capture(event_name, properties, options), + captureRaw: ( + event_name: string, + properties?: Properties | null, + options?: CaptureOptions + ): CaptureResult | undefined => originalPostHog.capture(event_name, properties, options), +} + +const posthog = new Proxy(enhanced, { + get(target, prop) { + if (prop in target) { + return target[prop as string] + } + return (originalPostHog as unknown as Record)[prop] + }, + set(_target, prop, value) { + ;(originalPostHog as unknown as Record)[prop] = value + return true + }, +}) as unknown as typeof originalPostHog & { captureRaw: typeof originalPostHog.capture } + +export default posthog + +// Re-export everything else from posthog-js, matching lib/posthog-typed's surface +export * from 'posthog-js' diff --git a/frontend/src/toolbar/shims/shims.test.ts b/frontend/src/toolbar/shims/shims.test.ts index 48fd9f7c94c3..336b120fc7c7 100644 --- a/frontend/src/toolbar/shims/shims.test.ts +++ b/frontend/src/toolbar/shims/shims.test.ts @@ -1,9 +1,13 @@ import { expectLogic } from 'kea-test-utils' +import posthogJs from 'posthog-js' + +import typedPosthog from 'lib/posthog-typed' import { initKeaTests } from '~/test/init' import { featureFlagLogic, getFeatureFlagPayload } from './featureFlagLogic' import { membersLogic } from './membersLogic' +import shimmedPosthog from './posthogTyped' import { sceneLogic } from './sceneLogic' import { surveyQuestionLabelsLogic } from './surveyQuestionLabelsLogic' import { isAuthenticatedTeam, teamLogic } from './teamLogic' @@ -76,4 +80,22 @@ describe('toolbar shims', () => { expect(getFeatureFlagPayload(flag)).toBeUndefined() }) }) + + describe('posthogTyped shim', () => { + it('exposes every runtime member lib/posthog-typed adds beyond posthog-js', () => { + // The toolbar build swaps lib/posthog-typed for the shim, invisible to the + // typechecker — if the generator grows a new runtime method, the shim must too, + // or toolbar code calling it crashes on customer pages. + for (const key of Object.keys(typedPosthog)) { + expect(typeof (shimmedPosthog as any)[key]).toBe(typeof (typedPosthog as any)[key]) + } + }) + + it.each([['capture'], ['captureRaw']] as const)('%s delegates to posthog-js capture', (method) => { + const captureSpy = jest.spyOn(posthogJs, 'capture').mockReturnValue(undefined) + shimmedPosthog[method]('some event', { foo: 'bar' }) + expect(captureSpy).toHaveBeenCalledWith('some event', { foo: 'bar' }, undefined) + captureSpy.mockRestore() + }) + }) }) diff --git a/frontend/toolbar-config.mjs b/frontend/toolbar-config.mjs index b801d33b2ec1..f71db7ddf1ec 100644 --- a/frontend/toolbar-config.mjs +++ b/frontend/toolbar-config.mjs @@ -1,8 +1,11 @@ import * as fs from 'fs' +import { createRequire } from 'module' import * as path from 'path' import { commonConfig, copyRRWebWorkerFiles, createHashlessEntrypoints, esbuildBuild, isDev } from '@posthog/esbuilder' +const require = createRequire(import.meta.url) + // `TOOLBAR_PUBLIC_PATH`, when set, overrides the default `publicPath` so the // toolbar bundle and its assets can be hosted under a versioned, content-pinned // URL on the posthog-js CDN. Used by posthog-js's release workflow to ship a @@ -34,6 +37,9 @@ const shimmedModules = { // app (TZLabel, Link, HeatmapEventsPanel), whose scenes/urls import would otherwise pull // every product manifest into the bundle. 'scenes/urls': 'src/toolbar/urls.ts', + // Not a kea shim: the generated event typings are ~200 KB of source the toolbar must + // not bundle. The shim mirrors the module's small runtime (capture/captureRaw proxy). + 'lib/posthog-typed': 'src/toolbar/shims/posthogTyped.ts', } // Modules replaced with an inert proxy that logs access in debug mode @@ -80,9 +86,28 @@ function createToolbarModulePlugin(dirname) { path.resolve(dirname, shimFile), ]) ) + // lowlight's package index re-exports `all` — every highlight.js grammar, ~1.2 MB of + // source — alongside the `common` set our code actually uses. The re-export alone pulls + // all 192 grammars into the bundle graph, so resolve 'lowlight' to a pared-down module + // instead. Its relative imports bypass the package's exports map (which hides lib/). + const lowlightDir = path.dirname(require.resolve('lowlight')) + return { name: 'toolbar-module-replacements', setup(build) { + build.onResolve({ filter: /^lowlight$/ }, () => ({ + path: 'lowlight', + namespace: 'lowlight-common-only', + })) + build.onLoad({ filter: /.*/, namespace: 'lowlight-common-only' }, () => ({ + contents: ` + export { createLowlight } from './lib/index.js' + export { grammars as common } from './lib/common.js' + `, + resolveDir: lowlightDir, + loader: 'js', + })) + build.onResolve({ filter: /.*/ }, (args) => { const shimFile = shimmedModules[args.path] ?? shimmedModules[args.path.replace(/^~\//, '')] if (shimFile) {