astro 6.4.3 → 6.4.4

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,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.4.3";
3
+ version = "6.4.4";
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 !== "6.4.3") {
200
+ if (previousAstroVersion && previousAstroVersion !== "6.4.4") {
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.3") {
209
- this.#store.metaStore().set("astro-version", "6.4.3");
208
+ if ("6.4.4") {
209
+ this.#store.metaStore().set("astro-version", "6.4.4");
210
210
  }
211
211
  if (currentConfigDigest) {
212
212
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -131,6 +131,15 @@ export declare abstract class BaseApp<P extends Pipeline = AppPipeline> {
131
131
  abstract createPipeline(streaming: boolean, manifest: SSRManifest, ...args: any[]): P;
132
132
  set setManifestData(newManifestData: RoutesList);
133
133
  removeBase(pathname: string): string;
134
+ /**
135
+ * Decodes a pathname with `decodeURI`, falling back to the raw pathname when it
136
+ * contains an invalid percent-sequence (e.g. `%C0%AF`, an overlong-UTF-8 encoding of
137
+ * `/` commonly sent by path-traversal scanners). A raw `decodeURI()` would throw
138
+ * `URIError: URI malformed`, and because `match()` runs before `render()` that error
139
+ * escapes the adapter's request handler as an uncaught exception (HTTP 500) that user
140
+ * middleware can't catch.
141
+ */
142
+ private safeDecodeURI;
134
143
  /**
135
144
  * Extracts the base-stripped, decoded pathname from a request.
136
145
  * Used by adapters to compute the pathname for dev-mode route matching.
@@ -1,12 +1,10 @@
1
1
  import {
2
- appendForwardSlash,
3
2
  collapseDuplicateLeadingSlashes,
4
- joinPaths,
5
3
  prependForwardSlash,
6
4
  removeTrailingForwardSlash
7
5
  } from "@astrojs/internal-helpers/path";
8
6
  import { matchPattern } from "@astrojs/internal-helpers/remote";
9
- import { normalizeTheLocale } from "../../i18n/index.js";
7
+ import { computePathnameFromDomain } from "../i18n/domain.js";
10
8
  import { PipelineFeatures } from "../base-pipeline.js";
11
9
  import { ASTRO_ERROR_HEADER, clientAddressSymbol } from "../constants.js";
12
10
  import { getSetCookiesFromResponse } from "../cookies/index.js";
@@ -117,19 +115,30 @@ class BaseApp {
117
115
  return pathname;
118
116
  }
119
117
  /**
120
- * Extracts the base-stripped, decoded pathname from a request.
121
- * Used by adapters to compute the pathname for dev-mode route matching.
118
+ * Decodes a pathname with `decodeURI`, falling back to the raw pathname when it
119
+ * contains an invalid percent-sequence (e.g. `%C0%AF`, an overlong-UTF-8 encoding of
120
+ * `/` commonly sent by path-traversal scanners). A raw `decodeURI()` would throw
121
+ * `URIError: URI malformed`, and because `match()` runs before `render()` that error
122
+ * escapes the adapter's request handler as an uncaught exception (HTTP 500) that user
123
+ * middleware can't catch.
122
124
  */
123
- getPathnameFromRequest(request) {
124
- const url = new URL(request.url);
125
- const pathname = prependForwardSlash(this.removeBase(url.pathname));
125
+ safeDecodeURI(pathname) {
126
126
  try {
127
127
  return decodeURI(pathname);
128
128
  } catch (e) {
129
- this.adapterLogger.error(e.toString());
129
+ this.adapterLogger.debug(e.toString());
130
130
  return pathname;
131
131
  }
132
132
  }
133
+ /**
134
+ * Extracts the base-stripped, decoded pathname from a request.
135
+ * Used by adapters to compute the pathname for dev-mode route matching.
136
+ */
137
+ getPathnameFromRequest(request) {
138
+ const url = new URL(request.url);
139
+ const pathname = prependForwardSlash(this.removeBase(url.pathname));
140
+ return this.safeDecodeURI(pathname);
141
+ }
133
142
  /**
134
143
  * Given a `Request`, it returns the `RouteData` that matches its `pathname`. By default, prerendered
135
144
  * routes aren't returned, even if they are matched.
@@ -145,14 +154,14 @@ class BaseApp {
145
154
  if (!pathname) {
146
155
  pathname = prependForwardSlash(this.removeBase(url.pathname));
147
156
  }
148
- const routeData = this.pipeline.matchRoute(decodeURI(pathname));
157
+ const routeData = this.pipeline.matchRoute(this.safeDecodeURI(pathname));
149
158
  if (!routeData) return void 0;
150
159
  if (allowPrerenderedRoutes) {
151
160
  return routeData;
152
161
  }
153
162
  if (routeData.prerender) {
154
163
  if (routeData.params.length > 0) {
155
- const allMatches = this.pipeline.matchAllRoutes(decodeURI(pathname));
164
+ const allMatches = this.pipeline.matchAllRoutes(this.safeDecodeURI(pathname));
156
165
  return allMatches.find((r) => !r.prerender);
157
166
  }
158
167
  return void 0;
@@ -170,55 +179,14 @@ class BaseApp {
170
179
  return void 0;
171
180
  }
172
181
  computePathnameFromDomain(request) {
173
- let pathname = void 0;
174
- const url = new URL(request.url);
175
- if (this.manifest.i18n && (this.manifest.i18n.strategy === "domains-prefix-always" || this.manifest.i18n.strategy === "domains-prefix-other-locales" || this.manifest.i18n.strategy === "domains-prefix-always-no-redirect")) {
176
- let host = request.headers.get("X-Forwarded-Host");
177
- let protocol = request.headers.get("X-Forwarded-Proto");
178
- if (protocol) {
179
- protocol = protocol + ":";
180
- } else {
181
- protocol = url.protocol;
182
- }
183
- if (!host) {
184
- host = request.headers.get("Host");
185
- }
186
- if (host && protocol) {
187
- host = host.split(":")[0];
188
- try {
189
- let locale;
190
- const hostAsUrl = new URL(`${protocol}//${host}`);
191
- for (const [domainKey, localeValue] of Object.entries(
192
- this.manifest.i18n.domainLookupTable
193
- )) {
194
- const domainKeyAsUrl = new URL(domainKey);
195
- if (hostAsUrl.host === domainKeyAsUrl.host && hostAsUrl.protocol === domainKeyAsUrl.protocol) {
196
- locale = localeValue;
197
- break;
198
- }
199
- }
200
- if (locale) {
201
- pathname = prependForwardSlash(
202
- joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname))
203
- );
204
- if (this.manifest.trailingSlash === "always") {
205
- pathname = appendForwardSlash(pathname);
206
- } else if (this.manifest.trailingSlash === "never") {
207
- pathname = removeTrailingForwardSlash(pathname);
208
- } else if (url.pathname.endsWith("/")) {
209
- pathname = appendForwardSlash(pathname);
210
- }
211
- }
212
- } catch (e) {
213
- this.logger.error(
214
- "router",
215
- `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
216
- );
217
- this.logger.error("router", `Error: ${e}`);
218
- }
219
- }
220
- }
221
- return pathname;
182
+ return computePathnameFromDomain(
183
+ request,
184
+ new URL(request.url),
185
+ this.manifest.i18n,
186
+ this.manifest.base,
187
+ this.manifest.trailingSlash,
188
+ this.logger
189
+ );
222
190
  }
223
191
  async render(request, {
224
192
  addCookieHeader = false,
@@ -259,7 +227,7 @@ class BaseApp {
259
227
  if (!routeData) {
260
228
  const domainPathname = this.computePathnameFromDomain(request);
261
229
  if (domainPathname) {
262
- routeData = this.pipeline.matchRoute(decodeURI(domainPathname));
230
+ routeData = this.pipeline.matchRoute(this.safeDecodeURI(domainPathname));
263
231
  }
264
232
  }
265
233
  const resolvedOptions = {
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.4.3";
1
+ const ASTRO_VERSION = "6.4.4";
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.3";
40
+ const currentVersion = "6.4.4";
41
41
  const isPrerelease = currentVersion.includes("-");
42
42
  if (!isPrerelease) {
43
43
  try {
@@ -29,6 +29,7 @@ import { Rewrites } from "../rewrites/handler.js";
29
29
  import { isRoute404or500, isRouteServerIsland } from "../routing/match.js";
30
30
  import { normalizeUrl } from "../util/normalized-url.js";
31
31
  import { getOriginPathname, setOriginPathname } from "../routing/rewrite.js";
32
+ import { computePathnameFromDomain } from "../i18n/domain.js";
32
33
  import { getCustom404Route, routeHasHtmlExtension } from "../routing/helpers.js";
33
34
  import { getRenderOptions } from "../app/render-options.js";
34
35
  import { getFirstForwardedValue, validateForwardedHeaders } from "../app/validate-headers.js";
@@ -136,6 +137,13 @@ class FetchState {
136
137
  #rewrites;
137
138
  /** Memoized Astro page partial. */
138
139
  #astroPagePartial;
140
+ /**
141
+ * Locale-prefixed pathname derived from the Host header for domain-based
142
+ * i18n routing (e.g. `/en/boats/1/foo`), or `undefined` when the request
143
+ * isn't served from a locale-mapped domain. When set, `this.pathname` is
144
+ * derived from it so locale/param resolution match the route pattern.
145
+ */
146
+ #domainPathname;
139
147
  /** Memoized current locale. */
140
148
  #currentLocale;
141
149
  /** Memoized preferred locale. */
@@ -158,7 +166,24 @@ class FetchState {
158
166
  this.componentInstance = void 0;
159
167
  this.slots = void 0;
160
168
  const url = new URL(request.url);
161
- this.pathname = this.#computePathname(url);
169
+ const domainPathname = computePathnameFromDomain(
170
+ request,
171
+ url,
172
+ pipeline.manifest.i18n,
173
+ pipeline.manifest.base,
174
+ pipeline.manifest.trailingSlash,
175
+ pipeline.logger
176
+ );
177
+ if (domainPathname) {
178
+ this.#domainPathname = domainPathname;
179
+ try {
180
+ this.pathname = decodeURI(domainPathname);
181
+ } catch {
182
+ this.pathname = domainPathname;
183
+ }
184
+ } else {
185
+ this.pathname = this.#computePathname(url);
186
+ }
162
187
  this.timeStart = performance.now();
163
188
  this.clientAddress = options?.clientAddress;
164
189
  this.locals = options?.locals ?? {};
@@ -462,7 +487,9 @@ class FetchState {
462
487
  }
463
488
  } else {
464
489
  let pathname = routeData.pathname;
465
- if (url && !routeData.pattern.test(url.pathname)) {
490
+ if (this.#domainPathname) {
491
+ pathname = this.pathname;
492
+ } else if (url && !routeData.pattern.test(url.pathname)) {
466
493
  for (const fallbackRoute of routeData.fallbackRoutes) {
467
494
  if (fallbackRoute.pattern.test(url.pathname)) {
468
495
  pathname = fallbackRoute.pathname;
@@ -589,11 +616,14 @@ class FetchState {
589
616
  */
590
617
  /**
591
618
  * Strip `.html` / `/index.html` suffixes from the pathname so the
592
- * rendering pipeline sees the canonical route path. Skipped when the
593
- * matched route itself has an `.html` extension in its definition.
619
+ * rendering pipeline sees the canonical route path. Only applies to
620
+ * page routes where `.html` is framework-injected. Endpoint routes
621
+ * preserve `.html` because any such suffix is user-provided (e.g.
622
+ * from `getStaticPaths` params). Skipped when the matched route
623
+ * itself has an `.html` extension in its definition.
594
624
  */
595
625
  #stripHtmlExtension() {
596
- if (this.routeData && !routeHasHtmlExtension(this.routeData)) {
626
+ if (this.routeData && this.routeData.type === "page" && !routeHasHtmlExtension(this.routeData)) {
597
627
  this.pathname = this.pathname.replace(/\/index\.html$/, "/").replace(/\.html$/, "");
598
628
  }
599
629
  }
@@ -0,0 +1,12 @@
1
+ import type { SSRManifest } from '../app/types.js';
2
+ import type { AstroLogger } from '../logger/core.js';
3
+ /**
4
+ * For domain-based i18n routing strategies, derives the locale-prefixed
5
+ * pathname from the request's `Host` header rather than its URL. For example,
6
+ * a request for `/foo` served from `https://example.fr` resolves to `/fr/foo`.
7
+ *
8
+ * Returns `undefined` when the strategy isn't domain-based or the host isn't
9
+ * mapped to a locale — in which case normal pathname routing applies.
10
+ *
11
+ */
12
+ export declare function computePathnameFromDomain(request: Request, url: URL, i18n: SSRManifest['i18n'], base: SSRManifest['base'], trailingSlash: SSRManifest['trailingSlash'], logger: AstroLogger): string | undefined;
@@ -0,0 +1,66 @@
1
+ import {
2
+ appendForwardSlash,
3
+ collapseDuplicateLeadingSlashes,
4
+ joinPaths,
5
+ prependForwardSlash,
6
+ removeTrailingForwardSlash
7
+ } from "@astrojs/internal-helpers/path";
8
+ import { normalizeTheLocale } from "../../i18n/index.js";
9
+ function computePathnameFromDomain(request, url, i18n, base, trailingSlash, logger) {
10
+ let pathname = void 0;
11
+ if (i18n && (i18n.strategy === "domains-prefix-always" || i18n.strategy === "domains-prefix-other-locales" || i18n.strategy === "domains-prefix-always-no-redirect")) {
12
+ let host = request.headers.get("X-Forwarded-Host");
13
+ let protocol = request.headers.get("X-Forwarded-Proto");
14
+ if (protocol) {
15
+ protocol = protocol + ":";
16
+ } else {
17
+ protocol = url.protocol;
18
+ }
19
+ if (!host) {
20
+ host = request.headers.get("Host");
21
+ }
22
+ if (host && protocol) {
23
+ host = host.split(":")[0];
24
+ try {
25
+ let locale;
26
+ const hostAsUrl = new URL(`${protocol}//${host}`);
27
+ for (const [domainKey, localeValue] of Object.entries(i18n.domainLookupTable)) {
28
+ const domainKeyAsUrl = new URL(domainKey);
29
+ if (hostAsUrl.host === domainKeyAsUrl.host && hostAsUrl.protocol === domainKeyAsUrl.protocol) {
30
+ locale = localeValue;
31
+ break;
32
+ }
33
+ }
34
+ if (locale) {
35
+ pathname = prependForwardSlash(
36
+ joinPaths(normalizeTheLocale(locale), removeBase(url.pathname, base))
37
+ );
38
+ if (trailingSlash === "always") {
39
+ pathname = appendForwardSlash(pathname);
40
+ } else if (trailingSlash === "never") {
41
+ pathname = removeTrailingForwardSlash(pathname);
42
+ } else if (url.pathname.endsWith("/")) {
43
+ pathname = appendForwardSlash(pathname);
44
+ }
45
+ }
46
+ } catch (e) {
47
+ logger.error(
48
+ "router",
49
+ `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
50
+ );
51
+ logger.error("router", `Error: ${e}`);
52
+ }
53
+ }
54
+ }
55
+ return pathname;
56
+ }
57
+ function removeBase(pathname, base) {
58
+ pathname = collapseDuplicateLeadingSlashes(pathname);
59
+ if (pathname.startsWith(base)) {
60
+ return pathname.slice(removeTrailingForwardSlash(base).length + 1);
61
+ }
62
+ return pathname;
63
+ }
64
+ export {
65
+ computePathnameFromDomain
66
+ };
@@ -84,7 +84,7 @@ export declare class AstroLogger {
84
84
  */
85
85
  close(): void;
86
86
  /**
87
- * It calls the `flush` function of the provided destinatin, if it exists.
87
+ * It calls the `flush` function of the provided destination, if it exists.
88
88
  */
89
89
  flush(): void;
90
90
  }
@@ -136,7 +136,7 @@ class AstroLogger {
136
136
  }
137
137
  }
138
138
  /**
139
- * It calls the `flush` function of the provided destinatin, if it exists.
139
+ * It calls the `flush` function of the provided destination, if it exists.
140
140
  */
141
141
  flush() {
142
142
  if (this.options.destination.flush) {
@@ -276,7 +276,7 @@ function printHelp({
276
276
  message.push(
277
277
  linebreak(),
278
278
  ` ${bgGreen(black(` ${commandName} `))} ${green(
279
- `v${"6.4.3"}`
279
+ `v${"6.4.4"}`
280
280
  )} ${headline}`
281
281
  );
282
282
  }
@@ -677,7 +677,7 @@ function joinSegments(segments) {
677
677
  const arr = segments.map((segment) => {
678
678
  return segment.map((rp) => rp.dynamic ? `[${rp.content}]` : rp.content).join("");
679
679
  });
680
- return `/${arr.join("/")}`.toLowerCase();
680
+ return `/${arr.join("/")}`;
681
681
  }
682
682
  function replaceOrKeep(original, from, to) {
683
683
  if (original.startsWith(`/${to}/`) || original === `/${to}`) return original;
@@ -58,7 +58,7 @@ function joinSegments(segments) {
58
58
  const arr = segments.map((segment) => {
59
59
  return segment.map((part) => part.dynamic ? `[${part.content}]` : part.content).join("");
60
60
  });
61
- return `/${arr.join("/")}`.toLowerCase();
61
+ return `/${arr.join("/")}`;
62
62
  }
63
63
  export {
64
64
  parseRoute
@@ -101,7 +101,7 @@ function copyRequest(newUrl, oldRequest, isPrerendered, logger, routePattern) {
101
101
  signal: oldRequest.signal,
102
102
  keepalive: oldRequest.keepalive,
103
103
  // https://fetch.spec.whatwg.org/#dom-request-duplex
104
- // @ts-expect-error It isn't part of the types, but undici accepts it and it allows to carry over the body to a new request
104
+ // @ts-expect-error It isn't part of the types, but undici accepts it and it allows carrying over the body to a new request
105
105
  duplex: "half"
106
106
  }
107
107
  });
@@ -52,11 +52,7 @@ function renderAllHeadContent(result) {
52
52
  (link) => renderElement("link", link, false)
53
53
  );
54
54
  content += styles.join("\n") + links.join("\n") + scripts.join("\n");
55
- if (result._metadata.extraHead.length > 0) {
56
- for (const part of result._metadata.extraHead) {
57
- content += part;
58
- }
59
- }
55
+ content += result._metadata.extraHead.join("");
60
56
  return markHTMLString(content);
61
57
  }
62
58
  function renderHead() {
@@ -149,7 +149,7 @@ class BufferedRenderer {
149
149
  function createBufferedRenderer(destination, renderFunction) {
150
150
  return new BufferedRenderer(destination, renderFunction);
151
151
  }
152
- const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]";
152
+ const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]" && !(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers");
153
153
  const isDeno = typeof Deno !== "undefined";
154
154
  function promiseWithResolvers() {
155
155
  let resolve, reject;
@@ -67,6 +67,9 @@ function hmrReload() {
67
67
  if (hasSkippedStyleModules) {
68
68
  return [];
69
69
  }
70
+ if (modules.length > 0) {
71
+ return [];
72
+ }
70
73
  }
71
74
  }
72
75
  };
@@ -26,12 +26,18 @@ function normalizeFilename(filename, root) {
26
26
  } else if (filename.startsWith(".")) {
27
27
  const url = new URL(filename, root);
28
28
  filename = viteID(url);
29
- } else if (filename.startsWith("/") && !commonAncestorPath(filename, fileURLToPath(root))) {
29
+ } else if (filename.startsWith("/") && !isPathInRoot(filename, fileURLToPath(root))) {
30
30
  const url = new URL("." + filename, root);
31
31
  filename = viteID(url);
32
32
  }
33
33
  return removeLeadingForwardSlashWindows(filename);
34
34
  }
35
+ function isPathInRoot(filename, rootPath) {
36
+ if (commonAncestorPath(filename, rootPath)) {
37
+ return true;
38
+ }
39
+ return commonAncestorPath(filename.toLowerCase(), rootPath.toLowerCase()) !== "";
40
+ }
35
41
  const postfixRE = /[?#].*$/s;
36
42
  function cleanUrl(url) {
37
43
  return url.replace(postfixRE, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.4.3",
3
+ "version": "6.4.4",
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",