From bd374b7507de8d706c845946fd847e76de6fc06b Mon Sep 17 00:00:00 2001 From: fkatsuhiro <113022468+fkatsuhiro@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:28:43 +0900 Subject: [PATCH 01/21] fix: deprecation warnings and inline comments from Astro 7.0 to 8.0. (#17712) --- .changeset/cyan-pigs-take.md | 5 +++++ packages/astro/client.d.ts | 8 ++++---- packages/astro/src/content/runtime.ts | 4 ++-- packages/astro/src/core/compile/compile.ts | 2 +- packages/astro/src/core/create-vite.ts | 2 +- packages/astro/src/core/session/types.ts | 10 +++++----- packages/astro/src/runtime/server/astro-global.ts | 4 ++-- packages/astro/templates/content/module.mjs | 6 +++--- packages/astro/types/content.d.ts | 4 ++-- 9 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 .changeset/cyan-pigs-take.md diff --git a/.changeset/cyan-pigs-take.md b/.changeset/cyan-pigs-take.md new file mode 100644 index 000000000000..ef6248cca3b6 --- /dev/null +++ b/.changeset/cyan-pigs-take.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Updates deprecation messages target from Astro 7 to 8 diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 2672510d79b1..653e3b8bad2a 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -7,7 +7,7 @@ /// interface ImportMetaEnv { - // TODO: remove in Astro 7 + // TODO: remove in Astro 8 /** * The prefix for Astro-generated asset links if the build.assetsPrefix config option is set. This can be used to create asset links not handled by Astro. * @deprecated This will be removed in a future major version of Astro. Use `build.assetsPrefix` from `astro:config/server` instead. @@ -149,11 +149,11 @@ declare module 'astro:components' { export * from 'astro/components'; } -// TODO: remove in Astro 7 +// TODO: remove in Astro 8 /** * @deprecated * `import { z } from 'astro:schema'` is deprecated and will be removed - * in Astro 7. Use `import { z } from 'astro/zod'` instead. + * in Astro 8. Use `import { z } from 'astro/zod'` instead. */ declare module 'astro:schema' { export * from 'astro/zod'; @@ -162,7 +162,7 @@ declare module 'astro:schema' { /** * @deprecated * `import { z } from 'astro:schema'` is deprecated and will be removed - * in Astro 7. Use `import { z } from 'astro/zod'` instead. + * in Astro 8. Use `import { z } from 'astro/zod'` instead. */ export const z = zod.z; } diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index d63ec52077ca..018b59e0f1ea 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -214,13 +214,13 @@ export function createGetEntry({ liveCollections }: { liveCollections: LiveColle data, collection, } as DataEntryResult | ContentEntryResult; - // TODO: remove in Astro 7 + // TODO: remove in Astro 8 warnForPropertyAccess( result.data, 'slug', `[content] Attempted to access deprecated property on "${collection}" entry.\nThe "slug" property is no longer automatically added to entries. Please use the "id" property instead.`, ); - // TODO: remove in Astro 7 + // TODO: remove in Astro 8 warnForPropertyAccess( result, 'render', diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts index 1f1a47b348fa..1556d605ade2 100644 --- a/packages/astro/src/core/compile/compile.ts +++ b/packages/astro/src/core/compile/compile.ts @@ -50,7 +50,7 @@ export async function compile({ normalizedFilename: normalizeFilename(filename, astroConfig.root), sourcemap: 'both', internalURL: 'astro/compiler-runtime', - // TODO: remove in Astro v7 + // TODO: remove in Astro v8 astroGlobalArgs: JSON.stringify(astroConfig.site), scopedStyleStrategy: astroConfig.scopedStyleStrategy, resultScopedSlot: true, diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index ec6160a00cd1..3954b670ddb1 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -276,7 +276,7 @@ export async function createVite( find: 'astro:middleware', replacement: 'astro/virtual-modules/middleware.js', }, - // TODO: remove in Astro 7 + // TODO: remove in Astro 8 { find: 'astro:schema', replacement: 'astro/zod', diff --git a/packages/astro/src/core/session/types.ts b/packages/astro/src/core/session/types.ts index 280f63150d5c..79b7de11deba 100644 --- a/packages/astro/src/core/session/types.ts +++ b/packages/astro/src/core/session/types.ts @@ -44,7 +44,7 @@ export interface BaseSessionConfig { interface DriverConfig extends BaseSessionConfig { /** Config object for a session driver */ driver: TDriver; - /** @deprecated Pass options to the driver function directly. This will be removed in Astro 7 */ + /** @deprecated Pass options to the driver function directly. This will be removed in Astro 8 */ options?: never; } @@ -56,12 +56,12 @@ interface UnstorageConfig< > extends BaseSessionConfig { /** * Entrypoint for an unstorage session driver - * @deprecated Use `import { sessionDrivers } from 'astro/config'` instead. This will be removed in Astro 7 + * @deprecated Use `import { sessionDrivers } from 'astro/config'` instead. This will be removed in Astro 8 */ driver?: TDriver; /** * Options for the unstorage driver - * @deprecated Use `import { sessionDrivers } from 'astro/config'` instead. This will be removed in Astro 7 + * @deprecated Use `import { sessionDrivers } from 'astro/config'` instead. This will be removed in Astro 8 */ options?: TOptions; } @@ -69,12 +69,12 @@ interface UnstorageConfig< interface CustomConfig extends BaseSessionConfig { /** * Entrypoint for a custom session driver - * @deprecated Use the object shape (type `SessionDriverConfig`). This will be removed in Astro 7 + * @deprecated Use the object shape (type `SessionDriverConfig`). This will be removed in Astro 8 */ driver?: string; /** * Options for the custom session driver - * @deprecated Use the object shape (type `SessionDriverConfig`). This will be removed in Astro 7 + * @deprecated Use the object shape (type `SessionDriverConfig`). This will be removed in Astro 8 */ options?: Record; } diff --git a/packages/astro/src/runtime/server/astro-global.ts b/packages/astro/src/runtime/server/astro-global.ts index 3a93dcdb5081..e4e0c4f610e6 100644 --- a/packages/astro/src/runtime/server/astro-global.ts +++ b/packages/astro/src/runtime/server/astro-global.ts @@ -13,7 +13,7 @@ function createError(name: string) { // inside of getStaticPaths. See the `astroGlobalArgs` option for parameter type. export function createAstro(site: string | undefined): AstroGlobal { return { - // TODO: throw in Astro 7 + // TODO: throw in Astro 8 get site() { // This is created inside of the runtime so we don't have access to the Astro logger. console.warn( @@ -21,7 +21,7 @@ export function createAstro(site: string | undefined): AstroGlobal { ); return site ? new URL(site) : undefined; }, - // TODO: throw in Astro 7 + // TODO: throw in Astro 8 get generator() { // This is created inside of the runtime so we don't have access to the Astro logger. console.warn( diff --git a/packages/astro/templates/content/module.mjs b/packages/astro/templates/content/module.mjs index 52cdda1f62ee..fbf31b34617c 100644 --- a/packages/astro/templates/content/module.mjs +++ b/packages/astro/templates/content/module.mjs @@ -13,7 +13,7 @@ export { defineLiveCollection, renderEntry as render, } from 'astro/content/runtime'; -// TODO: remove in Astro 7 +// TODO: remove in Astro 8 export { z } from 'astro/zod'; /* @@LIVE_CONTENT_CONFIG@@ */ @@ -38,8 +38,8 @@ export const getLiveEntry = createGetLiveEntry({ liveCollections, }); -// TODO: remove in Astro 7 +// TODO: remove in Astro 8 export const getEntryBySlug = createDeprecatedFunction('getEntryBySlug'); -// TODO: remove in Astro 7 +// TODO: remove in Astro 8 export const getDataEntryById = createDeprecatedFunction('getDataEntryById'); diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index 25d3e2b07f33..d27d7242349d 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -10,11 +10,11 @@ declare module 'astro:content' { } from 'astro/content/config'; export { defineLiveCollection, defineCollection } from 'astro/content/config'; - // TODO: remove in Astro 7 + // TODO: remove in Astro 8 /** * @deprecated * `import { z } from 'astro:content'` is deprecated and will be removed - * in Astro 7. Use `import { z } from 'astro/zod'` instead. + * in Astro 8. Use `import { z } from 'astro/zod'` instead. */ export const z = zod.z; From d48312502ef33a32aef3f25b6b6035db8b38e189 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:16:33 -0700 Subject: [PATCH 02/21] Suppress CSP hashes when 'unsafe-inline' is present in a directive (#17664) Co-authored-by: ematipico --- .changeset/gold-roses-fetch.md | 5 + .../astro/src/runtime/server/render/csp.ts | 28 ++++-- packages/astro/src/types/public/config.ts | 2 +- .../astro/test/units/csp/render-csp.test.ts | 96 +++++++++++++++++++ 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 .changeset/gold-roses-fetch.md diff --git a/.changeset/gold-roses-fetch.md b/.changeset/gold-roses-fetch.md new file mode 100644 index 000000000000..0f0534d834d5 --- /dev/null +++ b/.changeset/gold-roses-fetch.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes an issue where Astro CSP support didn't correctly handle cases `"unsafe-inline"` resource. Now when `"unsafe-inline"`, Astro won't emit hashes for the directive specified. diff --git a/packages/astro/src/runtime/server/render/csp.ts b/packages/astro/src/runtime/server/render/csp.ts index 3c16e16e544a..5dc7041e903d 100644 --- a/packages/astro/src/runtime/server/render/csp.ts +++ b/packages/astro/src/runtime/server/render/csp.ts @@ -66,13 +66,20 @@ export function renderCspContent(result: SSRResult): string { const styleElemActive = isEnabled(style.element); const strictDynamicSuffix = scriptDirective.strictDynamic ? ` 'strict-dynamic'` : ''; - // On the baseline, omit the element hashes when the `-elem` directive will carry them instead. + // Per the CSP spec, browsers ignore `'unsafe-inline'` when a hash or nonce is present in the + // same directive. Suppress hashes on any directive whose resources include `'unsafe-inline'` so + // the user's intent is preserved. + const scriptDefaultHasUnsafeInline = hasUnsafeInline(script.default.resources); + const styleDefaultHasUnsafeInline = hasUnsafeInline(style.default.resources); + + // On the baseline, omit the element hashes when the `-elem` directive will carry them instead, + // and omit all hashes when `'unsafe-inline'` is present. const scriptBaselineTokens = [ - ...(scriptElemActive ? [] : [...finalScriptHashes]), + ...(scriptElemActive || scriptDefaultHasUnsafeInline ? [] : [...finalScriptHashes]), ...(scriptDirective.strictDynamic ? [`'strict-dynamic'`] : []), ]; const scriptSrc = `script-src ${scriptResources} ${scriptBaselineTokens.join(' ')};`; - const styleSrc = `style-src ${styleResources} ${(styleElemActive ? [] : [...finalStyleHashes]).join(' ')};`; + const styleSrc = `style-src ${styleResources} ${(styleElemActive || styleDefaultHasUnsafeInline ? [] : [...finalStyleHashes]).join(' ')};`; const scriptSrcElem = scriptElemActive ? renderSpecificDirective( @@ -125,6 +132,10 @@ export function renderCspContent(result: SSRResult): string { .join(' '); } +function hasUnsafeInline(resources: string[]): boolean { + return resources.includes("'unsafe-inline'"); +} + /** A more specific directive is emitted only when the user scoped at least one source/hash to it. */ function isEnabled(sources: CspDirectiveSources): boolean { return sources.resources.length > 0 || sources.hashes.length > 0; @@ -149,9 +160,14 @@ function renderSpecificDirective( ownHashes: string[], suffix = '', ): string { - const hashes = new Set(sharedHashes); - for (const hash of ownHashes) { - hashes.add(`'${hash}'`); + // Per the CSP spec, browsers ignore `'unsafe-inline'` when a hash is present in the same + // directive. Skip hashes entirely to preserve the user's `'unsafe-inline'` intent. + const unsafeInline = hasUnsafeInline(resources); + const hashes = new Set(unsafeInline ? undefined : sharedHashes); + if (!unsafeInline) { + for (const hash of ownHashes) { + hashes.add(`'${hash}'`); + } } let finalResources: string; if (resources.length > 0) { diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index b5399b3eac62..2307a38e8451 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -819,7 +819,7 @@ export interface AstroUserConfig< * - External scripts and external styles are not supported out of the box, but you can [provide your own hashes](https://docs.astro.build/en/reference/configuration-reference/#securitycspscriptdirectivehashes). * - [Astro's view transitions](https://docs.astro.build/en/guides/view-transitions/) using the `` are not supported, but you can [consider migrating to the browser native View Transition API](https://events-3bg.pages.dev/jotter/astro-view-transitions/) instead if you are not using Astro's enhancements to the native View Transitions and Navigation APIs. * - Shiki isn't currently supported. By design, Shiki functions use inline styles that cannot work with Astro CSP implementation. Consider [using ``](https://docs.astro.build/en/guides/syntax-highlighting/#prism-) when your project requires both CSP and syntax highlighting. - * - `unsafe-inline` directives are incompatible with Astro's CSP implementation. By default, Astro will emit hashes for all its bundled scripts (e.g. client islands) and all modern browsers will automatically reject `unsafe-inline` when it occurs in a directive with a hash or nonce. + * - When `'unsafe-inline'` is included as a resource in a directive, Astro will not emit hashes on that directive. Per the CSP spec, browsers ignore `'unsafe-inline'` when a hash or nonce is present in the same directive, so Astro suppresses hashes to preserve `'unsafe-inline'` behavior. Note that this reduces the security of that directive to the level of `'unsafe-inline'`. * * :::note * Due to the nature of the Vite dev server, this feature isn't supported while working in `dev` mode. Instead, you can test this in your Astro project using `build` and `preview`. diff --git a/packages/astro/test/units/csp/render-csp.test.ts b/packages/astro/test/units/csp/render-csp.test.ts index 4ba4b626b0cb..b37dc4bf91f0 100644 --- a/packages/astro/test/units/csp/render-csp.test.ts +++ b/packages/astro/test/units/csp/render-csp.test.ts @@ -211,4 +211,100 @@ describe('renderCspContent', () => { "script-src 'self' 'sha256-default'; style-src 'self' ;", ); }); + + it('suppresses hashes on style-src when unsafe-inline is present', () => { + assert.equal( + renderCspContent( + createCspResult({ + styleDirective: { + resources: ["'unsafe-inline'"], + hashes: ['sha256-abc'], + }, + }), + ), + "script-src 'self' ; style-src 'unsafe-inline' ;", + ); + }); + + it('suppresses hashes on script-src when unsafe-inline is present', () => { + assert.equal( + renderCspContent( + createCspResult({ + scriptDirective: { + resources: ["'unsafe-inline'"], + hashes: ['sha256-abc'], + strictDynamic: false, + }, + }), + ), + "script-src 'unsafe-inline' ; style-src 'self' ;", + ); + }); + + it('suppresses hashes on style-src-elem when unsafe-inline is present', () => { + assert.equal( + renderCspContent( + createCspResult({ + styleDirective: { + resources: [{ resource: "'unsafe-inline'", kind: 'element' }], + hashes: ['sha256-abc'], + }, + }), + ), + "script-src 'self' ; style-src 'self' ; style-src-elem 'unsafe-inline';", + ); + }); + + it('suppresses hashes on script-src-elem when unsafe-inline is present', () => { + assert.equal( + renderCspContent( + createCspResult({ + scriptDirective: { + resources: [{ resource: "'unsafe-inline'", kind: 'element' }], + hashes: ['sha256-abc'], + strictDynamic: false, + }, + }), + ), + "script-src 'self' ; script-src-elem 'unsafe-inline'; style-src 'self' ;", + ); + }); + + it('suppresses render-time extra hashes when unsafe-inline is on style-src', () => { + assert.equal( + renderCspContent( + createCspResult({ + styleDirective: { resources: ["'unsafe-inline'"] }, + extraStyleHashes: ['sha256-extra'], + }), + ), + "script-src 'self' ; style-src 'unsafe-inline' ;", + ); + }); + + it('suppresses render-time extra hashes when unsafe-inline is on script-src', () => { + assert.equal( + renderCspContent( + createCspResult({ + scriptDirective: { resources: ["'unsafe-inline'"] }, + extraScriptHashes: ['sha256-extra'], + }), + ), + "script-src 'unsafe-inline' ; style-src 'self' ;", + ); + }); + + it('keeps hashes on style-src when unsafe-inline is only on style-src-attr', () => { + assert.equal( + renderCspContent( + createCspResult({ + styleDirective: { + resources: [{ resource: "'unsafe-inline'", kind: 'attribute' }], + hashes: ['sha256-abc'], + }, + }), + ), + "script-src 'self' ; style-src 'self' 'sha256-abc'; style-src-attr 'unsafe-inline';", + ); + }); }); From 0762a8385b5b5b093def3768a0c4d0464a9dccc4 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:01:42 +0200 Subject: [PATCH 03/21] =?UTF-8?q?fix:=20accept=20all=20s=C3=A4tteri=20plug?= =?UTF-8?q?in=20entry=20types=20(#17766)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/grumpy-bats-return.md | 6 +++++ .../integrations/mdx/src/satteri/index.ts | 9 ++++---- packages/markdown/satteri/src/processor.ts | 16 +++++++++---- .../markdown/satteri/src/satteri-processor.ts | 19 +++++++++++---- .../markdown/satteri/test/markdown.test.ts | 23 ++++++++++++++++++- 5 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 .changeset/grumpy-bats-return.md diff --git a/.changeset/grumpy-bats-return.md b/.changeset/grumpy-bats-return.md new file mode 100644 index 000000000000..05df2d2b98e6 --- /dev/null +++ b/.changeset/grumpy-bats-return.md @@ -0,0 +1,6 @@ +--- +'@astrojs/mdx': patch +'@astrojs/markdown-satteri': patch +--- + +Fixes Sätteri processor option types to accept all plugin entries supported by Sätteri v0.10.3. diff --git a/packages/integrations/mdx/src/satteri/index.ts b/packages/integrations/mdx/src/satteri/index.ts index 4df623d3045b..9de7ff119993 100644 --- a/packages/integrations/mdx/src/satteri/index.ts +++ b/packages/integrations/mdx/src/satteri/index.ts @@ -13,7 +13,9 @@ import { mdxToJs, type HastNode, type HastPluginDefinition, + type HastPluginEntry, type MdastPluginDefinition, + type MdastPluginEntry, type MdxCompileOptions, } from 'satteri'; import { ASTRO_IMAGE_IMPORT, USES_ASTRO_IMAGE_FLAG } from '../image-constants.js'; @@ -90,12 +92,9 @@ export function createMdxProcessor( typeof syntaxHighlight === 'object' ? syntaxHighlight.excludeLangs : undefined; // Collect last so image-URL rewrites by user plugins are captured. - const allMdastPlugins: MdastPluginDefinition[] = [ - ...satteriOptions.mdastPlugins, - collectImages, - ]; + const allMdastPlugins: MdastPluginEntry[] = [...satteriOptions.mdastPlugins, collectImages]; - const hastPlugins: HastPluginDefinition[] = []; + const hastPlugins: HastPluginEntry[] = []; if (highlightFn) { hastPlugins.push(satteriHighlightPlugin(highlightFn, excludeLangs)); } diff --git a/packages/markdown/satteri/src/processor.ts b/packages/markdown/satteri/src/processor.ts index 517a36c9fa89..a643fe578cdc 100644 --- a/packages/markdown/satteri/src/processor.ts +++ b/packages/markdown/satteri/src/processor.ts @@ -1,10 +1,16 @@ import type { MarkdownProcessor } from '@astrojs/internal-helpers/markdown'; -import type { Features, HastPluginDefinition, MdastPluginDefinition } from 'satteri'; +import type { + Features, + HastPluginEntry, + HastPluginList, + MdastPluginEntry, + MdastPluginList, +} from 'satteri'; import { createSatteriMarkdownProcessor } from './satteri-processor.js'; export interface SatteriProcessorOptions { - mdastPlugins?: MdastPluginDefinition[]; - hastPlugins?: HastPluginDefinition[]; + mdastPlugins?: MdastPluginList; + hastPlugins?: HastPluginList; features?: Features; } @@ -13,8 +19,8 @@ export interface SatteriProcessorOptions { * (the factory normalises absent inputs into defaults). */ export interface SatteriResolvedOptions { - mdastPlugins: MdastPluginDefinition[]; - hastPlugins: HastPluginDefinition[]; + mdastPlugins: MdastPluginEntry[]; + hastPlugins: HastPluginEntry[]; features: Features; } diff --git a/packages/markdown/satteri/src/satteri-processor.ts b/packages/markdown/satteri/src/satteri-processor.ts index 6cbacf4e3f4a..3c905b8bbeb0 100644 --- a/packages/markdown/satteri/src/satteri-processor.ts +++ b/packages/markdown/satteri/src/satteri-processor.ts @@ -5,7 +5,16 @@ import { syntaxHighlightDefaults, } from '@astrojs/internal-helpers/markdown'; import Slugger from 'github-slugger'; -import type { Features, HastNode, HastPluginDefinition, MdastPluginDefinition } from 'satteri'; +import type { + Features, + HastNode, + HastPluginDefinition, + HastPluginEntry, + HastPluginList, + MdastPluginDefinition, + MdastPluginEntry, + MdastPluginList, +} from 'satteri'; import { createShikiHighlighter } from '@astrojs/internal-helpers/shiki'; import type { AstroMarkdownOptions, @@ -206,8 +215,8 @@ export function createHighlightPlugin( } export interface SatteriMarkdownProcessorOptions extends AstroMarkdownOptions { - mdastPlugins?: MdastPluginDefinition[]; - hastPlugins?: HastPluginDefinition[]; + mdastPlugins?: MdastPluginList; + hastPlugins?: HastPluginList; features?: Features; } @@ -291,12 +300,12 @@ export async function createSatteriMarkdownProcessor( }; // Collect last so image-URL rewrites by user plugins are captured. - const allMdastPlugins: MdastPluginDefinition[] = [ + const allMdastPlugins: MdastPluginEntry[] = [ ...userMdastPlugins, createCollectImagesPlugin(opts?.image), ]; - const hastPlugins: HastPluginDefinition[] = []; + const hastPlugins: HastPluginEntry[] = []; if (highlightFn) { hastPlugins.push(createHighlightPlugin(highlightFn, syntaxHighlightExcludeLangs)); } diff --git a/packages/markdown/satteri/test/markdown.test.ts b/packages/markdown/satteri/test/markdown.test.ts index 19574fc0ed13..0b95f2a61d84 100644 --- a/packages/markdown/satteri/test/markdown.test.ts +++ b/packages/markdown/satteri/test/markdown.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import type { HastPluginDefinition, MdastPluginDefinition } from 'satteri'; -import { createSatteriMarkdownProcessor, satteriHeadingIdsPlugin } from '../dist/index.js'; +import { createSatteriMarkdownProcessor, satteri, satteriHeadingIdsPlugin } from '../dist/index.js'; describe('satteri markdown', () => { it('renders basic markdown', async () => { @@ -136,4 +136,25 @@ describe('satteri markdown', () => { assert.equal(metadata.frontmatter.title, 'hello'); assert.equal(metadata.frontmatter.injected, 'HELLO'); }); + + it('accepts conditional plugin factories', async () => { + const mdUppercasePlugin: MdastPluginDefinition = { + name: 'mdx-uppercase', + text(node, ctx) { + ctx.setProperty(node, 'value', node.value.toUpperCase()); + }, + }; + + const satteriProcessor = satteri({ + mdastPlugins: [(ctx) => (ctx.sourceFormat === 'markdown' ? [mdUppercasePlugin] : null)], + }); + + const processor = await createSatteriMarkdownProcessor({ + mdastPlugins: satteriProcessor.options.mdastPlugins, + }); + + const { code } = await processor.render('Hello'); + + assert.match(code, /

HELLO<\/p>/); + }); }); From b872d6c602686ebf5298317dac7828d282d545a2 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 20 Aug 2026 12:12:58 +0200 Subject: [PATCH 04/21] Modify issue template for chat and support options (#17746) --- .github/ISSUE_TEMPLATE/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5c5f4e11b8ef..20384533499d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,10 @@ blank_issues_enabled: false contact_links: - name: 💁 Support url: https://astro.build/chat - about: 'This issue tracker is not for support questions. Join us on Discord for assistance!' + about: This issue tracker is not for support questions. Join us on Discord for assistance! + - name: 💁 Support (GitHub) + url: https://github.com/withastro/community-support/discussions + about: No Discord account? Open a discussion on GitHub for assistance. - name: 📘 Documentation url: https://github.com/withastro/docs about: File an issue or make an improvement to the docs website. From 660991c820fbeb087b2f27361e6ebaeba8285358 Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:39:56 +0100 Subject: [PATCH 05/21] Fix build error location reporting for MDX and aggregate plugin errors (#17757) Co-authored-by: factory[bot] --- .changeset/gentle-regions-shave.md | 6 ++ packages/astro/src/core/errors/dev/utils.ts | 6 +- .../astro/test/units/errors/dev-utils.test.ts | 69 ++++++++++++++++++- .../integrations/mdx/src/vite-plugin-mdx.ts | 13 +++- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 .changeset/gentle-regions-shave.md diff --git a/.changeset/gentle-regions-shave.md b/.changeset/gentle-regions-shave.md new file mode 100644 index 000000000000..4ea58b32cf9d --- /dev/null +++ b/.changeset/gentle-regions-shave.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/mdx': patch +--- + +Fixes build errors showing wrong file location, missing line:col, and misleading hints when a plugin error (e.g. from MDX) is wrapped by Vite's build error diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts index e42e98f8ec24..31a98d114aeb 100644 --- a/packages/astro/src/core/errors/dev/utils.ts +++ b/packages/astro/src/core/errors/dev/utils.ts @@ -23,8 +23,8 @@ export function collectErrorMetadata(e: any, rootFolder?: URL): ErrorWithMetadat AggregateError.is(e) || Array.isArray(e.errors) ? (e.errors as SSRError[]) : [e as SSRError]; err.forEach((error) => { - if (e.stack) { - const stackInfo = collectInfoFromStacktrace(e); + if (error.stack) { + const stackInfo = collectInfoFromStacktrace(error); try { error.stack = stripVTControlCharacters(stackInfo.stack); } catch {} @@ -68,7 +68,7 @@ export function collectErrorMetadata(e: any, rootFolder?: URL): ErrorWithMetadat } // Generic error (probably from Vite, and already formatted) - error.hint = generateHint(e); + error.hint = generateHint(error); // Strip ANSI for `message` property. Note that ESBuild errors may not have the property, // but it will be handled and added below, which is already ANSI-free diff --git a/packages/astro/test/units/errors/dev-utils.test.ts b/packages/astro/test/units/errors/dev-utils.test.ts index 775e6f7ec05e..742c09870e35 100644 --- a/packages/astro/test/units/errors/dev-utils.test.ts +++ b/packages/astro/test/units/errors/dev-utils.test.ts @@ -1,6 +1,6 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { renderErrorMarkdown } from '../../../dist/core/errors/dev/utils.js'; +import { collectErrorMetadata, renderErrorMarkdown } from '../../../dist/core/errors/dev/utils.js'; describe('renderErrorMarkdown', () => { describe('html target', () => { @@ -222,3 +222,70 @@ describe('renderErrorMarkdown', () => { }); }); }); + +describe('collectErrorMetadata', () => { + it('uses sub-error properties when the parent error has an errors array', () => { + // Simulate how rolldown/Vite wraps a plugin error: the parent error has + // its own stack but no loc/plugin, while the sub-error carries the real info. + const subError = new Error('Something went wrong in component.astro'); + subError.stack = `Error: Something went wrong in component.astro + at renderComponent (file:///project/src/components/Foo.astro:10:5)`; + + const parentError = new Error('Build failed with 1 error'); + parentError.stack = `Error: Build failed with 1 error + at buildEnvironment (file:///node_modules/vite/dist/node/chunks/node.js:33011:66)`; + // @ts-ignore - adding errors array like rolldown does + parentError.errors = [subError]; + + const result = collectErrorMetadata(parentError); + + // The sub-error's own stack should be used, not the parent's + assert.ok(result.stack?.includes('renderComponent')); + assert.ok(!result.stack?.includes('buildEnvironment')); + }); + + it('preserves sub-error loc when parent error has no loc', () => { + const subError = new Error('Parse error'); + // @ts-ignore + subError.loc = { file: '/project/src/pages/test.mdx', line: 10, column: 5 }; + // @ts-ignore + subError.plugin = 'astro:mdx'; + subError.stack = `Error: Parse error + at transform (file:///project/node_modules/@astrojs/mdx/dist/index.js:42:7)`; + + const parentError = new Error('Build failed'); + parentError.stack = `Error: Build failed + at buildEnvironment (file:///node_modules/vite/dist/node.js:100:20)`; + // @ts-ignore + parentError.errors = [subError]; + + const result = collectErrorMetadata(parentError); + + assert.equal(result.loc?.file, '/project/src/pages/test.mdx'); + assert.equal(result.loc?.line, 10); + assert.equal(result.loc?.column, 5); + assert.equal(result.plugin, 'astro:mdx'); + }); + + it('does not generate misleading hints from parent error message', () => { + // The sub-error message has no browser API references, but the parent's + // stack/message might mention "window" or "document" incidentally. + const subError = new Error('Could not parse expression with oxc'); + subError.stack = `Error: Could not parse expression with oxc + at transform (file:///project/node_modules/@astrojs/mdx/dist/index.js:42:7)`; + + const parentError = new Error('Build failed - check document for details'); + parentError.stack = `Error: Build failed + at build (file:///node_modules/vite/dist/node.js:100:20)`; + // @ts-ignore + parentError.errors = [subError]; + + const result = collectErrorMetadata(parentError); + + // Should not get a "Browser APIs are not available" hint from the parent message + assert.ok( + !result.hint?.includes('Browser APIs'), + 'Should not generate browser API hint from parent error', + ); + }); +}); diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts index 10c7d605390b..58a6a2924c35 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts @@ -72,7 +72,18 @@ export function vitePluginMdx(opts: VitePluginMdxOptions): Plugin { // Surface compile failures as a dedicated MDX error with a source // location so the dev overlay can point at the offending file. err.name = 'MDXError'; - err.loc = { file: id, line: e.line, column: e.column }; + // Some parser errors (e.g. from oxc) embed line:col only in the + // message as a "line:col: ..." prefix instead of setting properties. + let line = e.line; + let column = e.column; + if (line == null || column == null) { + const match = /^(\d+):(\d+):/.exec(e.message); + if (match) { + line ??= Number(match[1]); + column ??= Number(match[2]); + } + } + err.loc = { file: id, line, column }; // Compiler errors may arrive without a JS stack; capture one here. Error.captureStackTrace(err); throw err; From e362d4cf540b27730482455c8fc02efe57d16702 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 20 Aug 2026 07:42:18 -0400 Subject: [PATCH 06/21] Fix generated Netlify image config URLs (#17752) --- .changeset/moody-geckos-train.md | 5 +++++ packages/integrations/netlify/src/index.ts | 4 ++-- .../netlify/test/functions/image-cdn.test.ts | 13 +++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .changeset/moody-geckos-train.md diff --git a/.changeset/moody-geckos-train.md b/.changeset/moody-geckos-train.md new file mode 100644 index 000000000000..033bebc96236 --- /dev/null +++ b/.changeset/moody-geckos-train.md @@ -0,0 +1,5 @@ +--- +'@astrojs/netlify': patch +--- + +Fixes generated Netlify Image CDN allowlists to reject remote URLs that contain an allowed image origin only within their path or query string diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index 90060a9c8673..4eb474cd3b60 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -49,7 +49,7 @@ export function remotePatternToRegex( ): string | undefined { let { protocol, hostname, port, pathname } = pattern; - let regexStr = ''; + let regexStr = '^'; if (protocol) { regexStr += `${protocol}://`; @@ -125,7 +125,7 @@ function remoteImagesFromAstroConfig( const remoteImages: string[] = []; // Domains get a simple regex match remoteImages.push( - ...config.image.domains.map((domain) => `https?:\/\/${escapeRegex(domain)}\/.*`), + ...config.image.domains.map((domain) => `^https?:\/\/${escapeRegex(domain)}\/.*$`), ); // Remote patterns need to be converted to regexes remoteImages.push( diff --git a/packages/integrations/netlify/test/functions/image-cdn.test.ts b/packages/integrations/netlify/test/functions/image-cdn.test.ts index da4b487799b4..3d8d267722e0 100644 --- a/packages/integrations/netlify/test/functions/image-cdn.test.ts +++ b/packages/integrations/netlify/test/functions/image-cdn.test.ts @@ -116,6 +116,19 @@ describe('Image CDN', { timeout: 120000 }, () => { ); }); + it('rejects allowed patterns embedded in another URL', async () => { + assert.equal( + regexes[0]!.test( + 'http://169.254.169.254/latest/meta-data/?url=https://example.net/image.jpg', + ), + false, + ); + assert.equal( + regexes[2]!.test('http://127.0.0.1:6379/?url=https://www.example.org/images/a.jpg'), + false, + ); + }); + it('treats metacharacters in a literal pathname as literals', async () => { const spyLogger = new SpyLogger(); const logger = spyLogger.forkIntegrationLogger('test-spy'); From 86c9c3806a1934938544d78570e5c2ff36bc1e44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:42:53 +0100 Subject: [PATCH 07/21] Update @astrojs/node (#17577) --- packages/integrations/node/package.json | 8 +- pnpm-lock.yaml | 237 +++++++++++++----------- 2 files changed, 133 insertions(+), 112 deletions(-) diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index ae99dfab80f5..32af2e1e1d23 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -40,7 +40,7 @@ "astro": "^7.2.1" }, "devDependencies": { - "@fastify/middie": "^9.1.0", + "@fastify/middie": "^9.3.3", "@fastify/static": "^9.0.0", "@types/express": "^5.0.6", "@types/node": "^22.10.6", @@ -49,10 +49,10 @@ "astro": "workspace:*", "astro-scripts": "workspace:*", "cheerio": "1.2.0", - "devalue": "^5.8.1", + "devalue": "^5.9.0", "express": "^5.2.1", - "fastify": "^5.7.4", - "node-mocks-http": "^1.17.2" + "fastify": "^5.12.0", + "node-mocks-http": "^1.18.1" }, "astro": { "external": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a877f81b1274..d0965924728b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,7 +230,7 @@ importers: version: 2.29.8(@types/node@22.19.19) '@earendil-works/pi-ai': specifier: ^0.83.0 - version: 0.83.0(supports-color@8.1.1)(ws@8.20.1)(zod@4.3.6) + version: 0.83.0(ws@8.20.1)(zod@4.3.6) '@flue/runtime': specifier: ^2.0.3 version: 2.0.3(typescript@6.0.3)(ws@8.20.1)(zod@4.3.6) @@ -242,7 +242,7 @@ importers: version: 0.2.0 eslint: specifier: ^10.4.0 - version: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + version: 10.4.0(jiti@2.6.1) eslint-plugin-regexp: specifier: ^3.1.0 version: 3.1.0(eslint@10.4.0) @@ -272,7 +272,7 @@ importers: version: 6.0.3 typescript-eslint: specifier: ^8.59.1 - version: 8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3) + version: 8.59.2(eslint@10.4.0)(typescript@6.0.3) valibot: specifier: ^1.2.0 version: 1.4.2(typescript@6.0.3) @@ -768,7 +768,7 @@ importers: version: 2.0.1 devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 diff: specifier: ^8.0.3 version: 8.0.4 @@ -937,7 +937,7 @@ importers: version: 4.12.18 node-mocks-http: specifier: ^1.17.2 - version: 1.17.2(@types/express@5.0.6)(@types/node@22.19.19) + version: 1.18.1(@types/express@5.0.6)(@types/node@22.19.19) parse-srcset: specifier: ^1.0.2 version: 1.0.2 @@ -4592,7 +4592,7 @@ importers: version: 1.2.0 devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 prismjs: specifier: ^1.30.0 version: 1.30.0 @@ -5132,7 +5132,7 @@ importers: version: link:../../../scripts devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 linkedom: specifier: ^0.18.12 version: 0.18.12 @@ -5394,7 +5394,7 @@ importers: version: 4.0.2 '@shikijs/twoslash': specifier: ^4.0.2 - version: 4.0.2(supports-color@8.1.1)(typescript@6.0.3) + version: 4.0.2(typescript@6.0.3) '@types/estree': specifier: ^1.0.8 version: 1.0.8 @@ -5649,7 +5649,7 @@ importers: version: 5.2.0 '@netlify/vite-plugin': specifier: ^2.12.3 - version: 2.12.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(supports-color@8.1.1)(vite@8.1.0) + version: 2.12.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(vite@8.1.0) '@vercel/nft': specifier: ^1.3.2 version: 1.3.2 @@ -5677,7 +5677,7 @@ importers: version: 1.2.0 devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -5786,8 +5786,8 @@ importers: version: 1.0.1 devDependencies: '@fastify/middie': - specifier: ^9.1.0 - version: 9.3.2 + specifier: ^9.3.3 + version: 9.3.3 '@fastify/static': specifier: ^9.0.0 version: 9.0.0 @@ -5813,17 +5813,17 @@ importers: specifier: 1.2.0 version: 1.2.0 devalue: - specifier: ^5.8.1 - version: 5.8.1 + specifier: ^5.9.0 + version: 5.9.0 express: specifier: ^5.2.1 - version: 5.2.1(supports-color@8.1.1) + version: 5.2.1 fastify: - specifier: ^5.7.4 - version: 5.8.5 + specifier: ^5.12.0 + version: 5.12.0 node-mocks-http: - specifier: ^1.17.2 - version: 1.17.2(@types/express@5.0.6)(@types/node@22.19.19) + specifier: ^1.18.1 + version: 1.18.1(@types/express@5.0.6)(@types/node@22.19.19) packages/integrations/node/test/fixtures/api-route: dependencies: @@ -6022,13 +6022,13 @@ importers: version: link:../../internal-helpers '@preact/preset-vite': specifier: ^2.10.5 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(supports-color@8.1.1)(vite@8.1.0) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(vite@8.1.0) '@preact/signals': specifier: ^2.8.2 version: 2.8.2(preact@10.29.0) devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 preact-render-to-string: specifier: ^6.6.6 version: 6.6.6(preact@10.29.0) @@ -6056,7 +6056,7 @@ importers: version: 5.2.0(vite@8.1.0) devalue: specifier: ^5.8.1 - version: 5.8.1 + version: 5.9.0 ultrahtml: specifier: ^1.6.0 version: 1.6.0 @@ -6568,7 +6568,7 @@ importers: version: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0) vite-plugin-vue-devtools: specifier: ^8.1.0 - version: 8.1.0(supports-color@8.1.1)(vite@8.1.0)(vue@3.5.30) + version: 8.1.0(vite@8.1.0)(vue@3.5.30) devDependencies: astro: specifier: workspace:* @@ -6596,7 +6596,7 @@ importers: version: link:../../../../../astro vite-svg-loader: specifier: 5.1.1 - version: 5.1.1(supports-color@8.1.1)(vue@3.5.30) + version: 5.1.1(vue@3.5.30) vue: specifier: ^3.5.30 version: 3.5.30(typescript@6.0.3) @@ -6611,7 +6611,7 @@ importers: version: link:../../../../../astro vite-svg-loader: specifier: 5.1.1 - version: 5.1.1(supports-color@8.1.1)(vue@3.5.30) + version: 5.1.1(vue@3.5.30) vue: specifier: ^3.5.30 version: 3.5.30(typescript@6.0.3) @@ -6635,7 +6635,7 @@ importers: version: link:../../../../../astro vite-svg-loader: specifier: 5.1.1 - version: 5.1.1(supports-color@8.1.1)(vue@3.5.30) + version: 5.1.1(vue@3.5.30) vue: specifier: ^3.5.30 version: 3.5.30(typescript@6.0.3) @@ -6971,7 +6971,7 @@ importers: version: 11.7.5 ovsx: specifier: ^0.10.10 - version: 0.10.10(supports-color@8.1.1) + version: 0.10.10 tsx: specifier: ^4.22.0 version: 4.22.3 @@ -8756,8 +8756,8 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - '@fastify/middie@9.3.2': - resolution: {integrity: sha512-5C3xMHJxpfqoHd+xZSHPBI71fpzkoF6wMsYtgzXRyQUNvsIAxJm2yY4r2fUjF0h3rS9MXlo/aXLaXv3s4TL+JQ==} + '@fastify/middie@9.3.3': + resolution: {integrity: sha512-N2VRS+sfw/lxA/uLD6TvBpvfPfOV/RdYzM7ctAB58Cn2TAYxbRd8+JLFhNhMJoXlSbgChXYUnkn3hPU/JdA4Pg==} '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} @@ -11799,8 +11799,8 @@ packages: dettle@1.0.5: resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -12203,6 +12203,9 @@ packages: fast-json-stringify@6.3.0: resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -12215,6 +12218,9 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@4.1.2: + resolution: {integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==} + fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -12229,8 +12235,11 @@ packages: fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - fastify@5.8.5: - resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + + fastify@5.12.0: + resolution: {integrity: sha512-A3RNEaDIHWaxFW8n8rNJaW1wQ+XAXuoU71llfUQJjuh5WaYLmKfRhhanaJBOx8m2EBPQkR6sDBovYdHNe9F6rA==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -12280,8 +12289,8 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - find-my-way@9.5.0: - resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + find-my-way@9.8.0: + resolution: {integrity: sha512-JtyUgATO7qxRp2zKhrmWof74Mqxc1ikbwpwMY97p8ipuTj2QtreA4gK2JNAF6SOqqHnYYkwMUvsgQVi2AJxIyw==} engines: {node: '>=20'} find-process@2.1.1: @@ -13776,8 +13785,8 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-mocks-http@1.17.2: - resolution: {integrity: sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==} + node-mocks-http@1.18.1: + resolution: {integrity: sha512-hPMOLJZzhgT4i/zbYpfy1P2ulAlHtzFcrEGxPh/4pDRegCY3+p2sxDhQm1fxte5EgI/RwDx+NNW7v8kJcyxtMg==} engines: {node: '>=14'} peerDependencies: '@types/express': ^4.17.21 || ^5.0.0 @@ -14413,8 +14422,8 @@ packages: process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} - process-warning@5.0.0: - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} @@ -17551,7 +17560,7 @@ snapshots: '@earendil-works/pi-agent-core@0.83.0(ws@8.20.1)(zod@4.3.6)': dependencies: - '@earendil-works/pi-ai': 0.83.0(supports-color@8.1.1)(ws@8.20.1)(zod@4.3.6) + '@earendil-works/pi-ai': 0.83.0(ws@8.20.1)(zod@4.3.6) diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -17564,7 +17573,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(supports-color@8.1.1)(ws@8.20.1)(zod@4.3.6)': + '@earendil-works/pi-ai@0.83.0(ws@8.20.1)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -17572,7 +17581,7 @@ snapshots: '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 - http-proxy-agent: 7.0.2(supports-color@8.1.1) + http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 openai: 6.26.0(ws@8.20.1)(zod@4.3.6) partial-json: 0.1.7 @@ -17855,12 +17864,12 @@ snapshots: '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0)': dependencies: - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5(supports-color@8.1.1)': + '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@8.1.1) @@ -17905,11 +17914,11 @@ snapshots: dependencies: dequal: 2.0.3 - '@fastify/middie@9.3.2': + '@fastify/middie@9.3.3': dependencies: '@fastify/error': 4.2.0 - fastify-plugin: 5.1.0 - find-my-way: 9.5.0 + fastify-plugin: 6.0.0 + find-my-way: 9.8.0 path-to-regexp: 8.4.2 reusify: 1.1.0 @@ -17938,7 +17947,7 @@ snapshots: '@flue/runtime@2.0.3(typescript@6.0.3)(ws@8.20.1)(zod@4.3.6)': dependencies: '@earendil-works/pi-agent-core': 0.83.0(ws@8.20.1)(zod@4.3.6) - '@earendil-works/pi-ai': 0.83.0(supports-color@8.1.1)(ws@8.20.1)(zod@4.3.6) + '@earendil-works/pi-ai': 0.83.0(ws@8.20.1)(zod@4.3.6) '@hono/node-server': 2.0.4(hono@4.12.18) '@modelcontextprotocol/client': 2.0.0 '@valibot/to-json-schema': 1.5.0(valibot@1.4.2) @@ -18434,7 +18443,7 @@ snapshots: uuid: 13.0.0 write-file-atomic: 5.0.1 - '@netlify/dev@4.18.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(supports-color@8.1.1)': + '@netlify/dev@4.18.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)': dependencies: '@netlify/ai': 0.4.1 '@netlify/blobs': 10.7.5 @@ -18442,7 +18451,7 @@ snapshots: '@netlify/database-dev': 0.10.1 '@netlify/dev-utils': 4.4.3 '@netlify/edge-functions-dev': 1.0.17 - '@netlify/functions-dev': 1.2.8(supports-color@8.1.1) + '@netlify/functions-dev': 1.2.8 '@netlify/headers': 2.1.8 '@netlify/images': 1.3.7(@azure/identity@4.13.0)(@netlify/blobs@10.7.5)(@vercel/functions@3.4.3) '@netlify/redirects': 3.1.10 @@ -18514,7 +18523,7 @@ snapshots: dependencies: '@netlify/types': 2.6.0 - '@netlify/functions-dev@1.2.8(supports-color@8.1.1)': + '@netlify/functions-dev@1.2.8': dependencies: '@netlify/blobs': 10.7.5 '@netlify/dev-utils': 4.4.3 @@ -18522,7 +18531,7 @@ snapshots: '@netlify/zip-it-and-ship-it': 14.5.6 cron-parser: 4.9.0 decache: 4.6.2 - extract-zip: 2.0.1(supports-color@8.1.1) + extract-zip: 2.0.1 is-stream: 4.0.1 jwt-decode: 4.0.0 lambda-local: 2.2.0 @@ -18625,9 +18634,9 @@ snapshots: '@netlify/types@2.6.0': {} - '@netlify/vite-plugin@2.12.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(supports-color@8.1.1)(vite@8.1.0)': + '@netlify/vite-plugin@2.12.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(vite@8.1.0)': dependencies: - '@netlify/dev': 4.18.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3)(supports-color@8.1.1) + '@netlify/dev': 4.18.3(@azure/identity@4.13.0)(@vercel/functions@3.4.3) '@netlify/dev-utils': 4.4.3 dedent: 1.7.1 vite: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0) @@ -18980,7 +18989,7 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(supports-color@8.1.1)(vite@8.1.0)': + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(vite@8.1.0)': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) @@ -19237,11 +19246,11 @@ snapshots: dependencies: '@shikijs/types': 4.0.2 - '@shikijs/twoslash@4.0.2(supports-color@8.1.1)(typescript@6.0.3)': + '@shikijs/twoslash@4.0.2(typescript@6.0.3)': dependencies: '@shikijs/core': 4.0.2 '@shikijs/types': 4.0.2 - twoslash: 0.3.8(supports-color@8.1.1)(typescript@6.0.3) + twoslash: 0.3.8(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -19690,15 +19699,15 @@ snapshots: '@types/node': 22.19.19 optional: true - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2)(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2)(eslint@10.4.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.4.0)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/type-utils': 8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.59.2(eslint@10.4.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.59.2(eslint@10.4.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -19706,14 +19715,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/parser@8.59.2(eslint@10.4.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -19749,13 +19758,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.59.2(eslint@10.4.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/utils': 8.59.2(eslint@10.4.0)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -19799,7 +19808,7 @@ snapshots: '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -19809,7 +19818,7 @@ snapshots: '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 - '@typescript/vfs@1.6.4(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript/vfs@1.6.4(typescript@6.0.3)': dependencies: debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 @@ -19818,7 +19827,7 @@ snapshots: '@typespec/ts-http-runtime@0.3.3': dependencies: - http-proxy-agent: 7.0.2(supports-color@8.1.1) + http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 tslib: 2.8.1 transitivePeerDependencies: @@ -20064,7 +20073,7 @@ snapshots: '@vscode/test-electron@2.5.2': dependencies: - http-proxy-agent: 7.0.2(supports-color@8.1.1) + http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jszip: 3.10.1 ora: 8.2.0 @@ -20074,7 +20083,7 @@ snapshots: '@vscode/test-electron@3.1.0': dependencies: - http-proxy-agent: 7.0.2(supports-color@8.1.1) + http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jszip: 3.10.1 ora: 8.2.0 @@ -20152,7 +20161,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vscode/vsce@3.7.1(supports-color@8.1.1)': + '@vscode/vsce@3.7.1': dependencies: '@azure/identity': 4.13.0 '@secretlint/node': 10.2.2 @@ -20175,7 +20184,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 10.2.2(supports-color@8.1.1) + secretlint: 10.2.2 semver: 7.8.5 tmp: 0.2.5 typed-rest-client: 1.8.11 @@ -20675,7 +20684,7 @@ snapshots: blake3-wasm@2.1.5: {} - body-parser@2.2.2(supports-color@8.1.1): + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -21239,7 +21248,7 @@ snapshots: dettle@1.0.5: {} - devalue@5.8.1: {} + devalue@5.9.0: {} devlop@1.1.0: dependencies: @@ -21526,7 +21535,7 @@ snapshots: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.5 - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 @@ -21543,11 +21552,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.4.0(jiti@2.6.1)(supports-color@8.1.1): + eslint@10.4.0(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5(supports-color@8.1.1) + '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 @@ -21681,10 +21690,10 @@ snapshots: expect-type@1.3.0: {} - express@5.2.1(supports-color@8.1.1): + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2(supports-color@8.1.1) + body-parser: 2.2.2 content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 @@ -21694,7 +21703,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1(supports-color@8.1.1) + finalhandler: 2.1.1 fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -21705,7 +21714,7 @@ snapshots: proxy-addr: 2.0.7 qs: 6.14.2 range-parser: 1.2.1 - router: 2.2.0(supports-color@8.1.1) + router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 statuses: 2.0.2 @@ -21718,7 +21727,7 @@ snapshots: extendable-error@0.1.7: {} - extract-zip@2.0.1(supports-color@8.1.1): + extract-zip@2.0.1: dependencies: debug: 4.4.3(supports-color@8.1.1) get-stream: 5.2.0 @@ -21759,6 +21768,15 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1 + fast-uri: 4.1.2 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} fast-querystring@1.1.2: @@ -21769,6 +21787,8 @@ snapshots: fast-uri@3.1.0: {} + fast-uri@4.1.2: {} + fast-xml-builder@1.2.0: dependencies: path-expression-matcher: 1.5.0 @@ -21791,7 +21811,9 @@ snapshots: fastify-plugin@5.1.0: {} - fastify@5.8.5: + fastify-plugin@6.0.0: {} + + fastify@5.12.0: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -21799,11 +21821,11 @@ snapshots: '@fastify/proxy-addr': 5.1.0 abstract-logging: 2.0.1 avvio: 9.2.0 - fast-json-stringify: 6.3.0 - find-my-way: 9.5.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.8.0 light-my-request: 6.6.0 pino: 10.3.1 - process-warning: 5.0.0 + process-warning: 5.1.0 rfdc: 1.4.1 secure-json-parse: 4.1.0 semver: 7.8.5 @@ -21848,7 +21870,7 @@ snapshots: filter-obj@6.1.0: {} - finalhandler@2.1.1(supports-color@8.1.1): + finalhandler@2.1.1: dependencies: debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 @@ -21859,7 +21881,7 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way@9.5.0: + find-my-way@9.8.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -22384,7 +22406,7 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@7.0.2(supports-color@8.1.1): + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@8.1.1) @@ -23726,7 +23748,7 @@ snapshots: node-mock-http@1.0.4: {} - node-mocks-http@1.17.2(@types/express@5.0.6)(@types/node@22.19.19): + node-mocks-http@1.18.1(@types/express@5.0.6)(@types/node@22.19.19): dependencies: accepts: 1.3.8 content-disposition: 0.5.4 @@ -23735,7 +23757,6 @@ snapshots: merge-descriptors: 1.0.3 methods: 1.1.2 mime: 1.6.0 - parseurl: 1.3.3 range-parser: 1.2.1 type-is: 1.6.18 optionalDependencies: @@ -23866,9 +23887,9 @@ snapshots: outdent@0.5.0: {} - ovsx@0.10.10(supports-color@8.1.1): + ovsx@0.10.10: dependencies: - '@vscode/vsce': 3.7.1(supports-color@8.1.1) + '@vscode/vsce': 3.7.1 commander: 6.2.1 follow-redirects: 1.15.11 is-ci: 2.0.0 @@ -24114,7 +24135,7 @@ snapshots: on-exit-leak-free: 2.1.2 pino-abstract-transport: 3.0.0 pino-std-serializers: 7.1.0 - process-warning: 5.0.0 + process-warning: 5.1.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 @@ -24459,7 +24480,7 @@ snapshots: process-warning@4.0.1: {} - process-warning@5.0.0: {} + process-warning@5.1.0: {} process@0.11.10: {} @@ -24931,7 +24952,7 @@ snapshots: rosie-skills-freebsd-x64: 0.6.4 rosie-skills-linux-x64: 0.6.4 - router@2.2.0(supports-color@8.1.1): + router@2.2.0: dependencies: debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 @@ -25010,7 +25031,7 @@ snapshots: scule@1.3.0: {} - secretlint@10.2.2(supports-color@8.1.1): + secretlint@10.2.2: dependencies: '@secretlint/config-creator': 10.2.2 '@secretlint/formatter': 10.2.2 @@ -25450,7 +25471,7 @@ snapshots: aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.1 + devalue: 5.9.0 esm-env: 1.2.2 esrap: 2.2.4 is-reference: 3.0.3 @@ -25643,9 +25664,9 @@ snapshots: twoslash-protocol@0.3.8: {} - twoslash@0.3.8(supports-color@8.1.1)(typescript@6.0.3): + twoslash@0.3.8(typescript@6.0.3): dependencies: - '@typescript/vfs': 1.6.4(supports-color@8.1.1)(typescript@6.0.3) + '@typescript/vfs': 1.6.4(typescript@6.0.3) twoslash-protocol: 0.3.8 typescript: 6.0.3 transitivePeerDependencies: @@ -25692,13 +25713,13 @@ snapshots: dependencies: semver: 7.8.5 - typescript-eslint@8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3): + typescript-eslint@8.59.2(eslint@10.4.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2)(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.59.2(eslint@10.4.0)(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2)(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.4.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/utils': 8.59.2(eslint@10.4.0)(typescript@6.0.3) - eslint: 10.4.0(jiti@2.6.1)(supports-color@8.1.1) + eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -25935,7 +25956,7 @@ snapshots: dependencies: vite: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0) - vite-plugin-inspect@11.3.3(supports-color@8.1.1)(vite@8.1.0): + vite-plugin-inspect@11.3.3(vite@8.1.0): dependencies: ansis: 4.2.0 debug: 4.4.3(supports-color@8.1.1) @@ -25963,14 +25984,14 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.1.0(supports-color@8.1.1)(vite@8.1.0)(vue@3.5.30): + vite-plugin-vue-devtools@8.1.0(vite@8.1.0)(vue@3.5.30): dependencies: '@vue/devtools-core': 8.1.0(vue@3.5.30) '@vue/devtools-kit': 8.1.0 '@vue/devtools-shared': 8.1.0 sirv: 3.0.2 vite: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0) - vite-plugin-inspect: 11.3.3(supports-color@8.1.1)(vite@8.1.0) + vite-plugin-inspect: 11.3.3(vite@8.1.0) vite-plugin-vue-inspector: 5.3.2(vite@8.1.0) transitivePeerDependencies: - '@nuxt/kit' @@ -26002,7 +26023,7 @@ snapshots: stack-trace: 1.0.0-pre2 vite: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0) - vite-svg-loader@5.1.1(supports-color@8.1.1)(vue@3.5.30): + vite-svg-loader@5.1.1(vue@3.5.30): dependencies: debug: 4.4.3(supports-color@8.1.1) svgo: 3.3.3 From 1f60921b8e263e9bc9152eaa666c72b0a8a9a787 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 20 Aug 2026 13:46:13 +0200 Subject: [PATCH 08/21] chore: knip prod mode (#17734) --- knip.js | 54 ++++++++++++++++++++++++++++++++++++++++++-------- package.json | 11 +++++----- pnpm-lock.yaml | 7 +++---- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/knip.js b/knip.js index 0a36a8b58989..27bb395321fe 100644 --- a/knip.js +++ b/knip.js @@ -1,17 +1,36 @@ // @ts-check -const srcEntry = 'src/**/*.{js,ts,cts}'; -const dtsEntry = '*.d.ts'; + +// Patterns suffixed with `!` are the ones used in production mode (`knip --production`), which only +// analyzes the code we ship. Patterns without the suffix are dev-only: Knip automatically negates +// them in production mode. See https://knip.dev/features/production-mode +const srcEntry = 'src/**/*.{js,ts,cts}!'; +const dtsEntry = '*.d.ts!'; const testEntry = 'test/**/*.test.{js,ts}'; +// `project` defines the files Knip analyzes, so it is where files are excluded from the analysis +// altogether (`ignore` only suppresses issues in files that are still analyzed). +// See https://knip.dev/guides/configuring-project-files +const project = [ + '**/*!', + // Fixtures and hosted test apps are standalone projects of their own + '!**/{test,e2e}/**/{fixtures,_temp-fixtures}/**', + '!test/hosted/hosted-astro-project/**', + // Tests are part of the analysis, but never of the production graph + '!test/**!', + '!e2e/**!', +]; + /** @type {import('knip').KnipConfig} */ export default { - ignore: ['**/test/**/{fixtures,_temp-fixtures}/**', 'triage/**', '.github/scripts/**'], tags: ['-lintignore'], ignoreWorkspaces: [ 'examples/**', '**/{test,e2e}/**/{fixtures,_temp-fixtures}/**', 'benchmark/**', 'packages/language-tools/**/*', + // Standalone projects living inside packages + 'packages/astro/performance/**', + '**/test/hosted/hosted-astro-project/**', ], workspaces: { '.': { @@ -24,14 +43,21 @@ export default { // to be installed in the vscode package, but knip is expecting them to be in the root node_modules ignoreBinaries: ['docgen', 'docgen:errors', 'playwright', 'vsce', 'ovsx'], entry: ['.agents/evals/*.ts'], + // The root workspace ships nothing, so none of its files are part of the production graph + project: ['**/*', '!triage/**', '!.github/scripts/**'], + }, + // Internal tooling package: it publishes nothing, so all of its commands are entry points + scripts: { + entry: ['*.js!', '{deps,smoke}/*.js!'], }, 'packages/*': { entry: [srcEntry, dtsEntry, testEntry], + project, }, 'packages/astro': { entry: [ // Can't be detected automatically since it's only in package.json#files - 'templates/**/*', + 'templates/**/*!', srcEntry, dtsEntry, testEntry, @@ -42,11 +68,10 @@ export default { 'test/test-image-service.ts', 'test/test-remote-image-service.ts', // Can't detect this file when using inside a vite plugin - 'src/vite-plugin-app/createAstroServerApp.ts', + 'src/vite-plugin-app/createAstroServerApp.ts!', ], + project, ignore: [ - '**/e2e/**/{fixtures,_temp-fixtures}/**', - 'performance/**/*', // This export is resolved dynamically in packages/astro/src/vite-plugin-app/index.ts 'src/vite-plugin-app/createExports.ts', ], @@ -64,43 +89,56 @@ export default { }, 'packages/astro-prism': { entry: [srcEntry, dtsEntry, testEntry], + project, ignoreUnresolved: ['#prism-loadLanguages'], }, 'packages/integrations/*': { entry: [srcEntry, dtsEntry, testEntry], + project, }, 'packages/integrations/cloudflare': { entry: [srcEntry, dtsEntry, testEntry], + project, // False positive because of cloudflare:workers ignoreDependencies: ['cloudflare'], }, 'packages/integrations/netlify': { entry: [srcEntry, dtsEntry, testEntry], + project, + // Runtime dependency of the Netlify Blobs session driver, which the adapter enables but + // never imports by name + ignoreDependencies: ['@netlify/blobs'], }, 'packages/integrations/solid': { entry: [srcEntry, dtsEntry, testEntry], + project, // It's an optional peer dep (triggers a warning) but it's fine in this case ignoreDependencies: ['solid-devtools'], }, 'packages/integrations/svelte': { entry: [srcEntry, dtsEntry, testEntry], + project, // Used in testing-library compatibility tests but not directly imported ignoreDependencies: ['@testing-library/svelte'], }, 'packages/integrations/mdx': { entry: [srcEntry, dtsEntry, testEntry], + project, // Optional peer dep: type-only imports for narrowing the `satteri()` processor. // Knip flags it because the peer is referenced from source; the runtime stays gated by name-check. ignoreDependencies: ['@astrojs/markdown-satteri'], }, 'packages/markdown/remark': { entry: [srcEntry, dtsEntry, testEntry], + project, }, 'packages/markdown/satteri': { entry: [srcEntry, dtsEntry, testEntry], + project, }, 'packages/upgrade': { - entry: ['src/index.ts', testEntry], + entry: ['src/index.ts!', testEntry], + project, }, }, }; diff --git a/package.json b/package.json index bb55a066267e..bf730e90406d 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,12 @@ "eval:skills:validate": "vitest list --config vitest.skills.config.ts", "typecheck": "tsc -b", "benchmark": "astro-benchmark", - "lint": "biome lint && knip && eslint --cache --concurrency=auto", - "lint:ai": "biome lint --reporter=concise && knip && eslint --cache --concurrency=auto", - "lint:ci": "knip && pnpm run eslint:ci", + "lint": "biome lint && pnpm run knip && eslint --cache --concurrency=auto", + "lint:ai": "biome lint --reporter=concise && pnpm run knip && eslint --cache --concurrency=auto", + "lint:ci": "pnpm run knip && pnpm run eslint:ci", "eslint:ci": "NODE_OPTIONS=\"--max-old-space-size=8192\" eslint --cache --concurrency=auto", "lint:fix": "biome lint --write --unsafe", + "knip": "knip && knip --production", "publint": "pnpm -r --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --no-bail exec publint", "version": "changeset version && node ./scripts/deps/update-example-versions.js && pnpm install --no-frozen-lockfile && pnpm run format" }, @@ -62,9 +63,6 @@ "node": ">=22.12.0" }, "packageManager": "pnpm@11.13.1", - "dependencies": { - "astro-benchmark": "workspace:*" - }, "devDependencies": { "@astrojs/check": "^0.9.5", "@biomejs/biome": "2.5.3", @@ -73,6 +71,7 @@ "@earendil-works/pi-ai": "^0.83.0", "@flue/runtime": "^2.0.3", "@types/node": "^22.10.6", + "astro-benchmark": "workspace:*", "bgproc": "^0.2.0", "eslint": "^10.4.0", "eslint-plugin-regexp": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0965924728b..b0784cfed33e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,10 +211,6 @@ overrides: importers: .: - dependencies: - astro-benchmark: - specifier: workspace:* - version: link:benchmark devDependencies: '@astrojs/check': specifier: ^0.9.5 @@ -237,6 +233,9 @@ importers: '@types/node': specifier: ^22.19.0 version: 22.19.19 + astro-benchmark: + specifier: workspace:* + version: link:benchmark bgproc: specifier: ^0.2.0 version: 0.2.0 From 5f419e25c570002a2ce0e10a973aa13336016b0c Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:26:28 +0100 Subject: [PATCH 09/21] Fall back to requestUrl.origin in RemoteRuntimeFontFileUrlResolver when server address is null (#17758) Co-authored-by: factory[bot] Co-authored-by: ematipico --- .changeset/better-zebras-notice.md | 5 +++ .../remote-runtime-font-file-url-resolver.ts | 36 +++++++++++++------ .../test/units/assets/fonts/infra.test.ts | 35 +++++++++++++++--- 3 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 .changeset/better-zebras-notice.md diff --git a/.changeset/better-zebras-notice.md b/.changeset/better-zebras-notice.md new file mode 100644 index 000000000000..ff1c2e5799aa --- /dev/null +++ b/.changeset/better-zebras-notice.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug where `experimental_getFontFileURL()` rejected valid font URLs when using the Cloudflare adapter diff --git a/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts b/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts index a397db7221eb..7bf2770020a4 100644 --- a/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts +++ b/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts @@ -6,9 +6,11 @@ import type { RuntimeFontFileUrlResolver } from '../definitions.js'; * During prerendering, a temporary Node HTTP server is started to * serve font files. * - * We send request to the provided server address. `requestUrl` on - * `fetch` is not implemented because we have the information from - * within the Vite plugin already. + * When possible, the resolver uses a statically known server + * {@link address}. When the address is not yet available (e.g. the + * virtual module was evaluated before the HTTP server started + * listening — see #17722), the resolver falls back to deriving the + * origin from the caller-supplied {@link requestUrl}. */ export class RemoteRuntimeFontFileUrlResolver implements RuntimeFontFileUrlResolver { #urls: Set; @@ -25,19 +27,31 @@ export class RemoteRuntimeFontFileUrlResolver implements RuntimeFontFileUrlResol this.#address = address; } - resolve(url: string): string | null { + resolve(url: string, requestUrl: URL | undefined): string | null { if (!this.#urls.has(url)) { return null; } - if (!this.#address) { - throw new Error('Server address unavailable, this should not happen. Open an issue.'); - } // assetsPrefix if (!url.startsWith('/')) { - url = new URL(url).pathname; + if (this.#address) { + url = new URL(url).pathname; + } else { + return url; + } + } + if (this.#address) { + const host = + this.#address.family === 'IPv6' + ? `[${this.#address.address}]` + : this.#address.address; + return `http://${host}:${this.#address.port}${url}`; + } + // Fallback when the server address was not available at module + // load time (e.g. an adapter's dep optimizer pre-bundled the + // font runtime before the HTTP server started listening, #17722). + if (requestUrl) { + return `${requestUrl.origin}${url}`; } - const host = - this.#address.family === 'IPv6' ? `[${this.#address.address}]` : this.#address.address; - return `http://${host}:${this.#address.port}${url}`; + throw new Error('Server address unavailable, this should not happen. Open an issue.'); } } diff --git a/packages/astro/test/units/assets/fonts/infra.test.ts b/packages/astro/test/units/assets/fonts/infra.test.ts index f9c06b3e6ab1..8d4c3b252875 100644 --- a/packages/astro/test/units/assets/fonts/infra.test.ts +++ b/packages/astro/test/units/assets/fonts/infra.test.ts @@ -783,7 +783,7 @@ describe('fonts infra', () => { address: { address: '127.0.0.1', family: 'IPv4', port: 3000 }, }); - assert.equal(resolver.resolve('/_astro/fonts/bar.woff2'), null); + assert.equal(resolver.resolve('/_astro/fonts/bar.woff2', undefined), null); }); it('works with ipv4', () => { @@ -793,7 +793,7 @@ describe('fonts infra', () => { }); assert.equal( - resolver.resolve('/test/_astro/fonts/foo.woff2'), + resolver.resolve('/test/_astro/fonts/foo.woff2', undefined), 'http://127.0.0.1:3000/test/_astro/fonts/foo.woff2', ); }); @@ -805,7 +805,7 @@ describe('fonts infra', () => { }); assert.equal( - resolver.resolve('/_astro/fonts/foo.woff2'), + resolver.resolve('/_astro/fonts/foo.woff2', undefined), 'http://[::]:3000/_astro/fonts/foo.woff2', ); }); @@ -817,10 +817,37 @@ describe('fonts infra', () => { }); assert.equal( - resolver.resolve('http://cdn.example.com/_astro/fonts/foo.woff2'), + resolver.resolve('http://cdn.example.com/_astro/fonts/foo.woff2', undefined), 'http://127.0.0.1:3000/_astro/fonts/foo.woff2', ); }); + + it('falls back to requestUrl when address is null', () => { + const resolver = new RemoteRuntimeFontFileUrlResolver({ + urls: new Set(['/_astro/fonts/foo.woff2']), + address: null, + }); + + assert.equal( + resolver.resolve( + '/_astro/fonts/foo.woff2', + new URL('http://localhost:4321/og.png'), + ), + 'http://localhost:4321/_astro/fonts/foo.woff2', + ); + }); + + it('returns full url directly when address is null and url is absolute (assetsPrefix)', () => { + const resolver = new RemoteRuntimeFontFileUrlResolver({ + urls: new Set(['http://cdn.example.com/_astro/fonts/foo.woff2']), + address: null, + }); + + assert.equal( + resolver.resolve('http://cdn.example.com/_astro/fonts/foo.woff2', undefined), + 'http://cdn.example.com/_astro/fonts/foo.woff2', + ); + }); }); describe('SsrRuntimeFontFileUrlResolver', () => { From c7811b8f15b29b28a2921c944b9aa83a78af0585 Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" Date: Thu, 20 Aug 2026 12:27:44 +0000 Subject: [PATCH 10/21] [ci] format --- .../fonts/infra/remote-runtime-font-file-url-resolver.ts | 4 +--- packages/astro/test/units/assets/fonts/infra.test.ts | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts b/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts index 7bf2770020a4..43ea23a3217c 100644 --- a/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts +++ b/packages/astro/src/assets/fonts/infra/remote-runtime-font-file-url-resolver.ts @@ -41,9 +41,7 @@ export class RemoteRuntimeFontFileUrlResolver implements RuntimeFontFileUrlResol } if (this.#address) { const host = - this.#address.family === 'IPv6' - ? `[${this.#address.address}]` - : this.#address.address; + this.#address.family === 'IPv6' ? `[${this.#address.address}]` : this.#address.address; return `http://${host}:${this.#address.port}${url}`; } // Fallback when the server address was not available at module diff --git a/packages/astro/test/units/assets/fonts/infra.test.ts b/packages/astro/test/units/assets/fonts/infra.test.ts index 8d4c3b252875..cdf54e65f31c 100644 --- a/packages/astro/test/units/assets/fonts/infra.test.ts +++ b/packages/astro/test/units/assets/fonts/infra.test.ts @@ -829,10 +829,7 @@ describe('fonts infra', () => { }); assert.equal( - resolver.resolve( - '/_astro/fonts/foo.woff2', - new URL('http://localhost:4321/og.png'), - ), + resolver.resolve('/_astro/fonts/foo.woff2', new URL('http://localhost:4321/og.png')), 'http://localhost:4321/_astro/fonts/foo.woff2', ); }); From 3d50dfdd14e2eff09f28645b8e788aca36323ff1 Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:40:48 -0400 Subject: [PATCH 11/21] fix(dev): treat lock file as stale when PID matches current process (#17744) (#17754) In Docker containers, PID namespaces reset on restart, so the new `astro dev` process often inherits the same PID the old one had. The lock file from the previous run persists, and the process detects itself as the "already running" server. Add a self-PID guard to `isLockFileProcessAlive()`: if the lock file's PID matches `process.pid`, treat it as stale immediately. The current process cannot be the server recorded in the lock file because it hasn't started one yet. Co-authored-by: factory[bot] --- .changeset/wicked-zebras-sip.md | 5 +++ packages/astro/src/core/dev/lockfile.ts | 8 ++++ .../astro/test/units/dev/lockfile.test.ts | 39 ++++++++++++++++--- pnpm-lock.yaml | 6 +++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 .changeset/wicked-zebras-sip.md diff --git a/.changeset/wicked-zebras-sip.md b/.changeset/wicked-zebras-sip.md new file mode 100644 index 000000000000..0699a0ab6451 --- /dev/null +++ b/.changeset/wicked-zebras-sip.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes the dev server refusing to start in Docker containers after a restart due to PID reuse in the lock file check diff --git a/packages/astro/src/core/dev/lockfile.ts b/packages/astro/src/core/dev/lockfile.ts index b27f9bcacd26..de67ddbc1103 100644 --- a/packages/astro/src/core/dev/lockfile.ts +++ b/packages/astro/src/core/dev/lockfile.ts @@ -124,6 +124,14 @@ export async function isLockFileProcessAlive( data: LockFileData, find: ProcessLookup = findProcess, ): Promise { + // The current process cannot be the server recorded in the lock file — it hasn't + // started one yet. In Docker containers the PID namespace resets on restart, so the + // new `astro dev` process often inherits the same PID the old one had. Without this + // guard, the process detects itself as the "already running" server. (#17744) + if (data.pid === process.pid) { + return false; + } + if (!isProcessAlive(data.pid)) { return false; } diff --git a/packages/astro/test/units/dev/lockfile.test.ts b/packages/astro/test/units/dev/lockfile.test.ts index 87bdfac92017..65cbbefcb149 100644 --- a/packages/astro/test/units/dev/lockfile.test.ts +++ b/packages/astro/test/units/dev/lockfile.test.ts @@ -220,7 +220,22 @@ describe('isAstroCommand', () => { }); describe('isLockFileProcessAlive', () => { - it('returns true when the recorded process command is Astro', async () => { + /** A long-lived child process whose PID is alive but is not the current process. */ + let child: ReturnType; + let childPid: number; + + before(() => { + child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 60_000)'], { + stdio: 'ignore', + }); + childPid = child.pid!; + }); + + after(() => { + child.kill('SIGKILL'); + }); + + it('returns false when the lock file PID matches the current process', async () => { const data = { ...validData, pid: process.pid }; const findProcess = async () => [ { @@ -231,15 +246,29 @@ describe('isLockFileProcessAlive', () => { }, ]; + assert.equal(await isLockFileProcessAlive(data, findProcess), false); + }); + + it('returns true when the recorded process command is Astro', async () => { + const data = { ...validData, pid: childPid }; + const findProcess = async () => [ + { + pid: childPid, + ppid: process.pid, + name: 'node', + cmd: 'node /workspace/node_modules/astro/bin/astro.mjs dev', + }, + ]; + assert.equal(await isLockFileProcessAlive(data, findProcess), true); }); it('returns false when the PID belongs to another command', async () => { - const data = { ...validData, pid: process.pid }; + const data = { ...validData, pid: childPid }; const findProcess = async () => [ { - pid: process.pid, - ppid: process.ppid, + pid: childPid, + ppid: process.pid, name: 'node', cmd: 'node /app/server.mjs', }, @@ -249,7 +278,7 @@ describe('isLockFileProcessAlive', () => { }); it('keeps the PID-only result when the command cannot be inspected', async () => { - const data = { ...validData, pid: process.pid }; + const data = { ...validData, pid: childPid }; assert.equal(await isLockFileProcessAlive(data, async () => []), true); assert.equal( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0784cfed33e..46f138303994 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7169,6 +7169,12 @@ importers: specifier: ^4.22.0 version: 4.22.3 + triage/gh-17744: + dependencies: + astro: + specifier: ^7.2.4 + version: link:../../packages/astro + packages: '@anthropic-ai/sdk@0.91.1': From aacf76fda4f600983de16a037b9d0808e49f3266 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 20 Aug 2026 08:44:20 -0400 Subject: [PATCH 12/21] Make triage bot changesets non-optional (#17475) * Require changesets in triage fix and PR-writer skills * Update .agents/skills/astro-pr-writer/SKILL.md * Update .agents/skills/astro-pr-writer/SKILL.md * Update .agents/skills/triage/fix.md --- .agents/skills/astro-pr-writer/SKILL.md | 4 ++-- .agents/skills/triage/fix.md | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.agents/skills/astro-pr-writer/SKILL.md b/.agents/skills/astro-pr-writer/SKILL.md index a5a10933d402..7fb5076a09e9 100644 --- a/.agents/skills/astro-pr-writer/SKILL.md +++ b/.agents/skills/astro-pr-writer/SKILL.md @@ -120,7 +120,7 @@ Load the `changeset` skill to create the changeset file and write the message. I When writing the PR body: -- Always check that a changeset exists before posting the PR +- Before posting, check whether a changeset exists. If the PR modifies a package and none exists, **create it now** using the `changeset` skill — do not post the PR without one. - Do not mention "added changeset" in the `Changes` section — it is process noise, not a behavior change ## Self-Check Before Posting @@ -129,4 +129,4 @@ When writing the PR body: - `Changes` bullets describe behavior/implementation/impact - `Testing` lists test code added/changed, not test run results - `Docs` decision is explicit -- Changeset file exists in `.changeset/` for any package-modifying PR +- Changeset file exists in `.changeset/` for any package-modifying PR — if missing, create it before posting diff --git a/.agents/skills/triage/fix.md b/.agents/skills/triage/fix.md index 0784a20991af..010978e06c85 100644 --- a/.agents/skills/triage/fix.md +++ b/.agents/skills/triage/fix.md @@ -25,7 +25,7 @@ These variables are referenced throughout this skill. They may be passed as args 6. Write a unit test 7. Ensure no regressions 8. Generate git diff -9. Create a changeset +9. Create a changeset (required for any fix that modifies a package) 10. Append fix details to `report.md` 11. Clean up the working directory @@ -141,9 +141,22 @@ This captures all your changes for the report. ## Step 9: Create a Changeset -**Only do this if the fix was successful** (i.e., you are on the high-confidence path and the fix resolves the issue). If the fix failed or was skipped, skip this step entirely. +A changeset is **required** for every successful fix that modifies a package under `packages/`. This is not optional and does not scale with the size of the fix: one-line fixes, type-only fixes, and comment-only behavior changes all need a changeset. The only fixes that skip this step are ones that failed, were skipped, or touch nothing under `packages/` (e.g. `examples/*`-only changes). -Load the `changeset` skill and follow its instructions to create a changeset for the fix. Since this is a bug fix, the bump type will almost always be `patch`. +Create the changeset now: + +1. Run `pnpm changeset --empty` from the repo root. This writes a randomly-named `.md` file to `.changeset/`. +2. Edit that file to add the package bump and a user-facing message. The bump type for a bug fix is almost always `patch`: + + ```md + --- + '': patch + --- + + + ``` + +Load the `changeset` skill for the message conventions (present-tense verb, name the affected API, write for Astro users not reviewers) and the exact package name to use. ## Step 10: Write Output @@ -158,7 +171,7 @@ The report must include all information needed for a final GitHub comment to be - Whether the fix was successful or not - Verification results (did the fix resolve the original error?) - Unit test details: what test was added, where it lives, and what it verifies. If no test was added, explain why. -- Changeset details: what changeset file was created and which packages it covers. If no changeset was created, explain why. +- Changeset details: the name of the changeset file created in `.changeset/` and which packages it covers. A successful fix that modified a package must have a changeset — there is no "skipped because trivial" outcome. The only valid reason to have no changeset is that the fix failed/was skipped or touched nothing under `packages/`; state which. - Any alternative approaches considered and their tradeoffs - If the fix failed: what was tried and why it didn't work @@ -170,7 +183,7 @@ The report must include all information needed for a final GitHub comment to be - Changes outside `packages/` that were only needed for diagnosis/reproduction - Build artifacts that shouldn't be committed 3. Use `git checkout -- ` to discard unwanted changes -4. Confirm with a final `git status` that only the intended fix files remain +4. Confirm with a final `git status` that only the intended fix files remain, and that they include a new `.changeset/*.md` file. If the fix modified a package but no changeset is present, go back and create it before finishing. 5. DO NOT commit or push anything yet! The user will handle that at a later step. The `triage/` directory is already gitignored, so it won't appear in `git status`. From f88c875c2c89af3b8cbb9f9eebdcddde6e0645c8 Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:53:42 -0400 Subject: [PATCH 13/21] fix(create-astro): approve esbuild install scripts for npm v11+ compatibility (#17756) npm v11+ warns about packages with unapproved install scripts, and npm v12 will make this a hard failure. Astro depends on esbuild which has a postinstall script that downloads platform-specific binaries. Add `allowScripts` for esbuild to all example package.json files and add `ensureNpmScriptsAllowed()` to create-astro to pre-approve esbuild in package.json before running `npm install`. Fixes #17745 Co-authored-by: factory[bot] Co-authored-by: Matthew Phillips --- .changeset/short-tables-thank.md | 5 ++++ examples/advanced-routing/package.json | 3 +++ examples/basics/package.json | 3 +++ examples/blog/package.json | 3 +++ examples/component/package.json | 3 +++ examples/container-with-vitest/package.json | 3 +++ examples/framework-alpine/package.json | 3 +++ examples/framework-multiple/package.json | 3 +++ examples/framework-preact/package.json | 3 +++ examples/framework-react/package.json | 3 +++ examples/framework-solid/package.json | 3 +++ examples/framework-svelte/package.json | 3 +++ examples/framework-vue/package.json | 3 +++ examples/hackernews/package.json | 3 +++ examples/integration/package.json | 3 +++ examples/minimal/package.json | 3 +++ examples/portfolio/package.json | 3 +++ examples/ssr/package.json | 3 +++ examples/starlog/package.json | 3 +++ examples/toolbar-app/package.json | 3 +++ examples/with-markdoc/package.json | 3 +++ examples/with-mdx/package.json | 3 +++ examples/with-nanostores/package.json | 3 +++ examples/with-tailwindcss/package.json | 3 +++ examples/with-vitest/package.json | 3 +++ .../create-astro/src/actions/dependencies.ts | 25 +++++++++++++++++++ 26 files changed, 102 insertions(+) create mode 100644 .changeset/short-tables-thank.md diff --git a/.changeset/short-tables-thank.md b/.changeset/short-tables-thank.md new file mode 100644 index 000000000000..85488172c250 --- /dev/null +++ b/.changeset/short-tables-thank.md @@ -0,0 +1,5 @@ +--- +'create-astro': patch +--- + +Fixes `npm install` warnings on npm v11+ about esbuild's install scripts not being covered by `allowScripts`. Adds `ensureNpmScriptsAllowed()` to pre-approve esbuild in `package.json` before running `npm install`, matching the existing pnpm v11 compatibility fix. diff --git a/examples/advanced-routing/package.json b/examples/advanced-routing/package.json index eedf3c865a21..1ddddb8b9ac7 100644 --- a/examples/advanced-routing/package.json +++ b/examples/advanced-routing/package.json @@ -16,5 +16,8 @@ "@astrojs/node": "^11.1.4", "astro": "^7.2.4", "hono": "^4.12.14" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/basics/package.json b/examples/basics/package.json index deaf8ab28b7a..c48360da0a18 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -14,5 +14,8 @@ }, "dependencies": { "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/blog/package.json b/examples/blog/package.json index c9db9ef3a99b..2f91beddffc0 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -18,5 +18,8 @@ "@astrojs/sitemap": "^3.7.3", "astro": "^7.2.4", "sharp": "^0.35.0" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/component/package.json b/examples/component/package.json index 68741a682a52..a66e7451df75 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -22,5 +22,8 @@ }, "peerDependencies": { "astro": "^5.0.0 || ^6.0.0" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index 17680fc49da6..6ae7c7b2016a 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -23,5 +23,8 @@ "devDependencies": { "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 46f29528c737..1881b4140f66 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -17,5 +17,8 @@ "@types/alpinejs": "^3.13.11", "alpinejs": "^3.15.8", "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index f94b30c0a56a..4e47f06ca8b8 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -27,5 +27,8 @@ "solid-js": "^1.9.11", "svelte": "^5.53.5", "vue": "^3.5.29" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 25014ced0441..e7abe20da8de 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -17,5 +17,8 @@ "@preact/signals": "^2.8.1", "astro": "^7.2.4", "preact": "^10.28.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 1c2024d0c534..49cc0ca31c19 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -19,5 +19,8 @@ "astro": "^7.2.4", "react": "^18.3.1", "react-dom": "^18.3.1" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 8826ad707c7b..554f799d8807 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -16,5 +16,8 @@ "@astrojs/solid-js": "^7.0.2", "astro": "^7.2.4", "solid-js": "^1.9.11" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 86dac97c83f2..2c55b78d8eba 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -16,5 +16,8 @@ "@astrojs/svelte": "^9.0.1", "astro": "^7.2.4", "svelte": "^5.53.5" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 24fc3c122055..202202ee80de 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -16,5 +16,8 @@ "@astrojs/vue": "^7.0.2", "astro": "^7.2.4", "vue": "^3.5.29" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index b71a520e7a67..15b61c81d144 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -15,5 +15,8 @@ "dependencies": { "@astrojs/node": "^11.1.4", "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/integration/package.json b/examples/integration/package.json index a19b0a75a9dc..d87db02ef3a1 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -22,5 +22,8 @@ }, "peerDependencies": { "astro": "^4.0.0" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 6edf5f8309ab..8ea8d9837e33 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -14,5 +14,8 @@ }, "dependencies": { "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index dc45d5e2fed4..bac2f217ae56 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -14,5 +14,8 @@ }, "dependencies": { "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index a5236ab64c87..f498797979d8 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -18,5 +18,8 @@ "@astrojs/svelte": "^9.0.1", "astro": "^7.2.4", "svelte": "^5.53.5" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 3b465f28f07a..12d44981e785 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -15,5 +15,8 @@ }, "engines": { "node": ">=22.12.0" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index 973a93eb5d8e..b561a8f063f7 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -20,5 +20,8 @@ }, "engines": { "node": ">=22.12.0" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index f46d24e068ca..7044bfd0b898 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -15,5 +15,8 @@ "dependencies": { "@astrojs/markdoc": "^2.0.8", "astro": "^7.2.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index e8a4b9953690..db64f8e9962a 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -17,5 +17,8 @@ "@astrojs/preact": "^6.0.4", "astro": "^7.2.4", "preact": "^10.28.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index a7b840f2a2de..e5471a8a3010 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -18,5 +18,8 @@ "astro": "^7.2.4", "nanostores": "^1.1.1", "preact": "^10.28.4" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index b0dcc0e8d279..359b244b373a 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -20,5 +20,8 @@ "canvas-confetti": "^1.9.4", "tailwindcss": "^4.2.1", "vite": "^8.0.13" + }, + "allowScripts": { + "esbuild": true } } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index a03ed9530be8..f7bbd3021346 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -16,5 +16,8 @@ "dependencies": { "astro": "^7.2.4", "vitest": "^5.0.0-beta.2" + }, + "allowScripts": { + "esbuild": true } } diff --git a/packages/create-astro/src/actions/dependencies.ts b/packages/create-astro/src/actions/dependencies.ts index 337c713a0341..c26d443f465b 100644 --- a/packages/create-astro/src/actions/dependencies.ts +++ b/packages/create-astro/src/actions/dependencies.ts @@ -123,6 +123,7 @@ async function astroAdd({ async function install({ packageManager, cwd }: { packageManager: string; cwd: string }) { if (packageManager === 'yarn') await ensureYarnLock({ cwd }); if (packageManager === 'pnpm') await ensurePnpmBuildsAllowed({ cwd }); + if (packageManager === 'npm') await ensureNpmScriptsAllowed({ cwd }); return shell(packageManager, ['install'], { cwd, timeout: 90_000, stdio: 'ignore' }); } @@ -152,6 +153,30 @@ async function ensurePnpmBuildsAllowed({ cwd }: { cwd: string }) { return fs.promises.writeFile(workspaceFile, content + allowBuildsBlock, 'utf-8'); } +/** + * npm v11+ warns about packages with unapproved install scripts, and npm v12 will + * make this a hard failure. Astro depends on `esbuild` which has a postinstall script + * that downloads platform-specific binaries. + * + * This function ensures esbuild is pre-approved in `package.json` via the `allowScripts` + * field so that `npm install` succeeds without warnings or failures. + * See https://docs.npmjs.com/cli/v11/using-npm/config#allow-scripts + */ +async function ensureNpmScriptsAllowed({ cwd }: { cwd: string }) { + const pkgFile = path.join(cwd, 'package.json'); + if (!fs.existsSync(pkgFile)) return; + + const content = await fs.promises.readFile(pkgFile, 'utf-8'); + const packageJson = JSON.parse(content); + + // If allowScripts is already configured, don't touch it + if (packageJson.allowScripts) return; + + const indent = /(^\s+)/m.exec(content)?.[1] ?? '\t'; + packageJson.allowScripts = { esbuild: true }; + return fs.promises.writeFile(pkgFile, JSON.stringify(packageJson, null, indent) + '\n', 'utf-8'); +} + /** * Yarn Berry (PnP) versions will throw an error if there isn't an existing `yarn.lock` file * If a `yarn.lock` file doesn't exist, this function writes an empty `yarn.lock` one. From d035290a14afac8834885b727327a7f44d3a3a48 Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Thu, 20 Aug 2026 18:07:21 +0500 Subject: [PATCH 14/21] fix: rebuild module imports after content entry deletion (#17713) * fix: rebuild module imports after content entry deletion (#17707) * test: exercise debounced module-import trigger on delete/clear/clearAll (#17707) Prior tests called writeModuleImports() explicitly right after delete/clear, which forced the rebuild to run synchronously regardless of whether the new #writeModulesImportsDebounced() calls inside delete()/clear()/clearAll() were wired correctly. Reverting those three trigger lines still passed all existing assertions. Rewrite the delete/clear tests to rely on waitUntilSaveComplete() alone, add a clearAll()-specific test, and add a rename test (delete(oldId) + set(newId)) matching the issue's actual reported scenario. Verified: reverting the three trigger lines makes the delete/clear/clearAll tests fail; restoring them makes all pass. * fix: return early after writing an empty content-modules.mjs The zero-size branch in writeModuleImports() fell through to the generator below it, writing the file twice on every call where #moduleImports ends up empty (e.g. after deleting the last deferred-render entry) -- first 'export default new Map();', then a second, differently-formatted empty-map write. That path only became reachable once #moduleImports could shrink back to zero, which this PR introduces. Add the missing return, matching the pattern review comment. Also note in the changeset that addModuleImport() callers without a backing deferredRender entry no longer survive a write, now that #moduleImports is fully derived state. --- .../fix-content-modules-stale-import.md | 7 + .../astro/src/content/mutable-data-store.ts | 26 ++++ .../mutable-data-store.test.ts | 142 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 .changeset/fix-content-modules-stale-import.md diff --git a/.changeset/fix-content-modules-stale-import.md b/.changeset/fix-content-modules-stale-import.md new file mode 100644 index 000000000000..52b5d4116e2f --- /dev/null +++ b/.changeset/fix-content-modules-stale-import.md @@ -0,0 +1,7 @@ +--- +'astro': patch +--- + +Fixes `content-modules.mjs` not removing entries for deleted or renamed content files, which could cause Vite to attempt to resolve non-existent modules + +As part of this fix, `#moduleImports` is now fully rebuilt from `deferredRender` entries before every write, so a module import added only through the public `addModuleImport()` API without a corresponding `deferredRender` entry in the store will no longer be preserved across writes. diff --git a/packages/astro/src/content/mutable-data-store.ts b/packages/astro/src/content/mutable-data-store.ts index ee28d277768c..ef6c0878ec5d 100644 --- a/packages/astro/src/content/mutable-data-store.ts +++ b/packages/astro/src/content/mutable-data-store.ts @@ -59,6 +59,7 @@ export class MutableDataStore extends ImmutableDataStore { collection.delete(String(key)); this.#saveToDiskDebounced(); this.#writeAssetsImportsDebounced(); + this.#writeModulesImportsDebounced(); } } @@ -66,12 +67,14 @@ export class MutableDataStore extends ImmutableDataStore { this._collections.delete(collectionName); this.#saveToDiskDebounced(); this.#writeAssetsImportsDebounced(); + this.#writeModulesImportsDebounced(); } clearAll() { this._collections.clear(); this.#saveToDiskDebounced(); this.#writeAssetsImportsDebounced(); + this.#writeModulesImportsDebounced(); } addAssetImport(assetImport: string, filePath?: string) { @@ -123,6 +126,27 @@ export class MutableDataStore extends ImmutableDataStore { } } + /** + * Rebuilds #moduleImports from the current entries in _collections. + * This ensures stale module entries are removed when content files are + * deleted or renamed, preventing Vite from attempting to resolve + * non-existent files listed in content-modules.mjs. + */ + #rebuildModuleImports() { + this.#moduleImports.clear(); + for (const collection of this._collections.values()) { + for (const entry of collection.values()) { + const typedEntry = entry as DataEntry; + if (typedEntry.deferredRender && typedEntry.filePath) { + const id = contentModuleToId(typedEntry.filePath); + if (id) { + this.#moduleImports.set(typedEntry.filePath, id); + } + } + } + } + } + async writeAssetImports(filePath: PathLike) { this.#assetsFile = filePath; this.#rebuildAssetImports(); @@ -164,6 +188,7 @@ export default new Map([${exports.join(', ')}]); async writeModuleImports(filePath: PathLike) { this.#modulesFile = filePath; + this.#rebuildModuleImports(); if (this.#moduleImports.size === 0) { try { @@ -171,6 +196,7 @@ export default new Map([${exports.join(', ')}]); } catch (err) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); } + return; } if (!this.#modulesDirty && existsSync(filePath)) { diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.ts b/packages/astro/test/units/content-collections/mutable-data-store.test.ts index eef3efe004ca..821c270369af 100644 --- a/packages/astro/test/units/content-collections/mutable-data-store.test.ts +++ b/packages/astro/test/units/content-collections/mutable-data-store.test.ts @@ -128,6 +128,148 @@ describe('MutableDataStore', () => { ); }); + it('removes stale module imports when an entry is deleted (via debounced write trigger)', async () => { + const modulesFilePath = path.join(tmpDir, 'content-modules-delete.mjs'); + const store = new MutableDataStore(); + const scoped = store.scopedStore('docs'); + + scoped.set({ + id: 'page-a', + data: {}, + filePath: 'src/content/docs/page-a.mdx', + deferredRender: true, + }); + + scoped.set({ + id: 'page-b', + data: {}, + filePath: 'src/content/docs/page-b.mdx', + deferredRender: true, + }); + + await store.writeModuleImports(modulesFilePath); + const contentBefore = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok(contentBefore.includes('page-a.mdx'), 'should contain page-a before deletion'); + assert.ok(contentBefore.includes('page-b.mdx'), 'should contain page-b before deletion'); + + // Do NOT call writeModuleImports() again here: that would rebuild and write + // synchronously regardless of whether delete() actually schedules a rewrite. + // Rely solely on the debounced trigger that delete() is supposed to schedule, + // flushed via waitUntilSaveComplete(), so this test exercises the real + // dev-server code path (a filesystem delete triggers a rewrite on its own). + scoped.delete('page-a'); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('page-a.mdx'), + 'should NOT contain page-a after the entry is deleted', + ); + assert.ok(contentAfter.includes('page-b.mdx'), 'should still contain page-b'); + }); + + it('removes stale module imports when a collection is cleared (via debounced write trigger)', async () => { + const modulesFilePath = path.join(tmpDir, 'content-modules-clear.mjs'); + const store = new MutableDataStore(); + const scoped = store.scopedStore('docs'); + + scoped.set({ + id: 'page-1', + data: {}, + filePath: 'src/content/docs/page-1.mdx', + deferredRender: true, + }); + + await store.writeModuleImports(modulesFilePath); + const contentBefore = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok(contentBefore.includes('page-1.mdx'), 'should contain page-1 before clear'); + + scoped.clear(); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('page-1.mdx'), + 'should NOT contain page-1 after the collection is cleared', + ); + }); + + it('removes stale module imports when the entire store is cleared via clearAll (via debounced write trigger)', async () => { + const modulesFilePath = path.join(tmpDir, 'content-modules-clear-all.mjs'); + const store = new MutableDataStore(); + const docsScoped = store.scopedStore('docs'); + const blogScoped = store.scopedStore('blog'); + + docsScoped.set({ + id: 'page-1', + data: {}, + filePath: 'src/content/docs/page-1.mdx', + deferredRender: true, + }); + blogScoped.set({ + id: 'post-1', + data: {}, + filePath: 'src/content/blog/post-1.mdx', + deferredRender: true, + }); + + await store.writeModuleImports(modulesFilePath); + const contentBefore = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok(contentBefore.includes('page-1.mdx'), 'should contain page-1 before clearAll'); + assert.ok(contentBefore.includes('post-1.mdx'), 'should contain post-1 before clearAll'); + + store.clearAll(); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('page-1.mdx'), + 'should NOT contain page-1 after clearAll', + ); + assert.ok( + !contentAfter.includes('post-1.mdx'), + 'should NOT contain post-1 after clearAll', + ); + }); + + it('removes the old module import and adds the new one when an entry is renamed (issue #17707)', async () => { + // A rename is how glob.ts actually models it: delete the old id, then set the new one. + const modulesFilePath = path.join(tmpDir, 'content-modules-rename.mjs'); + const store = new MutableDataStore(); + const scoped = store.scopedStore('docs'); + + scoped.set({ + id: 'otp', + data: {}, + filePath: 'src/content/docs/otp.mdx', + deferredRender: true, + }); + + await store.writeModuleImports(modulesFilePath); + const contentBefore = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok(contentBefore.includes('otp.mdx'), 'should contain the original file before rename'); + + // Rename: delete the old entry, then set the new one under a new id/filePath. + scoped.delete('otp'); + scoped.set({ + id: 'the-otp', + data: {}, + filePath: 'src/content/docs/the-otp.mdx', + deferredRender: true, + }); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(modulesFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('"src/content/docs/otp.mdx"'), + 'should NOT reference the old (renamed-away) file path', + ); + assert.ok( + contentAfter.includes('the-otp.mdx'), + 'should reference the new (renamed-to) file path', + ); + }); + it('reproduces race condition: concurrent writeToDisk() calls lose data', async () => { const filePath = pathToFileURL(path.join(tmpDir, 'data-store.json')); const store = await MutableDataStore.fromFile(filePath); From 1b41d099ca1920dd1b3302c1fabbd0c10fd7fdcd Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Thu, 20 Aug 2026 13:08:38 +0000 Subject: [PATCH 15/21] [ci] format --- .../content-collections/mutable-data-store.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.ts b/packages/astro/test/units/content-collections/mutable-data-store.test.ts index 821c270369af..f48b7f150f83 100644 --- a/packages/astro/test/units/content-collections/mutable-data-store.test.ts +++ b/packages/astro/test/units/content-collections/mutable-data-store.test.ts @@ -222,14 +222,8 @@ describe('MutableDataStore', () => { await store.waitUntilSaveComplete(); const contentAfter = await fs.readFile(modulesFilePath, 'utf-8'); - assert.ok( - !contentAfter.includes('page-1.mdx'), - 'should NOT contain page-1 after clearAll', - ); - assert.ok( - !contentAfter.includes('post-1.mdx'), - 'should NOT contain post-1 after clearAll', - ); + assert.ok(!contentAfter.includes('page-1.mdx'), 'should NOT contain page-1 after clearAll'); + assert.ok(!contentAfter.includes('post-1.mdx'), 'should NOT contain post-1 after clearAll'); }); it('removes the old module import and adds the new one when an entry is renamed (issue #17707)', async () => { From dd0e3aca0b898307431120a5bc1541681190178d Mon Sep 17 00:00:00 2001 From: Kosta <47947996+dobrodob@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:28:16 +0200 Subject: [PATCH 16/21] fix(transitions): do not re-create media inside persisted subtrees (#17750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(transitions): do not re-create media inside persisted subtrees reifyMediaElements() (#17603) runs after transition:persist elements from the old document have been moved into the new body, so it also replaced the live

Page 1

+ go to 2 +
+
+ +
+
+ diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/nested-persist-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/nested-persist-two.astro new file mode 100644 index 000000000000..f2ddfe7fc29e --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/nested-persist-two.astro @@ -0,0 +1,12 @@ +--- +import Layout from '../components/Layout.astro'; +--- + +

Page 2

+ go to 1 + +
+

replaced by the persisted subtree

+
+
diff --git a/packages/astro/e2e/view-transitions.test.ts b/packages/astro/e2e/view-transitions.test.ts index f498d49fc008..e1ad24e8460d 100644 --- a/packages/astro/e2e/view-transitions.test.ts +++ b/packages/astro/e2e/view-transitions.test.ts @@ -563,6 +563,58 @@ test.describe('View Transitions', () => { }).toPass(); }); + test('persisted