astro 6.3.1 → 6.3.2

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.
@@ -38,7 +38,7 @@ async function loadLocalImage(src, url) {
38
38
  } else {
39
39
  const sourceUrl = new URL(src, url.origin);
40
40
  if (sourceUrl.origin !== url.origin) {
41
- returnValue = void 0;
41
+ return void 0;
42
42
  }
43
43
  return loadRemoteImage(sourceUrl);
44
44
  }
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.3.1";
3
+ version = "6.3.2";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -191,7 +191,7 @@ ${contentConfig.error.message}`
191
191
  logger.info("Content config changed");
192
192
  shouldClear = true;
193
193
  }
194
- if (previousAstroVersion && previousAstroVersion !== "6.3.1") {
194
+ if (previousAstroVersion && previousAstroVersion !== "6.3.2") {
195
195
  logger.info("Astro version changed");
196
196
  shouldClear = true;
197
197
  }
@@ -199,8 +199,8 @@ ${contentConfig.error.message}`
199
199
  logger.info("Clearing content store");
200
200
  this.#store.clearAll();
201
201
  }
202
- if ("6.3.1") {
203
- this.#store.metaStore().set("astro-version", "6.3.1");
202
+ if ("6.3.2") {
203
+ this.#store.metaStore().set("astro-version", "6.3.2");
204
204
  }
205
205
  if (currentConfigDigest) {
206
206
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -16,6 +16,7 @@ import { DefaultFetchHandler } from "../fetch/default-handler.js";
16
16
  import { appSymbol } from "../constants.js";
17
17
  import { DefaultErrorHandler } from "../errors/default-handler.js";
18
18
  import { setRenderOptions } from "./render-options.js";
19
+ import { MultiLevelEncodingError } from "../util/pathname.js";
19
20
  class BaseApp {
20
21
  manifest;
21
22
  manifestData;
@@ -266,13 +267,20 @@ class BaseApp {
266
267
  waitUntil
267
268
  };
268
269
  let response;
269
- if (this.#fetchHandler instanceof DefaultFetchHandler) {
270
- Reflect.set(request, appSymbol, this);
271
- response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
272
- } else {
273
- setRenderOptions(request, resolvedOptions);
274
- Reflect.set(request, appSymbol, this);
275
- response = await this.#fetchHandler.fetch(request);
270
+ try {
271
+ if (this.#fetchHandler instanceof DefaultFetchHandler) {
272
+ Reflect.set(request, appSymbol, this);
273
+ response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
274
+ } else {
275
+ setRenderOptions(request, resolvedOptions);
276
+ Reflect.set(request, appSymbol, this);
277
+ response = await this.#fetchHandler.fetch(request);
278
+ }
279
+ } catch (err) {
280
+ if (err instanceof MultiLevelEncodingError) {
281
+ return new Response("Bad Request", { status: 400 });
282
+ }
283
+ throw err;
276
284
  }
277
285
  this.#warnMissingFeatures();
278
286
  if (response.headers.get(ASTRO_ERROR_HEADER)) {
@@ -151,7 +151,6 @@ class AstroBuilder {
151
151
  runtimeMode: this.runtimeMode,
152
152
  origin: this.origin,
153
153
  pageNames,
154
- teardownCompiler: this.teardownCompiler,
155
154
  viteConfig,
156
155
  key: keyPromise
157
156
  };
@@ -203,6 +202,13 @@ class AstroBuilder {
203
202
  } finally {
204
203
  this.settings.timer.end("Total build");
205
204
  this.settings.timer.writeStats();
205
+ if (this.teardownCompiler) {
206
+ try {
207
+ const { teardown } = await import("@astrojs/compiler");
208
+ teardown();
209
+ } catch {
210
+ }
211
+ }
206
212
  }
207
213
  }
208
214
  validateConfig() {
@@ -37,7 +37,6 @@ export interface StaticBuildOptions {
37
37
  origin: string;
38
38
  pageNames: string[];
39
39
  viteConfig: InlineConfig;
40
- teardownCompiler: boolean;
41
40
  key: Promise<CryptoKey>;
42
41
  }
43
42
  type ImportComponentInstance = () => Promise<ComponentInstance>;
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.3.1";
1
+ const ASTRO_VERSION = "6.3.2";
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,13 +37,25 @@ const ALLOWED_DIRECTIVES = [
37
37
  "upgrade-insecure-requests",
38
38
  "worker-src"
39
39
  ];
40
- const allowedDirectivesSchema = z.custom((value) => {
41
- if (typeof value !== "string") {
42
- return false;
43
- }
44
- return ALLOWED_DIRECTIVES.some((allowedValue) => {
40
+ const allowedDirectivesSchema = z.custom((v) => typeof v === "string").superRefine((value, ctx) => {
41
+ const isAllowed = ALLOWED_DIRECTIVES.some((allowedValue) => {
45
42
  return value.startsWith(allowedValue);
46
43
  });
44
+ if (!isAllowed) {
45
+ if (value.startsWith("script-src") || value.startsWith("style-src")) {
46
+ ctx.addIssue({
47
+ code: z.ZodIssueCode.custom,
48
+ message: `Directives \`script-src\` and \`style-src\` are not allowed in \`security.csp.directives\`. Please use \`security.csp.scriptDirective\` and \`security.csp.styleDirective\` instead.`,
49
+ fatal: true
50
+ });
51
+ } else {
52
+ ctx.addIssue({
53
+ code: z.ZodIssueCode.custom,
54
+ message: `Invalid directive: "${value}". Allowed directives are: ${ALLOWED_DIRECTIVES.join(", ")}`,
55
+ fatal: true
56
+ });
57
+ }
58
+ }
47
59
  });
48
60
  export {
49
61
  ALGORITHMS,
@@ -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.3.1";
40
+ const currentVersion = "6.3.2";
41
41
  const isPrerelease = currentVersion.includes("-");
42
42
  if (!isPrerelease) {
43
43
  try {
@@ -44,7 +44,9 @@ const errorMap = (issue) => {
44
44
  return errorMap(_issue);
45
45
  }
46
46
  const relativePath = flattenErrorPath(_issue.path).replace(baseErrorPath, "").replace(leadingPeriod, "");
47
- if ("expected" in _issue && typeof _issue.expected === "string") {
47
+ if (_issue.code === "custom" && _issue.message && _issue.message.includes("security.csp")) {
48
+ expectedShape.push(_issue.message);
49
+ } else if ("expected" in _issue && typeof _issue.expected === "string") {
48
50
  expectedShape.push(
49
51
  relativePath ? `${relativePath}: ${_issue.expected}` : _issue.expected
50
52
  );
@@ -276,7 +276,7 @@ function printHelp({
276
276
  message.push(
277
277
  linebreak(),
278
278
  ` ${bgGreen(black(` ${commandName} `))} ${green(
279
- `v${"6.3.1"}`
279
+ `v${"6.3.2"}`
280
280
  )} ${headline}`
281
281
  );
282
282
  }
@@ -94,11 +94,9 @@ class AstroHandler {
94
94
  state.status = defaultStatus;
95
95
  let response;
96
96
  try {
97
- if (this.#hasSession || this.#app.pipeline.cacheConfig) {
98
- const sessionP = this.#hasSession ? provideSession(state) : void 0;
99
- const cacheP = this.#app.pipeline.cacheConfig ? provideCache(state) : void 0;
100
- if (sessionP || cacheP) await Promise.all([sessionP, cacheP]);
101
- }
97
+ const sessionP = this.#hasSession ? provideSession(state) : void 0;
98
+ const cacheP = provideCache(state);
99
+ if (sessionP || cacheP) await Promise.all([sessionP, cacheP]);
102
100
  state.pipeline.usedFeatures |= PipelineFeatures.sessions;
103
101
  if (routeData.type === "redirect") {
104
102
  const redirectResponse = await renderRedirect(state);
@@ -1,12 +1,15 @@
1
1
  import { collapseDuplicateSlashes } from "@astrojs/internal-helpers/path";
2
- import { validateAndDecodePathname } from "./pathname.js";
2
+ import { MultiLevelEncodingError, 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 {
9
+ } catch (e) {
10
+ if (e instanceof MultiLevelEncodingError) {
11
+ throw e;
12
+ }
10
13
  try {
11
14
  url.pathname = decodeURI(url.pathname);
12
15
  } catch {
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Error thrown when multi-level URL encoding is detected in a pathname.
3
+ * This is a distinct error type so callers can handle it specifically
4
+ * (e.g., returning a 400 response) rather than falling back to partial decoding.
5
+ */
6
+ export declare class MultiLevelEncodingError extends Error {
7
+ constructor();
8
+ }
1
9
  /**
2
10
  * Validates that a pathname is not multi-level encoded.
3
11
  * Detects if a pathname contains encoding that was encoded again (e.g., %2561dmin where %25 decodes to %).
@@ -5,6 +13,7 @@
5
13
  *
6
14
  * @param pathname - The pathname to validate
7
15
  * @returns The decoded pathname if valid
8
- * @throws Error if multi-level encoding is detected
16
+ * @throws MultiLevelEncodingError if multi-level encoding is detected
17
+ * @throws Error if the pathname contains invalid URL encoding
9
18
  */
10
19
  export declare function validateAndDecodePathname(pathname: string): string;
@@ -1,3 +1,9 @@
1
+ class MultiLevelEncodingError extends Error {
2
+ constructor() {
3
+ super("Multi-level URL encoding is not allowed");
4
+ this.name = "MultiLevelEncodingError";
5
+ }
6
+ }
1
7
  function validateAndDecodePathname(pathname) {
2
8
  let decoded;
3
9
  try {
@@ -8,10 +14,11 @@ function validateAndDecodePathname(pathname) {
8
14
  const hasDecoding = decoded !== pathname;
9
15
  const decodedStillHasEncoding = /%[0-9a-fA-F]{2}/.test(decoded);
10
16
  if (hasDecoding && decodedStillHasEncoding) {
11
- throw new Error("Multi-level URL encoding is not allowed");
17
+ throw new MultiLevelEncodingError();
12
18
  }
13
19
  return decoded;
14
20
  }
15
21
  export {
22
+ MultiLevelEncodingError,
16
23
  validateAndDecodePathname
17
24
  };
@@ -1,6 +1,6 @@
1
1
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "./core/constants.js";
2
2
  function isAstroServerEnvironment(environment) {
3
- return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
3
+ return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro;
4
4
  }
5
5
  function isAstroClientEnvironment(environment) {
6
6
  return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
@@ -100,6 +100,7 @@ const build = {
100
100
  server: new URL(manifest.buildServerDir),
101
101
  client: new URL(manifest.buildClientDir),
102
102
  format: manifest.buildFormat,
103
+ assetsPrefix: manifest.assetsPrefix,
103
104
  };
104
105
 
105
106
  const cacheDir = new URL(manifest.cacheDir);
@@ -10,7 +10,7 @@ const toIdent = (k) => k.trim().replace(/(?!^)\b\w|\s+|\W+/g, (match, index) =>
10
10
  if (/\W/.test(match)) return "";
11
11
  return index === 0 ? match : match.toUpperCase();
12
12
  });
13
- const toAttributeString = (value, shouldEscape = true) => shouldEscape ? String(value).replace(AMPERSAND_REGEX, "&#38;").replace(DOUBLE_QUOTE_REGEX, "&#34;") : value;
13
+ const toAttributeString = (value, shouldEscape = true) => shouldEscape ? String(value).replace(AMPERSAND_REGEX, "&amp;").replace(DOUBLE_QUOTE_REGEX, "&quot;") : value;
14
14
  const kebab = (k) => k.toLowerCase() === k ? k : k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
15
15
  const toStyleString = (obj) => Object.entries(obj).filter(([_, v]) => typeof v === "string" && v.trim() || typeof v === "number").map(([k, v]) => {
16
16
  if (k[0] !== "-" && k[1] !== "-") return `${kebab(k)}:${v}`;
@@ -1628,19 +1628,6 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
1628
1628
  * ```
1629
1629
  */
1630
1630
  service?: ImageServiceConfig;
1631
- /**
1632
- * @docs
1633
- * @name image.dangerouslyProcessSVG
1634
- * @type {boolean}
1635
- * @default `false`
1636
- * @version 6.3.0
1637
- * @description
1638
- *
1639
- * Allows SVG source images to be processed by the image optimization pipeline.
1640
- *
1641
- * This is disabled by default as specifically formed SVGs can be prohibitively expensive to process and used by malicious actors to execute denial of service attacks. Only enable this option if you trust the source of your SVG images and understand the risks of processing them.
1642
- */
1643
- dangerouslyProcessSVG?: boolean;
1644
1631
  /**
1645
1632
  * @docs
1646
1633
  * @name image.service.config.limitInputPixels
@@ -1723,6 +1710,19 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
1723
1710
  * This can be used for options such as `compressionLevel`, `effort`, `palette`, or a default `quality`.
1724
1711
  * Per-image `quality` values from `<Image />`, `<Picture />`, and `getImage()` still take precedence.
1725
1712
  */
1713
+ /**
1714
+ * @docs
1715
+ * @name image.dangerouslyProcessSVG
1716
+ * @type {boolean}
1717
+ * @default `false`
1718
+ * @version 6.3.0
1719
+ * @description
1720
+ *
1721
+ * Allows SVG source images to be processed by the image optimization pipeline.
1722
+ *
1723
+ * This is disabled by default as specifically formed SVGs can be prohibitively expensive to process and used by malicious actors to execute denial of service attacks. Only enable this option if you trust the source of your SVG images and understand the risks of processing them.
1724
+ */
1725
+ dangerouslyProcessSVG?: boolean;
1726
1726
  /**
1727
1727
  * @docs
1728
1728
  * @name image.domains
@@ -13,7 +13,7 @@ type Dirs = Pick<SSRManifest, 'cacheDir' | 'outDir' | 'publicDir' | 'srcDir'>;
13
13
  type DeserializedDirs = Extend<Dirs, URL>;
14
14
  export type ServerDeserializedManifest = Pick<SSRManifest, 'base' | 'trailingSlash' | 'compressHTML' | 'site'> & DeserializedDirs & {
15
15
  i18n: AstroConfig['i18n'];
16
- build: Pick<AstroConfig['build'], 'server' | 'client' | 'format'>;
16
+ build: Pick<AstroConfig['build'], 'server' | 'client' | 'format' | 'assetsPrefix'>;
17
17
  root: URL;
18
18
  image: Pick<AstroConfig['image'], 'objectFit' | 'objectPosition' | 'layout'>;
19
19
  };
@@ -33,7 +33,13 @@ export declare class AstroServerApp extends BaseApp<RunnablePipeline> {
33
33
  devMatch(pathname: string): Promise<DevMatch | undefined>;
34
34
  static create(manifest: SSRManifest, routesList: RoutesList, logger: AstroLogger, loader: ModuleLoader, settings: AstroSettings, getDebugInfo: () => Promise<string>): Promise<AstroServerApp>;
35
35
  createPipeline(_streaming: boolean, manifest: SSRManifest, settings: AstroSettings, logger: AstroLogger, loader: ModuleLoader, manifestData: RoutesList, getDebugInfo: () => Promise<string>): RunnablePipeline;
36
- handleRequest({ controller, incomingRequest, incomingResponse, isHttps, }: HandleRequest): Promise<void>;
36
+ /**
37
+ * Handle a request.
38
+ * @returns The return value indicates whether or not the request was handled
39
+ * by this handler. If the result is not `true`, then the request has not
40
+ * been handled yet and other handlers can be run.
41
+ */
42
+ handleRequest({ controller, incomingRequest, incomingResponse, isHttps, prerenderOnly, }: HandleRequest): Promise<boolean>;
37
43
  match(request: Request, _allowPrerenderedRoutes: boolean): RouteData | undefined;
38
44
  protected createErrorHandler(): ErrorHandler;
39
45
  logRequest({ pathname, method, statusCode, isRewrite, reqTime }: LogRequestPayload): void;
@@ -43,5 +49,7 @@ type HandleRequest = {
43
49
  incomingRequest: http.IncomingMessage;
44
50
  incomingResponse: http.ServerResponse;
45
51
  isHttps: boolean;
52
+ /** When true, only handle prerendered routes. Returns false for SSR routes. */
53
+ prerenderOnly?: boolean;
46
54
  };
47
55
  export {};
@@ -92,11 +92,18 @@ class AstroServerApp extends BaseApp {
92
92
  });
93
93
  return pipeline;
94
94
  }
95
+ /**
96
+ * Handle a request.
97
+ * @returns The return value indicates whether or not the request was handled
98
+ * by this handler. If the result is not `true`, then the request has not
99
+ * been handled yet and other handlers can be run.
100
+ */
95
101
  async handleRequest({
96
102
  controller,
97
103
  incomingRequest,
98
104
  incomingResponse,
99
- isHttps
105
+ isHttps,
106
+ prerenderOnly
100
107
  }) {
101
108
  const validated = validateForwardedHeaders(
102
109
  getFirstForwardedValue(incomingRequest.headers["x-forwarded-proto"]),
@@ -131,14 +138,23 @@ class AstroServerApp extends BaseApp {
131
138
  }
132
139
  const self = this;
133
140
  await self.#loadFetchHandler();
141
+ let handled = true;
134
142
  await runWithErrorHandling({
135
143
  controller,
136
144
  pathname,
137
145
  async run() {
138
146
  const matchedRoute = await self.devMatch(pathname);
139
147
  if (!matchedRoute) {
148
+ if (prerenderOnly) {
149
+ handled = false;
150
+ return;
151
+ }
140
152
  throw new Error("No route matched, and default 404 route was not found.");
141
153
  }
154
+ if (prerenderOnly && !matchedRoute.routeData.prerender) {
155
+ handled = false;
156
+ return;
157
+ }
142
158
  const request = createRequest({
143
159
  url,
144
160
  headers: incomingRequest.headers,
@@ -174,6 +190,7 @@ class AstroServerApp extends BaseApp {
174
190
  return error;
175
191
  }
176
192
  });
193
+ return handled;
177
194
  }
178
195
  match(request, _allowPrerenderedRoutes) {
179
196
  return super.match(request, true);
@@ -4,5 +4,7 @@ import type { ModuleLoader } from '../core/module-loader/index.js';
4
4
  import type { AstroSettings } from '../types/astro.js';
5
5
  import type { DevServerController } from '../vite-plugin-astro-server/controller.js';
6
6
  export default function createAstroServerApp(controller: DevServerController, settings: AstroSettings, loader: ModuleLoader, logger?: AstroLogger): Promise<{
7
- handler(incomingRequest: http.IncomingMessage, incomingResponse: http.ServerResponse): void;
7
+ handler(incomingRequest: http.IncomingMessage, incomingResponse: http.ServerResponse, options?: {
8
+ prerenderOnly?: boolean;
9
+ }): Promise<boolean>;
8
10
  }>;
@@ -60,12 +60,13 @@ async function createAstroServerApp(controller, settings, loader, logger) {
60
60
  });
61
61
  }
62
62
  return {
63
- handler(incomingRequest, incomingResponse) {
64
- app.handleRequest({
63
+ handler(incomingRequest, incomingResponse, options) {
64
+ return app.handleRequest({
65
65
  controller,
66
66
  incomingRequest,
67
67
  incomingResponse,
68
- isHttps: loader?.isHttps() ?? false
68
+ isHttps: loader?.isHttps() ?? false,
69
+ prerenderOnly: options?.prerenderOnly
69
70
  });
70
71
  }
71
72
  };
@@ -113,9 +113,14 @@ function createVitePluginAstroServer({
113
113
  if (!matches.some((route) => route.prerender)) {
114
114
  return next();
115
115
  }
116
- localStorage.run(request, () => {
117
- prerenderHandler.handler(request, response);
116
+ const handled = await new Promise((resolve) => {
117
+ localStorage.run(request, () => {
118
+ prerenderHandler.handler(request, response, { prerenderOnly: true }).then((result) => resolve(result)).catch(() => resolve(true));
119
+ });
118
120
  });
121
+ if (!handled) {
122
+ return next();
123
+ }
119
124
  } catch (err) {
120
125
  next(err);
121
126
  }
@@ -1,5 +1,38 @@
1
1
  import type * as vite from 'vite';
2
2
  import type { AstroSettings } from '../types/astro.js';
3
+ /**
4
+ * Outcome of the route guard evaluation for a dev-server request.
5
+ *
6
+ * - **`next`** — Allow the request through to downstream middleware.
7
+ * - **`block`** — The file exists at the project root but outside srcDir/publicDir.
8
+ * Respond with a 404.
9
+ */
10
+ export type RouteGuardDecision = {
11
+ action: 'next';
12
+ } | {
13
+ action: 'block';
14
+ pathname: string;
15
+ };
16
+ /**
17
+ * Filesystem query results needed by the route guard decision function.
18
+ * Callers resolve these from the real filesystem; tests can provide them directly.
19
+ */
20
+ export interface RouteGuardFsInfo {
21
+ /** Whether the resolved pathname exists inside the project's `publicDir` (e.g. `public/robots.txt`). */
22
+ existsInPublic: boolean;
23
+ /** Whether the resolved pathname exists inside the project's `srcDir` (e.g. `src/pages/index.astro`). */
24
+ existsInSrc: boolean;
25
+ /** Whether the resolved pathname exists at the project root as a **file** (not a directory). Directories are allowed through because they may share names with valid page routes. */
26
+ existsAtRootAsFile: boolean;
27
+ }
28
+ /**
29
+ * Pure decision function for the route guard middleware.
30
+ *
31
+ * Determines whether a request should be blocked (file exists at project root
32
+ * but outside srcDir/publicDir) or allowed through. The filesystem lookups are
33
+ * injected via `fsInfo` so this function remains pure and unit-testable.
34
+ */
35
+ export declare function evaluateRouteGuard(url: string, acceptHeader: string, fsInfo: RouteGuardFsInfo): RouteGuardDecision;
3
36
  /**
4
37
  * Middleware that prevents Vite from serving files that exist outside
5
38
  * of srcDir and publicDir when accessed via direct URL navigation.
@@ -10,6 +10,30 @@ const VITE_INTERNAL_PREFIXES = [
10
10
  "/node_modules/",
11
11
  "/.astro/"
12
12
  ];
13
+ function evaluateRouteGuard(url, acceptHeader, fsInfo) {
14
+ if (!acceptHeader.includes("text/html")) {
15
+ return { action: "next" };
16
+ }
17
+ let pathname;
18
+ try {
19
+ pathname = decodeURI(new URL(url, "http://localhost").pathname);
20
+ } catch {
21
+ return { action: "next" };
22
+ }
23
+ if (VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
24
+ return { action: "next" };
25
+ }
26
+ if (url.includes("?")) {
27
+ return { action: "next" };
28
+ }
29
+ if (fsInfo.existsInPublic || fsInfo.existsInSrc) {
30
+ return { action: "next" };
31
+ }
32
+ if (fsInfo.existsAtRootAsFile) {
33
+ return { action: "block", pathname };
34
+ }
35
+ return { action: "next" };
36
+ }
13
37
  function routeGuardMiddleware(settings) {
14
38
  const { config } = settings;
15
39
  return function devRouteGuard(req, res, next) {
@@ -18,41 +42,36 @@ function routeGuardMiddleware(settings) {
18
42
  return next();
19
43
  }
20
44
  const accept = req.headers.accept || "";
21
- if (!accept.includes("text/html")) {
22
- return next();
23
- }
24
45
  let pathname;
25
46
  try {
26
47
  pathname = decodeURI(new URL(url, "http://localhost").pathname);
27
48
  } catch {
28
49
  return next();
29
50
  }
30
- if (VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
31
- return next();
32
- }
33
- if (url.includes("?")) {
34
- return next();
35
- }
36
- const publicFilePath = new URL("." + pathname, config.publicDir);
37
- if (fs.existsSync(publicFilePath)) {
38
- return next();
39
- }
40
- const srcFilePath = new URL("." + pathname, config.srcDir);
41
- if (fs.existsSync(srcFilePath)) {
42
- return next();
51
+ const fsInfo = {
52
+ existsInPublic: fs.existsSync(new URL("." + pathname, config.publicDir)),
53
+ existsInSrc: fs.existsSync(new URL("." + pathname, config.srcDir)),
54
+ existsAtRootAsFile: false
55
+ };
56
+ if (accept.includes("text/html") && !url.includes("?") && !VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
57
+ try {
58
+ const stat = fs.statSync(new URL("." + pathname, config.root));
59
+ fsInfo.existsAtRootAsFile = stat.isFile();
60
+ } catch {
61
+ }
43
62
  }
44
- const rootFilePath = new URL("." + pathname, config.root);
45
- try {
46
- const stat = fs.statSync(rootFilePath);
47
- if (stat.isFile()) {
48
- const html = notFoundTemplate(pathname);
63
+ const decision = evaluateRouteGuard(url, accept, fsInfo);
64
+ switch (decision.action) {
65
+ case "block": {
66
+ const html = notFoundTemplate(decision.pathname);
49
67
  return writeHtmlResponse(res, 404, html);
50
68
  }
51
- } catch {
69
+ case "next":
70
+ return next();
52
71
  }
53
- return next();
54
72
  };
55
73
  }
56
74
  export {
75
+ evaluateRouteGuard,
57
76
  routeGuardMiddleware
58
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.3.1",
3
+ "version": "6.3.2",
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",
@@ -164,8 +164,8 @@
164
164
  "xxhash-wasm": "^1.1.0",
165
165
  "yargs-parser": "^22.0.0",
166
166
  "zod": "^4.3.6",
167
- "@astrojs/internal-helpers": "0.9.0",
168
- "@astrojs/markdown-remark": "7.1.1",
167
+ "@astrojs/internal-helpers": "0.9.1",
168
+ "@astrojs/markdown-remark": "7.1.2",
169
169
  "@astrojs/telemetry": "3.3.2"
170
170
  },
171
171
  "optionalDependencies": {