diff --git a/.changeset/clean-planets-flow.md b/.changeset/clean-planets-flow.md new file mode 100644 index 000000000000..395589d4f9a8 --- /dev/null +++ b/.changeset/clean-planets-flow.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Improves rendering by preserving `hidden="until-found"` value in attribues diff --git a/.changeset/happy-frogs-glow.md b/.changeset/happy-frogs-glow.md new file mode 100644 index 000000000000..763ca0d46395 --- /dev/null +++ b/.changeset/happy-frogs-glow.md @@ -0,0 +1,7 @@ +--- +'@astrojs/markdown-remark': major +'@astrojs/mdx': major +'astro': major +--- + +Changes how styles applied to code blocks are emitted to support CSP - ([v6 upgrade guidance](https://v6.docs.astro.build/en/guides/upgrade-to/v6/#changed-how-shiki-code-block-styles-are-emitted)) diff --git a/.changeset/little-goats-poke.md b/.changeset/little-goats-poke.md new file mode 100644 index 000000000000..a13d2d125a19 --- /dev/null +++ b/.changeset/little-goats-poke.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Improves the JSDoc annotations for the `AstroAdapter` type diff --git a/.changeset/pre.json b/.changeset/pre.json index a3737e9890a2..5ff540bde922 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -64,6 +64,7 @@ "busy-humans-smoke", "busy-olives-chew", "calm-birds-fly", + "clean-planets-flow", "clear-areas-cry", "clever-clubs-listen", "cloudflare-dev-styles", @@ -120,6 +121,8 @@ "great-nails-brake", "green-garlics-heal", "grumpy-tables-serve", + "happy-frogs-glow", + "heavy-beers-unite", "heavy-cats-own", "heavy-parts-throw", "helpful-runtime-errors", @@ -139,6 +142,7 @@ "late-spiders-change", "legacy-collections-backwards-compat-docs", "light-parrots-find", + "little-goats-poke", "long-trams-see", "lovely-mice-sniff", "lowercase-style-tags", @@ -166,6 +170,7 @@ "puny-poems-create", "quick-dingos-itch", "quiet-cars-burn", + "quiet-owls-jump", "ready-eagles-wink", "ready-tigers-try", "remove-cloudflare-modules", @@ -178,6 +183,7 @@ "sad-teams-end", "short-pears-hammer", "shy-cats-grin", + "silly-eels-remain", "six-women-visit", "sixty-deer-fold", "slick-plums-remain", @@ -205,6 +211,7 @@ "tall-needles-cross", "tall-worms-live", "tangy-days-wink", + "tangy-tables-jog", "tangy-years-grin", "tender-bats-tan", "tender-moose-help", @@ -219,6 +226,7 @@ "vite-environments-breaking", "vite-plugin-react-v5", "vite-plugin-vue-v6", + "warm-donuts-learn", "warm-dots-glow", "wet-lines-wear", "wet-suits-help", diff --git a/.changeset/warm-donuts-learn.md b/.changeset/warm-donuts-learn.md new file mode 100644 index 000000000000..c676a50fc53e --- /dev/null +++ b/.changeset/warm-donuts-learn.md @@ -0,0 +1,19 @@ +--- +'astro': minor +--- + +Adds `streaming` option to the `createApp()` function in the Adapter API, mirroring the same functionality available when creating a new `App` instance + +An adapter's `createApp()` function now accepts `streaming` (defaults to `true`) as an option. HTML streaming breaks a document into chunks to send over the network and render on the page in order. This normally results in visitors seeing your HTML as fast as possible but factors such as network conditions and waiting for data fetches can block page rendering. + +HTML streaming helps with performance and generally provides a better visitor experience. In most cases, disabling streaming is not recommended. + +However, when you need to disable HTML streaming (e.g. your host only supports non-streamed HTML caching at the CDN level), you can opt out of the default behavior by passing `streaming: false` to `createApp()`: + +```ts +import { createApp } from 'astro/app/entrypoint' + +const app = createApp({ streaming: false }) +``` + +See more about [the `createApp()` function](https://v6.docs.astro.build/en/reference/adapter-reference/#createapp) in the Adapter API reference. diff --git a/biome.jsonc b/biome.jsonc index 02b6b56a973b..1c11e15b2b7b 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.15/schema.json", "files": { "includes": [ "**", diff --git a/examples/basics/package.json b/examples/basics/package.json index e28fdb8e2817..e1c85608030a 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index a9330ce13076..f0f22a94abe1 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -13,10 +13,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^5.0.0-beta.7", + "@astrojs/mdx": "^5.0.0-beta.8", "@astrojs/rss": "^4.0.15-beta.3", "@astrojs/sitemap": "^3.6.1-beta.3", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "sharp": "^0.34.3" } } diff --git a/examples/blog/src/styles/global.css b/examples/blog/src/styles/global.css index d6aa4508b42e..bd6f8ced4fd9 100644 --- a/examples/blog/src/styles/global.css +++ b/examples/blog/src/styles/global.css @@ -13,8 +13,8 @@ --gray-dark: 34, 41, 57; --gray-gradient: rgba(var(--gray-light), 50%), #fff; --box-shadow: - 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), 0 16px 32px - rgba(var(--gray), 33%); + 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), + 0 16px 32px rgba(var(--gray), 33%); } @font-face { font-family: "Atkinson"; diff --git a/examples/component/package.json b/examples/component/package.json index 91a72ae1bed0..c3445b299201 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" }, "peerDependencies": { "astro": "^5.0.0 || ^6.0.0" diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index 052be2638d89..488197feb4f2 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@astrojs/react": "^5.0.0-beta.3", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "react": "^18.3.1", "react-dom": "^18.3.1", "vitest": "^3.2.4" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 225227a50511..ed37576376a2 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -16,6 +16,6 @@ "@astrojs/alpinejs": "^0.5.0-beta.1", "@types/alpinejs": "^3.13.11", "alpinejs": "^3.15.8", - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 81ca8e4c081e..c2e1cf107ad9 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -20,7 +20,7 @@ "@astrojs/vue": "^6.0.0-beta.1", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "preact": "^10.28.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index a963ff23f0a0..bbaa0f24352a 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -15,7 +15,7 @@ "dependencies": { "@astrojs/preact": "^5.0.0-beta.3", "@preact/signals": "^2.7.1", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "preact": "^10.28.3" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index b11ecccc69e2..4ee156efbe81 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -16,7 +16,7 @@ "@astrojs/react": "^5.0.0-beta.3", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "react": "^18.3.1", "react-dom": "^18.3.1" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 0c4d6ad835ed..8252aefdab04 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/solid-js": "^6.0.0-beta.2", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "solid-js": "^1.9.11" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index e87cfe53cad8..89687fce66b0 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/svelte": "^8.0.0-beta.2", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "svelte": "^5.50.3" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index bc8e48cee290..840ea1febe0e 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/vue": "^6.0.0-beta.1", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "vue": "^3.5.28" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 60466223c2a3..411efea86cfa 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -14,6 +14,6 @@ }, "dependencies": { "@astrojs/node": "^10.0.0-beta.4", - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index c25d9e396058..5f43092de0f4 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/minimal/package.json b/examples/minimal/package.json index aa7cb6b9b121..72feb07977c8 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 946940e7de07..1b93a4960aca 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 5ab6df5dba5a..ad2fe0313d07 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -16,7 +16,7 @@ "dependencies": { "@astrojs/node": "^10.0.0-beta.4", "@astrojs/svelte": "^8.0.0-beta.2", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "svelte": "^5.50.3" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 834107c6010f..1454c39cfd7a 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -9,7 +9,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "sass": "^1.97.3", "sharp": "^0.34.3" }, diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index 63a2b2164c62..69249f9a80ea 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/node": "^18.17.8", - "astro": "^6.0.0-beta.12" + "astro": "^6.0.0-beta.13" }, "engines": { "node": ">=22.12.0" diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 770cef6479d3..09dc8dee9b9e 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -13,7 +13,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/markdoc": "^1.0.0-beta.10", - "astro": "^6.0.0-beta.12" + "@astrojs/markdoc": "^1.0.0-beta.11", + "astro": "^6.0.0-beta.13" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index d1c1e1860736..5857a77ea3df 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,9 +13,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^5.0.0-beta.7", + "@astrojs/mdx": "^5.0.0-beta.8", "@astrojs/preact": "^5.0.0-beta.3", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "preact": "^10.28.3" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index eaefb196c692..c9d1ce2bba86 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -15,7 +15,7 @@ "dependencies": { "@astrojs/preact": "^5.0.0-beta.3", "@nanostores/preact": "^1.0.0", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "nanostores": "^1.1.0", "preact": "^10.28.3" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 8efd75c50959..336ab3083633 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -13,10 +13,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^5.0.0-beta.7", + "@astrojs/mdx": "^5.0.0-beta.8", "@tailwindcss/vite": "^4.1.18", "@types/canvas-confetti": "^1.9.0", - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "canvas-confetti": "^1.9.4", "tailwindcss": "^4.1.18" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 43cd504d7eac..f015e3466eeb 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^6.0.0-beta.12", + "astro": "^6.0.0-beta.13", "vitest": "^3.2.4" } } diff --git a/package.json b/package.json index 3317533d273c..1badfc9c2977 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "@astrojs/check": "^0.9.5", - "@biomejs/biome": "2.3.6", + "@biomejs/biome": "2.3.15", "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", "@flue/cli": "^0.0.32", diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index a64a0f79b4df..0e78607fb5d9 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,89 @@ # astro +## 6.0.0-beta.13 + +### Major Changes + +- [#15451](https://github.com/withastro/astro/pull/15451) [`84d6efd`](https://github.com/withastro/astro/commit/84d6efd9f1036fdf3c29e9b786b4a96453a607ed) Thanks [@ematipico](https://github.com/ematipico)! - Changes how styles applied to code blocks are emitted to support CSP - ([v6 upgrade guidance](https://v6.docs.astro.build/en/guides/upgrade-to/v6/#changed-how-shiki-code-block-styles-are-emitted)) + +### Minor Changes + +- [#15529](https://github.com/withastro/astro/pull/15529) [`a509941`](https://github.com/withastro/astro/commit/a509941a7a7a1e53f402757234bb88e5503e5119) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds a new build-in font provider `npm` to access fonts installed as NPM packages + + You can now add web fonts specified in your `package.json` through Astro's type-safe Fonts API. The `npm` font provider allows you to add fonts either from locally installed packages in `node_modules` or from a CDN. + + Set `fontProviders.npm()` as your fonts provider along with the required `name` and `cssVariable` values, and add `options` as needed: + + ```js + import { defineConfig, fontProviders } from 'astro/config'; + + export default defineConfig({ + experimental: { + fonts: [ + { + name: 'Roboto', + provider: fontProviders.npm(), + cssVariable: '--font-roboto', + }, + ], + }, + }); + ``` + + See the [NPM font provider reference documentation](https://v6.docs.astro.build/en/reference/font-provider-reference/#npm) for more details. + +- [#15548](https://github.com/withastro/astro/pull/15548) [`5b8f573`](https://github.com/withastro/astro/commit/5b8f5737feb1a051b7cbd5d543dd230492e5211f) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds a new optional `embeddedLangs` prop to the `` component to support languages beyond the primary `lang` + + This allows, for example, highlighting `.vue` files with a ` + + `; + --- + + + ``` + + See the [`` component documentation](https://v6.docs.astro.build/en/guides/syntax-highlighting/#code-) for more details. + +- [#15483](https://github.com/withastro/astro/pull/15483) [`7be3308`](https://github.com/withastro/astro/commit/7be3308bf4b1710a3f378ba338d09a8528e01e76) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds `streaming` option to the `createApp()` function in the Adapter API, mirroring the same functionality available when creating a new `App` instance + + An adapter's `createApp()` function now accepts `streaming` (defaults to `true`) as an option. HTML streaming breaks a document into chunks to send over the network and render on the page in order. This normally results in visitors seeing your HTML as fast as possible but factors such as network conditions and waiting for data fetches can block page rendering. + + HTML streaming helps with performance and generally provides a better visitor experience. In most cases, disabling streaming is not recommended. + + However, when you need to disable HTML streaming (e.g. your host only supports non-streamed HTML caching at the CDN level), you can opt out of the default behavior by passing `streaming: false` to `createApp()`: + + ```ts + import { createApp } from 'astro/app/entrypoint'; + + const app = createApp({ streaming: false }); + ``` + + See more about [the `createApp()` function](https://v6.docs.astro.build/en/reference/adapter-reference/#createapp) in the Adapter API reference. + +### Patch Changes + +- [#15542](https://github.com/withastro/astro/pull/15542) [`9760404`](https://github.com/withastro/astro/commit/97604040b73ec1d029f5d5a489aa744aaecfd173) Thanks [@rururux](https://github.com/rururux)! - Improves rendering by preserving `hidden="until-found"` value in attribues + +- [#15550](https://github.com/withastro/astro/pull/15550) [`58df907`](https://github.com/withastro/astro/commit/58df9072391fbfcd703e8d791ca51b7bedefb730) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Improves the JSDoc annotations for the `AstroAdapter` type + +- [#15507](https://github.com/withastro/astro/pull/15507) [`07f6610`](https://github.com/withastro/astro/commit/07f66101ed2850874c8a49ddf1f1609e6b1339fb) Thanks [@matthewp](https://github.com/matthewp)! - Avoid bundling SSR renderers when only API endpoints are dynamic + +- [#15459](https://github.com/withastro/astro/pull/15459) [`a4406b4`](https://github.com/withastro/astro/commit/a4406b4dfbca3756983b82c37d7c74f84d2de096) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where `context.csp` was logging warnings in development that should be logged in production only + +- Updated dependencies [[`84d6efd`](https://github.com/withastro/astro/commit/84d6efd9f1036fdf3c29e9b786b4a96453a607ed)]: + - @astrojs/markdown-remark@7.0.0-beta.7 + ## 6.0.0-beta.12 ### Major Changes diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index a84083cebe8a..860ea9ceea2e 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -1,10 +1,18 @@ --- -import { type ThemePresets, createShikiHighlighter } from '@astrojs/markdown-remark'; +import { + type ThemePresets, + createShikiHighlighter, + globalShikiStyleCollector, + transformerStyleToClass, +} from '@astrojs/markdown-remark'; import type { ShikiTransformer, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; import { bundledLanguages } from 'shiki/langs'; import type { CodeLanguage } from '../dist/types/public/common.js'; import type { HTMLAttributes } from '../types.js'; +// Code.astro always uses Shiki, so import the virtual CSS module +import 'virtual:astro:shiki-styles.css'; + interface Props extends Omit, 'lang'> { /** The code to highlight. Required. */ code: string; @@ -116,11 +124,17 @@ const highlighter = await createShikiHighlighter({ themes, }); +// Combine style-to-class transformer with user-provided transformers +const allTransformers = [ + globalShikiStyleCollector.register(transformerStyleToClass()), + ...transformers, +]; + const html = await highlighter.codeToHtml(code, typeof lang === 'string' ? lang : lang.name, { defaultColor, wrap, inline, - transformers, + transformers: allTransformers, meta, attributes: rest as any, }); diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index f428b7699af9..7da96681eb3d 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -79,5 +79,9 @@ declare module 'virtual:astro:dev-css-all' { } declare module 'virtual:astro:app' { - export function createApp(): import('./src/core/app/base.js').BaseApp; + export const createApp: import('./src/core/app/types.js').CreateApp; +} + +declare module 'virtual:astro:shiki-styles.css' { + // CSS module - no exports, imported for side effects } diff --git a/packages/astro/package.json b/packages/astro/package.json index 497b5bde7548..a3bc99b85fe6 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "6.0.0-beta.12", + "version": "6.0.0-beta.13", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/astro/src/assets/fonts/types.ts b/packages/astro/src/assets/fonts/types.ts index cebc3a5d29b8..aa38cafc2719 100644 --- a/packages/astro/src/assets/fonts/types.ts +++ b/packages/astro/src/assets/fonts/types.ts @@ -88,35 +88,34 @@ export interface FamilyProperties { unicodeRange?: [string, ...Array] | undefined; } -type WithOptions = TFontProvider extends FontProvider< - infer TFamilyOptions -> - ? [TFamilyOptions] extends [never] - ? { - /** - * An object to pass provider specific options. It is typed automatically based on the font family provider. - */ - options?: undefined; - } - : undefined extends TFamilyOptions +type WithOptions = + TFontProvider extends FontProvider + ? [TFamilyOptions] extends [never] ? { /** * An object to pass provider specific options. It is typed automatically based on the font family provider. */ - options?: TFamilyOptions; + options?: undefined; } - : { - /** - * An object to pass provider specific options. It is typed automatically based on the font family provider. - */ - options: TFamilyOptions; - } - : { - /** - * An object to pass provider specific options. It is typed automatically based on the font family provider. - */ - options?: undefined; - }; + : undefined extends TFamilyOptions + ? { + /** + * An object to pass provider specific options. It is typed automatically based on the font family provider. + */ + options?: TFamilyOptions; + } + : { + /** + * An object to pass provider specific options. It is typed automatically based on the font family provider. + */ + options: TFamilyOptions; + } + : { + /** + * An object to pass provider specific options. It is typed automatically based on the font family provider. + */ + options?: undefined; + }; export type FontFamily = FamilyProperties & WithOptions> & { diff --git a/packages/astro/src/core/app/entrypoints/virtual/dev.ts b/packages/astro/src/core/app/entrypoints/virtual/dev.ts index 53bf243624c4..31eafcc72822 100644 --- a/packages/astro/src/core/app/entrypoints/virtual/dev.ts +++ b/packages/astro/src/core/app/entrypoints/virtual/dev.ts @@ -1,15 +1,14 @@ import { manifest } from 'virtual:astro:manifest'; -import type { BaseApp } from '../../base.js'; import { DevApp } from '../../dev/app.js'; import { createConsoleLogger } from '../../logging.js'; -import type { RouteInfo } from '../../types.js'; +import type { CreateApp, RouteInfo } from '../../types.js'; import type { RoutesList } from '../../../../types/astro.js'; let currentDevApp: DevApp | null = null; -export function createApp(): BaseApp { +export const createApp: CreateApp = ({ streaming } = {}) => { const logger = createConsoleLogger(manifest.logLevel); - currentDevApp = new DevApp(manifest, true, logger); + currentDevApp = new DevApp(manifest, streaming, logger); // Listen for route updates via HMR if (import.meta.hot) { @@ -30,4 +29,4 @@ export function createApp(): BaseApp { } return currentDevApp; -} +}; diff --git a/packages/astro/src/core/app/entrypoints/virtual/index.ts b/packages/astro/src/core/app/entrypoints/virtual/index.ts index 88bbdd19485b..13c88755cc32 100644 --- a/packages/astro/src/core/app/entrypoints/virtual/index.ts +++ b/packages/astro/src/core/app/entrypoints/virtual/index.ts @@ -1,4 +1,4 @@ import { createApp as _createApp } from 'virtual:astro:app'; -import type { BaseApp } from '../../base.js'; +import type { CreateApp } from '../../types.js'; -export const createApp: () => BaseApp = _createApp; +export const createApp: CreateApp = _createApp; diff --git a/packages/astro/src/core/app/entrypoints/virtual/prod.ts b/packages/astro/src/core/app/entrypoints/virtual/prod.ts index 75135148e60a..0ec893eb0a83 100644 --- a/packages/astro/src/core/app/entrypoints/virtual/prod.ts +++ b/packages/astro/src/core/app/entrypoints/virtual/prod.ts @@ -1,7 +1,7 @@ import { manifest } from 'virtual:astro:manifest'; -import type { BaseApp } from '../../base.js'; +import type { CreateApp } from '../../types.js'; import { App } from '../../app.js'; -export function createApp(): BaseApp { - return new App(manifest); -} +export const createApp: CreateApp = ({ streaming } = {}) => { + return new App(manifest, streaming); +}; diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index e56a6445a586..99646ca2a12f 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -19,6 +19,7 @@ import type { LoggerLevel } from '../logger/core.js'; import type { RoutingStrategies } from './common.js'; import type { BaseSessionConfig, SessionDriverFactory } from '../session/types.js'; import type { DevToolbarPlacement } from '../../types/public/toolbar.js'; +import type { BaseApp } from './base.js'; type ComponentPath = string; @@ -205,3 +206,5 @@ export type NodeAppHeadersJson = { value: string; }[]; }[]; + +export type CreateApp = (options?: { streaming?: boolean }) => BaseApp; diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index cd05ce7186e6..3a86c66b06e8 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -51,6 +51,7 @@ import { vitePluginEnvironment } from '../vite-plugin-environment/index.js'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from './constants.js'; import { vitePluginChromedevtools } from '../vite-plugin-chromedevtools/index.js'; import { vitePluginAstroServerClient } from '../vite-plugin-overlay/index.js'; +import { vitePluginShikiStyles } from '../vite-plugin-shiki-styles/index.js'; type CreateViteOptions = { settings: AstroSettings; @@ -166,6 +167,7 @@ export async function createVite( astroContainer(), astroHmrReloadPlugin(), vitePluginChromedevtools({ settings }), + vitePluginShikiStyles(), ], publicDir: fileURLToPath(settings.config.publicDir), root: fileURLToPath(settings.config.root), diff --git a/packages/astro/src/core/csp/config.ts b/packages/astro/src/core/csp/config.ts index 3aef47c57907..e559dc4852dc 100644 --- a/packages/astro/src/core/csp/config.ts +++ b/packages/astro/src/core/csp/config.ts @@ -6,11 +6,10 @@ type UnionToIntersection = (U extends never ? never : (arg: U) => never) exte ? I : never; -type UnionToTuple = UnionToIntersection T> extends ( - _: never, -) => infer W - ? [...UnionToTuple>, W] - : []; +type UnionToTuple = + UnionToIntersection T> extends (_: never) => infer W + ? [...UnionToTuple>, W] + : []; export const ALGORITHMS = { 'SHA-256': 'sha256-', diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts index e7bca104ccd7..7768e6c38403 100644 --- a/packages/astro/src/core/errors/dev/utils.ts +++ b/packages/astro/src/core/errors/dev/utils.ts @@ -179,9 +179,7 @@ function collectInfoFromStacktrace(error: SSRError & { stack: string }): StackIn error.pluginCode || error.id || // TODO: this could be better, `src` might be something else - stackText - .split('\n') - .find((ln) => ln.includes('src') || ln.includes('node_modules')); + stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules')); // Disable eslint as we're not sure how to improve this regex yet // eslint-disable-next-line regexp/no-super-linear-backtracking const source = possibleFilePath?.replace?.(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, ''); diff --git a/packages/astro/src/env/schema.ts b/packages/astro/src/env/schema.ts index 723c2643f8e8..87943b27bd1d 100644 --- a/packages/astro/src/env/schema.ts +++ b/packages/astro/src/env/schema.ts @@ -33,11 +33,9 @@ const EnumSchema = z.object({ type: z.literal('enum'), values: z.array( // We use "'" for codegen so it can't be passed here - z - .string() - .refine((v) => !v.includes("'"), { - message: `The "'" character can't be used as an enum value`, - }), + z.string().refine((v) => !v.includes("'"), { + message: `The "'" character can't be used as an enum value`, + }), ), optional: z.boolean().optional(), default: z.string().optional(), diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 5c3dcf4c0d19..7123b47a87e9 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -7,7 +7,7 @@ import type { RenderDestination, RenderDestinationChunk, RenderFunction } from ' export const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const htmlBooleanAttributes = - /^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|inert|loop|muted|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i; + /^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|inert|loop|muted|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i; const AMPERSAND_REGEX = /&/g; const DOUBLE_QUOTE_REGEX = /"/g; @@ -142,6 +142,9 @@ Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the if (key === 'download' && typeof value === 'boolean') { return handleBooleanAttribute(key, value, shouldEscape, tagName); } + if (key === 'hidden' && typeof value === 'boolean') { + return handleBooleanAttribute(key, value, shouldEscape, tagName); + } return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`); } diff --git a/packages/astro/src/types/public/integrations.ts b/packages/astro/src/types/public/integrations.ts index 767bb45d6037..994fa7699257 100644 --- a/packages/astro/src/types/public/integrations.ts +++ b/packages/astro/src/types/public/integrations.ts @@ -82,21 +82,23 @@ export type AdapterSupport = AdapterSupportsKind | AdapterSupportWithMessage; export interface AstroAdapterFeatures { /** - * Creates an edge function that will communicate with the Astro middleware + * Defines whether any on-demand rendering middleware code will be bundled when built. When enabled, this prevents + * middleware code from being bundled and imported by all pages during the build. */ edgeMiddleware: boolean; + /** - * Determine the type of build output the adapter is intended for. Defaults to `server`; + * Allows you to force a specific output shape for the build. This can be useful for adapters that only work with + * a specific output type. For example, your adapter might expect a static website so it can create host-specific + * files. Defaults to `server` if not specified. */ buildOutput?: 'static' | 'server'; /** - * If supported by the adapter and enabled, Astro won't add any `` tags - * in the static pages, instead it will return a mapping in the - * `astro:build:generated` hook, so adapters can consume them and add them inside - * their hosting headers configuration file. - * - * Future features may decide to use this feature to create/add headers for static pages. + * Whether or not the adapter provides support for setting response headers for static pages. When this feature is + * enabled, Astro will return a map of the `Headers` emitted by the static pages. This map is available as `routeToHeaders` + * in the `astro:build:generated` hook and can be used to generate platform-specific output that controls HTTP headers, + * for example, to create a `_headers` file for platforms that support it. */ staticHeaders?: boolean; } @@ -112,6 +114,7 @@ export interface AstroAdapterClientConfig { * Can be an object of headers or a function that returns headers. */ internalFetchHeaders?: Record | (() => Record); + /** * Query parameters to append to all asset URLs (images, stylesheets, scripts, etc.). * Useful for adapters that need to track deployment versions or other metadata. @@ -121,43 +124,88 @@ export interface AstroAdapterClientConfig { interface AdapterExplicitProperties { /** - * Determines how the adapter's entrypoint is handled during the build. - * - `'auto'`: The adapter defines its own entrypoint and provides either serverEntrypoint or rollupOptions.input - * - `'explicit'`: Uses the virtual module entrypoint with dynamic exports - * @default 'explicit' - * @deprecated This will be removed in a future major version and `'auto'` will become the default + * @deprecated `entrypointResolution: "explicit"` is deprecated. `entrypointResolution: "auto"` will become the default, + * and only, behavior in a future major version. See [how to migrate](https://v6.docs.astro.build/en/guides/upgrade-to/v6/#deprecated-createexports-and-start-adapter-api). + * + * Specifies the method Astro will use to resolve the server entrypoint: `"auto"` (recommended) + * or `"explicit"` (default, but deprecated): + * + * - **`"auto"` (recommended):** You are responsible for providing a valid module as an entrypoint + * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rollupOptions.input`. + * - **`"explicit"` (deprecated)**: You must provide the exports required by the host in the server entrypoint + * using a `createExports()` function before passing them to `setAdapter()` as an [`exports`](#exports) list. This supports + * adapters built using the Astro 5 version of the Adapter API. By default, all adapters will receive this value to allow backwards + * compatibility. **However, no new adapters should be created with this value.** Existing adapters should override this default + * value with `"auto"` as soon as they are able to migrate to the new v6 API. */ entrypointResolution?: 'explicit'; + + /** + * Defines the entrypoint for on-demand rendering. + */ serverEntrypoint?: string | URL; - /** @deprecated This will be removed in a future major version, alongside `entrypointResolution: 'explicit'` */ + + /** + * @deprecated This will be removed in a future major version, alongside `entrypointResolution: 'explicit'`. + * + * Defines an array of named exports to use in conjunction with the `createExports()` function of your server entrypoint. + */ exports?: string[]; - /** @deprecated This will be removed in a future major version, alongside `entrypointResolution: 'explicit'` */ + + /** + * @deprecated This will be removed in a future major version, alongside `entrypointResolution: 'explicit'`. + * + * A JSON-serializable value that will be passed to the adapter's server entrypoint at runtime. This is useful to pass an object containing build-time configuration (e.g. paths, secrets) to your server runtime code. + */ args?: any; } interface AdapterAutoProperties { /** - * Determines how the adapter's entrypoint is handled during the build. - * - `'auto'`: The adapter defines its own entrypoint and provides either serverEntrypoint or rollupOptions.input - * - `'explicit'`: Uses the virtual module entrypoint with dynamic exports - * @default 'explicit' + * Specifies the method Astro will use to resolve the server entrypoint: `"auto"` (recommended) + * or `"explicit"` (default, but deprecated): + * + * - **`"auto"` (recommended):** You are responsible for providing a valid module as an entrypoint + * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rollupOptions.input`. + * - **`"explicit"` (deprecated)**: You must provide the exports required by the host in the server entrypoint + * using a `createExports()` function before passing them to `setAdapter()` as an [`exports`](#exports) list. This supports + * adapters built using the Astro 5 version of the Adapter API. By default, all adapters will receive this value to allow backwards + * compatibility. **However, no new adapters should be created with this value.** Existing adapters should override this default + * value with `"auto"` as soon as they are able to migrate to the new v6 API. */ entrypointResolution: 'auto'; + + /** + * Defines the entrypoint for on-demand rendering. + */ serverEntrypoint?: string | URL; } export type AstroAdapter = { + /** + * Defines a unique name for your adapter. This will be used for logging. + */ name: string; + + /** + * Defines the path or ID of a module in the adapter's package that is responsible for starting up the built + * server when `astro preview` is run. + */ previewEntrypoint?: string | URL; + + /** + * An object that specifies which adapter features that change the build output are supported by the adapter. + */ adapterFeatures?: AstroAdapterFeatures; + /** - * List of features supported by an adapter. - * - * If the adapter is not able to handle certain configurations, Astro will throw an error. + * A map of Astro's built-in features supported by the adapter. This allows Astro to determine which features an + * adapter supports, so appropriate error messages can be provided. */ supportedAstroFeatures: AstroAdapterFeatureMap; + /** - * Configuration for Astro's client-side code. + * A configuration object for Astro's client-side code. */ client?: AstroAdapterClientConfig; } & (AdapterExplicitProperties | AdapterAutoProperties); @@ -199,32 +247,36 @@ export interface AstroPrerenderer { export type AstroAdapterFeatureMap = { /** - * The adapter is able to serve static pages + * Defines whether the adapter is able to serve static pages. */ staticOutput?: AdapterSupport; /** - * The adapter is able to serve pages that are static or rendered via server + * Defines whether the adapter is able to serve sites that include a mix of static and on-demand rendered pages. */ hybridOutput?: AdapterSupport; /** - * The adapter is able to serve SSR pages + * Defines whether the adapter is able to serve on-demand rendered pages. */ serverOutput?: AdapterSupport; /** - * The adapter is able to support i18n domains + * Defines whether the adapter is able to support i18n domains. */ i18nDomains?: AdapterSupport; /** - * The adapter is able to support `getSecret` exported from `astro:env/server` + * Defines whether the adapter is able to support `getSecret()` exported from `astro:env/server`. When enabled, this feature + * allows your adapter to retrieve secrets configured by users in `env.schema`. + * + * The `astro/env/setup` module allows you to provide an implementation for `getSecret()`. In your server entrypoint, call + * `setGetEnv()` as soon as possible. */ envGetSecret?: AdapterSupport; /** - * The adapter supports image transformation using the built-in Sharp image service + * Defines whether the adapter supports image transformation using the built-in Sharp image service. */ sharpImageService?: AdapterSupport; }; diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index a3710a36f5c2..3b84418b0ed3 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -127,12 +127,19 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug ); } + // Only inject Shiki styles if using Shiki syntax highlighter (default) AND document contains code blocks + const usesShiki = + settings.config.markdown.syntaxHighlight === 'shiki' || + settings.config.markdown.syntaxHighlight === undefined; + const hasCodeBlocks = renderResult.metadata.hasCodeBlocks ?? false; + const code = ` - import { unescapeHTML, spreadAttributes, createComponent, render, renderComponent, maybeRenderHead } from ${JSON.stringify( - astroServerRuntimeModulePath, - )}; - import { AstroError, AstroErrorData } from ${JSON.stringify(astroErrorModulePath)}; - ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} + import { unescapeHTML, spreadAttributes, createComponent, render, renderComponent, maybeRenderHead } from ${JSON.stringify( + astroServerRuntimeModulePath, + )}; + import { AstroError, AstroErrorData } from ${JSON.stringify(astroErrorModulePath)}; + ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} + ${usesShiki && hasCodeBlocks ? `import 'virtual:astro:shiki-styles.css';` : ''} ${ // Only include the code relevant to `astro:assets` if there's images in the file diff --git a/packages/astro/src/vite-plugin-shiki-styles/index.ts b/packages/astro/src/vite-plugin-shiki-styles/index.ts new file mode 100644 index 000000000000..cf8b46f2a8b8 --- /dev/null +++ b/packages/astro/src/vite-plugin-shiki-styles/index.ts @@ -0,0 +1,59 @@ +import type { Plugin } from 'vite'; +import { globalShikiStyleCollector } from '@astrojs/markdown-remark'; + +const VIRTUAL_SHIKI_STYLES_ID = 'virtual:astro:shiki-styles.css'; +const RESOLVED_VIRTUAL_SHIKI_STYLES_ID = '\0virtual:astro:shiki-styles.css'; + +/** + * Vite plugin that provides a virtual CSS module containing all Shiki syntax highlighting styles. + * + * This plugin collects styles from the style-to-class transformer used by both Code.astro + * and Markdown processing, and bundles them into a single CSS file. The .css extension + * ensures Vite processes this through its CSS pipeline (minification, hashing, etc.). + */ +export function vitePluginShikiStyles(): Plugin { + return { + name: 'astro:shiki-styles', + + buildStart() { + // Clear styles at the start of each build to prevent stale data + globalShikiStyleCollector.clear(); + }, + + resolveId: { + filter: { + id: new RegExp(`^${VIRTUAL_SHIKI_STYLES_ID}$`), + }, + handler(id) { + if (id === VIRTUAL_SHIKI_STYLES_ID) { + return RESOLVED_VIRTUAL_SHIKI_STYLES_ID; + } + }, + }, + + load: { + filter: { + id: new RegExp(`^${RESOLVED_VIRTUAL_SHIKI_STYLES_ID}$`), + }, + handler(id) { + if (id === RESOLVED_VIRTUAL_SHIKI_STYLES_ID) { + const css = globalShikiStyleCollector.collectCSS(); + // Return CSS or a comment if no styles generated yet + return css || '/* Shiki styles will be generated during build */'; + } + }, + }, + + // Handle HMR invalidation when markdown/astro files change + handleHotUpdate({ file, server }) { + // If a Markdown or astro file changed, invalidate the virtual CSS module + // so it regenerates with updated styles + if (file.endsWith('.md') || file.endsWith('.mdx') || file.endsWith('.astro')) { + const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SHIKI_STYLES_ID); + if (module) { + server.moduleGraph.invalidateModule(module); + } + } + }, + }; +} diff --git a/packages/astro/test/astro-attrs.test.js b/packages/astro/test/astro-attrs.test.js index 73f8f798ec8c..dd5ffdd4d993 100644 --- a/packages/astro/test/astro-attrs.test.js +++ b/packages/astro/test/astro-attrs.test.js @@ -25,6 +25,11 @@ describe('Attributes', async () => { 'popover-true': { attribute: 'popover', value: '' }, 'popover-false': { attribute: 'popover', value: undefined }, 'popover-string-empty': { attribute: 'popover', value: '' }, + // Note: cheerio normalizes boolean `hidden` to the string "hidden", + // so we use "hidden" as the expected value instead of "" + 'hidden-true': { attribute: 'hidden', value: 'hidden' }, + 'hidden-false': { attribute: 'hidden', value: undefined }, + 'hidden-string-empty': { attribute: 'hidden', value: 'hidden' }, 'boolean-attr-true': { attribute: 'allowfullscreen', value: '' }, 'boolean-attr-false': { attribute: 'allowfullscreen', value: undefined }, 'boolean-attr-string-truthy': { attribute: 'allowfullscreen', value: '' }, @@ -50,6 +55,11 @@ describe('Attributes', async () => { 'html-enum-false': { attribute: 'draggable', value: 'false' }, }; + // cheerio normalizes hidden="until-found" to just hidden, so we check the raw HTML + assert.ok( + html.includes('id="hidden-until-found" hidden="until-found"'), + 'hidden="until-found" should preserve the attribute value', + ); assert.ok(!/allowfullscreen=/.test(html), 'boolean attributes should not have values'); assert.ok( !/id="data-attr-string-falsy"\s+data-foobar=/.test(html), diff --git a/packages/astro/test/astro-component-code.test.js b/packages/astro/test/astro-component-code.test.js index 3c7b7738479b..74b60c1f6a3d 100644 --- a/packages/astro/test/astro-component-code.test.js +++ b/packages/astro/test/astro-component-code.test.js @@ -15,11 +15,10 @@ describe('', () => { let html = await fixture.readFile('/no-lang/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - 'applies default and overflow', - ); + // Styles are now class-based - no inline styles + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); + assert.ok($('pre').attr('class'), 'has classes for styling'); assert.equal($('pre > code').length, 1); // test: contains some generated spans @@ -30,7 +29,10 @@ describe('', () => { let html = await fixture.readFile('/basic/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code github-dark'); + // Classes now include token style classes and overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('github-dark'), 'has github-dark theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); assert.equal($('pre > code').length, 1); // test: contains many generated spans assert.equal($('pre > code span').length >= 6, true); @@ -40,12 +42,11 @@ describe('', () => { let html = await fixture.readFile('/custom-theme/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code nord'); - assert.equal( - $('pre').attr('style'), - 'background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;', - 'applies custom theme', - ); + // Classes now include token style classes and overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('nord'), 'has nord theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); }); it('', async () => { @@ -53,28 +54,28 @@ describe('', () => { let html = await fixture.readFile('/wrap-true/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;', - ); + // Wrap styles are now classes, not inline + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok($('pre').hasClass('astro-code-wrap'), 'has wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } { let html = await fixture.readFile('/wrap-false/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - ); + // Overflow is now a class, not inline + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').hasClass('astro-code-wrap'), 'should NOT have wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } { let html = await fixture.readFile('/wrap-null/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal($('pre').attr('style'), 'background-color:#24292e;color:#e1e4e8'); + // When wrap is null, no overflow or wrap classes + assert.ok(!$('pre').hasClass('astro-code-overflow'), 'should not have overflow class'); + assert.ok(!$('pre').hasClass('astro-code-wrap'), 'should not have wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } }); @@ -82,20 +83,18 @@ describe('', () => { let html = await fixture.readFile('/css-theme/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code css-variables'); - assert.deepEqual( - $('pre, pre span') - .map((_i, f) => (f.attribs ? f.attribs.style : 'no style found')) - .toArray(), - [ - 'background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;', - 'color:var(--astro-code-token-constant)', - 'color:var(--astro-code-token-function)', - 'color:var(--astro-code-foreground)', - 'color:var(--astro-code-token-string-expression)', - 'color:var(--astro-code-foreground)', - ], - ); + // Classes now include overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('css-variables'), 'has css-variables theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + + // CSS variables theme still uses inline styles on spans (not transformed by style-to-class) + // but pre background/color should be in classes, overflow should be a class + assert.ok(!$('pre').attr('style'), 'pre should have no inline style'); + + // Spans should have CSS variable colors as inline styles + const spans = $('pre span'); + assert.ok(spans.length > 0, 'should have spans'); }); it(' with custom theme and lang', async () => { @@ -103,10 +102,9 @@ describe('', () => { const $ = cheerio.load(html); assert.equal($('#theme > pre').length, 1); - assert.equal( - $('#theme > pre').attr('style'), - 'background-color:#FDFDFE;color:#4E5377; overflow-x: auto;', - ); + // Styles are now class-based - no inline styles + assert.ok($('#theme > pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('#theme > pre').attr('style'), 'should have no inline style'); assert.equal($('#lang > pre').length, 1); assert.equal($('#lang > pre > code span').length, 3); @@ -118,7 +116,8 @@ describe('', () => { const codeEl = $('.astro-code'); assert.equal(codeEl.prop('tagName'), 'CODE'); - assert.match(codeEl.attr('style'), /background-color:/); + // Inline code now uses classes instead of inline styles + assert.ok(codeEl.attr('class'), 'should have classes for styling'); assert.equal($('pre').length, 0); }); diff --git a/packages/astro/test/astro-markdown-shiki-conditional.test.js b/packages/astro/test/astro-markdown-shiki-conditional.test.js new file mode 100644 index 000000000000..614b99ad4cb1 --- /dev/null +++ b/packages/astro/test/astro-markdown-shiki-conditional.test.js @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('Markdown Shiki CSS conditional injection', () => { + describe('With code blocks', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/with-code/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should inject Shiki CSS when code blocks present', () => { + const styles = $('style').text(); + // Check for Shiki class prefix + assert.ok(styles.includes('.__a_'), 'Should have Shiki token class definitions'); + // Check for base utility classes + assert.ok(styles.includes('.astro-code-overflow'), 'Should have overflow class'); + }); + + it('should render code blocks with Shiki classes', () => { + const codeBlock = $('pre.astro-code'); + assert.ok(codeBlock.length > 0, 'Should have code blocks with astro-code class'); + + const classes = codeBlock.attr('class'); + assert.ok(classes && classes.includes('__a_'), 'Code block should have Shiki token class'); + }); + }); + + describe('Without code blocks', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/no-code/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should NOT inject Shiki CSS when no code blocks', () => { + const styles = $('style').text(); + // Should not have Shiki classes + assert.ok(!styles.includes('.__a_'), 'Should NOT have Shiki token class definitions'); + assert.ok(!styles.includes('.astro-code-overflow'), 'Should NOT have overflow class'); + }); + + it('should NOT have code blocks', () => { + const codeBlocks = $('pre.astro-code'); + assert.equal(codeBlocks.length, 0, 'Should not have code blocks'); + }); + + it('should still render markdown content', () => { + assert.equal($('h1').text(), 'Hello world'); + assert.ok($('p').length > 0, 'Should have paragraph elements'); + // Inline code should still be present + assert.ok($('code').length > 0, 'Should have inline code elements'); + }); + }); + + describe('With excluded language', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/excluded-lang/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should NOT inject CSS for excluded languages', () => { + const styles = $('style').text(); + // Should not have Shiki classes since mermaid is excluded + assert.ok(!styles.includes('.__a_'), 'Should NOT have Shiki token class definitions'); + assert.ok(!styles.includes('.astro-code-overflow'), 'Should NOT have overflow class'); + }); + + it('should still have the code block element (unstyled)', () => { + // The code block should exist but without Shiki styling + const codeElements = $('pre code'); + assert.ok(codeElements.length > 0, 'Should have code block element'); + + // Should have language-mermaid class from markdown processing + const code = codeElements.first(); + const className = code.attr('class') || ''; + assert.ok(className.includes('language-mermaid'), 'Should have language-mermaid class'); + }); + + it('should NOT have astro-code class', () => { + const astroCodeBlocks = $('pre.astro-code'); + assert.equal(astroCodeBlocks.length, 0, 'Should not have astro-code class'); + }); + }); +}); diff --git a/packages/astro/test/astro-markdown-shiki.test.js b/packages/astro/test/astro-markdown-shiki.test.js index 8140b4d5dfee..42417897965c 100644 --- a/packages/astro/test/astro-markdown-shiki.test.js +++ b/packages/astro/test/astro-markdown-shiki.test.js @@ -16,23 +16,24 @@ describe('Astro Markdown Shiki', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - // There should be no HTML from Prism - assert.equal($('.token').length, 0); - + // There are 2 code blocks in index.md (yaml and diff) assert.equal($('pre').length, 2); - assert.ok($('pre').hasClass('astro-code')); - assert.equal( - $('pre').attr().style, - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - ); + const yamlBlock = $('pre').first(); + assert.ok(yamlBlock.hasClass('astro-code')); + // Styles are now class-based - no inline styles + assert.ok(yamlBlock.hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!yamlBlock.attr('style'), 'should have no inline style attribute'); }); it('Can render diff syntax with "user-select: none"', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const diffBlockHtml = $('pre').last().html(); - assert.ok(diffBlockHtml.includes(`+`)); - assert.ok(diffBlockHtml.includes(`-`)); + // The diff block is the second
 in index.html
+			const diffBlock = $('pre').eq(1);
+			const diffHtml = $.html(diffBlock);
+			// user-select: none is now a class, not inline style
+			assert.ok(diffHtml.includes(`+`));
+			assert.ok(diffHtml.includes(`-`));
 		});
 	});
 
@@ -51,10 +52,9 @@ describe('Astro Markdown Shiki', () => {
 
 				assert.equal($('pre').length, 1);
 				assert.ok($('pre').hasClass('astro-code'));
-				assert.equal(
-					$('pre').attr().style,
-					'background-color:#fff;color:#24292e; overflow-x: auto;',
-				);
+				// Styles are now class-based - no inline styles
+				assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class');
+				assert.ok(!$('pre').attr('style'), 'should have no inline style attribute');
 			});
 		});
 
@@ -70,12 +70,15 @@ describe('Astro Markdown Shiki', () => {
 				const html = await fixture.readFile('/index.html');
 				const $ = cheerio.load(html);
 
-				assert.equal($('pre').length, 1);
-				assert.ok($('pre').hasClass('astro-code'));
-				assert.equal(
-					$('pre').attr().style,
-					'background-color:#FDFDFE;color:#4E5377; overflow-x: auto;',
-				);
+				// With class-based styles, ALL styles are in CSS classes (no inline styles)
+				assert.ok(!$('pre').attr('style'), 'should have no inline style attribute');
+				assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class');
+
+				// Verify styles are in the