astro 7.0.3 → 7.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/assets/services/sharp.js +1 -1
  2. package/dist/cli/dev/background.js +4 -2
  3. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  4. package/dist/content/content-layer.js +3 -3
  5. package/dist/content/vite-plugin-content-virtual-mod.js +5 -1
  6. package/dist/core/app/node.js +7 -2
  7. package/dist/core/build/plugins/index.js +2 -0
  8. package/dist/core/build/plugins/plugin-css-target-lowering.d.ts +14 -0
  9. package/dist/core/build/plugins/plugin-css-target-lowering.js +82 -0
  10. package/dist/core/constants.js +1 -1
  11. package/dist/core/create-vite.js +2 -0
  12. package/dist/core/dev/dev.js +1 -1
  13. package/dist/core/fetch/fetch-state.js +2 -0
  14. package/dist/core/head-propagation/buffer.d.ts +14 -3
  15. package/dist/core/head-propagation/buffer.js +17 -7
  16. package/dist/core/messages/runtime.js +1 -1
  17. package/dist/core/render/route-cache.d.ts +7 -2
  18. package/dist/core/routing/create-manifest.js +9 -7
  19. package/dist/prefetch/index.js +10 -0
  20. package/dist/runtime/client/dev-toolbar/apps/audit/rules/a11y.js +49 -9
  21. package/dist/runtime/server/hydration.js +1 -1
  22. package/dist/runtime/server/render/astro/instance.js +3 -0
  23. package/dist/runtime/server/render/page.js +7 -2
  24. package/dist/runtime/server/render/server-islands-shared.d.ts +1 -0
  25. package/dist/runtime/server/render/server-islands-shared.js +4 -0
  26. package/dist/runtime/server/render/server-islands.js +3 -2
  27. package/dist/template/4xx.js +1 -1
  28. package/dist/transitions/swap-functions.js +9 -2
  29. package/dist/types/public/internal.d.ts +14 -0
  30. package/dist/vite-plugin-astro/index.js +3 -1
  31. package/dist/vite-plugin-config-alias/index.d.ts +19 -1
  32. package/dist/vite-plugin-config-alias/index.js +6 -5
  33. package/dist/vite-plugin-html/transform/index.js +16 -7
  34. package/dist/vite-plugin-html/transform/slots.d.ts +10 -7
  35. package/dist/vite-plugin-html/transform/slots.js +19 -26
  36. package/dist/vite-plugin-html/transform/utils.d.ts +0 -3
  37. package/dist/vite-plugin-html/transform/utils.js +1 -20
  38. package/package.json +4 -26
  39. package/dist/vite-plugin-html/transform/escape.d.ts +0 -7
  40. package/dist/vite-plugin-html/transform/escape.js +0 -30
@@ -110,7 +110,7 @@ const sharpService = {
110
110
  });
111
111
  }
