From 0c996155d82acbb0c3accad1e56ba356bf47b743 Mon Sep 17 00:00:00 2001 From: Barry <91018388+barry166@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:11:16 +0800 Subject: [PATCH 1/7] fix(markdown-satteri): clarify smartPunctuation default (#17314) Co-authored-by: Princesseuh <3019731+Princesseuh@users.noreply.github.com> --- .../satteri-smart-punctuation-default-jsdoc.md | 5 +++++ packages/markdown/satteri/src/index.ts | 1 + packages/markdown/satteri/src/processor.ts | 13 +++++++++++-- packages/markdown/satteri/test/markdown.test.ts | 9 +++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/satteri-smart-punctuation-default-jsdoc.md diff --git a/.changeset/satteri-smart-punctuation-default-jsdoc.md b/.changeset/satteri-smart-punctuation-default-jsdoc.md new file mode 100644 index 000000000000..6a8bdba54f17 --- /dev/null +++ b/.changeset/satteri-smart-punctuation-default-jsdoc.md @@ -0,0 +1,5 @@ +--- +'@astrojs/markdown-satteri': patch +--- + +Fixes the editor tooltip for `smartPunctuation` claiming it defaults to `false` when Astro enables it by default. diff --git a/packages/markdown/satteri/src/index.ts b/packages/markdown/satteri/src/index.ts index 2865ef3057d1..3861f89edd45 100644 --- a/packages/markdown/satteri/src/index.ts +++ b/packages/markdown/satteri/src/index.ts @@ -14,6 +14,7 @@ export { export { isSatteriProcessor, satteri, + type SatteriFeatures, type SatteriProcessorOptions, type SatteriResolvedOptions, } from './processor.js'; diff --git a/packages/markdown/satteri/src/processor.ts b/packages/markdown/satteri/src/processor.ts index a643fe578cdc..e208907d6341 100644 --- a/packages/markdown/satteri/src/processor.ts +++ b/packages/markdown/satteri/src/processor.ts @@ -8,10 +8,19 @@ import type { } from 'satteri'; import { createSatteriMarkdownProcessor } from './satteri-processor.js'; +export interface SatteriFeatures extends Omit { + /** + * Smart punctuation à la SmartyPants. + * + * Default: `true` in Astro. + */ + smartPunctuation?: Features['smartPunctuation']; +} + export interface SatteriProcessorOptions { mdastPlugins?: MdastPluginList; hastPlugins?: HastPluginList; - features?: Features; + features?: SatteriFeatures; } /** @@ -21,7 +30,7 @@ export interface SatteriProcessorOptions { export interface SatteriResolvedOptions { mdastPlugins: MdastPluginEntry[]; hastPlugins: HastPluginEntry[]; - features: Features; + features: SatteriFeatures; } /** diff --git a/packages/markdown/satteri/test/markdown.test.ts b/packages/markdown/satteri/test/markdown.test.ts index 0b95f2a61d84..fc7fc9bc0531 100644 --- a/packages/markdown/satteri/test/markdown.test.ts +++ b/packages/markdown/satteri/test/markdown.test.ts @@ -50,6 +50,15 @@ describe('satteri markdown', () => { assert.ok(code.includes('"hello"')); }); + it('accepts a granular options object for `smartPunctuation`', async () => { + const renderer = await satteri({ + features: { smartPunctuation: { dashes: false } }, + }).createRenderer({}); + const { code } = await renderer.render('He said "hello" -- really'); + assert.match(code, /“hello”/); + assert.ok(code.includes('--')); + }); + it('collects local image paths into metadata', async () => { const processor = await createSatteriMarkdownProcessor(); const { metadata } = await processor.render('![alt](./local.png)'); From 493796b4c318b19985eccaac7a11aa7b787e1efe Mon Sep 17 00:00:00 2001 From: Konrad Szajna Date: Mon, 24 Aug 2026 14:23:56 +0200 Subject: [PATCH 2/7] Skip URL normalization writes that would not change the path (#17416) The pathname setter re-parses the whole URL, so a no-op assignment is still expensive. Guard each write in normalizeUrl and FetchState. Collapse stays after the decode is assigned: the setter turns `\` into `/`, so `/a%5C/b` only becomes `/a//b` once written back. --- .changeset/cyan-bikes-visit.md | 5 ++ packages/astro/src/core/fetch/fetch-state.ts | 5 +- .../astro/src/core/util/normalized-url.ts | 19 +++++- .../test/units/util/normalized-url.test.ts | 68 +++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 .changeset/cyan-bikes-visit.md create mode 100644 packages/astro/test/units/util/normalized-url.test.ts diff --git a/.changeset/cyan-bikes-visit.md b/.changeset/cyan-bikes-visit.md new file mode 100644 index 000000000000..7b227c2c558e --- /dev/null +++ b/.changeset/cyan-bikes-visit.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Skips no-op pathname writes when normalizing SSR request URLs diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts index bd0be821aa90..754fd0a73fd8 100644 --- a/packages/astro/src/core/fetch/fetch-state.ts +++ b/packages/astro/src/core/fetch/fetch-state.ts @@ -34,6 +34,7 @@ import { getParams, getProps } from '../render/index.js'; import { executeRewrite } from '../rewrites/handler.js'; import { isRoute404or500, isRouteServerIsland } from '../routing/match.js'; import { MultiLevelEncodingError, validateAndDecodePathname } from '../util/pathname.js'; +import { setPathname } from '../util/normalized-url.js'; import { getOriginPathname, setOriginPathname } from '../routing/rewrite.js'; import { computePathnameFromDomain } from '../i18n/domain.js'; import { getCustom404Route, routeHasHtmlExtension } from '../routing/helpers.js'; @@ -331,8 +332,8 @@ export class FetchState implements AstroFetchState { const url = new URL(request.url); const publicPathname = this.#normalizePathname(url.pathname); const pathname = this.#computePathname(publicPathname); - url.pathname = publicPathname; - url.pathname = collapseDuplicateSlashes(url.pathname); + setPathname(url, publicPathname); + setPathname(url, collapseDuplicateSlashes(url.pathname)); // For domain-based i18n routing, the locale prefix is derived from the // request's Host header rather than its URL. When a locale is detected, // the resulting pathname includes the prefix (e.g. /en/boats/1/foo) that diff --git a/packages/astro/src/core/util/normalized-url.ts b/packages/astro/src/core/util/normalized-url.ts index 88b30b993ef1..68e63eb85611 100644 --- a/packages/astro/src/core/util/normalized-url.ts +++ b/packages/astro/src/core/util/normalized-url.ts @@ -9,21 +9,34 @@ export function createNormalizedUrl(requestUrl: string): URL { return normalizeUrl(new URL(requestUrl)); } +/** + * Assigns `url.pathname` only when the value differs. + * The setter re-parses the whole URL, so a no-op write is still expensive. + */ +export function setPathname(url: URL, pathname: string): void { + if (url.pathname !== pathname) { + url.pathname = pathname; + } +} + /** * Normalizes an already-parsed URL in place: decodes and validates the * pathname, collapses duplicate slashes. Returns the same URL object. + * + * Collapse runs after the decode is written back: the pathname setter + * rewrites `\` to `/`, so a decoded backslash only becomes `//` once assigned. */ export function normalizeUrl(url: URL): URL { try { - url.pathname = validateAndDecodePathname(url.pathname); + setPathname(url, validateAndDecodePathname(url.pathname)); } catch { // For decoding failures (truly malformed URLs), fall back gracefully. try { - url.pathname = decodeURI(url.pathname); + setPathname(url, decodeURI(url.pathname)); } catch { // If even basic decoding fails, return URL as-is } } - url.pathname = collapseDuplicateSlashes(url.pathname); + setPathname(url, collapseDuplicateSlashes(url.pathname)); return url; } diff --git a/packages/astro/test/units/util/normalized-url.test.ts b/packages/astro/test/units/util/normalized-url.test.ts new file mode 100644 index 000000000000..fbe80d006221 --- /dev/null +++ b/packages/astro/test/units/util/normalized-url.test.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { createNormalizedUrl, normalizeUrl } from '../../../dist/core/util/normalized-url.js'; + +describe('normalizeUrl', () => { + // #region Plain paths (the common case: nothing to rewrite) + + it('leaves an ordinary path unchanged', () => { + assert.equal(normalizeUrl(new URL('https://e.com/about')).pathname, '/about'); + }); + + it('leaves the root path unchanged', () => { + assert.equal(normalizeUrl(new URL('https://e.com/')).pathname, '/'); + }); + + it('preserves the search and hash', () => { + assert.equal(normalizeUrl(new URL('https://e.com/x?q=1#h')).href, 'https://e.com/x?q=1#h'); + }); + + it('returns the same URL object', () => { + const url = new URL('https://e.com/a'); + assert.equal(normalizeUrl(url), url); + }); + + // #endregion + // #region Duplicate slashes + + it('collapses duplicate slashes', () => { + assert.equal(normalizeUrl(new URL('https://e.com/a//b')).pathname, '/a/b'); + assert.equal(normalizeUrl(new URL('https://e.com///a///b')).pathname, '/a/b'); + }); + + // #endregion + // #region Encoding + + it('decodes single-encoded unreserved characters', () => { + assert.equal(normalizeUrl(new URL('https://e.com/api/%61dmin')).pathname, '/api/admin'); + }); + + it('fully decodes multi-encoded unreserved characters', () => { + assert.equal(normalizeUrl(new URL('https://e.com/api/%2561dmin')).pathname, '/api/admin'); + }); + + it('keeps reserved characters encoded', () => { + assert.equal(normalizeUrl(new URL('https://e.com/path%3Fname')).pathname, '/path%3Fname'); + }); + + it('re-encodes characters the pathname setter escapes', () => { + assert.equal(normalizeUrl(new URL('https://e.com/a%20b')).pathname, '/a%20b'); + }); + + // #endregion + // #region Backslash + + // Pathname setter rewrites `\` to `/`, so collapse must run after the + // decode is assigned: `/a%5C/b` -> `/a\/b` -> `/a//b` -> `/a/b`. + it('collapses a slash introduced by decoding a backslash', () => { + assert.equal(normalizeUrl(new URL('https://e.com/a%5C/b')).pathname, '/a/b'); + }); + + // #endregion +}); + +describe('createNormalizedUrl', () => { + it('parses and normalizes a request URL string', () => { + assert.equal(createNormalizedUrl('https://e.com/a//%61dmin').pathname, '/a/admin'); + }); +}); From 426eaa17530f27f65a366e7f19a86279178bbc92 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 24 Aug 2026 08:24:30 -0400 Subject: [PATCH 3/7] Fix Vue auto-import completions with Astro extension (#17791) --- .changeset/legal-pets-leave.md | 5 ++++ .../units/proxy-language-service.test.mts | 27 +++++++++++++++++++ patches/@volar__typescript@2.4.28.patch | 12 +++++++++ pnpm-lock.yaml | 13 +++++---- pnpm-workspace.yaml | 3 +++ 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 .changeset/legal-pets-leave.md create mode 100644 packages/language-tools/ts-plugin/test/units/proxy-language-service.test.mts create mode 100644 patches/@volar__typescript@2.4.28.patch diff --git a/.changeset/legal-pets-leave.md b/.changeset/legal-pets-leave.md new file mode 100644 index 000000000000..9d1f67694752 --- /dev/null +++ b/.changeset/legal-pets-leave.md @@ -0,0 +1,5 @@ +--- +'astro-vscode': patch +--- + +Fixes missing Vue template auto-import completions when the Astro extension loads first diff --git a/packages/language-tools/ts-plugin/test/units/proxy-language-service.test.mts b/packages/language-tools/ts-plugin/test/units/proxy-language-service.test.mts new file mode 100644 index 000000000000..359803d32490 --- /dev/null +++ b/packages/language-tools/ts-plugin/test/units/proxy-language-service.test.mts @@ -0,0 +1,27 @@ +import 'mocha'; +import assert from 'node:assert'; +import type { Language } from '@volar/language-core'; +import { createProxyLanguageService } from '@volar/typescript/lib/node/proxyLanguageService.js'; +import type ts from 'typescript'; + +suite('Proxy language service', () => { + test('uses language service methods assigned by a later plugin', () => { + let called = 'original'; + const languageService = { + getCompletionsAtPosition() { + called = 'original'; + }, + } as unknown as ts.LanguageService; + const { initialize, proxy } = createProxyLanguageService(languageService); + + initialize({ scripts: { get: () => undefined } } as unknown as Language); + void proxy.getCompletionsAtPosition; + proxy.getCompletionsAtPosition = () => { + called = 'decorated'; + return undefined; + }; + proxy.getCompletionsAtPosition('component.vue', 0, undefined); + + assert.strictEqual(called, 'decorated'); + }); +}); diff --git a/patches/@volar__typescript@2.4.28.patch b/patches/@volar__typescript@2.4.28.patch new file mode 100644 index 000000000000..78d76767e98d --- /dev/null +++ b/patches/@volar__typescript@2.4.28.patch @@ -0,0 +1,12 @@ +diff --git a/lib/node/proxyLanguageService.js b/lib/node/proxyLanguageService.js +index 95f2cc3ef6336b7762bcd0a5036af27b114367a5..9b0c9e0a68bed912737dd8d2179afe21997277f7 100644 +--- a/lib/node/proxyLanguageService.js ++++ b/lib/node/proxyLanguageService.js +@@ -112,6 +112,7 @@ function createProxyLanguageService(languageService) { + return Reflect.get(target, p, receiver); + }, + set(target, p, value, receiver) { ++ proxyCache.delete(p); + return Reflect.set(target, p, value, receiver); + }, + }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77222f64390c..5efeacb60ed4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,9 @@ overrides: '@types/node@22': ^22.19.0 modern-tar: '>=0.7.7' +patchedDependencies: + '@volar/typescript@2.4.28': 08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad + importers: .: @@ -6811,7 +6814,7 @@ importers: version: 2.4.28 '@volar/typescript': specifier: ~2.4.28 - version: 2.4.28 + version: 2.4.28(patch_hash=08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad) astro-scripts: specifier: workspace:* version: link:../../../scripts @@ -6869,7 +6872,7 @@ importers: version: 2.4.28 '@volar/typescript': specifier: ~2.4.28 - version: 2.4.28 + version: 2.4.28(patch_hash=08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad) semver: specifier: ^7.7.4 version: 7.8.5 @@ -20083,7 +20086,7 @@ snapshots: '@volar/kit@2.4.28(typescript@6.0.3)': dependencies: '@volar/language-service': 2.4.28 - '@volar/typescript': 2.4.28 + '@volar/typescript': 2.4.28(patch_hash=08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad) typesafe-path: 0.2.2 typescript: 6.0.3 vscode-languageserver-textdocument: 1.0.12 @@ -20097,7 +20100,7 @@ snapshots: dependencies: '@volar/language-core': 2.4.28 '@volar/language-service': 2.4.28 - '@volar/typescript': 2.4.28 + '@volar/typescript': 2.4.28(patch_hash=08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad) path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 @@ -20121,7 +20124,7 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - '@volar/typescript@2.4.28': + '@volar/typescript@2.4.28(patch_hash=08646e153f568c63dc1e7a3dae0333806b3d8395c5ffbad47fa695f7c26bacad)': dependencies: '@volar/language-core': 2.4.28 path-browserify: 1.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c68a5edddc66..7589639b297d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -101,3 +101,6 @@ allowBuilds: protobufjs: false sharp: false workerd: false + +patchedDependencies: + '@volar/typescript@2.4.28': patches/@volar__typescript@2.4.28.patch From 0fc5f655ff33701711e91ca950d4f6143ba13c9a Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Mon, 24 Aug 2026 15:32:01 +0200 Subject: [PATCH 4/7] fix: content image regression (#17810) Co-authored-by: Claude Opus 5 (1M context) --- .changeset/jolly-lilies-shake.md | 5 + .../astro/src/content/mutable-data-store.ts | 26 ++- .../units/content-layer/asset-imports.test.ts | 149 +++++++++++++++++- 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 .changeset/jolly-lilies-shake.md diff --git a/.changeset/jolly-lilies-shake.md b/.changeset/jolly-lilies-shake.md new file mode 100644 index 000000000000..a8e485b85726 --- /dev/null +++ b/.changeset/jolly-lilies-shake.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a regression in the content collections that could cause images to not be resolved diff --git a/packages/astro/src/content/mutable-data-store.ts b/packages/astro/src/content/mutable-data-store.ts index ef6c0878ec5d..f843ed122958 100644 --- a/packages/astro/src/content/mutable-data-store.ts +++ b/packages/astro/src/content/mutable-data-store.ts @@ -363,7 +363,17 @@ export default new Map([\n${lines.join(',\n')}]); entries: () => this.entries(collectionName), values: () => this.values(collectionName), keys: () => this.keys(collectionName), - set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => { + set: ({ + id: key, + data, + body, + filePath, + deferredRender, + digest, + rendered, + assetImports, + imageImports: incomingImageImports, + }) => { if (!key) { throw new Error(`ID must be a non-empty string`); } @@ -376,6 +386,18 @@ export default new Map([\n${lines.join(',\n')}]); } const foundAssets = new Set(assetImports); const imageImports: (string | number)[][] = []; + const seenImageImportPaths = new Set(); + const recordImageImport = (imagePath: (string | number)[]) => { + const pathKey = JSON.stringify(imagePath); + if (seenImageImportPaths.has(pathKey)) { + return; + } + seenImageImportPaths.add(pathKey); + imageImports.push(imagePath); + }; + for (const existingImagePath of incomingImageImports ?? []) { + recordImageImport([...existingImagePath]); + } // Image fields are prefixed during schema parsing. Record their locations and // strip the prefix so the stored data holds a plain, devalue-serializable src // string. The recorded paths let read-time resolution rewrite only these fields @@ -384,7 +406,7 @@ export default new Map([\n${lines.join(',\n')}]); if (typeof val === 'string' && val.startsWith(IMAGE_IMPORT_PREFIX)) { const src = val.replace(IMAGE_IMPORT_PREFIX, ''); foundAssets.add(src); - imageImports.push(ctx.path.map((segment) => segment as string | number)); + recordImageImport(ctx.path.map((segment) => segment as string | number)); ctx.update(src); } }); diff --git a/packages/astro/test/units/content-layer/asset-imports.test.ts b/packages/astro/test/units/content-layer/asset-imports.test.ts index e4024e324f25..8781ad7b65ff 100644 --- a/packages/astro/test/units/content-layer/asset-imports.test.ts +++ b/packages/astro/test/units/content-layer/asset-imports.test.ts @@ -1,8 +1,102 @@ import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; import fs from 'node:fs/promises'; +import { describe, it } from 'node:test'; +import { z } from 'zod'; +import { imageSrcToImportId } from '../../../dist/assets/utils/resolveImports.js'; +import { defineCollection } from '../../../dist/content/config.js'; +import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { createTempDir } from './test-helpers.ts'; +import { resolveEntryData } from '../../../dist/content/runtime.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createMinimalSettings, createTempDir, createTestConfigObserver } from './test-helpers.ts'; + +const FILE_PATH = 'src/data/posts/shuttle/index.md'; + +const SRCS: Record = { + dotSlash: './shuttle.jpg', + dotDot: '../data/shuttle.jpg', + bare: 'shuttle.jpg', + bareNested: 'nested/shuttle.jpg', + absolute: '/src/data/shuttle.jpg', + alias: '@images/shuttle.jpg', +}; + +// Both a top-level and a nested image field, so the recorded paths cover both +// shapes: `['image']` and `['banner', 'image']`. +const EXPECTED_IMAGE_PATHS = [['image'], ['banner', 'image']]; + +const RESOLVED: any = { src: '/_astro/shuttle.hash.jpg', width: 100, height: 100, format: 'jpg' }; + +const imageSchema = ({ image }: any) => + z.object({ + id: z.string(), + image: image(), + banner: z.object({ image: image(), alt: z.string() }), + enriched: z.boolean().optional(), + }); + +function makeEntryData(id: string, image: string) { + return { id, image, banner: { image, alt: `${id} banner` } }; +} + +async function syncCollection(loader: any) { + const root = new URL('../../fixtures/content-layer/', import.meta.url); + const store = new MutableDataStore(); + const contentLayer = new ContentLayer({ + settings: createMinimalSettings(root), + logger: new AstroLogger({ destination: { write: () => true }, level: 'silent' }), + store, + contentConfigObserver: createTestConfigObserver({ + imgs: defineCollection({ loader, schema: imageSchema }), + }), + }); + await contentLayer.sync(); + return store.values('imgs'); +} + +/** + * Mirrors what the build does: every `assetImports` src becomes a Vite import id + * that resolves to the built `ImageMetadata`. + */ +function buildAssetMap(entries: Array) { + const map = new Map(); + for (const entry of entries) { + for (const src of entry.assetImports ?? []) { + const id = imageSrcToImportId(src, entry.filePath); + if (id) map.set(id, RESOLVED); + } + } + return map; +} + +function assertImagesResolve(entries: Array) { + assert.equal(entries.length, Object.keys(SRCS).length); + const map = buildAssetMap(entries); + + for (const entry of entries) { + const src = SRCS[entry.id]; + + // 7.2.3 stores the plain src and records where the image fields live, + // instead of keeping an `__ASTRO_IMAGE_`-prefixed string in the data. + assert.equal(entry.data.image, src, `${entry.id}: stored src`); + assert.equal(entry.data.banner.image, src, `${entry.id}: stored nested src`); + assert.deepEqual( + entry.imageImports, + EXPECTED_IMAGE_PATHS, + `${entry.id}: image field paths must be recorded on the entry`, + ); + + const data: any = resolveEntryData(entry, map); + assert.equal(data.image, RESOLVED, `${entry.id}: image must resolve to ImageMetadata`); + assert.equal( + data.banner.image, + RESOLVED, + `${entry.id}: nested image must resolve to ImageMetadata`, + ); + // Untouched siblings are passed through. + assert.equal(data.banner.alt, `${entry.id} banner`); + } +} describe('Content Layer - Asset Imports', () => { it('generates unique symbol names for imports with colliding shorthashes', async () => { @@ -33,4 +127,55 @@ describe('Content Layer - Asset Imports', () => { assert.equal(importNames.length, 2, 'should have exactly 2 imports'); assert.notEqual(importNames[0], importNames[1], 'import identifiers must be unique'); }); + + it('resolves images for entries the loader stores once', async () => { + const entries = await syncCollection({ + name: 'store-once', + async load(context: any) { + for (const [id, image] of Object.entries(SRCS)) { + const data = await context.parseData({ + id, + data: makeEntryData(id, image), + filePath: FILE_PATH, + }); + context.store.set({ id, data, filePath: FILE_PATH }); + } + }, + }); + + assertImagesResolve(entries); + }); + + it('resolves images for entries the loader reads back and re-stores', async () => { + const entries = await syncCollection({ + name: 'read-modify-write', + async load(context: any) { + for (const [id, image] of Object.entries(SRCS)) { + const data = await context.parseData({ + id, + data: makeEntryData(id, image), + filePath: FILE_PATH, + }); + context.store.set({ id, data, filePath: FILE_PATH }); + } + + // Second pass: a loader that enriches an entry after the initial store + // (attaching a sibling file's contents, a computed field, ...) reads the + // stored entry back and re-stores it. `set()` strips the image prefix in + // place on the first pass, so on this pass there is no prefix left to + // re-discover — the entry's recorded `imageImports` are the only record + // of where the images live, and must survive the round-trip. + for (const entry of context.store.values()) { + context.store.set({ ...entry, data: { ...entry.data, enriched: true } }); + } + }, + }); + + // Guard: the second pass really did re-store every entry. + for (const entry of entries) { + assert.equal(entry.data.enriched, true, `${entry.id}: entry was re-stored`); + } + + assertImagesResolve(entries); + }); }); From dd29ce81f5b562f5d0dccdd96e28dade350c5e4c Mon Sep 17 00:00:00 2001 From: "astro-factory[bot]" <316791938+astro-factory[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:57:20 -0400 Subject: [PATCH 5/7] Fix NFT trace base to include outDir when it is outside root in @astrojs/vercel (#17794) * fix(vercel): derive NFT trace base from common ancestor of root and entry (#17761) When `outDir` is outside `root`, `@vercel/nft` silently drops all traced dependency files because the trace base (derived from `root` alone) does not encompass the build output directory. Walk up from `root` to find the common ancestor of `root` and the entry path before passing it to `searchForWorkspaceRoot`, so the NFT base always includes both. * fix(vercel): make trace base lookup platform-aware --------- Co-authored-by: factory[bot] Co-authored-by: Matthew Phillips --- .changeset/little-bushes-deny.md | 5 ++++ packages/integrations/vercel/src/lib/nft.ts | 26 ++++++++++++++-- .../outdir-outside-root/app/astro.config.mjs | 8 +++++ .../outdir-outside-root/app/package.json | 9 ++++++ .../app/src/pages/index.astro | 10 +++++++ .../vercel/test/outdir-outside-root.test.ts | 30 +++++++++++++++++++ .../vercel/test/units/nft.test.ts | 30 +++++++++++++++++++ pnpm-lock.yaml | 9 ++++++ 8 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 .changeset/little-bushes-deny.md create mode 100644 packages/integrations/vercel/test/fixtures/outdir-outside-root/app/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/outdir-outside-root/app/package.json create mode 100644 packages/integrations/vercel/test/fixtures/outdir-outside-root/app/src/pages/index.astro create mode 100644 packages/integrations/vercel/test/outdir-outside-root.test.ts create mode 100644 packages/integrations/vercel/test/units/nft.test.ts diff --git a/.changeset/little-bushes-deny.md b/.changeset/little-bushes-deny.md new file mode 100644 index 000000000000..9093af3938fa --- /dev/null +++ b/.changeset/little-bushes-deny.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': patch +--- + +Fixes a bug where `@vercel/nft` file tracing silently dropped all dependency files when `outDir` was configured outside `root`, causing deployed functions to crash with `ERR_MODULE_NOT_FOUND` diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts index 7275158548c7..c79cd48e873c 100644 --- a/packages/integrations/vercel/src/lib/nft.ts +++ b/packages/integrations/vercel/src/lib/nft.ts @@ -1,10 +1,28 @@ -import { relative as relativePath } from 'node:path'; +import { dirname, isAbsolute, relative as relativePath, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { copyFilesToFolder } from '@astrojs/internal-helpers/fs'; import { appendForwardSlash } from '@astrojs/internal-helpers/path'; import type { AstroIntegrationLogger } from 'astro'; import { searchForWorkspaceRoot } from './searchRoot.js'; +export function findCommonAncestor(rootPath: string, entryPath: string): string { + let ancestor = rootPath; + let relativeEntryPath = relativePath(ancestor, entryPath); + + while ( + relativeEntryPath === '..' || + relativeEntryPath.startsWith(`..${sep}`) || + isAbsolute(relativeEntryPath) + ) { + const parent = dirname(ancestor); + if (parent === ancestor) break; + ancestor = parent; + relativeEntryPath = relativePath(ancestor, entryPath); + } + + return ancestor; +} + export async function copyDependenciesToFunction( { entry, @@ -29,8 +47,10 @@ export async function copyDependenciesToFunction( const entryPath = fileURLToPath(entry); logger.info(`Bundling function ${relativePath(fileURLToPath(outDir), entryPath)}`); - // Set the base to the workspace root - const base = pathToFileURL(appendForwardSlash(searchForWorkspaceRoot(fileURLToPath(root)))); + // NFT only traces files within `base`, so it must encompass the project root and build output. + // https://github.com/withastro/astro/issues/17761 + const searchStart = findCommonAncestor(fileURLToPath(root), entryPath); + const base = pathToFileURL(appendForwardSlash(searchForWorkspaceRoot(searchStart))); // The Vite bundle includes an import to `@vercel/nft` for some reason, // and that trips up `@vercel/nft` itself during the adapter build. Using a diff --git a/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/astro.config.mjs b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/astro.config.mjs new file mode 100644 index 000000000000..6267aa67fb86 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/astro.config.mjs @@ -0,0 +1,8 @@ +import vercel from '@astrojs/vercel'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: vercel(), + output: 'server', + outDir: '../dist', +}); diff --git a/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/package.json b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/package.json new file mode 100644 index 000000000000..2475b918a98d --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-vercel-outdir-outside-root", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/src/pages/index.astro new file mode 100644 index 000000000000..a28c8ca1b154 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/outdir-outside-root/app/src/pages/index.astro @@ -0,0 +1,10 @@ +--- +const time = new Date().toISOString(); +--- + + Test + +

Hello

+

Server time: {time}

+ + diff --git a/packages/integrations/vercel/test/outdir-outside-root.test.ts b/packages/integrations/vercel/test/outdir-outside-root.test.ts new file mode 100644 index 000000000000..18ef189a1852 --- /dev/null +++ b/packages/integrations/vercel/test/outdir-outside-root.test.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { type Fixture, loadFixture } from './test-utils.ts'; + +describe('outDir outside root', () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/outdir-outside-root/app/', + }); + await fixture.build({}); + }); + + it('build successful', { timeout: 30000 }, async () => { + // .vercel/output is created relative to root (app/), readFile resolves + // relative to outDir (dist/), so we go up to the fixture dir and into app. + assert.ok(await fixture.readFile('../app/.vercel/output/config.json')); + }); + + it('function includes traced chunks, not just entry', { timeout: 30000 }, async () => { + const files = await fixture.glob('../app/.vercel/output/functions/_render.func/**/*.mjs'); + // The function should contain the entry plus at least one chunk. + // If NFT tracing fails (the bug), only the entry is included. + assert.ok( + files.length > 1, + `Expected more than 1 .mjs file in the function, got ${files.length}`, + ); + }); +}); diff --git a/packages/integrations/vercel/test/units/nft.test.ts b/packages/integrations/vercel/test/units/nft.test.ts new file mode 100644 index 000000000000..984a5d99bb4b --- /dev/null +++ b/packages/integrations/vercel/test/units/nft.test.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { join, parse } from 'node:path'; +import { describe, it } from 'node:test'; +import { findCommonAncestor } from '../../dist/lib/nft.js'; + +describe('findCommonAncestor', () => { + const volumeRoot = parse(process.cwd()).root; + + it('returns the root path when it contains the entry', () => { + const root = join(volumeRoot, 'workspace', 'app'); + const entry = join(root, 'dist', 'entry.mjs'); + + assert.equal(findCommonAncestor(root, entry), root); + }); + + it('returns a shared parent when the entry is outside the root', () => { + const parent = join(volumeRoot, 'workspace'); + const root = join(parent, 'app'); + const entry = join(parent, 'dist', 'entry.mjs'); + + assert.equal(findCommonAncestor(root, entry), parent); + }); + + it('stops at the filesystem root', () => { + const root = join(volumeRoot, 'app'); + const entry = join(volumeRoot, 'dist', 'entry.mjs'); + + assert.equal(findCommonAncestor(root, entry), volumeRoot); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5efeacb60ed4..6b12533f26a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6437,6 +6437,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/outdir-outside-root/app: + dependencies: + '@astrojs/vercel': + specifier: workspace:* + version: link:../../../.. + astro: + specifier: workspace:* + version: link:../../../../../../astro + packages/integrations/vercel/test/fixtures/prerendered-error-pages: dependencies: '@astrojs/vercel': From d38ed60390abfc9087a0cbfce99b9a4893229de5 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:54:54 -0700 Subject: [PATCH 6/7] [ci] release (#17753) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/better-zebras-notice.md | 5 -- .changeset/cyan-bikes-visit.md | 5 -- .changeset/cyan-pigs-take.md | 5 -- .changeset/famous-trains-like.md | 5 -- .changeset/few-toys-rhyme.md | 5 -- .../fix-content-modules-stale-import.md | 7 --- .../fix-preferred-locale-quality-sort.md | 5 -- .changeset/gentle-regions-shave.md | 6 --- .changeset/gold-bugs-glow.md | 5 -- .changeset/gold-roses-fetch.md | 5 -- .changeset/grumpy-bats-return.md | 6 --- .changeset/jolly-lilies-shake.md | 5 -- .changeset/legal-pets-leave.md | 5 -- .changeset/little-bushes-deny.md | 5 -- .changeset/long-tips-care.md | 5 -- .changeset/modern-canyons-obey.md | 5 -- .changeset/moody-geckos-train.md | 5 -- .changeset/persisted-media-keep-identity.md | 5 -- .changeset/ready-monkeys-train.md | 5 -- ...satteri-smart-punctuation-default-jsdoc.md | 5 -- .changeset/short-tables-thank.md | 5 -- .changeset/strong-lines-sit.md | 5 -- .changeset/sunny-planes-rush.md | 5 -- .changeset/tidy-pears-shake.md | 5 -- .changeset/tricky-hornets-take.md | 5 -- .changeset/wacky-teeth-wear.md | 5 -- .changeset/wicked-zebras-sip.md | 5 -- .changeset/witty-cloths-do.md | 5 -- .changeset/young-suns-walk.md | 5 -- examples/advanced-routing/package.json | 2 +- examples/basics/package.json | 2 +- examples/blog/package.json | 4 +- examples/component/package.json | 2 +- examples/container-with-vitest/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-multiple/package.json | 2 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 2 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 2 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 2 +- examples/integration/package.json | 2 +- examples/minimal/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 2 +- examples/starlog/package.json | 2 +- examples/toolbar-app/package.json | 2 +- examples/with-markdoc/package.json | 2 +- examples/with-mdx/package.json | 4 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 4 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 53 ++++++++++++++++++ packages/astro/package.json | 2 +- packages/create-astro/CHANGELOG.md | 6 +++ packages/create-astro/package.json | 2 +- packages/integrations/cloudflare/CHANGELOG.md | 9 ++++ packages/integrations/cloudflare/package.json | 2 +- packages/integrations/mdx/CHANGELOG.md | 8 +++ packages/integrations/mdx/package.json | 2 +- packages/integrations/netlify/CHANGELOG.md | 9 ++++ packages/integrations/netlify/package.json | 2 +- packages/integrations/vercel/CHANGELOG.md | 6 +++ packages/integrations/vercel/package.json | 2 +- packages/language-tools/vscode/CHANGELOG.md | 6 +++ packages/language-tools/vscode/package.json | 2 +- packages/markdown/satteri/CHANGELOG.md | 8 +++ packages/markdown/satteri/package.json | 2 +- pnpm-lock.yaml | 54 +++++++++---------- 70 files changed, 167 insertions(+), 211 deletions(-) delete mode 100644 .changeset/better-zebras-notice.md delete mode 100644 .changeset/cyan-bikes-visit.md delete mode 100644 .changeset/cyan-pigs-take.md delete mode 100644 .changeset/famous-trains-like.md delete mode 100644 .changeset/few-toys-rhyme.md delete mode 100644 .changeset/fix-content-modules-stale-import.md delete mode 100644 .changeset/fix-preferred-locale-quality-sort.md delete mode 100644 .changeset/gentle-regions-shave.md delete mode 100644 .changeset/gold-bugs-glow.md delete mode 100644 .changeset/gold-roses-fetch.md delete mode 100644 .changeset/grumpy-bats-return.md delete mode 100644 .changeset/jolly-lilies-shake.md delete mode 100644 .changeset/legal-pets-leave.md delete mode 100644 .changeset/little-bushes-deny.md delete mode 100644 .changeset/long-tips-care.md delete mode 100644 .changeset/modern-canyons-obey.md delete mode 100644 .changeset/moody-geckos-train.md delete mode 100644 .changeset/persisted-media-keep-identity.md delete mode 100644 .changeset/ready-monkeys-train.md delete mode 100644 .changeset/satteri-smart-punctuation-default-jsdoc.md delete mode 100644 .changeset/short-tables-thank.md delete mode 100644 .changeset/strong-lines-sit.md delete mode 100644 .changeset/sunny-planes-rush.md delete mode 100644 .changeset/tidy-pears-shake.md delete mode 100644 .changeset/tricky-hornets-take.md delete mode 100644 .changeset/wacky-teeth-wear.md delete mode 100644 .changeset/wicked-zebras-sip.md delete mode 100644 .changeset/witty-cloths-do.md delete mode 100644 .changeset/young-suns-walk.md diff --git a/.changeset/better-zebras-notice.md b/.changeset/better-zebras-notice.md deleted file mode 100644 index ff1c2e5799aa..000000000000 --- a/.changeset/better-zebras-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes a bug where `experimental_getFontFileURL()` rejected valid font URLs when using the Cloudflare adapter diff --git a/.changeset/cyan-bikes-visit.md b/.changeset/cyan-bikes-visit.md deleted file mode 100644 index 7b227c2c558e..000000000000 --- a/.changeset/cyan-bikes-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Skips no-op pathname writes when normalizing SSR request URLs diff --git a/.changeset/cyan-pigs-take.md b/.changeset/cyan-pigs-take.md deleted file mode 100644 index ef6248cca3b6..000000000000 --- a/.changeset/cyan-pigs-take.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Updates deprecation messages target from Astro 7 to 8 diff --git a/.changeset/famous-trains-like.md b/.changeset/famous-trains-like.md deleted file mode 100644 index fcb9c402bd64..000000000000 --- a/.changeset/famous-trains-like.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes session ID validation to reject non-UUID cookie values before using them as storage keys diff --git a/.changeset/few-toys-rhyme.md b/.changeset/few-toys-rhyme.md deleted file mode 100644 index 2d73c1c45f2f..000000000000 --- a/.changeset/few-toys-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes `--mode`, `--site`, `--base`, `--out-dir`, `--verbose`, `--silent`, and `--open` flags being silently dropped when using `astro dev --background` or `astro preview --background` diff --git a/.changeset/fix-content-modules-stale-import.md b/.changeset/fix-content-modules-stale-import.md deleted file mode 100644 index 52b5d4116e2f..000000000000 --- a/.changeset/fix-content-modules-stale-import.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'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/.changeset/fix-preferred-locale-quality-sort.md b/.changeset/fix-preferred-locale-quality-sort.md deleted file mode 100644 index a675a66296ad..000000000000 --- a/.changeset/fix-preferred-locale-quality-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes `Astro.preferredLocale` and `Astro.preferredLocaleList` ignoring `Accept-Language` quality values when they are absent or `0`. An entry without an explicit `q=` now correctly counts as quality `1.0` (per RFC 7231) and an entry with `q=0` is treated as not acceptable, so the highest-quality locale is selected regardless of header order. diff --git a/.changeset/gentle-regions-shave.md b/.changeset/gentle-regions-shave.md deleted file mode 100644 index 4ea58b32cf9d..000000000000 --- a/.changeset/gentle-regions-shave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'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/.changeset/gold-bugs-glow.md b/.changeset/gold-bugs-glow.md deleted file mode 100644 index de81aeffcfa0..000000000000 --- a/.changeset/gold-bugs-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes a type error when passing an image from a content collection `image()` schema to a component or ``. The schema returned by `image()` was missing the `apng` format, so it no longer matched the type of an imported image. diff --git a/.changeset/gold-roses-fetch.md b/.changeset/gold-roses-fetch.md deleted file mode 100644 index 0f0534d834d5..000000000000 --- a/.changeset/gold-roses-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'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/.changeset/grumpy-bats-return.md b/.changeset/grumpy-bats-return.md deleted file mode 100644 index 05df2d2b98e6..000000000000 --- a/.changeset/grumpy-bats-return.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@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/.changeset/jolly-lilies-shake.md b/.changeset/jolly-lilies-shake.md deleted file mode 100644 index a8e485b85726..000000000000 --- a/.changeset/jolly-lilies-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes a regression in the content collections that could cause images to not be resolved diff --git a/.changeset/legal-pets-leave.md b/.changeset/legal-pets-leave.md deleted file mode 100644 index 9d1f67694752..000000000000 --- a/.changeset/legal-pets-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro-vscode': patch ---- - -Fixes missing Vue template auto-import completions when the Astro extension loads first diff --git a/.changeset/little-bushes-deny.md b/.changeset/little-bushes-deny.md deleted file mode 100644 index 9093af3938fa..000000000000 --- a/.changeset/little-bushes-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/vercel': patch ---- - -Fixes a bug where `@vercel/nft` file tracing silently dropped all dependency files when `outDir` was configured outside `root`, causing deployed functions to crash with `ERR_MODULE_NOT_FOUND` diff --git a/.changeset/long-tips-care.md b/.changeset/long-tips-care.md deleted file mode 100644 index e3e06abaa1e4..000000000000 --- a/.changeset/long-tips-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes `memoryCache()` storing responses that set cookies through `Astro.cookies` or `Astro.session` diff --git a/.changeset/modern-canyons-obey.md b/.changeset/modern-canyons-obey.md deleted file mode 100644 index bf11760bb3ac..000000000000 --- a/.changeset/modern-canyons-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes `server:defer` crashing the dev server with "undefined is not a function" when a deferred component imports from `astro:i18n` diff --git a/.changeset/moody-geckos-train.md b/.changeset/moody-geckos-train.md deleted file mode 100644 index 033bebc96236..000000000000 --- a/.changeset/moody-geckos-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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/.changeset/persisted-media-keep-identity.md b/.changeset/persisted-media-keep-identity.md deleted file mode 100644 index 591521d921df..000000000000 --- a/.changeset/persisted-media-keep-identity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes a regression where `transition:persist` stopped working for `