astro 6.4.6 → 6.4.7

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.
@@ -1,11 +1,11 @@
1
1
  import fs, { readFileSync } from "node:fs";
2
2
  import { basename } from "node:path/posix";
3
3
  import colors from "piccolore";
4
- import { getOutDirWithinCwd } from "../../core/build/common.js";
5
4
  import { getTimeStat } from "../../core/build/util.js";
6
5
  import { AstroError } from "../../core/errors/errors.js";
7
6
  import { AstroErrorData } from "../../core/errors/index.js";
8
7
  import { isRemotePath, removeLeadingForwardSlash } from "../../core/path.js";
8
+ import { getClientOutputDirectory } from "../../prerender/utils.js";
9
9
  import { getConfiguredImageService } from "../internal.js";
10
10
  import { isESMImportedImage } from "../utils/imageKind.js";
11
11
  import { loadRemoteImage, revalidateRemoteImage } from "./remote.js";
@@ -29,8 +29,9 @@ async function prepareAssetsGenerationEnv(options, totalCount) {
29
29
  serverRoot = new URL(".prerender/", settings.config.build.server);
30
30
  clientRoot = settings.config.build.client;
31
31
  } else {
32
- serverRoot = getOutDirWithinCwd(settings.config.outDir);
33
- clientRoot = settings.config.outDir;
32
+ const clientOutputDir = getClientOutputDirectory(settings);
33
+ serverRoot = clientOutputDir;
34
+ clientRoot = clientOutputDir;
34
35
  }