112
112
  const result = sharp(inputBuffer, {
113
- failOnError: false,
113
+ failOn: "none",
114
114
  pages: -1,
115
115
  limitInputPixels: config.service.config.limitInputPixels
116
116
  });
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, openSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, resolve } from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import {
6
7
  checkExistingServer,
@@ -11,6 +12,7 @@ import {
11
12
  GRACEFUL_SHUTDOWN_TIMEOUT
12
13
  } from "../../core/dev/lockfile.js";
13
14
  import { resolveRoot } from "../../core/config/config.js";
15
+ const require2 = createRequire(import.meta.url);
14
16
  function formatBackgroundOutput(result) {
15
17
  return JSON.stringify(result);
16
18
  }
@@ -76,7 +78,7 @@ async function background({
76
78
  }
77
79
  const logFd = openSync(logFilePath, "w");
78
80
  const rootPath = fileURLToPath(root);
79
- const astroBin = resolve(rootPath, "node_modules", "astro", "bin", "astro.mjs");
81
+ const astroBin = resolve(dirname(require2.resolve("astro/package.json")), "bin", "astro.mjs");
80
82
  const child = spawn(process.execPath, [astroBin, ...args], {
81
83
  detached: true,
82
84
  stdio: ["ignore", logFd, logFd],
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.0.3";
3
+ version = "7.0.5";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -197,7 +197,7 @@ ${contentConfig.error.message}`
197
197
  logger.info("Content config changed");
198
198
  shouldClear = true;
199
199
  }
200
- if (previousAstroVersion && previousAstroVersion !== "7.0.3") {
200
+ if (previousAstroVersion && previousAstroVersion !== "7.0.5") {
201
201
  logger.info("Astro version changed");
202
202
  shouldClear = true;
203
203
  }
@@ -205,8 +205,8 @@ ${contentConfig.error.message}`
205
205
  logger.info("Clearing content store");
206
206
  this.#store.clearAll();
207
207
  }
208
- if ("7.0.3") {
209
- this.#store.metaStore().set("astro-version", "7.0.3");
208
+ if ("7.0.5") {
209
+ this.#store.metaStore().set("astro-version", "7.0.5");
210
210
  }
211
211
  if (currentConfigDigest) {
212
212
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -23,6 +23,7 @@ import {
23
23
  } from "./consts.js";
24
24
  import { getDataStoreFile } from "./content-layer.js";
25
25
  import { getContentPaths, isDeferredModule } from "./utils.js";
26
+ const LARGE_DATA_STORE_THRESHOLD = 5 * 1024 * 1024;
26
27
  function invalidateAssetImports(viteServer, filePath) {
27
28
  const timestamp = Date.now();
28
29
  for (const environment of Object.values(viteServer.environments)) {
@@ -70,10 +71,12 @@ function astroContentVirtualModPlugin({
70
71
  let dataStoreFile;
71
72
  let devServer;
72
73
  let liveConfig;
74
+ let isDev = false;
73
75
  return {
74
76
  name: "astro-content-virtual-mod-plugin",
75
77
  enforce: "pre",
76
78
  config(_, env) {
79
+ isDev = env.command === "serve";
77
80
  dataStoreFile = getDataStoreFile(settings, env.command === "serve");
78
81
  const contentPaths = getContentPaths(
79
82
  settings.config,
@@ -169,8 +172,9 @@ function astroContentVirtualModPlugin({
169
172
  const jsonData = await fs.promises.readFile(dataStoreFile, "utf-8");
170
173
  try {
171
174
  const parsed = JSON.parse(jsonData);
175
+ const useRuntimeJsonParse = isDev && Buffer.byteLength(jsonData) > LARGE_DATA_STORE_THRESHOLD;
172
176
  return {
173
- code: dataToEsm(parsed, {
177
+ code: useRuntimeJsonParse ? `export default JSON.parse(${JSON.stringify(jsonData)})` : dataToEsm(parsed, {
174
178
  compact: true
175
179
  }),
176
180
  map: { mappings: "" }
@@ -42,8 +42,13 @@ function createRequestFromNodeRequest(req, {
42
42
  protocol,
43
43
  allowedDomains
44
44
  );
45
- const forwardedHost = getFirstForwardedValue(req.headers["x-forwarded-host"]);
46
- const hostValidated = validatedHostname !== void 0 || forwardedHost !== void 0 && allowedDomains.length > 0;
45
+ const validatedForwardedHost = validateForwardedHeaders(
46
+ void 0,
47
+ getFirstForwardedValue(req.headers["x-forwarded-host"]),
48
+ void 0,
49
+ allowedDomains
50
+ ).host;
51
+ const hostValidated = validatedHostname !== void 0 || validatedForwardedHost !== void 0;
47
52
  const forwardedClientIp = hostValidated ? getFirstForwardedValue(req.headers["x-forwarded-for"]) : void 0;
48
53
  const clientIp = forwardedClientIp || req.socket?.remoteAddress;
49
54
  if (clientIp) {
@@ -3,6 +3,7 @@ import { astroHeadBuildPlugin } from "../../../vite-plugin-head/index.js";
3
3
  import { pluginAnalyzer } from "./plugin-analyzer.js";
4
4
  import { pluginComponentEntry } from "./plugin-component-entry.js";
5
5
  import { pluginCSS } from "./plugin-css.js";
6
+ import { pluginCssTargetLowering } from "./plugin-css-target-lowering.js";
6
7
  import { pluginInternals } from "./plugin-internals.js";
7
8
  import { pluginMiddleware } from "./plugin-middleware.js";
8
9
  import { pluginPrerender } from "./plugin-prerender.js";
@@ -18,6 +19,7 @@ function getAllBuildPlugins(internals, options) {
18
19
  pluginInternals(options, internals),
19
20
  pluginMiddleware(options, internals),
20
21
  vitePluginActionsBuild(options, internals),
22
+ pluginCssTargetLowering(),
21
23
  ...pluginCSS(options, internals),
22
24
  astroHeadBuildPlugin(internals),
23
25
  pluginPrerender(options, internals),
@@ -0,0 +1,14 @@
1
+ import type { Plugin as VitePlugin } from 'vite';
2
+ /**
3
+ * Vite plugin that applies CSS target lowering without minification.
4
+ *
5
+ * Vite's `finalizeCss` only applies CSS target lowering (via lightningcss) when
6
+ * `config.build.cssMinify` is truthy, because target lowering and minification
7
+ * are coupled in `minifyCSS`. When users set `minify: false`, Astro forces
8
+ * `cssMinify: false`, which disables target lowering for all CSS.
9
+ *
10
+ * This plugin fills that gap: when `cssMinify` is falsy and a CSS target is
11
+ * configured, it runs lightningcss with `minify: false` on all CSS assets in
12
+ * `generateBundle`, applying target lowering without minification.
13
+ */
14
+ export declare function pluginCssTargetLowering(): VitePlugin;
@@ -0,0 +1,82 @@
1
+ import { createRequire } from "node:module";
2
+ const BROWSER_MAP = {
3
+ chrome: "chrome",
4
+ edge: "edge",
5
+ firefox: "firefox",
6
+ hermes: false,
7
+ ie: "ie",
8
+ ios: "ios_saf",
9
+ node: false,
10
+ opera: "opera",
11
+ rhino: false,
12
+ safari: "safari"
13
+ };
14
+ const VERSION_RE = /\d/;
15
+ function convertTargets(esbuildTarget) {
16
+ if (!esbuildTarget) return {};
17
+ const targets = {};
18
+ const entries = Array.isArray(esbuildTarget) ? esbuildTarget : [esbuildTarget];
19
+ for (const entry of entries) {
20
+ if (entry === "esnext") continue;
21
+ const index = entry.search(VERSION_RE);
22
+ if (index >= 0) {
23
+ const browserName = entry.slice(0, index);
24
+ const browser = BROWSER_MAP[browserName];
25
+ if (browser === false) continue;
26
+ if (browser) {
27
+ const [major, minor = 0] = entry.slice(index).split(".").map((v) => Number.parseInt(v, 10));
28
+ if (!isNaN(major) && !isNaN(minor)) {
29
+ const version = major << 16 | minor << 8;
30
+ if (!targets[browser] || version < targets[browser]) {
31
+ targets[browser] = version;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return targets;
38
+ }
39
+ function pluginCssTargetLowering() {
40
+ let resolvedConfig;
41
+ return {
42
+ name: "astro:css-target-lowering",
43
+ enforce: "post",
44
+ configResolved(config) {
45
+ resolvedConfig = config;
46
+ },
47
+ async generateBundle(_outputOptions, bundle) {
48
+ if (resolvedConfig.build.cssMinify) return;
49
+ const cssTarget = resolvedConfig.build.cssTarget;
50
+ if (!cssTarget) return;
51
+ const targets = convertTargets(cssTarget);
52
+ if (Object.keys(targets).length === 0) return;
53
+ let lcssTransform;
54
+ try {
55
+ const requireFromVite = createRequire(import.meta.resolve("vite"));
56
+ lcssTransform = requireFromVite("lightningcss").transform;
57
+ } catch {
58
+ return;
59
+ }
60
+ for (const [, asset] of Object.entries(bundle)) {
61
+ if (asset.type !== "asset") continue;
62
+ if (!asset.fileName.endsWith(".css")) continue;
63
+ if (typeof asset.source !== "string") continue;
64
+ try {
65
+ const result = lcssTransform({
66
+ ...resolvedConfig.css?.lightningcss,
67
+ targets,
68
+ cssModules: void 0,
69
+ filename: asset.fileName,
70
+ code: Buffer.from(asset.source),
71
+ minify: false
72
+ });
73
+ asset.source = new TextDecoder().decode(result.code);
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ };
79
+ }
80
+ export {
81
+ pluginCssTargetLowering
82
+ };
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "7.0.3";
1
+ const ASTRO_VERSION = "7.0.5";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const ASTRO_ERROR_HEADER = "X-Astro-Error";
4
4
  const DEFAULT_404_COMPONENT = "astro-default-404.astro";
@@ -201,6 +201,8 @@ async function createVite(commandConfig, { settings, logger, mode, command, fs =
201
201
  }
202
202
  },
203
203
  resolve: {
204
+ // Vite's native tsconfig path resolution; see configAliasVitePlugin for the deprecated fallback.
205
+ tsconfigPaths: true,
204
206
  alias: [
205
207
  {
206
208
  // This is needed for Deno compatibility, as the non-browser version
@@ -26,7 +26,7 @@ async function dev(inlineConfig) {
26
26
  await telemetry.record([]);
27
27
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
28
28
  const logger = restart.container.logger;
29
- const currentVersion = "7.0.3";
29
+ const currentVersion = "7.0.5";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -297,6 +297,8 @@ class FetchState {
297
297
  extraStyleHashes,
298
298
  extraScriptHashes,
299
299
  propagators: /* @__PURE__ */ new Set(),
300
+ routeHasPropagation: false,
301
+ pendingSlotEvaluations: [],
300
302
  templateDepth: 0
301
303
  },
302
304
  cspDestination: manifest.csp?.cspDestination ?? (routeData.prerender ? "meta" : "header"),
@@ -3,10 +3,21 @@ export interface HeadPropagator {
3
3
  init(result: SSRResult): unknown | Promise<unknown>;
4
4
  }
5
5
  /**
6
- * Runs all registered propagators and collects emitted head HTML strings.
6
+ * Runs all registered propagators and collects the head HTML they emit.
7
7
  *
8
- * This iterates the live `Set`, so propagators discovered during iteration
9
- * are also processed in the same pass.
8
+ * Components with head content are discovered as we go. Initializing one
9
+ * propagator can register more of them: a component marked `in-tree` renders
10
+ * its children, and one of those children may be a `self` component that emits
11
+ * styles. Slots add a second way to find them — a slot whose markup contains an
12
+ * `await` only reaches the components after that `await` once it resolves, so
13
+ * we also wait for those pending slot pre-renders. We keep initializing
14
+ * propagators and waiting on slots until no new ones appear.
15
+ *
16
+ * Propagators are tracked in a `seen` set rather than read through a single
17
+ * live `Set` iterator. A propagator can be registered after we have already
18
+ * iterated to the end of the set (e.g. once a slot's `await` resolves), and a
19
+ * `Set` iterator that has reported `done` would never report those late
20
+ * additions.
10
21
  *
11
22
  * @example
12
23
  * If a layout initializes and discovers a nested component that also emits
@@ -1,15 +1,25 @@
1
1
  async function collectPropagatedHeadParts(input) {
2
2
  const collectedHeadParts = [];
3
- const iterator = input.propagators.values();
3
+ const seen = /* @__PURE__ */ new Set();
4
+ const pendingSlotEvaluations = input.result._metadata?.pendingSlotEvaluations ?? [];
4
5
  while (true) {
5
- const { value, done } = iterator.next();
6
- if (done) {
7
- break;
6
+ if (pendingSlotEvaluations.length > 0) {
7
+ const batch = pendingSlotEvaluations.splice(0, pendingSlotEvaluations.length);
8
+ await Promise.all(batch);
9
+ continue;
8
10
  }
9
- const returnValue = await value.init(input.result);
10
- if (input.isHeadAndContent(returnValue) && returnValue.head) {
11
- collectedHeadParts.push(returnValue.head);
11
+ let progressed = false;
12
+ for (const propagator of input.propagators) {
13
+ if (seen.has(propagator)) continue;
14
+ seen.add(propagator);
15
+ progressed = true;
16
+ const returnValue = await propagator.init(input.result);
17
+ if (input.isHeadAndContent(returnValue) && returnValue.head) {
18
+ collectedHeadParts.push(returnValue.head);
19
+ }
20
+ break;
12
21
  }
22
+ if (!progressed) break;
13
23
  }
14
24
  return collectedHeadParts;
15
25
  }
@@ -269,7 +269,7 @@ function printHelp({
269
269
  message.push(
270
270
  linebreak(),
271
271
  ` ${bgGreen(black(` ${commandName} `))} ${green(
272
- `v${"7.0.3"}`
272
+ `v${"7.0.5"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -18,8 +18,13 @@ interface RouteCacheEntry {
18
18
  }
19
19
  /**
20
20
  * Manage the route cache, responsible for caching data related to each route,
21
- * including the result of calling getStaticPath() so that it can be reused across
22
- * responses during dev and only ever called once during build.
21
+ * including the result of calling getStaticPaths(). This gives route matching,
22
+ * params/props resolution, and prerender generation a shared static-path table.
23
+ *
24
+ * In dev, this cache intentionally survives requests. It is invalidated by route
25
+ * module identity changes after HMR or by explicit cache clears from content data
26
+ * changes, not by each request. Dev route matching can call getStaticPaths()
27
+ * before rendering, and render-time props resolution may ask for it again.
23
28
  */
24
29
  export declare class RouteCache {
25
30
  private logger;
@@ -158,13 +158,14 @@ function createFileBasedRoutes({ settings, cwd, fsMod }, logger) {
158
158
  } else {
159
159
  const component = item.file;
160
160
  const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
161
+ const route = joinSegments(segments);
161
162
  const trailingSlash = trailingSlashForPath(
162
163
  pathname,
163
164
  settings.config,
164
- item.isPage ? "page" : "endpoint"
165
+ item.isPage ? "page" : "endpoint",
166
+ route
165
167
  );
166
168
  const pattern = getPattern(segments, settings.config.base, trailingSlash);
167
- const route = joinSegments(segments);
168
169
  routes.push({
169
170
  route,
170
171
  isIndex: item.isIndex,
@@ -277,13 +278,14 @@ function createRoutesFromEntriesByDir(entriesByDir, settings, logger, pagesDirRe
277
278
  } else {
278
279
  const component = item.file;
279
280
  const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
281
+ const route = joinSegments(segments);
280
282
  const trailingSlash = trailingSlashForPath(
281
283
  pathname,
282
284
  settings.config,
283
- item.isPage ? "page" : "endpoint"
285
+ item.isPage ? "page" : "endpoint",
286
+ route
284
287
  );
285
288
  const pattern = getPattern(segments, settings.config.base, trailingSlash);
286
- const route = joinSegments(segments);
287
289
  routes.push({
288
290
  route,
289
291
  isIndex: item.isIndex,
@@ -320,7 +322,7 @@ function groupEntriesByDir(entries) {
320
322
  }
321
323
  return entriesByDir;
322
324
  }
323
- const trailingSlashForPath = (pathname, config, type) => type === "endpoint" && pathname && hasFileExtension(pathname) ? "never" : config.trailingSlash;
325
+ const trailingSlashForPath = (pathname, config, type, route) => type === "endpoint" && hasFileExtension(pathname ?? route ?? "") ? "never" : config.trailingSlash;
324
326
  function createInjectedRoutes({ settings, cwd }) {
325
327
  const { config } = settings;
326
328
  const prerender = getPrerenderDefault(config);
@@ -334,10 +336,10 @@ function createInjectedRoutes({ settings, cwd }) {
334
336
  });
335
337
  const type = resolved.endsWith(".astro") ? "page" : "endpoint";
336
338
  const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
337
- const trailingSlash = trailingSlashForPath(pathname, config, type);
339
+ const route = joinSegments(segments);
340
+ const trailingSlash = trailingSlashForPath(pathname, config, type, route);
338
341
  const pattern = getPattern(segments, settings.config.base, trailingSlash);
339
342
  const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
340
- const route = joinSegments(segments);
341
343
  routes.push({
342
344
  type,
343
345
  // For backwards compatibility, an injected route is never considered an index route.
@@ -191,6 +191,16 @@ function onPageLoad(cb) {
191
191
  }
192
192
  cb();
193
193
  });
194
+ new MutationObserver((mutations) => {
195
+ for (const mutation of mutations) {
196
+ for (const node of mutation.addedNodes) {
197
+ if (node instanceof Element && (node.tagName === "A" || node.querySelector?.("a"))) {
198
+ cb();
199
+ return;
200
+ }
201
+ }
202
+ }
203
+ }).observe(document.body, { childList: true, subtree: true });
194
204
  }
195
205
  function appendSpeculationRules(url, eagerness) {
196
206
  const script = document.createElement("script");
@@ -200,6 +200,32 @@ function isInteractive(element) {
200
200
  }
201
201
  return true;
202
202
  }
203
+ function hasAccessibleName(element) {
204
+ return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby");
205
+ }
206
+ const conditional_implicit_roles = /* @__PURE__ */ new Map([
207
+ ["a", (el) => el.hasAttribute("href") ? "link" : null],
208
+ [
209
+ "aside",
210
+ (el) => {
211
+ const isNested = !!el.parentElement?.closest("article, aside, nav, section");
212
+ return !isNested || hasAccessibleName(el) ? "complementary" : null;
213
+ }
214
+ ],
215
+ [
216
+ "img",
217
+ (el) => {
218
+ if (el.getAttribute("alt") === "") return null;
219
+ return "image";
220
+ }
221
+ ],
222
+ [
223
+ "section",
224
+ (el) => {
225
+ return hasAccessibleName(el) ? "region" : null;
226
+ }
227
+ ]
228
+ ]);
203
229
  const a11y = [
204
230
  {
205
231
  code: "a11y-accesskey",
@@ -378,19 +404,33 @@ const a11y = [
378
404
  code: "a11y-no-redundant-roles",
379
405
  title: "HTML element has redundant ARIA roles",
380
406
  message: "Giving these elements an ARIA role that is already set by the browser has no effect and is redundant.",
381
- selector: [...a11y_implicit_semantics.keys()].join(","),
407
+ // The selector is all elements that have an implicit role, as well as input elements since their implicit role can change based on their attributes
408
+ selector: `[role]:is(${[...a11y_implicit_semantics.keys()].join(",")},input)`,
382
409
  match(element) {
383
410
  const role = element.getAttribute("role");
384
- if (element.localName === "input") {
385
- const type = element.getAttribute("type");
386
- if (!type) return true;
411
+ if (!role) return false;
412
+ const localName = element.localName;
413
+ if (
414
+ // <ul>, <ol>, and <li> are legitimate workarounds to restore list semantics
415
+ // that some browsers (e.g., Safari) strip when CSS `list-style: none` is applied.
416
+ // Reference: https://bugs.webkit.org/show_bug.cgi?id=170179
417
+ localName === "ul" && role === "list" || localName === "ol" && role === "list" || localName === "li" && role === "listitem"
418
+ ) {
419
+ return false;
420
+ }
421
+ if (localName === "input") {
422
+ const type = element.getAttribute("type") || "text";
387
423
  const implicitRoleForType = input_type_to_implicit_role.get(type);
388
- if (!implicitRoleForType) return true;
389
- if (role === implicitRoleForType) return false;
424
+ return role === implicitRoleForType;
425
+ }
426
+ const getConditionalRole = conditional_implicit_roles.get(localName);
427
+ if (getConditionalRole) {
428
+ const effectiveRole = getConditionalRole(element);
429
+ return effectiveRole ? role === effectiveRole : false;
390
430
  }
391
- const implicitRole = a11y_implicit_semantics.get(element.localName);
392
- if (!implicitRole) return true;
393
- if (role === implicitRole) return false;
431
+ const implicitRole = a11y_implicit_semantics.get(localName);
432
+ if (!implicitRole) return false;
433
+ return role === implicitRole;
394
434
  }
395
435
  },
396
436
  {
@@ -116,7 +116,7 @@ async function generateHydrateScript(scriptOptions, metadata) {
116
116
  );
117
117
  transitionDirectivesToCopyOnIsland.forEach((name) => {
118
118
  if (typeof props[name] !== "undefined") {
119
- island.props[name] = props[name];
119
+ island.props[name] = escapeHTML(String(props[name]));
120
120
  }
121
121
  });
122
122
  return island;
@@ -18,6 +18,9 @@ class AstroComponentInstance {
18
18
  for (const name in slots) {
19
19
  let didRender = false;
20
20
  let value = slots[name](result);
21
+ if (result._metadata.routeHasPropagation && isPromise(value)) {
22
+ result._metadata.pendingSlotEvaluations.push(value);
23
+ }
21
24
  this.slotValues[name] = () => {
22
25
  if (!didRender) {
23
26
  didRender = true;
@@ -1,4 +1,5 @@
1
1
  import { isRoute404, isRoute500 } from "../../../core/routing/internal/route-errors.js";
2
+ import { isPropagatingHint } from "../../../core/head-propagation/resolver.js";
2
3
  import { renderToAsyncIterable, renderToReadableStream, renderToString } from "./astro/render.js";
3
4
  import { encoder } from "./common.js";
4
5
  import { renderComponentToString } from "./component.js";
@@ -7,7 +8,9 @@ import { isDeno, isNode } from "./util.js";
7
8
  import { isAstroComponentFactory } from "./astro/factory.js";
8
9
  async function renderPage(result, componentFactory, props, children, streaming, route) {
9
10
  if (!isAstroComponentFactory(componentFactory)) {
10
- result._metadata.headInTree = result.componentMetadata.get(componentFactory.moduleId)?.containsHead ?? false;
11
+ const nonAstroMeta = result.componentMetadata.get(componentFactory.moduleId);
12
+ result._metadata.headInTree = nonAstroMeta?.containsHead ?? false;
13
+ result._metadata.routeHasPropagation = isPropagatingHint(nonAstroMeta?.propagation ?? "none");
11
14
  const pageProps = { ...props ?? {}, "server:root": true };
12
15
  const str = await renderComponentToString(
13
16
  result,
@@ -31,7 +34,9 @@ async function renderPage(result, componentFactory, props, children, streaming,
31
34
  status: result.response.status
32
35
  });
33
36
  }
34
- result._metadata.headInTree = result.componentMetadata.get(componentFactory.moduleId)?.containsHead ?? false;
37
+ const pageMeta = result.componentMetadata.get(componentFactory.moduleId);
38
+ result._metadata.headInTree = pageMeta?.containsHead ?? false;
39
+ result._metadata.routeHasPropagation = isPropagatingHint(pageMeta?.propagation ?? "none");
35
40
  let body;
36
41
  if (streaming) {
37
42
  if (isNode && !isDeno) {
@@ -0,0 +1 @@
1
+ export declare const SERVER_ISLAND_START = "[if astro]>server-island-start<![endif]";
@@ -0,0 +1,4 @@
1
+ const SERVER_ISLAND_START = "[if astro]>server-island-start<![endif]";
2
+ export {
3
+ SERVER_ISLAND_START
4
+ };
@@ -3,6 +3,7 @@ import { markHTMLString, stringifyForScript } from "../escape.js";
3
3
  import { renderChild } from "./any.js";
4
4
  import { createThinHead } from "./astro/head-and-content.js";
5
5
  import { createRenderInstruction } from "./instruction.js";
6
+ import { SERVER_ISLAND_START } from "./server-islands-shared.js";
6
7
  import { renderSlotToString } from "./slot.js";
7
8
  const internalProps = /* @__PURE__ */ new Set([
8
9
  "server:component-path",
@@ -56,7 +57,7 @@ class ServerIslandComponent {
56
57
  const hostId = await this.getHostId();
57
58
  const islandContent = await this.getIslandContent();
58
59
  destination.write(createRenderInstruction({ type: "server-island-runtime" }));
59
- destination.write("<!--[if astro]>server-island-start<![endif]-->");
60
+ destination.write(`<!--${SERVER_ISLAND_START}-->`);
60
61
  for (const name in this.slots) {
61
62
  if (name === "fallback") {
62
63
  await renderChild(destination, this.slots.fallback(this.result));
@@ -186,7 +187,7 @@ const SERVER_ISLAND_REPLACER = markHTMLString(
186
187
  // Load the HTML before modifying the DOM in case of errors
187
188
  let html = await r.text();
188
189
  // Remove any placeholder content before the island script
189
- while (s.previousSibling && s.previousSibling.nodeType !== 8 && s.previousSibling.data !== '[if astro]>server-island-start<![endif]')
190
+ while (s.previousSibling && s.previousSibling.nodeType !== 8 && s.previousSibling.data !== '${SERVER_ISLAND_START}')
190
191
  s.previousSibling.remove();
191
192
  s.previousSibling?.remove();
192
193
  // Insert the new HTML
@@ -10,7 +10,7 @@ function template({
10
10
  return `<!doctype html>
11
11
  <html lang="en">
12
12
  <head>
13
- <meta charset="UTF-8">
13
+ <meta charset="utf-8">
14
14
  <title>${tabTitle}</title>
15
15
  <style>
16
16
  :root {
@@ -1,3 +1,4 @@
1
+ import { SERVER_ISLAND_START } from "../runtime/server/render/server-islands-shared.js";
1
2
  const PERSIST_ATTR = "data-astro-transition-persist";
2
3
  const NON_OVERRIDABLE_ASTRO_ATTRS = ["data-astro-transition", "data-astro-transition-fallback"];
3
4
  const knownVueScopedStyles = /* @__PURE__ */ new Map();
@@ -29,6 +30,11 @@ function swapRootAttributes(newDoc) {
29
30
  );
30
31
  }
31
32
  function swapHeadElements(doc) {
33
+ const relevantNodes = (parent, what = "both") => [
34
+ ...[...parent.childNodes].filter(
35
+ (child) => child.nodeType === 8 && child.textContent === SERVER_ISLAND_START || what === "both" && child.nodeType === 1
36
+ )
37
+ ];
32
38
  for (const el of Array.from(document.head.children)) {
33
39
  const newEl = persistedHeadElement(el, doc);
34
40
  if (newEl) {
@@ -41,12 +47,13 @@ function swapHeadElements(doc) {
41
47
  el.remove();
42
48
  }
43
49
  }
50
+ relevantNodes(document.head, "commentsOnly").forEach((node) => node.remove());
44
51
  if (import.meta.env.DEV) {
45
- [...doc.head.children].forEach((child) => {
52
+ relevantNodes(doc.head).forEach((child) => {
46
53
  document.head.append(knownVueScopedStyles.get(child.dataset?.viteDevId) || child);
47
54
  });
48
55
  } else {
49
- document.head.append(...doc.head.children);
56
+ document.head.append(...relevantNodes(doc.head));
50
57
  }
51
58
  }
52
59
  function swapBodyElement(newElement, oldElement) {
@@ -280,6 +280,20 @@ export interface SSRMetadata {
280
280
  extraStyleHashes: string[];
281
281
  extraScriptHashes: string[];
282
282
  propagators: Set<AstroComponentInstance | ServerIslandComponent>;
283
+ /**
284
+ * `true` when the page being rendered is on a head-propagation path (its
285
+ * component metadata hint is `in-tree`/`self`). Only then do we await async
286
+ * slot pre-renders before collecting head content, so that propagating
287
+ * components hidden behind an `await` in slot markup are discovered in time.
288
+ * Pages with no propagation keep streaming without paying that cost.
289
+ */
290
+ routeHasPropagation: boolean;
291
+ /**
292
+ * Promises from async slot pre-renders that may still need to register
293
+ * propagating components. Drained by `collectPropagatedHeadParts` before
294
+ * head content is flushed. Only populated when `routeHasPropagation` is true.
295
+ */
296
+ pendingSlotEvaluations: Promise<unknown>[];
283
297
  /**
284
298
  * Tracks nesting depth of HTML `<template>` elements during rendering.
285
299
  * Scripts rendered inside `<template>` tags should not be deduplicated,
@@ -55,7 +55,9 @@ function astro({ settings, logger }) {
55
55
  viteConfig.resolve.conditions = [...defaultServerConditions];
56
56
  }
57
57
  }
58
- viteConfig.resolve.conditions.push("astro");
58
+ if (!viteConfig.resolve.conditions.includes("astro")) {
59
+ viteConfig.resolve.conditions.push("astro");
60
+ }
59
61
  },
60
62
  async configResolved(viteConfig) {
61
63
  const toolbarEnabled = await settings.preferences.get("devToolbar.enabled");
@@ -1,6 +1,24 @@
1
1
  import { type Plugin as VitePlugin } from 'vite';
2
2
  import type { AstroSettings } from '../types/astro.js';
3
- /** Returns Vite plugins used to alias paths from tsconfig.json and jsconfig.json. */
3
+ /**
4
+ * Fallback for tsconfig path aliases that Vite's `resolve.tsconfigPaths` does
5
+ * not currently handle in Astro's pipeline.
6
+ *
7
+ * This plugin is intentionally limited to the syntax Astro already supported
8
+ * before enabling `resolve.tsconfigPaths`:
9
+ * - CSS: `@import "..."`
10
+ * - CSS: `@import url("...")`
11
+ * - CSS: quoted `url("...")` references
12
+ * - Modules: JS, TS, and Astro import specifiers handled by `resolveId`
13
+ *
14
+ * Do not expand this fallback to new CSS at-rules or preprocessor syntax. It
15
+ * does not support `@use`, `@forward`, `@reference`, `@config`, unquoted
16
+ * `url(...)`, or every place a CSS tool might accept a file path. Those should
17
+ * be handled by Vite's native resolver instead.
18
+ *
19
+ * @deprecated This fallback will be removed in a future Astro version once Vite
20
+ * handles these remaining alias paths.
21
+ */
4
22
  export default function configAliasVitePlugin({ settings, }: {
5
23
  settings: AstroSettings;
6
24
  }): VitePlugin[] | null;
@@ -51,10 +51,10 @@ function configAliasVitePlugin({
51
51
  const configAlias = getConfigAlias(settings);
52
52
  if (!configAlias) return null;
53
53
  return [
54
- // Pre-plugin: rewrite CSS @import aliases to absolute paths before Vite's CSS plugin.
55
- // Vite's internal CSS @import resolver (postcss-import) uses a mini plugin container
56
- // that doesn't include user resolveId hooks, so we must rewrite aliases in a transform
57
- // hook that runs before Vite's CSS processing.
54
+ // Deprecated CSS fallback for Vite's transform pipeline. Only supports
55
+ // `@import "..."`, `@import url("...")`, and quoted `url("...")`.
56
+ // Do not add support here for `@use`, `@forward`, `@reference`, `@config`,
57
+ // or other CSS/preprocessor file-reference syntax.
58
58
  {
59
59
  name: "astro:tsconfig-alias-css",
60
60
  enforce: "pre",
@@ -88,7 +88,8 @@ function configAliasVitePlugin({
88
88
  }
89
89
  }
90
90
  },
91
- // Post-plugin: resolve JS/TS imports using tsconfig path aliases via resolveId.
91
+ // Deprecated module fallback for JS, TS, and Astro import specifiers that
92
+ // Vite's native tsconfig path resolution does not currently resolve.
92
93
  {
93
94
  name: "astro:tsconfig-alias",
94
95
  // use post to only resolve ids that all other plugins before it can't
@@ -1,13 +1,15 @@
1
1
  import MagicString from "magic-string";
2
- import { rehype } from "rehype";
3
- import { VFile } from "vfile";
4
- import escape from "./escape.js";
5
- import slots, { SLOT_PREFIX } from "./slots.js";
2
+ import { collectSlots, SLOT_PREFIX } from "./slots.js";
3
+ import { escapeTemplateLiteralCharacters, needsEscape } from "./utils.js";
6
4
  async function transform(code, id) {
7
5
  const s = new MagicString(code, { filename: id });
8
- const parser = rehype().data("settings", { fragment: true }).use(escape, { s }).use(slots, { s });
9
- const vfile = new VFile({ value: code, path: id });
10
- await parser.process(vfile);
6
+ let cursor = 0;
7
+ for (const slot of collectSlots(code)) {
8
+ escapeRange(s, code, cursor, slot.start);
9
+ s.overwrite(slot.start, slot.end, slot.value);
10
+ cursor = slot.end;
11
+ }
12
+ escapeRange(s, code, cursor, code.length);
11
13
  s.prepend(`function render({ slots: ${SLOT_PREFIX} }) {
12
14
  return \``);
13
15
  s.append('`\n }\nrender["astro:html"] = true;\nexport default render;');
@@ -16,6 +18,13 @@ async function transform(code, id) {
16
18
  map: s.generateMap({ hires: "boundary" })
17
19
  };
18
20
  }
21
+ function escapeRange(s, code, start, end) {
22
+ if (start >= end) return;
23
+ const segment = code.slice(start, end);
24
+ if (needsEscape(segment)) {
25
+ s.overwrite(start, end, escapeTemplateLiteralCharacters(segment));
26
+ }
27
+ }
19
28
  export {
20
29
  transform
21
30
  };
@@ -1,8 +1,11 @@
1
- import type { Root } from 'hast';
2
- import type MagicString from 'magic-string';
3
- import type { Plugin } from 'unified';
4
- declare const rehypeSlots: Plugin<[{
5
- s: MagicString;
6
- }], Root>;
7
- export default rehypeSlots;
8
1
  export declare const SLOT_PREFIX = "___SLOTS___";
2
+ export interface SlotReplacement {
3
+ start: number;
4
+ end: number;
5
+ value: string;
6
+ }
7
+ /**
8
+ * Find every `<slot>` (excluding `is:inline` ones) and describe how to replace it
9
+ * with a `${___SLOTS___[name] ?? `fallback`}` template literal expression.
10
+ */
11
+ export declare function collectSlots(code: string): SlotReplacement[];
@@ -1,31 +1,24 @@
1
- import { visit } from "unist-util-visit";
1
+ import { ELEMENT_NODE, parse, walkSync } from "ultrahtml";
2
2
  import { escapeTemplateLiteralCharacters } from "./utils.js";
3
- const rehypeSlots = ({ s }) => {
4
- return (tree, file) => {
5
- visit(tree, (node) => {
6
- if (node.type === "element" && node.tagName === "slot") {
7
- if (typeof node.properties?.["is:inline"] !== "undefined") return;
8
- const name = node.properties?.["name"] ?? "default";
9
- const start = node.position?.start.offset ?? 0;
10
- const end = node.position?.end.offset ?? 0;
11
- const first = node.children.at(0) ?? node;
12
- const last = node.children.at(-1) ?? node;
13
- const text = file.value.slice(
14
- first.position?.start.offset ?? 0,
15
- last.position?.end.offset ?? 0
16
- ).toString();
17
- s.overwrite(
18
- start,
19
- end,
20
- `\${${SLOT_PREFIX}["${name}"] ?? \`${escapeTemplateLiteralCharacters(text).trim()}\`}`
21
- );
22
- }
23
- });
24
- };
25
- };
26
- var slots_default = rehypeSlots;
27
3
  const SLOT_PREFIX = `___SLOTS___`;
4
+ function collectSlots(code) {
5
+ const slots = [];
6
+ walkSync(parse(code), (node) => {
7
+ if (node.type !== ELEMENT_NODE || node.name !== "slot") return;
8
+ if ("is:inline" in node.attributes) return;
9
+ const [open, close] = node.loc;
10
+ if (!close) return;
11
+ const name = node.attributes.name ?? "default";
12
+ const fallback = open.end < close.start ? code.slice(open.end, close.start) : code.slice(open.start, close.end);
13
+ slots.push({
14
+ start: open.start,
15
+ end: close.end,
16
+ value: `\${${SLOT_PREFIX}["${name}"] ?? \`${escapeTemplateLiteralCharacters(fallback).trim()}\`}`
17
+ });
18
+ });
19
+ return slots;
20
+ }
28
21
  export {
29
22
  SLOT_PREFIX,
30
- slots_default as default
23
+ collectSlots
31
24
  };
@@ -1,5 +1,2 @@
1
- import type { Element } from 'hast';
2
- import type MagicString from 'magic-string';
3
- export declare function replaceAttribute(s: MagicString, node: Element, key: string, newValue: string): MagicString | undefined;
4
1
  export declare function needsEscape(value: any): value is string;
5
2
  export declare function escapeTemplateLiteralCharacters(value: string): string;
@@ -1,21 +1,3 @@
1
- const splitAttrsTokenizer = /([${}@\w:\-]*)\s*=\s*?(['"]?)(.*?)\2\s+/g;
2
- function replaceAttribute(s, node, key, newValue) {
3
- splitAttrsTokenizer.lastIndex = 0;
4
- const text = s.original.slice(node.position?.start.offset ?? 0, node.position?.end.offset ?? 0).toString();
5
- const offset = text.indexOf(key);
6
- if (offset === -1) return;
7
- const start = node.position.start.offset + offset;
8
- const tokens = text.slice(offset).split(splitAttrsTokenizer);
9
- const token = tokens[0].replace(/([^>])>[\s\S]*$/gm, "$1");
10
- if (token.trim() === key) {
11
- const end = start + key.length;
12
- return s.overwrite(start, end, newValue, { contentOnly: true });
13
- } else {
14
- const length = token.length;
15
- const end = start + length;
16
- return s.overwrite(start, end, newValue, { contentOnly: true });
17
- }
18
- }
19
1
  const NEEDS_ESCAPE_RE = /[`\\]|\$\{/g;
20
2
  function needsEscape(value) {
21
3
  NEEDS_ESCAPE_RE.lastIndex = 0;
@@ -42,6 +24,5 @@ function escapeTemplateLiteralCharacters(value) {
42
24
  }
43
25
  export {
44
26
  escapeTemplateLiteralCharacters,
45
- needsEscape,
46
- replaceAttribute
27
+ needsEscape
47
28
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "7.0.3",
3
+ "version": "7.0.5",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -12,23 +12,6 @@
12
12
  },
13
13
  "bugs": "https://github.com/withastro/astro/issues",
14
14
  "homepage": "https://astro.build",
15
- "types": "./dist/index.d.ts",
16
- "typesVersions": {
17
- "*": {
18
- "app": [
19
- "./dist/core/app/index"
20
- ],
21
- "app/*": [
22
- "./dist/core/app/*"
23
- ],
24
- "hono": [
25
- "./dist/core/hono/index"
26
- ],
27
- "middleware": [
28
- "./dist/virtual-modules/middleware.d.ts"
29
- ]
30
- }
31
- },
32
15
  "exports": {
33
16
  ".": "./dist/index.js",
34
17
  "./env": "./env.d.ts",
@@ -113,7 +96,7 @@
113
96
  "README.md"
114
97
  ],
115
98
  "dependencies": {
116
- "@astrojs/compiler-rs": "^0.2.2",
99
+ "@astrojs/compiler-rs": "^0.3.0",
117
100
  "@capsizecss/unpack": "^4.0.0",
118
101
  "@clack/prompts": "^1.1.0",
119
102
  "@oslojs/encoding": "^1.1.0",
@@ -148,7 +131,6 @@
148
131
  "package-manager-detector": "^1.6.0",
149
132
  "piccolore": "^0.1.3",
150
133
  "picomatch": "^4.0.4",
151
- "rehype": "^13.0.2",
152
134
  "semver": "^7.7.4",
153
135
  "shiki": "^4.0.2",
154
136
  "smol-toml": "^1.6.0",
@@ -158,20 +140,18 @@
158
140
  "tinyglobby": "^0.2.15",
159
141
  "ultrahtml": "^1.6.0",
160
142
  "unifont": "~0.7.4",
161
- "unist-util-visit": "^5.1.0",
162
143
  "unstorage": "^1.17.5",
163
- "vfile": "^6.0.3",
164
144
  "vite": "^8.0.13",
165
145
  "vitefu": "^1.1.2",
166
146
  "xxhash-wasm": "^1.1.0",
167
147
  "yargs-parser": "^22.0.0",
168
148
  "zod": "^4.3.6",
169
- "@astrojs/markdown-satteri": "0.3.2",
170
149
  "@astrojs/internal-helpers": "0.10.0",
150
+ "@astrojs/markdown-satteri": "0.3.2",
171
151
  "@astrojs/telemetry": "3.3.2"
172
152
  },
173
153
  "optionalDependencies": {
174
- "sharp": "^0.34.0"
154
+ "sharp": "^0.34.0 || ^0.35.0"
175
155
  },
176
156
  "peerDependencies": {
177
157
  "@astrojs/markdown-remark": "7.2.0"
@@ -184,7 +164,6 @@
184
164
  "devDependencies": {
185
165
  "@playwright/test": "1.58.2",
186
166
  "@types/aria-query": "^5.0.4",
187
- "@types/hast": "^3.0.4",
188
167
  "@types/html-escaper": "3.0.4",
189
168
  "@types/http-cache-semantics": "^4.2.0",
190
169
  "@types/js-yaml": "^4.0.9",
@@ -206,7 +185,6 @@
206
185
  "sass": "^1.98.0",
207
186
  "typescript": "^6.0.3",
208
187
  "undici": "^7.22.0",
209
- "unified": "^11.0.5",
210
188
  "vitest": "^4.1.0",
211
189
  "@astrojs/check": "0.9.9",
212
190
  "@astrojs/markdown-remark": "7.2.0",
@@ -1,7 +0,0 @@
1
- import type { Root } from 'hast';
2
- import type MagicString from 'magic-string';
3
- import type { Plugin } from 'unified';
4
- declare const rehypeEscape: Plugin<[{
5
- s: MagicString;
6
- }], Root>;
7
- export default rehypeEscape;
@@ -1,30 +0,0 @@
1
- import { visit } from "unist-util-visit";
2
- import { escapeTemplateLiteralCharacters, needsEscape, replaceAttribute } from "./utils.js";
3
- const rehypeEscape = ({ s }) => {
4
- return (tree) => {
5
- visit(tree, (node) => {
6
- if (node.type === "text" || node.type === "comment") {
7
- if (needsEscape(node.value)) {
8
- s.overwrite(
9
- node.position.start.offset,
10
- node.position.end.offset,
11
- escapeTemplateLiteralCharacters(node.value)
12
- );
13
- }
14
- } else if (node.type === "element") {
15
- if (!node.properties) return;
16
- for (let [key, value] of Object.entries(node.properties)) {
17
- key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
18
- const newKey = needsEscape(key) ? escapeTemplateLiteralCharacters(key) : key;
19
- const newValue = needsEscape(value) ? escapeTemplateLiteralCharacters(value) : value;
20
- if (newKey === key && newValue === value) continue;
21
- replaceAttribute(s, node, key, value === "" ? newKey : `${newKey}="${newValue}"`);
22
- }
23
- }
24
- });
25
- };
26
- };
27
- var escape_default = rehypeEscape;
28
- export {
29
- escape_default as default
30
- };