From f047be3f59dd915394aef33ba4a4a02fda87014a Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Wed, 7 Oct 2026 01:41:50 +0200 Subject: [PATCH] integration: clear cached Markdown when the options change (#13) Astro's content layer keeps rendered .md pages until its digest of the Astro config changes, and that digest skips integrations. Put a hash of the options on the Markdown processor's options, which the digest covers. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/starlight-codeblocks/src/integration.ts | 15 ++++++++++++++- packages/starlight-codeblocks/test/astro.test.ts | 12 +++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/starlight-codeblocks/src/integration.ts b/packages/starlight-codeblocks/src/integration.ts index e2c0ec3..94f4787 100644 --- a/packages/starlight-codeblocks/src/integration.ts +++ b/packages/starlight-codeblocks/src/integration.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import type { ExpressiveCodePlugin } from '@expressive-code/core'; @@ -49,8 +50,14 @@ export function codeblocksIntegration({ 'astro:config:setup'({ command, config, updateConfig, injectScript, logger }) { // The shapes `isSatteriProcessor()` and `isUnifiedProcessor()` check, without depending on either package. const processor = config.markdown.processor as - | { name?: string; options?: { mdastPlugins?: unknown[]; remarkPlugins?: unknown[] } } + | { + name?: string; + options?: { mdastPlugins?: unknown[]; remarkPlugins?: unknown[]; starlightCodeblocks?: string }; + } | undefined; + // The content layer keeps rendered Markdown until its digest of the Astro config changes. That digest + // skips integrations and functions but covers these options, which both processors otherwise ignore. + if (processor?.options) processor.options.starlightCodeblocks = optionsDigest(options); if (processor?.name === 'unified') { processor.options?.remarkPlugins?.push(remarkFromSatteri(mdastPlugins(options, logger))); } else processor?.options?.mdastPlugins?.push(...mdastPlugins(options, logger)); @@ -81,6 +88,12 @@ export function codeblocksIntegration({ }; } +const optionsDigest = (options: ResolvedOptions) => + createHash('sha256') + .update(JSON.stringify(options, (_, value) => (typeof value === 'function' ? String(value) : value))) + .digest('base64url') + .slice(0, 16); + /** The site's `ec.config.mjs`, imported fresh, as Expressive Code does. */ export async function loadEcConfig(root: URL) { const url = new URL('./ec.config.mjs', root); diff --git a/packages/starlight-codeblocks/test/astro.test.ts b/packages/starlight-codeblocks/test/astro.test.ts index 9767d2f..00d325c 100644 --- a/packages/starlight-codeblocks/test/astro.test.ts +++ b/packages/starlight-codeblocks/test/astro.test.ts @@ -11,7 +11,10 @@ import { mdastPlugins, restoreDirectives } from '../src/satteri/index.ts'; afterEach(() => setRegistry(undefined)); -type Processor = { name: string; options: { features?: { directive?: boolean }; mdastPlugins: unknown[] } }; +type Processor = { + name: string; + options: { features?: { directive?: boolean }; mdastPlugins: unknown[]; starlightCodeblocks?: string }; +}; async function setup(options: AstroCodeblocksOptions = {}, ecConfig?: string, others: string[] = []) { const root = mkdtempSync(join(tmpdir(), 'scb-')); @@ -57,6 +60,13 @@ test('changes nothing outside code blocks unless the site turns on code tabs or expect(getRegistry()?.options).toMatchObject({ codeTabs: false, inlineHighlighting: false }); }); +test('puts a digest of its options in the hashed Astro config, so that a change clears rendered Markdown', async () => { + const digest = async (options: AstroCodeblocksOptions) => + (await setup(options)).processor.options.starlightCodeblocks; + expect(await digest({})).toBe(await digest({})); + expect(await digest({ footnotes: false })).not.toBe(await digest({})); +}); + test('adds the inline code stylesheet on every page, and never the API card page styles', async () => { const { scripts } = await setup({ inlineHighlighting: {} }); expect(scripts).toEqual(['import "virtual:starlight-codeblocks/inline-code.css";']);