35
36
  return {
36
37
  logger,
@@ -62,6 +62,7 @@ export default async function seed() {
62
62
  }
63
63
  `,
64
64
  CLOUDFLARE_WRANGLER_CONFIG: (name, compatibilityDate) => `{
65
+ "$schema": "./node_modules/wrangler/config-schema.json",
65
66
  "compatibility_date": ${JSON.stringify(compatibilityDate)},
66
67
  "compatibility_flags": ["global_fetch_strictly_public"],
67
68
  "name": ${JSON.stringify(name)},
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.4.6";
3
+ version = "6.4.7";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -67,7 +67,7 @@ export type ContainerRenderOptions = {
67
67
  */
68
68
  routeType?: RouteType;
69
69
  /**
70
- * Allows to pass `Astro.props` to an Astro component:
70
+ * Allows passing `Astro.props` to an Astro component:
71
71
  *
72
72
  * ```js
73
73
  * container.renderToString(Endpoint, { props: { "lorem": "ipsum" } });
@@ -75,9 +75,9 @@ export type ContainerRenderOptions = {
75
75
  */
76
76
  props?: Props;
77
77
  /**
78
- * When `false`, it forces to render the component as it was a full-fledged page.
78
+ * When `false`, it forces the component to render as if it were a full-fledged page.
79
79
  *
80
- * By default, the container API render components as [partials](https://docs.astro.build/en/basics/astro-pages/#page-partials).
80
+ * By default, the container API renders components as [partials](https://docs.astro.build/en/basics/astro-pages/#page-partials).
81
81
  *
82
82
  */
83
83
  partial?: boolean;
@@ -197,7 +197,7 @@ ${contentConfig.error.message}`
197
197
  logger.info("Content config changed");
198
198
  shouldClear = true;
199
199
  }
200
- if (previousAstroVersion && previousAstroVersion !== "6.4.6") {
200
+ if (previousAstroVersion && previousAstroVersion !== "6.4.7") {
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 ("6.4.6") {
209
- this.#store.metaStore().set("astro-version", "6.4.6");
208
+ if ("6.4.7") {
209
+ this.#store.metaStore().set("astro-version", "6.4.7");
210
210
  }
211
211
  if (currentConfigDigest) {
212
212
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -70,7 +70,7 @@ type RenderResult = {
70
70
  };
71
71
  export declare function updateImageReferencesInData<T extends Record<string, unknown>>(data: T, fileName?: string, imageAssetMap?: Map<string, ImageMetadata>): T;
72
72
  export declare function renderEntry(entry: DataEntry): Promise<RenderResult>;
73
- export declare function createReference(): (collection: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
73
+ export declare function createReference(): (collection: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodPipe<z.ZodNumber, z.ZodTransform<string, number>>, z.ZodString, z.ZodObject<{
74
74
  id: z.ZodString;
75
75
  collection: z.ZodString;
76
76
  }, z.core.$strip>, z.ZodObject<{
@@ -484,6 +484,7 @@ async function render({
484
484
  function createReference() {
485
485
  return function reference(collection) {
486
486
  return z.union([
487
+ z.number().transform((num) => num.toString(10)),
487
488
  z.string(),
488
489
  z.object({
489
490
  id: z.string(),
@@ -14,7 +14,6 @@ import { DefaultFetchHandler } from "../fetch/default-handler.js";
14
14
  import { appSymbol } from "../constants.js";
15
15
  import { DefaultErrorHandler } from "../errors/default-handler.js";
16
16
  import { setRenderOptions } from "./render-options.js";
17
- import { MultiLevelEncodingError } from "../util/pathname.js";
18
17
  class BaseApp {
19
18
  manifest;
20
19
  manifestData;
@@ -237,20 +236,13 @@ class BaseApp {
237
236
  waitUntil
238
237
  };
239
238
  let response;
240
- try {
241
- if (this.#fetchHandler instanceof DefaultFetchHandler) {
242
- Reflect.set(request, appSymbol, this);
243
- response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
244
- } else {
245
- setRenderOptions(request, resolvedOptions);
246
- Reflect.set(request, appSymbol, this);
247
- response = await this.#fetchHandler.fetch(request);
248
- }
249
- } catch (err) {
250
- if (err instanceof MultiLevelEncodingError) {
251
- return new Response("Bad Request", { status: 400 });
252
- }
253
- throw err;
239
+ if (this.#fetchHandler instanceof DefaultFetchHandler) {
240
+ Reflect.set(request, appSymbol, this);
241
+ response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
242
+ } else {
243
+ setRenderOptions(request, resolvedOptions);
244
+ Reflect.set(request, appSymbol, this);
245
+ response = await this.#fetchHandler.fetch(request);
254
246
  }
255
247
  this.#warnMissingFeatures();
256
248
  if (response.headers.get(ASTRO_ERROR_HEADER)) {
@@ -107,6 +107,7 @@ function rollupPluginAstroBuildCSS(options) {
107
107
  if (meta.importedCss.size < 1) continue;
108
108
  if (this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
109
109
  for (const id of Object.keys(chunk.modules)) {
110
+ if (!isCSSRequest(id)) continue;
110
111
  for (const pageData of getParentClientOnlys(id, this, internals)) {
111
112
  for (const importedCssImport of meta.importedCss) {
112
113
  const cssToInfoRecord = pagesToCss[pageData.moduleSpecifier] ??= {};
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.4.6";
1
+ const ASTRO_VERSION = "6.4.7";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
4
4
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
@@ -37,7 +37,7 @@ async function dev(inlineConfig) {
37
37
  await telemetry.record([]);
38
38
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
39
39
  const logger = restart.container.logger;
40
- const currentVersion = "6.4.6";
40
+ const currentVersion = "6.4.7";
41
41
  const isPrerelease = currentVersion.includes("-");
42
42
  if (!isPrerelease) {
43
43
  try {
@@ -29,6 +29,7 @@ import { getParams, getProps } from "../render/index.js";
29
29
  import { Rewrites } from "../rewrites/handler.js";
30
30
  import { isRoute404or500, isRouteServerIsland } from "../routing/match.js";
31
31
  import { normalizeUrl } from "../util/normalized-url.js";
32
+ import { validateAndDecodePathname } from "../util/pathname.js";
32
33
  import { getOriginPathname, setOriginPathname } from "../routing/rewrite.js";
33
34
  import { computePathnameFromDomain } from "../i18n/domain.js";
34
35
  import { getCustom404Route, routeHasHtmlExtension } from "../routing/helpers.js";
@@ -678,7 +679,7 @@ class FetchState {
678
679
  }
679
680
  pathname = prependForwardSlash(pathname);
680
681
  try {
681
- return decodeURI(pathname);
682
+ return validateAndDecodePathname(pathname);
682
683
  } catch (e) {
683
684
  this.pipeline.logger.error(null, e.toString());
684
685
  return pathname;
@@ -276,7 +276,7 @@ function printHelp({
276
276
  message.push(
277
277
  linebreak(),
278
278
  ` ${bgGreen(black(` ${commandName} `))} ${green(
279
- `v${"6.4.6"}`
279
+ `v${"6.4.7"}`
280
280
  )} ${headline}`
281
281
  );
282
282
  }
@@ -3,6 +3,7 @@ import type { AstroSettings } from '../../types/astro.js';
3
3
  import type { BuildInternals } from '../build/internal.js';
4
4
  import type { StaticBuildOptions } from '../build/types.js';
5
5
  export declare const MIDDLEWARE_MODULE_ID = "virtual:astro:middleware";
6
+ export declare function isMiddlewarePath(relativePath: string): boolean;
6
7
  export declare function vitePluginMiddleware({ settings }: {
7
8
  settings: AstroSettings;
8
9
  }): VitePlugin;
@@ -11,6 +11,9 @@ import { normalizePath } from "../viteUtils.js";
11
11
  const MIDDLEWARE_MODULE_ID = "virtual:astro:middleware";
12
12
  const MIDDLEWARE_RESOLVED_MODULE_ID = "\0" + MIDDLEWARE_MODULE_ID;
13
13
  const NOOP_MIDDLEWARE = "\0noop-middleware";
14
+ function isMiddlewarePath(relativePath) {
15
+ return relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}.`) || relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}/`);
16
+ }
14
17
  function vitePluginMiddleware({ settings }) {
15
18
  let resolvedMiddlewareId = void 0;
16
19
  const hasIntegrationMiddleware = settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
@@ -26,7 +29,7 @@ function vitePluginMiddleware({ settings }) {
26
29
  const normalizedPath = viteNormalizePath(path);
27
30
  if (!normalizedPath.startsWith(normalizedSrcDir)) return;
28
31
  const relativePath = normalizedPath.slice(normalizedSrcDir.length);
29
- if (!relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}.`)) return;
32
+ if (!isMiddlewarePath(relativePath)) return;
30
33
  for (const name of [
31
34
  ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
32
35
  ASTRO_VITE_ENVIRONMENT_NAMES.astro
@@ -135,6 +138,7 @@ function vitePluginMiddlewareBuild(opts, internals) {
135
138
  }
136
139
  export {
137
140
  MIDDLEWARE_MODULE_ID,
141
+ isMiddlewarePath,
138
142
  vitePluginMiddleware,
139
143
  vitePluginMiddlewareBuild
140
144
  };
@@ -1,15 +1,12 @@
1
1
  import { collapseDuplicateSlashes } from "@astrojs/internal-helpers/path";
2
- import { MultiLevelEncodingError, validateAndDecodePathname } from "./pathname.js";
2
+ import { validateAndDecodePathname } from "./pathname.js";
3
3
  function createNormalizedUrl(requestUrl) {
4
4
  return normalizeUrl(new URL(requestUrl));
5
5
  }
6
6
  function normalizeUrl(url) {
7
7
  try {
8
8
  url.pathname = validateAndDecodePathname(url.pathname);
9
- } catch (e) {
10
- if (e instanceof MultiLevelEncodingError) {
11
- throw e;
12
- }
9
+ } catch {
13
10
  try {
14
11
  url.pathname = decodeURI(url.pathname);
15
12
  } catch {
@@ -2,18 +2,24 @@
2
2
  * Error thrown when multi-level URL encoding is detected in a pathname.
3
3
  * This is a distinct error type so callers can handle it specifically
4
4
  * (e.g., returning a 400 response) rather than falling back to partial decoding.
5
+ *
6
+ * @deprecated No longer thrown internally — multi-level encoding is now
7
+ * decoded iteratively instead of rejected. Kept for backwards compatibility
8
+ * in case third-party code references the class.
5
9
  */
6
10
  export declare class MultiLevelEncodingError extends Error {
7
11
  constructor();
8
12
  }
9
13
  /**
10
- * Validates that a pathname is not multi-level encoded.
11
- * Detects if a pathname contains encoding that was encoded again (e.g., %2561dmin where %25 decodes to %).
12
- * This prevents double/triple encoding bypasses of security checks.
14
+ * Decodes a pathname iteratively until stable, collapsing all levels of
15
+ * percent-encoding into a single canonical form. This prevents
16
+ * double/triple encoding from bypassing middleware authorization checks
17
+ * (CVE-2025-66202) — instead of rejecting multi-level encoding, we
18
+ * fully resolve it so middleware always sees the true decoded path.
13
19
  *
14
- * @param pathname - The pathname to validate
15
- * @returns The decoded pathname if valid
16
- * @throws MultiLevelEncodingError if multi-level encoding is detected
17
- * @throws Error if the pathname contains invalid URL encoding
20
+ * @param pathname - The pathname to decode
21
+ * @returns The fully decoded pathname
22
+ * @throws Error if the pathname contains invalid URL encoding that
23
+ * cannot be decoded at all (e.g., a bare `%` not followed by hex digits)
18
24
  */
19
25
  export declare function validateAndDecodePathname(pathname: string): string;
@@ -4,19 +4,22 @@ class MultiLevelEncodingError extends Error {
4
4
  this.name = "MultiLevelEncodingError";
5
5
  }
6
6
  }
7
- const ENCODING_REGEX = /%25[0-9a-fA-F]{2}/;
8
7
  function validateAndDecodePathname(pathname) {
9
- if (ENCODING_REGEX.test(pathname)) {
10
- throw new MultiLevelEncodingError();
11
- }
12
8
  let decoded;
13
9
  try {
14
10
  decoded = decodeURI(pathname);
15
11
  } catch (_e) {
16
12
  throw new Error("Invalid URL encoding");
17
13
  }
18
- if (ENCODING_REGEX.test(decoded)) {
19
- throw new MultiLevelEncodingError();
14
+ let iterations = 0;
15
+ while (decoded !== pathname && iterations < 10) {
16
+ pathname = decoded;
17
+ try {
18
+ decoded = decodeURI(pathname);
19
+ } catch {
20
+ break;
21
+ }
22
+ iterations++;
20
23
  }
21
24
  return decoded;
22
25
  }
@@ -1,4 +1,8 @@
1
- import { appendForwardSlash, joinPaths } from "@astrojs/internal-helpers/path";
1
+ import {
2
+ appendForwardSlash,
3
+ joinPaths,
4
+ removeTrailingForwardSlash
5
+ } from "@astrojs/internal-helpers/path";
2
6
  import { shouldAppendForwardSlash } from "../core/build/util.js";
3
7
  import { REROUTE_DIRECTIVE_HEADER } from "../core/constants.js";
4
8
  import { i18nNoLocaleFoundInPath, MissingLocale } from "../core/errors/errors-data.js";
@@ -55,7 +59,7 @@ function getLocaleRelativeUrl({
55
59
  if (shouldAppendForwardSlash(trailingSlash, format)) {
56
60
  relativePath = appendForwardSlash(joinPaths(...pathsToJoin));
57
61
  } else {
58
- relativePath = joinPaths(...pathsToJoin);
62
+ relativePath = removeTrailingForwardSlash(joinPaths(...pathsToJoin));
59
63
  }
60
64
  if (relativePath === "") {
61
65
  return "/";
@@ -87,7 +87,7 @@ Did you forget to import the component or is it possible there is a typo?`);
87
87
  _slots.default.push(child);
88
88
  return;
89
89
  }
90
- if ("slot" in child.props) {
90
+ if ("slot" in child.props && !isCustomElement) {
91
91
  _slots[child.props.slot] = [..._slots[child.props.slot] ?? [], child];
92
92
  delete child.props.slot;
93
93
  return;
@@ -116,6 +116,7 @@ Did you forget to import the component or is it possible there is a typo?`);
116
116
  const _slots = {
117
117
  default: []
118
118
  };
119
+ const isCustomElement = typeof vnode.type === "string" && vnode.type.includes("-");
119
120
  extractSlots2(children);
120
121
  for (const [key, value] of Object.entries(props)) {
121
122
  if (value?.["$$slot"]) {
@@ -51,7 +51,8 @@ function renderAllHeadContent(result) {
51
51
  const links = deduplicateElements(Array.from(result.links)).map(
52
52
  (link) => renderElement("link", link, false)
53
53
  );
54
- content += styles.join("\n") + links.join("\n") + scripts.join("\n");
54
+ const sep = result.compressHTML === true || result.compressHTML === "jsx" ? "" : "\n";
55
+ content += styles.join(sep) + links.join(sep) + scripts.join(sep);
55
56
  content += result._metadata.extraHead.join("");
56
57
  return markHTMLString(content);
57
58
  }
@@ -1,15 +1,17 @@
1
1
  import { isRunnableDevEnvironment } from "vite";
2
2
  import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from "../vite-plugin-pages/const.js";
3
+ import { RESOLVED_MODULE_DEV_CSS_PREFIX } from "../vite-plugin-css/const.js";
3
4
  import { getDevCssModuleNameFromPageVirtualModuleName } from "../vite-plugin-css/util.js";
4
5
  import { isAstroServerEnvironment } from "../environments.js";
5
6
  const STYLE_EXT_REGEX = /\.(?:css|scss|sass|less|styl|pcss)$/i;
7
+ const RAW_QUERY_REGEX = /(?:\?|&)raw(?:&|$)/;
8
+ function hasStyleExtension(id) {
9
+ return STYLE_EXT_REGEX.test(id.split("?")[0]);
10
+ }
6
11
  function isStyleModule(mod) {
7
- if (mod.file && STYLE_EXT_REGEX.test(mod.file)) return true;
8
- if (mod.id) {
9
- const idPath = mod.id.split("?")[0];
10
- if (STYLE_EXT_REGEX.test(idPath)) return true;
11
- }
12
- return false;
12
+ if (mod.id && RAW_QUERY_REGEX.test(mod.id) && hasStyleExtension(mod.id)) return false;
13
+ if (mod.file && hasStyleExtension(mod.file)) return true;
14
+ return mod.id ? hasStyleExtension(mod.id) : false;
13
15
  }
14
16
  function hmrReload() {
15
17
  return {
@@ -65,6 +67,17 @@ function hmrReload() {
65
67
  return [];
66
68
  }
67
69
  if (hasSkippedStyleModules) {
70
+ for (const [id, mod] of this.environment.moduleGraph.idToModuleMap) {
71
+ if (id.startsWith(RESOLVED_MODULE_DEV_CSS_PREFIX)) {
72
+ this.environment.moduleGraph.invalidateModule(mod, void 0, timestamp, true);
73
+ if (isRunnableDevEnvironment(this.environment)) {
74
+ const runnerMod = this.environment.runner.evaluatedModules.getModuleById(id);
75
+ if (runnerMod) {
76
+ this.environment.runner.evaluatedModules.invalidateModule(runnerMod);
77
+ }
78
+ }
79
+ }
80
+ }
68
81
  return [];
69
82
  }
70
83
  if (modules.length > 0) {
@@ -1,8 +1,19 @@
1
1
  import type { Plugin as VitePlugin } from 'vite';
2
2
  import type { RoutesList } from '../types/astro.js';
3
+ import type { RouteData } from '../types/public/internal.js';
3
4
  export declare const VIRTUAL_PAGES_MODULE_ID = "virtual:astro:pages";
4
5
  interface PagesPluginOptions {
5
6
  routesList: RoutesList;
6
7
  }
8
+ /**
9
+ * Filters routes for a specific build environment.
10
+ *
11
+ * Redirect target routes do not need special handling here: they already
12
+ * appear in the routes list with their own `prerender` flag and are
13
+ * included when it matches `isPrerender`. At SSR runtime, redirect
14
+ * responses are generated from route metadata alone (no component is
15
+ * loaded), so the target's component is never needed in the SSR page map.
16
+ */
17
+ export declare function getRoutesForEnvironment(routes: RouteData[], isPrerender: boolean): Set<RouteData>;
7
18
  export declare function pluginPages({ routesList }: PagesPluginOptions): VitePlugin;
8
19
  export {};
@@ -11,9 +11,6 @@ function getRoutesForEnvironment(routes, isPrerender) {
11
11
  if (route.prerender === isPrerender) {
12
12
  result.add(route);
13
13
  }
14
- if (route.redirectRoute) {
15
- result.add(route.redirectRoute);
16
- }
17
14
  }
18
15
  return result;
19
16
  }
@@ -75,5 +72,6 @@ export { pageMap };`;
75
72
  }
76
73
  export {
77
74
  VIRTUAL_PAGES_MODULE_ID,
75
+ getRoutesForEnvironment,
78
76
  pluginPages
79
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.4.6",
3
+ "version": "6.4.7",
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",
@@ -166,14 +166,14 @@
166
166
  "yargs-parser": "^22.0.0",
167
167
  "zod": "^4.3.6",
168
168
  "@astrojs/internal-helpers": "0.10.0",
169
- "@astrojs/telemetry": "3.3.2",
170
- "@astrojs/markdown-remark": "7.2.0"
169
+ "@astrojs/markdown-remark": "7.2.0",
170
+ "@astrojs/telemetry": "3.3.2"
171
171
  },
172
172
  "optionalDependencies": {
173
173
  "sharp": "^0.34.0"
174
174
  },
175
175
  "devDependencies": {
176
- "@astrojs/compiler-rs": "^0.1.6",
176
+ "@astrojs/compiler-rs": "^0.2.2",
177
177
  "@playwright/test": "1.58.2",
178
178
  "@types/aria-query": "^5.0.4",
179
179
  "@types/hast": "^3.0.4",