From d3c7de9253e9cb31fa5c4bf9f4bdf59dd1ada7b0 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Tue, 17 Mar 2026 16:34:36 +0100 Subject: [PATCH 1/3] feat: drop node 20 support (#15864) --- .changeset/proud-apples-eat.md | 14 ++++++++++++++ packages/astro-prism/package.json | 2 +- packages/astro/bin/astro.mjs | 7 ++----- packages/astro/package.json | 2 +- packages/create-astro/create-astro.mjs | 4 +--- packages/create-astro/package.json | 2 +- packages/integrations/markdoc/package.json | 2 +- packages/integrations/mdx/package.json | 2 +- packages/integrations/preact/package.json | 2 +- packages/integrations/react/package.json | 2 +- packages/integrations/solid/package.json | 2 +- packages/integrations/svelte/package.json | 2 +- packages/upgrade/package.json | 2 +- packages/upgrade/upgrade.mjs | 4 +--- 14 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 .changeset/proud-apples-eat.md diff --git a/.changeset/proud-apples-eat.md b/.changeset/proud-apples-eat.md new file mode 100644 index 000000000000..63d752f79080 --- /dev/null +++ b/.changeset/proud-apples-eat.md @@ -0,0 +1,14 @@ +--- +'@astrojs/markdoc': patch +'@astrojs/preact': patch +'@astrojs/svelte': patch +'@astrojs/react': patch +'@astrojs/solid-js': patch +'@astrojs/mdx': patch +'create-astro': patch +'@astrojs/prism': patch +'@astrojs/upgrade': patch +'astro': patch +--- + +Removes temporary support for Node >=20.19.1 because Stackblitz now uses Node 22 by default diff --git a/packages/astro-prism/package.json b/packages/astro-prism/package.json index 2bf99b6558b6..1ac569d537ac 100644 --- a/packages/astro-prism/package.json +++ b/packages/astro-prism/package.json @@ -39,6 +39,6 @@ "astro-scripts": "workspace:*" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" } } diff --git a/packages/astro/bin/astro.mjs b/packages/astro/bin/astro.mjs index 4e7f59abb9b4..cd36f28080aa 100755 --- a/packages/astro/bin/astro.mjs +++ b/packages/astro/bin/astro.mjs @@ -8,12 +8,9 @@ const CI_INSTRUCTIONS = { VERCEL: 'https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-version', }; -// TODO: remove once Stackblitz supports Node 22 -const IS_STACKBLITZ = !!process.versions.webcontainer; - // Hardcode supported Node.js version so we don't have to read differently in CJS & ESM. -const engines = IS_STACKBLITZ ? '>=20.19.1' : '>=22.12.0'; -const skipSemverCheckIfAbove = IS_STACKBLITZ ? 21 : 23; +const engines = '>=22.12.0'; +const skipSemverCheckIfAbove = 23; /** `astro *` */ async function main() { diff --git a/packages/astro/package.json b/packages/astro/package.json index 501ecb80f133..839c678eac7a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -210,7 +210,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": "^20.19.1 || >=22.12.0", + "node": ">=22.12.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0" }, diff --git a/packages/create-astro/create-astro.mjs b/packages/create-astro/create-astro.mjs index f72274e157fa..a14435a8c8c4 100755 --- a/packages/create-astro/create-astro.mjs +++ b/packages/create-astro/create-astro.mjs @@ -4,9 +4,7 @@ const currentVersion = process.versions.node; const requiredMajorVersion = Number.parseInt(currentVersion.split('.')[0], 10); -// TODO: remove once Stackblitz supports Node 22 -const IS_STACKBLITZ = !!process.versions.webcontainer; -const minimumMajorVersion = IS_STACKBLITZ ? 20 : 22; +const minimumMajorVersion = 22; if (requiredMajorVersion < minimumMajorVersion) { console.error(`Node.js v${currentVersion} is out-of-date and unsupported!`); diff --git a/packages/create-astro/package.json b/packages/create-astro/package.json index 358f2be71f32..7c04260ad88c 100644 --- a/packages/create-astro/package.json +++ b/packages/create-astro/package.json @@ -40,7 +40,7 @@ "astro-scripts": "workspace:*" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 33772b42859b..edbf04431580 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -82,7 +82,7 @@ "vite": "^7.3.1" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 5a689df34acc..d3dfd118dc55 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -73,7 +73,7 @@ "vite": "^7.3.1" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json index 2c4da81b14c2..147899f80618 100644 --- a/packages/integrations/preact/package.json +++ b/packages/integrations/preact/package.json @@ -51,7 +51,7 @@ "preact": "^10.6.5" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index f73ad6c10fa3..25855556c7e0 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -61,7 +61,7 @@ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/solid/package.json b/packages/integrations/solid/package.json index 8506b1df299e..f5b74481e087 100644 --- a/packages/integrations/solid/package.json +++ b/packages/integrations/solid/package.json @@ -52,7 +52,7 @@ } }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 5680d5d716c8..733f67101411 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -54,7 +54,7 @@ "typescript": "^5.3.3" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" }, "publishConfig": { "provenance": true diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index 93fd479336d9..7f20a1a48073 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -40,6 +40,6 @@ "astro-scripts": "workspace:*" }, "engines": { - "node": "^20.19.1 || >=22.12.0" + "node": ">=22.12.0" } } diff --git a/packages/upgrade/upgrade.mjs b/packages/upgrade/upgrade.mjs index f72274e157fa..a14435a8c8c4 100755 --- a/packages/upgrade/upgrade.mjs +++ b/packages/upgrade/upgrade.mjs @@ -4,9 +4,7 @@ const currentVersion = process.versions.node; const requiredMajorVersion = Number.parseInt(currentVersion.split('.')[0], 10); -// TODO: remove once Stackblitz supports Node 22 -const IS_STACKBLITZ = !!process.versions.webcontainer; -const minimumMajorVersion = IS_STACKBLITZ ? 20 : 22; +const minimumMajorVersion = 22; if (requiredMajorVersion < minimumMajorVersion) { console.error(`Node.js v${currentVersion} is out-of-date and unsupported!`); From 5201ed464258e799a1e898f4c4adc84d7445bad3 Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:25:47 +0100 Subject: [PATCH 2/3] fix: content collections schema types is any for all cases (#15916) * fix(content): fix inferred schema types for custom loaders with CollectionConfig union * test: create fixture and tests for type inference * docs: changeset * fix: expected too much --------- Co-authored-by: astrobot-houston --- .changeset/frank-actors-brush.md | 5 ++ packages/astro/templates/content/types.d.ts | 5 +- ...content-collections-type-inference.test.js | 83 +++++++++++++++++++ .../astro.config.mjs | 3 + .../package.json | 9 ++ .../src/content.config.ts | 37 +++++++++ .../src/pages/index.astro | 1 + .../src/type-checks.ts | 56 +++++++++++++ .../tsconfig.json | 5 ++ pnpm-lock.yaml | 6 ++ 10 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 .changeset/frank-actors-brush.md create mode 100644 packages/astro/test/content-collections-type-inference.test.js create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/astro.config.mjs create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/package.json create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/src/content.config.ts create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/src/type-checks.ts create mode 100644 packages/astro/test/fixtures/content-collections-type-inference/tsconfig.json diff --git a/.changeset/frank-actors-brush.md b/.changeset/frank-actors-brush.md new file mode 100644 index 000000000000..d935dc111486 --- /dev/null +++ b/.changeset/frank-actors-brush.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes `InferLoaderSchema` type inference for content collections defined with a loader that includes a `schema` diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 0589f06b67a8..54e4a92f69b6 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -111,11 +111,12 @@ declare module 'astro:content' { type InferEntrySchema = import('astro/zod').infer< ReturnTypeOrOriginal['schema']> >; + type ExtractLoaderConfig = T extends { loader: infer L } ? L : never; type InferLoaderSchema< C extends keyof DataEntryMap, - L = Required['loader'], + L = ExtractLoaderConfig, > = L extends { schema: import('astro/zod').ZodSchema } - ? import('astro/zod').infer['loader']['schema']> + ? import('astro/zod').infer : any; type DataEntryMap = { diff --git a/packages/astro/test/content-collections-type-inference.test.js b/packages/astro/test/content-collections-type-inference.test.js new file mode 100644 index 000000000000..a8d0916f1342 --- /dev/null +++ b/packages/astro/test/content-collections-type-inference.test.js @@ -0,0 +1,83 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { execSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import { before, describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { loadFixture } from './test-utils.js'; + +describe('Content collection type inference', () => { + /** @type {Awaited>} */ + let fixture; + /** @type {string} */ + let fixtureRoot; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/content-collections-type-inference/', + }); + fixtureRoot = fileURLToPath(fixture.config.root); + + // Clean previous .astro directory + fs.rmSync(new URL('./.astro/', fixture.config.root), { force: true, recursive: true }); + + // Run astro sync to generate .astro/content.d.ts from the real template + await fixture.sync({ root: fixtureRoot }); + }); + + it('generates content.d.ts with ExtractLoaderConfig type utility', async () => { + const contentDts = fs.readFileSync( + new URL('./.astro/content.d.ts', fixture.config.root), + 'utf-8', + ); + assert.ok( + contentDts.includes('ExtractLoaderConfig'), + 'Generated content.d.ts should contain ExtractLoaderConfig type utility', + ); + assert.ok( + contentDts.includes('InferLoaderSchema'), + 'Generated content.d.ts should contain InferLoaderSchema type utility', + ); + }); + + it('generates correct DataEntryMap with all three collection types', async () => { + const contentDts = fs.readFileSync( + new URL('./.astro/content.d.ts', fixture.config.root), + 'utf-8', + ); + assert.ok(contentDts.includes('"blog"'), 'DataEntryMap should include "blog" collection'); + assert.ok(contentDts.includes('"legacy"'), 'DataEntryMap should include "legacy" collection'); + assert.ok( + contentDts.includes('"schemaless"'), + 'DataEntryMap should include "schemaless" collection', + ); + }); + + it('type-checks correctly against the generated types', () => { + // Run tsc on the fixture to verify the type assertions in src/type-checks.ts + // pass against the real generated content.d.ts. + // + // The type-checks.ts file uses @ts-expect-error to assert that: + // - Case 1 (loader with schema): data is NOT any, is { test: string } + // - Case 2 (legacy schema): data is NOT any, is { title: string; legacyField: boolean } + // - Case 3 (schemaless loader): data IS any (the correct fallback) + // + // If any @ts-expect-error is unused (type collapsed to `any` when it shouldn't), + // tsc will report an error and this test fails. + try { + execSync('npx tsc --noEmit', { + cwd: fixtureRoot, + stdio: 'pipe', + encoding: 'utf-8', + }); + } catch (err) { + const stdout = /** @type {{ stdout?: string }} */ (err).stdout ?? ''; + const stderr = /** @type {{ stderr?: string }} */ (err).stderr ?? ''; + assert.fail( + `TypeScript type-checking failed on fixture.\n` + + `This means the content collection type inference is broken.\n\n` + + `stdout:\n${stdout}\n\nstderr:\n${stderr}`, + ); + } + }); +}); diff --git a/packages/astro/test/fixtures/content-collections-type-inference/astro.config.mjs b/packages/astro/test/fixtures/content-collections-type-inference/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/content-collections-type-inference/package.json b/packages/astro/test/fixtures/content-collections-type-inference/package.json new file mode 100644 index 000000000000..8eb07dd84cce --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/content-collections-type-inference", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/content-collections-type-inference/src/content.config.ts b/packages/astro/test/fixtures/content-collections-type-inference/src/content.config.ts new file mode 100644 index 000000000000..98a6fb124730 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/src/content.config.ts @@ -0,0 +1,37 @@ +import { defineCollection } from 'astro:content'; +import { z } from 'astro/zod'; +import type { Loader } from 'astro/loaders'; + +function myLoader() { + return { + name: 'my-loader', + load: async () => {}, + schema: z.object({ + test: z.string(), + }), + } satisfies Loader; +} + +// Case 1: Loader with schema defined on the loader object +const blog = defineCollection({ + loader: myLoader(), +}); + +// Case 2: Legacy collection with schema on the collection (no loader) +const legacy = defineCollection({ + schema: z.object({ + title: z.string(), + legacyField: z.boolean(), + }), +}); + +// Case 3: Loader with no schema at all +const schemaless = defineCollection({ + loader: async () => [{ id: '1' }], +}); + +export const collections = { + blog, + legacy, + schemaless, +}; diff --git a/packages/astro/test/fixtures/content-collections-type-inference/src/pages/index.astro b/packages/astro/test/fixtures/content-collections-type-inference/src/pages/index.astro new file mode 100644 index 000000000000..2c1a7371dadb --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/src/pages/index.astro @@ -0,0 +1 @@ +

Type Inference Test Fixture

diff --git a/packages/astro/test/fixtures/content-collections-type-inference/src/type-checks.ts b/packages/astro/test/fixtures/content-collections-type-inference/src/type-checks.ts new file mode 100644 index 000000000000..33a0fd944983 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/src/type-checks.ts @@ -0,0 +1,56 @@ +/** + * Type assertions for content collection type inference. + * + * This file is NOT executed at runtime. It is type-checked by tsc after + * `astro sync` generates the .astro/content.d.ts types for this fixture. + * + * Each @ts-expect-error comment asserts that the line below it IS a type error, + * meaning the type is NOT `any` or `never` where it shouldn't be. + * + * If the patch in templates/content/types.d.ts regresses, tsc will fail here + * because a @ts-expect-error will become unused (the type collapsed to `any`). + */ +import type { CollectionEntry, InferLoaderSchema } from 'astro:content'; + +// ============================================================================ +// Case 1: Loader with schema on the loader object ("blog" collection) +// The patched ExtractLoaderConfig should correctly extract the loader's schema. +// ============================================================================ + +type BlogEntry = CollectionEntry<'blog'>; +type BlogData = BlogEntry['data']; + +// BlogData should be { test: string }, NOT any. +// If it were `any`, assigning a number to a string field would not error. +// @ts-expect-error - `test` is string, not number +const _blogDataCheck: BlogData = { test: 123 }; + +type InferredBlogSchema = InferLoaderSchema<'blog'>; +// @ts-expect-error - `test` is string, not number +const _inferredBlogCheck: InferredBlogSchema = { test: 123 }; + +// ============================================================================ +// Case 2: Legacy collection with schema on the collection ("legacy") +// Should NOT be broken by the ExtractLoaderConfig patch. +// ============================================================================ + +type LegacyData = CollectionEntry<'legacy'>['data']; + +// LegacyData should be { title: string; legacyField: boolean }. +// @ts-expect-error - `title` is string, not number +const _legacyTitleCheck: LegacyData = { title: 123, legacyField: true }; +// @ts-expect-error - `legacyField` is boolean, not string +const _legacyFieldCheck: LegacyData = { title: 'ok', legacyField: 'not a boolean' }; + +// ============================================================================ +// Case 3: Loader with no schema ("schemaless" collection) +// Should fall back to `any` — this is the correct behavior. +// ============================================================================ + +type SchemalessData = InferLoaderSchema<'schemaless'>; + +// If the type is correctly `any`, then any assignment is valid and +// a ts-expect-error on a valid assignment would be an error itself. +// So we verify `any` by checking that arbitrary property access works: +const _schemalessValue: SchemalessData = { anything: 'goes', count: 42 }; +const _schemalessAccess: string = _schemalessValue.nonExistentProp; diff --git a/packages/astro/test/fixtures/content-collections-type-inference/tsconfig.json b/packages/astro/test/fixtures/content-collections-type-inference/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/packages/astro/test/fixtures/content-collections-type-inference/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3581faeff88f..e913d642d5c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2792,6 +2792,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/content-collections-type-inference: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/content-collections-with-config-mjs: dependencies: astro: From 7eddf22cd4d4719d966ed7168e9890fac8fc29f5 Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 17 Mar 2026 14:26:25 -0500 Subject: [PATCH 3/3] fix(hmr): eagerly recompile on style-only change to prevent stale slots render (#15953) Co-authored-by: Desel72 --- .changeset/fix-hmr-slots-render.md | 5 ++ packages/astro/src/vite-plugin-astro/hmr.ts | 18 +++-- packages/astro/src/vite-plugin-astro/index.ts | 2 +- .../hmr-slots-render/astro.config.mjs | 3 + .../fixtures/hmr-slots-render/package.json | 8 +++ .../src/components/Each.astro | 22 ++++++ .../src/components/Item.astro | 10 +++ .../hmr-slots-render/src/pages/index.astro | 17 +++++ packages/astro/test/hmr-slots-render.test.js | 71 +++++++++++++++++++ pnpm-lock.yaml | 6 ++ 10 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-hmr-slots-render.md create mode 100644 packages/astro/test/fixtures/hmr-slots-render/astro.config.mjs create mode 100644 packages/astro/test/fixtures/hmr-slots-render/package.json create mode 100644 packages/astro/test/fixtures/hmr-slots-render/src/components/Each.astro create mode 100644 packages/astro/test/fixtures/hmr-slots-render/src/components/Item.astro create mode 100644 packages/astro/test/fixtures/hmr-slots-render/src/pages/index.astro create mode 100644 packages/astro/test/hmr-slots-render.test.js diff --git a/.changeset/fix-hmr-slots-render.md b/.changeset/fix-hmr-slots-render.md new file mode 100644 index 000000000000..7f9eb55544bf --- /dev/null +++ b/.changeset/fix-hmr-slots-render.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +fix(hmr): eagerly recompile on style-only change to prevent stale slots render diff --git a/packages/astro/src/vite-plugin-astro/hmr.ts b/packages/astro/src/vite-plugin-astro/hmr.ts index f7bdd7389043..3af57edc920e 100644 --- a/packages/astro/src/vite-plugin-astro/hmr.ts +++ b/packages/astro/src/vite-plugin-astro/hmr.ts @@ -1,17 +1,19 @@ import type { HmrContext } from 'vite'; import type { Logger } from '../core/logger/core.js'; +import type { CompileAstroResult } from './compile.js'; import { parseAstroRequest } from './query.js'; import type { CompileMetadata } from './types.js'; import { frontmatterRE } from './utils.js'; interface HandleHotUpdateOptions { logger: Logger; + compile: (code: string, filename: string) => Promise; astroFileToCompileMetadata: Map; } export async function handleHotUpdate( ctx: HmrContext, - { logger, astroFileToCompileMetadata }: HandleHotUpdateOptions, + { logger, compile, astroFileToCompileMetadata }: HandleHotUpdateOptions, ) { // HANDLING 1: Invalidate compile metadata if CSS dependency updated // @@ -35,9 +37,17 @@ export async function handleHotUpdate( if (isStyleOnlyChanged(oldCode, newCode)) { logger.debug('watch', 'style-only change'); - // Invalidate its `astroFileToCompileMetadata` so that the next transform of Astro style virtual module - // will re-generate it - astroFileToCompileMetadata.delete(ctx.file); + // Eagerly re-compile to update the metadata with the new CSS. This ensures + // the compile metadata stays consistent so that subsequent SSR requests + // (e.g. page refresh) can load the style virtual modules without needing + // to re-compile from disk, avoiding potential stale state. + try { + await compile(newCode, ctx.file); + } catch { + // If re-compilation fails, fall back to deleting the metadata so the + // load hook will re-compile lazily on the next request. + astroFileToCompileMetadata.delete(ctx.file); + } return ctx.modules.filter((mod) => { if (!mod.id) { return false; diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 11f8819c1472..73692dd576a3 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -310,7 +310,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl }, }, async handleHotUpdate(ctx) { - return handleHotUpdate(ctx, { logger, astroFileToCompileMetadata }); + return handleHotUpdate(ctx, { logger, compile, astroFileToCompileMetadata }); }, }, { diff --git a/packages/astro/test/fixtures/hmr-slots-render/astro.config.mjs b/packages/astro/test/fixtures/hmr-slots-render/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/hmr-slots-render/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/hmr-slots-render/package.json b/packages/astro/test/fixtures/hmr-slots-render/package.json new file mode 100644 index 000000000000..cb7dca914f07 --- /dev/null +++ b/packages/astro/test/fixtures/hmr-slots-render/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/hmr-slots-render", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/hmr-slots-render/src/components/Each.astro b/packages/astro/test/fixtures/hmr-slots-render/src/components/Each.astro new file mode 100644 index 000000000000..070cb3af0896 --- /dev/null +++ b/packages/astro/test/fixtures/hmr-slots-render/src/components/Each.astro @@ -0,0 +1,22 @@ +--- +type Props = { + items: unknown[]; +}; + +const { items, ...attrs } = Astro.props; +--- + +
    + { + items.map((item, index, list) => ( +
  • + )) + } +
+ + diff --git a/packages/astro/test/fixtures/hmr-slots-render/src/components/Item.astro b/packages/astro/test/fixtures/hmr-slots-render/src/components/Item.astro new file mode 100644 index 000000000000..24591e67b995 --- /dev/null +++ b/packages/astro/test/fixtures/hmr-slots-render/src/components/Item.astro @@ -0,0 +1,10 @@ +--- +const { name } = Astro.props; +--- +
{name}
+ + diff --git a/packages/astro/test/fixtures/hmr-slots-render/src/pages/index.astro b/packages/astro/test/fixtures/hmr-slots-render/src/pages/index.astro new file mode 100644 index 000000000000..5522489b18e4 --- /dev/null +++ b/packages/astro/test/fixtures/hmr-slots-render/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import Each from '../components/Each.astro'; +import Item from '../components/Item.astro'; +const items = ['one', 'two', 'three']; +--- + + + HMR Slots Render Test + + +
+ + {(item) => } + +
+ + diff --git a/packages/astro/test/hmr-slots-render.test.js b/packages/astro/test/hmr-slots-render.test.js new file mode 100644 index 000000000000..1fa462e28b0a --- /dev/null +++ b/packages/astro/test/hmr-slots-render.test.js @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { isWindows, loadFixture } from './test-utils.js'; + +describe('HMR: slots.render with callback args after style change', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + /** @type {import('./test-utils').DevServer} */ + let devServer; + + before(async () => { + fixture = await loadFixture({ root: './fixtures/hmr-slots-render/' }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + fixture.resetAllFiles(); + }); + + function verifyRendering($, label) { + const items = $('#result .item-wrapper'); + assert.ok( + items.length >= 3, + `[${label}] Expected 3 item-wrappers, got ${items.length}. HTML:\n${$('#result').html()?.substring(0, 500)}`, + ); + assert.equal($(items[0]).text(), 'one'); + assert.equal($(items[1]).text(), 'two'); + assert.equal($(items[2]).text(), 'three'); + + // Verify no escaped HTML source code visible (the bug symptom from #15925) + const resultText = $('#result').text(); + assert.ok( + !resultText.includes('data-astro-cid'), + `[${label}] Found escaped data-astro-cid in output: ${resultText.substring(0, 300)}`, + ); + } + + it( + 'should render after style change in the slot-render component', + { skip: isWindows }, + async () => { + // Initial fetch - verify correct rendering + let res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'initial'); + + // Style-only edit (triggers HMR style-only path) + await fixture.editFile('/src/components/Each.astro', (c) => + c.replace('font-size: 0.5rem;', 'font-size: 1rem;'), + ); + await new Promise((r) => setTimeout(r, 500)); + + // Page refresh after HMR - must still render correctly + res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'after style change'); + + // Second style edit + refresh + await fixture.editFile('/src/components/Each.astro', (c) => + c.replace('font-size: 1rem;', 'font-size: 2rem;'), + ); + await new Promise((r) => setTimeout(r, 500)); + + res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'after 2nd style change'); + }, + ); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e913d642d5c4..64f01dfee866 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3400,6 +3400,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/hmr-slots-render: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/hoisted-imports: dependencies: astro: