astro 4.8.3 → 4.8.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 (41) hide show
  1. package/astro-jsx.d.ts +3 -0
  2. package/client.d.ts +1 -1
  3. package/config.d.ts +5 -1
  4. package/dist/@types/astro.d.ts +7 -5
  5. package/dist/actions/index.js +8 -1
  6. package/dist/actions/runtime/middleware.js +4 -1
  7. package/dist/actions/runtime/route.js +20 -9
  8. package/dist/actions/runtime/utils.d.ts +1 -1
  9. package/dist/actions/runtime/utils.js +2 -2
  10. package/dist/actions/runtime/virtual/shared.d.ts +1 -0
  11. package/dist/actions/runtime/virtual/shared.js +14 -7
  12. package/dist/assets/utils/metadata.js +17 -10
  13. package/dist/core/app/index.js +12 -0
  14. package/dist/core/app/pipeline.d.ts +1 -1
  15. package/dist/core/app/pipeline.js +7 -4
  16. package/dist/core/base-pipeline.d.ts +1 -1
  17. package/dist/core/build/internal.d.ts +8 -0
  18. package/dist/core/build/internal.js +15 -1
  19. package/dist/core/build/pipeline.d.ts +1 -1
  20. package/dist/core/build/pipeline.js +7 -4
  21. package/dist/core/build/plugins/plugin-analyzer.js +15 -4
  22. package/dist/core/build/plugins/plugin-css.js +14 -3
  23. package/dist/core/constants.js +1 -1
  24. package/dist/core/dev/dev.js +2 -1
  25. package/dist/core/errors/errors-data.d.ts +13 -0
  26. package/dist/core/errors/errors-data.js +7 -0
  27. package/dist/core/messages.js +2 -2
  28. package/dist/core/render-context.js +3 -3
  29. package/dist/core/routing/manifest/generator.d.ts +1 -1
  30. package/dist/core/routing/manifest/generator.js +12 -1
  31. package/dist/i18n/index.d.ts +1 -0
  32. package/dist/i18n/index.js +6 -1
  33. package/dist/i18n/middleware.js +5 -1
  34. package/dist/prefetch/index.d.ts +6 -2
  35. package/dist/prefetch/index.js +10 -12
  36. package/dist/runtime/server/render/astro/render.js +13 -6
  37. package/dist/runtime/server/render/script.js +6 -2
  38. package/dist/vite-plugin-astro-server/pipeline.d.ts +1 -1
  39. package/dist/vite-plugin-astro-server/pipeline.js +7 -4
  40. package/package.json +4 -2
  41. package/templates/actions.mjs +4 -0
package/astro-jsx.d.ts CHANGED
@@ -605,6 +605,7 @@ declare namespace astroHTML.JSX {
605
605
  href?: string | URL | undefined | null;
606
606
  hreflang?: string | undefined | null;
607
607
  media?: string | undefined | null;
608
+ name?: string | undefined | null;
608
609
  ping?: string | undefined | null;
609
610
  rel?: string | undefined | null;
610
611
  target?: HTMLAttributeAnchorTarget | undefined | null;
@@ -649,6 +650,7 @@ declare namespace astroHTML.JSX {
649
650
  type?: 'submit' | 'reset' | 'button' | undefined | null;
650
651
  value?: string | string[] | number | undefined | null;
651
652
  popovertarget?: string | undefined | null;
653
+ popovertargetaction?: 'hide' | 'show' | 'toggle' | undefined | null;
652
654
  }
653
655
 
654
656
  interface CanvasHTMLAttributes extends HTMLAttributes {
@@ -815,6 +817,7 @@ declare namespace astroHTML.JSX {
815
817
  value?: string | string[] | number | undefined | null;
816
818
  width?: number | string | undefined | null;
817
819
  popovertarget?: string | undefined | null;
820
+ popovertargetaction?: 'hide' | 'show' | 'toggle' | undefined | null;
818
821
  }
819
822
 
820
823
  interface KeygenHTMLAttributes extends HTMLAttributes {
package/client.d.ts CHANGED
@@ -12,7 +12,7 @@ interface ImportMetaEnv {
12
12
  /**
13
13
  * The prefix for Astro-generated asset links if the build.assetsPrefix config option is set. This can be used to create asset links not handled by Astro.
14
14
  */
15
- readonly ASSETS_PREFIX: string;
15
+ readonly ASSETS_PREFIX: string | Record<string, string>;
16
16
  /**
17
17
  * This is set to the site option specified in your project’s Astro config file.
18
18
  */
package/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  type ViteUserConfig = import('vite').UserConfig;
2
2
  type ViteUserConfigFn = import('vite').UserConfigFn;
3
3
  type AstroUserConfig = import('./dist/@types/astro.js').AstroUserConfig;
4
+ type AstroInlineConfig = import('./dist/@types/astro.js').AstroInlineConfig;
4
5
  type ImageServiceConfig = import('./dist/@types/astro.js').ImageServiceConfig;
5
6
  type SharpImageServiceConfig = import('./dist/assets/services/sharp.js').SharpImageServiceConfig;
6
7
 
@@ -13,7 +14,10 @@ export function defineConfig(config: AstroUserConfig): AstroUserConfig;
13
14
  /**
14
15
  * Use Astro to generate a fully resolved Vite config
15
16
  */
16
- export function getViteConfig(config: ViteUserConfig): ViteUserConfigFn;
17
+ export function getViteConfig(
18
+ config: ViteUserConfig,
19
+ inlineAstroConfig?: AstroInlineConfig
20
+ ): ViteUserConfigFn;
17
21
 
18
22
  /**
19
23
  * Return the configuration needed to use the Sharp-based image service
@@ -1612,7 +1612,7 @@ export interface AstroUserConfig {
1612
1612
  *
1613
1613
  * Declare all your actions in `src/actions/index.ts`. This file is the global actions handler.
1614
1614
  *
1615
- * Define an action using the `defineAction()` utility from the `astro:actions` module. These accept the `handler` property to define your server-side request handler. If your action accepts arguments, apply the `input` property to validate parameters with Zod.
1615
+ * Define an action using the `defineAction()` utility from the `astro:actions` module. An action accepts the `handler` property to define your server-side request handler. If your action accepts arguments, apply the `input` property to validate parameters with Zod.
1616
1616
  *
1617
1617
  * This example defines two actions: `like` and `comment`. The `like` action accepts a JSON object with a `postId` string, while the `comment` action accepts [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) with `postId`, `author`, and `body` strings. Each `handler` updates your database and return a type-safe response.
1618
1618
  *
@@ -1645,12 +1645,14 @@ export interface AstroUserConfig {
1645
1645
  * };
1646
1646
  * ```
1647
1647
  *
1648
- * Then, call an action from your client components using the `actions` object from `astro:actions`. You can pass a type-safe object when using JSON, or a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) object when using `accept: 'form'` in your action definition:
1648
+ * Then, call an action from your client components using the `actions` object from `astro:actions`. You can pass a type-safe object when using JSON, or a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) object when using `accept: 'form'` in your action definition.
1649
+ *
1650
+ * This example calls the `like` and `comment` actions from a React component:
1649
1651
  *
1650
1652
  * ```tsx "actions"
1651
1653
  * // src/components/blog.tsx
1652
1654
  * import { actions } from "astro:actions";
1653
- * import { useState } from "preact/hooks";
1655
+ * import { useState } from "react";
1654
1656
  *
1655
1657
  * export function Like({ postId }: { postId: string }) {
1656
1658
  * const [likes, setLikes] = useState(0);
@@ -1671,13 +1673,13 @@ export interface AstroUserConfig {
1671
1673
  * <form
1672
1674
  * onSubmit={async (e) => {
1673
1675
  * e.preventDefault();
1674
- * const formData = new FormData(e.target);
1676
+ * const formData = new FormData(e.target as HTMLFormElement);
1675
1677
  * const result = await actions.blog.comment(formData);
1676
1678
  * // handle result
1677
1679
  * }}
1678
1680
  * >
1679
1681
  * <input type="hidden" name="postId" value={postId} />
1680
- * <label for="author">Author</label>
1682
+ * <label htmlFor="author">Author</label>
1681
1683
  * <input id="author" type="text" name="author" />
1682
1684
  * <textarea rows={10} name="body"></textarea>
1683
1685
  * <button type="submit">Post</button>
@@ -1,11 +1,18 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { viteID } from "../core/util.js";
2
+ import { ActionsWithoutServerOutputError } from "../core/errors/errors-data.js";
3
+ import { AstroError } from "../core/errors/errors.js";
4
+ import { isServerLikeOutput, viteID } from "../core/util.js";
3
5
  import { ACTIONS_TYPES_FILE, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from "./consts.js";
4
6
  function astroActions() {
5
7
  return {
6
8
  name: VIRTUAL_MODULE_ID,
7
9
  hooks: {
8
10
  async "astro:config:setup"(params) {
11
+ if (!isServerLikeOutput(params.config)) {
12
+ const error = new AstroError(ActionsWithoutServerOutputError);
13
+ error.stack = void 0;
14
+ throw error;
15
+ }
9
16
  const stringifiedActionsImport = JSON.stringify(
10
17
  viteID(new URL("./actions", params.config.srcDir))
11
18
  );
@@ -4,16 +4,19 @@ import { formContentTypes, getAction, hasContentType } from "./utils.js";
4
4
  import { callSafely } from "./virtual/shared.js";
5
5
  const onRequest = defineMiddleware(async (context, next) => {
6
6
  const locals = context.locals;
7
+ if (locals._actionsInternal) return next();
7
8
  const { request, url } = context;
8
9
  const contentType = request.headers.get("Content-Type");
9
10
  if (url.pathname.startsWith("/_actions")) return nextWithLocalsStub(next, locals);
10
- if (!contentType || !hasContentType(contentType, formContentTypes))
11
+ if (!contentType || !hasContentType(contentType, formContentTypes)) {
11
12
  return nextWithLocalsStub(next, locals);
13
+ }
12
14
  const formData = await request.clone().formData();
13
15
  const actionPath = formData.get("_astroAction");
14
16
  if (typeof actionPath !== "string") return nextWithLocalsStub(next, locals);
15
17
  const actionPathKeys = actionPath.replace("/_actions/", "").split(".");
16
18
  const action = await getAction(actionPathKeys);
19
+ if (!action) return nextWithLocalsStub(next, locals);
17
20
  const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));
18
21
  const actionsInternal = {
19
22
  getActionResult: (actionFn) => {
@@ -5,9 +5,15 @@ const POST = async (context) => {
5
5
  const { request, url } = context;
6
6
  const actionPathKeys = url.pathname.replace("/_actions/", "").split(".");
7
7
  const action = await getAction(actionPathKeys);
8
+ if (!action) {
9
+ return new Response(null, { status: 404 });
10
+ }
8
11
  const contentType = request.headers.get("Content-Type");
12
+ const contentLength = request.headers.get("Content-Length");
9
13
  let args;
10
- if (contentType && hasContentType(contentType, formContentTypes)) {
14
+ if (contentLength === "0") {
15
+ args = void 0;
16
+ } else if (contentType && hasContentType(contentType, formContentTypes)) {
11
17
  args = await request.clone().formData();
12
18
  } else if (contentType && hasContentType(contentType, ["application/json"])) {
13
19
  args = await request.clone().json();
@@ -16,17 +22,22 @@ const POST = async (context) => {
16
22
  }
17
23
  const result = await ApiContextStorage.run(context, () => callSafely(() => action(args)));
18
24
  if (result.error) {
19
- if (import.meta.env.PROD) {
20
- result.error.stack = void 0;
21
- }
22
- return new Response(JSON.stringify(result.error), {
23
- status: result.error.status,
24
- headers: {
25
- "Content-Type": "application/json"
25
+ return new Response(
26
+ JSON.stringify({
27
+ ...result.error,
28
+ message: result.error.message,
29
+ stack: import.meta.env.PROD ? void 0 : result.error.stack
30
+ }),
31
+ {
32
+ status: result.error.status,
33
+ headers: {
34
+ "Content-Type": "application/json"
35
+ }
26
36
  }
27
- });
37
+ );
28
38
  }
29
39
  return new Response(JSON.stringify(result.data), {
40
+ status: result.data ? 200 : 204,
30
41
  headers: {
31
42
  "Content-Type": "application/json"
32
43
  }
@@ -1,4 +1,4 @@
1
1
  export declare const formContentTypes: string[];
2
2
  export declare function hasContentType(contentType: string, expected: string[]): boolean;
3
3
  export type MaybePromise<T> = T | Promise<T>;
4
- export declare function getAction(pathKeys: string[]): Promise<(param: unknown) => MaybePromise<unknown>>;
4
+ export declare function getAction(pathKeys: string[]): Promise<((param: unknown) => MaybePromise<unknown>) | undefined>;
@@ -7,12 +7,12 @@ async function getAction(pathKeys) {
7
7
  let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH);
8
8
  for (const key of pathKeys) {
9
9
  if (!(key in actionLookup)) {
10
- throw new Error("Action not found");
10
+ return void 0;
11
11
  }
12
12
  actionLookup = actionLookup[key];
13
13
  }
14
14
  if (typeof actionLookup !== "function") {
15
- throw new Error("Action not found");
15
+ return void 0;
16
16
  }
17
17
  return actionLookup;
18
18
  }
@@ -9,6 +9,7 @@ export declare class ActionError<T extends ErrorInferenceObject = ErrorInference
9
9
  constructor(params: {
10
10
  message?: string;
11
11
  code: ActionErrorCode;
12
+ stack?: string;
12
13
  });
13
14
  static codeToStatus(code: ActionErrorCode): number;
14
15
  static statusToCode(status: number): ActionErrorCode;
@@ -28,6 +28,9 @@ class ActionError extends Error {
28
28
  super(params.message);
29
29
  this.code = params.code;
30
30
  this.status = ActionError.codeToStatus(params.code);
31
+ if (params.stack) {
32
+ this.stack = params.stack;
33
+ }
31
34
  }
32
35
  static codeToStatus(code) {
33
36
  return codeToStatusMap[code];
@@ -36,15 +39,16 @@ class ActionError extends Error {
36
39
  return statusToCodeMap[status] ?? "INTERNAL_SERVER_ERROR";
37
40
  }
38
41
  static async fromResponse(res) {
39
- if (res.status === 400 && res.headers.get("Content-Type")?.toLowerCase().startsWith("application/json")) {
40
- const body = await res.json();
41
- if (typeof body === "object" && body?.type === "AstroActionInputError" && Array.isArray(body.issues)) {
42
- return new ActionInputError(body.issues);
43
- }
42
+ const body = await res.clone().json();
43
+ if (typeof body === "object" && body?.type === "AstroActionInputError" && Array.isArray(body.issues)) {
44
+ return new ActionInputError(body.issues);
45
+ }
46
+ if (typeof body === "object" && body?.type === "AstroActionError") {
47
+ return new ActionError(body);
44
48
  }
45
49
  return new ActionError({
46
50
  message: res.statusText,
47
- code: this.statusToCode(res.status)
51
+ code: ActionError.statusToCode(res.status)
48
52
  });
49
53
  }
50
54
  }
@@ -59,7 +63,10 @@ class ActionInputError extends ActionError {
59
63
  issues;
60
64
  fields;
61
65
  constructor(issues) {
62
- super({ message: "Failed to validate", code: "BAD_REQUEST" });
66
+ super({
67
+ message: `Failed to validate: ${JSON.stringify(issues, null, 2)}`,
68
+ code: "BAD_REQUEST"
69
+ });
63
70
  this.issues = issues;
64
71
  this.fields = {};
65
72
  for (const issue of issues) {
@@ -1,21 +1,28 @@
1
1
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
2
  import { lookup as probe } from "../utils/vendor/image-size/lookup.js";
3
3
  async function imageMetadata(data, src) {
4
- const result = probe(data);
5
- if (!result.height || !result.width || !result.type) {
4
+ try {
5
+ const result = probe(data);
6
+ if (!result.height || !result.width || !result.type) {
7
+ throw new AstroError({
8
+ ...AstroErrorData.NoImageMetadata,
9
+ message: AstroErrorData.NoImageMetadata.message(src)
10
+ });
11
+ }
12
+ const { width, height, type, orientation } = result;
13
+ const isPortrait = (orientation || 0) >= 5;
14
+ return {
15
+ width: isPortrait ? height : width,
16
+ height: isPortrait ? width : height,
17
+ format: type,
18
+ orientation
19
+ };
20
+ } catch (e) {
6
21
  throw new AstroError({
7
22
  ...AstroErrorData.NoImageMetadata,
8
23
  message: AstroErrorData.NoImageMetadata.message(src)
9
24
  });
10
25
  }
11
- const { width, height, type, orientation } = result;
12
- const isPortrait = (orientation || 0) >= 5;
13
- return {
14
- width: isPortrait ? height : width,
15
- height: isPortrait ? width : height,
16
- format: type,
17
- orientation
18
- };
19
26
  }
20
27
  export {
21
28
  imageMetadata
@@ -186,6 +186,14 @@ class App {
186
186
  this.#logRenderOptionsDeprecationWarning();
187
187
  }
188
188
  }
189
+ if (routeData) {
190
+ this.#logger.debug(
191
+ "router",
192
+ "The adapter " + this.#manifest.adapterName + " provided a custom RouteData for ",
193
+ request.url
194
+ );
195
+ this.#logger.debug("router", "RouteData:\n" + routeData);
196
+ }
189
197
  if (locals) {
190
198
  if (typeof locals !== "object") {
191
199
  this.#logger.error(null, new AstroError(AstroErrorData.LocalsNotAnObject).stack);
@@ -201,8 +209,12 @@ class App {
201
209
  }
202
210
  if (!routeData) {
203
211
  routeData = this.match(request);
212
+ this.#logger.debug("router", "Astro matched the following route for " + request.url);
213
+ this.#logger.debug("router", "RouteData:\n" + routeData);
204
214
  }
205
215
  if (!routeData) {
216
+ this.#logger.debug("router", "Astro hasn't found routes that match " + request.url);
217
+ this.#logger.debug("router", "Here's the available routes:\n", this.#manifestData);
206
218
  return this.#renderError(request, { locals, status: 404 });
207
219
  }
208
220
  const pathname = this.#getPathnameFromRequest(request);
@@ -7,6 +7,6 @@ export declare class AppPipeline extends Pipeline {
7
7
  headElements(routeData: RouteData): Pick<SSRResult, 'scripts' | 'styles' | 'links'>;
8
8
  componentMetadata(): void;
9
9
  getComponentByRoute(routeData: RouteData): Promise<ComponentInstance>;
10
- tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]>;
10
+ tryRewrite(payload: RewritePayload, request: Request): Promise<[RouteData, ComponentInstance]>;
11
11
  getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule>;
12
12
  }
@@ -50,7 +50,7 @@ class AppPipeline extends Pipeline {
50
50
  const module = await this.getModuleForRoute(routeData);
51
51
  return module.page();
52
52
  }
53
- async tryRewrite(payload) {
53
+ async tryRewrite(payload, request) {
54
54
  let foundRoute;
55
55
  for (const route of this.#manifestData.routes) {
56
56
  if (payload instanceof URL) {
@@ -64,9 +64,12 @@ class AppPipeline extends Pipeline {
64
64
  foundRoute = route;
65
65
  break;
66
66
  }
67
- } else if (route.pattern.test(decodeURI(payload))) {
68
- foundRoute = route;
69
- break;
67
+ } else {
68
+ const newUrl = new URL(payload, new URL(request.url).origin);
69
+ if (route.pattern.test(decodeURI(newUrl.pathname))) {
70
+ foundRoute = route;
71
+ break;
72
+ }
70
73
  }
71
74
  }
72
75
  if (foundRoute) {
@@ -64,7 +64,7 @@ export declare abstract class Pipeline {
64
64
  *
65
65
  * @param {RewritePayload} rewritePayload
66
66
  */
67
- abstract tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]>;
67
+ abstract tryRewrite(rewritePayload: RewritePayload, request: Request): Promise<[RouteData, ComponentInstance]>;
68
68
  /**
69
69
  * Tells the pipeline how to retrieve a component give a `RouteData`
70
70
  * @param routeData
@@ -40,6 +40,10 @@ export interface BuildInternals {
40
40
  * A map for page-specific information by a client:only component
41
41
  */
42
42
  pagesByClientOnly: Map<string, Set<PageBuildData>>;
43
+ /**
44
+ * A map for page-specific information by a script in an Astro file
45
+ */
46
+ pagesByScriptId: Map<string, Set<PageBuildData>>;
43
47
  /**
44
48
  * A map of hydrated components to export names that are discovered during the SSR build.
45
49
  * These will be used as the top-level entrypoints for the client build.
@@ -96,6 +100,10 @@ export declare function trackPageData(internals: BuildInternals, component: stri
96
100
  * Tracks client-only components to the pages they are associated with.
97
101
  */
98
102
  export declare function trackClientOnlyPageDatas(internals: BuildInternals, pageData: PageBuildData, clientOnlys: string[]): void;
103
+ /**
104
+ * Tracks scripts to the pages they are associated with. (experimental.directRenderScript)
105
+ */
106
+ export declare function trackScriptPageDatas(internals: BuildInternals, pageData: PageBuildData, scriptIds: string[]): void;
99
107
  export declare function getPageDatasByChunk(internals: BuildInternals, chunk: Rollup.RenderedChunk): Generator<PageBuildData, void, unknown>;
100
108
  export declare function getPageDatasByClientOnlyID(internals: BuildInternals, viteid: ViteID): Generator<PageBuildData, void, unknown>;
101
109
  /**
@@ -16,6 +16,7 @@ function createBuildInternals() {
16
16
  pageOptionsByPage: /* @__PURE__ */ new Map(),
17
17
  pagesByViteID: /* @__PURE__ */ new Map(),
18
18
  pagesByClientOnly: /* @__PURE__ */ new Map(),
19
+ pagesByScriptId: /* @__PURE__ */ new Map(),
19
20
  propagatedStylesMap: /* @__PURE__ */ new Map(),
20
21
  propagatedScriptsMap: /* @__PURE__ */ new Map(),
21
22
  discoveredHydratedComponents: /* @__PURE__ */ new Map(),
@@ -45,6 +46,18 @@ function trackClientOnlyPageDatas(internals, pageData, clientOnlys) {
45
46
  pageDataSet.add(pageData);
46
47
  }
47
48
  }
49
+ function trackScriptPageDatas(internals, pageData, scriptIds) {
50
+ for (const scriptId of scriptIds) {
51
+ let pageDataSet;
52
+ if (internals.pagesByScriptId.has(scriptId)) {
53
+ pageDataSet = internals.pagesByScriptId.get(scriptId);
54
+ } else {
55
+ pageDataSet = /* @__PURE__ */ new Set();
56
+ internals.pagesByScriptId.set(scriptId, pageDataSet);
57
+ }
58
+ pageDataSet.add(pageData);
59
+ }
60
+ }
48
61
  function* getPageDatasByChunk(internals, chunk) {
49
62
  const pagesByViteID = internals.pagesByViteID;
50
63
  for (const [modulePath] of Object.entries(chunk.modules)) {
@@ -180,5 +193,6 @@ export {
180
193
  hasPrerenderedPages,
181
194
  mergeInlineCss,
182
195
  trackClientOnlyPageDatas,
183
- trackPageData
196
+ trackPageData,
197
+ trackScriptPageDatas
184
198
  };
@@ -38,6 +38,6 @@ export declare class BuildPipeline extends Pipeline {
38
38
  */
39
39
  retrieveRoutesToGenerate(): Map<PageBuildData, string>;
40
40
  getComponentByRoute(routeData: RouteData): Promise<ComponentInstance>;
41
- tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]>;
41
+ tryRewrite(payload: RewritePayload, request: Request): Promise<[RouteData, ComponentInstance]>;
42
42
  retrieveSsrEntry(route: RouteData, filePath: string): Promise<SinglePageBuiltModule>;
43
43
  }
@@ -206,7 +206,7 @@ class BuildPipeline extends Pipeline {
206
206
  return module.page();
207
207
  }
208
208
  }
209
- async tryRewrite(payload) {
209
+ async tryRewrite(payload, request) {
210
210
  let foundRoute;
211
211
  for (const route of this.options.manifest.routes) {
212
212
  if (payload instanceof URL) {
@@ -220,9 +220,12 @@ class BuildPipeline extends Pipeline {
220
220
  foundRoute = route;
221
221
  break;
222
222
  }
223
- } else if (route.pattern.test(decodeURI(payload))) {
224
- foundRoute = route;
225
- break;
223
+ } else {
224
+ const newUrl = new URL(payload, new URL(request.url).origin);
225
+ if (route.pattern.test(decodeURI(newUrl.pathname))) {
226
+ foundRoute = route;
227
+ break;
228
+ }
226
229
  }
227
230
  }
228
231
  if (foundRoute) {
@@ -5,7 +5,11 @@ import {
5
5
  getTopLevelPageModuleInfos,
6
6
  moduleIsTopLevelPage
7
7
  } from "../graph.js";
8
- import { getPageDataByViteID, trackClientOnlyPageDatas } from "../internal.js";
8
+ import {
9
+ getPageDataByViteID,
10
+ trackClientOnlyPageDatas,
11
+ trackScriptPageDatas
12
+ } from "../internal.js";
9
13
  function isPropagatedAsset(id) {
10
14
  try {
11
15
  return new URL("file://" + id).searchParams.has(PROPAGATED_ASSET_FLAG);
@@ -120,9 +124,16 @@ function vitePluginAnalyzer(options, internals) {
120
124
  }
121
125
  }
122
126
  if (options.settings.config.experimental.directRenderScript && astro.scripts.length) {
123
- for (let i = 0; i < astro.scripts.length; i++) {
124
- const hid = `${id.replace("/@fs", "")}?astro&type=script&index=${i}&lang.ts`;
125
- internals.discoveredScripts.add(hid);
127
+ const scriptIds = astro.scripts.map(
128
+ (_, i) => `${id.replace("/@fs", "")}?astro&type=script&index=${i}&lang.ts`
129
+ );
130
+ for (const scriptId of scriptIds) {
131
+ internals.discoveredScripts.add(scriptId);
132
+ }
133
+ for (const pageInfo of getTopLevelPageModuleInfos(id, this)) {
134
+ const newPageData = getPageDataByViteID(internals, pageInfo.id);
135
+ if (!newPageData) continue;
136
+ trackScriptPageDatas(internals, newPageData, scriptIds);
126
137
  }
127
138
  }
128
139
  }
@@ -92,9 +92,20 @@ function rollupPluginAstroBuildCSS(options) {
92
92
  if (pageData) {
93
93
  appendCSSToPage(pageData, meta, pagesToCss, depth, order);
94
94
  }
95
- } else if (options.target === "client" && internals.hoistedScriptIdToPagesMap.has(pageInfo.id)) {
96
- for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
97
- appendCSSToPage(pageData, meta, pagesToCss, -1, order);
95
+ } else if (options.target === "client") {
96
+ if (buildOptions.settings.config.experimental.directRenderScript) {
97
+ const pageDatas = internals.pagesByScriptId.get(pageInfo.id);
98
+ if (pageDatas) {
99
+ for (const pageData of pageDatas) {
100
+ appendCSSToPage(pageData, meta, pagesToCss, -1, order);
101
+ }
102
+ }
103
+ } else {
104
+ if (internals.hoistedScriptIdToPagesMap.has(pageInfo.id)) {
105
+ for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
106
+ appendCSSToPage(pageData, meta, pagesToCss, -1, order);
107
+ }
108
+ }
98
109
  }
99
110
  }
100
111
  }
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "4.8.3";
1
+ const ASTRO_VERSION = "4.8.5";
2
2
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
3
3
  const ROUTE_TYPE_HEADER = "X-Astro-Route-Type";
4
4
  const DEFAULT_404_COMPONENT = "astro-default-404";
@@ -19,7 +19,7 @@ async function dev(inlineConfig) {
19
19
  await telemetry.record([]);
20
20
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
21
21
  const logger = restart.container.logger;
22
- const currentVersion = "4.8.3";
22
+ const currentVersion = "4.8.5";
23
23
  const isPrerelease = currentVersion.includes("-");
24
24
  if (!isPrerelease) {
25
25
  try {
@@ -42,6 +42,7 @@ async function dev(inlineConfig) {
42
42
  );
43
43
  }
44
44
  }
45
+ }).catch(() => {
45
46
  });
46
47
  } catch (e) {
47
48
  }
@@ -1334,6 +1334,19 @@ export declare const DuplicateContentEntrySlugError: {
1334
1334
  title: string;
1335
1335
  message(collection: string, slug: string, preExisting: string, alsoFound: string): string;
1336
1336
  };
1337
+ /**
1338
+ * @docs
1339
+ * @see
1340
+ * - [On-demand rendering](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered)
1341
+ * @description
1342
+ * Your project must have a server output to create backend functions with Actions.
1343
+ */
1344
+ export declare const ActionsWithoutServerOutputError: {
1345
+ name: string;
1346
+ title: string;
1347
+ message: string;
1348
+ hint: string;
1349
+ };
1337
1350
  /**
1338
1351
  * @docs
1339
1352
  * @see
@@ -502,6 +502,12 @@ Entries:
502
502
  - ${alsoFound}`;
503
503
  }
504
504
  };
505
+ const ActionsWithoutServerOutputError = {
506
+ name: "ActionsWithoutServerOutputError",
507
+ title: "Actions must be used with server output.",
508
+ message: "Actions enabled without setting a server build output. A server is required to create callable backend functions. To deploy routes to a server, add a server adapter to your astro config.",
509
+ hint: "Learn about on-demand rendering: https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered"
510
+ };
505
511
  const UnsupportedConfigTransformError = {
506
512
  name: "UnsupportedConfigTransformError",
507
513
  title: "Unsupported transform in content config.",
@@ -511,6 +517,7 @@ Full error: ${parseError}`,
511
517
  };
512
518
  const UnknownError = { name: "UnknownError", title: "Unknown Error." };
513
519
  export {
520
+ ActionsWithoutServerOutputError,
514
521
  AstroGlobNoMatch,
515
522
  AstroGlobUsedOutside,
516
523
  AstroResponseHeadersReassigned,
@@ -37,7 +37,7 @@ function serverStart({
37
37
  host,
38
38
  base
39
39
  }) {
40
- const version = "4.8.3";
40
+ const version = "4.8.5";
41
41
  const localPrefix = `${dim("\u2503")} Local `;
42
42
  const networkPrefix = `${dim("\u2503")} Network `;
43
43
  const emptyPrefix = " ".repeat(11);
@@ -269,7 +269,7 @@ function printHelp({
269
269
  message.push(
270
270
  linebreak(),
271
271
  ` ${bgGreen(black(` ${commandName} `))} ${green(
272
- `v${"4.8.3"}`
272
+ `v${"4.8.5"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -95,7 +95,7 @@ class RenderContext {
95
95
  if (payload) {
96
96
  if (this.pipeline.manifest.rewritingEnabled) {
97
97
  try {
98
- const [routeData, component] = await pipeline.tryRewrite(payload);
98
+ const [routeData, component] = await pipeline.tryRewrite(payload, this.request);
99
99
  this.routeData = routeData;
100
100
  componentInstance = component;
101
101
  } catch (e) {
@@ -173,7 +173,7 @@ class RenderContext {
173
173
  const rewrite = async (reroutePayload) => {
174
174
  pipeline.logger.debug("router", "Called rewriting to:", reroutePayload);
175
175
  try {
176
- const [routeData, component] = await pipeline.tryRewrite(reroutePayload);
176
+ const [routeData, component] = await pipeline.tryRewrite(reroutePayload, this.request);
177
177
  this.routeData = routeData;
178
178
  if (reroutePayload instanceof Request) {
179
179
  this.request = reroutePayload;
@@ -328,7 +328,7 @@ class RenderContext {
328
328
  const rewrite = async (reroutePayload) => {
329
329
  try {
330
330
  pipeline.logger.debug("router", "Calling rewrite: ", reroutePayload);
331
- const [routeData, component] = await pipeline.tryRewrite(reroutePayload);
331
+ const [routeData, component] = await pipeline.tryRewrite(reroutePayload, this.request);
332
332
  this.routeData = routeData;
333
333
  if (reroutePayload instanceof Request) {
334
334
  this.request = reroutePayload;
@@ -1,2 +1,2 @@
1
1
  import type { AstroConfig, RoutePart } from '../../../@types/astro.js';
2
- export declare function getRouteGenerator(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']): (params: object) => string;
2
+ export declare function getRouteGenerator(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']): (params: Record<string, string | number | undefined>) => string;
@@ -1,4 +1,14 @@
1
1
  import { compile } from "path-to-regexp";
2
+ function sanitizeParams(params) {
3
+ return Object.fromEntries(
4
+ Object.entries(params).map(([key, value]) => {
5
+ if (typeof value === "string") {
6
+ return [key, value.normalize().replace(/#/g, "%23").replace(/\?/g, "%3F")];
7
+ }
8
+ return [key, value];
9
+ })
10
+ );
11
+ }
2
12
  function getRouteGenerator(segments, addTrailingSlash) {
3
13
  const template = segments.map((segment) => {
4
14
  return "/" + segment.map((part) => {
@@ -17,7 +27,8 @@ function getRouteGenerator(segments, addTrailingSlash) {
17
27
  }
18
28
  const toPath = compile(template + trailing);
19
29
  return (params) => {
20
- const path = toPath(params);
30
+ const sanitizedParams = sanitizeParams(params);
31
+ const path = toPath(sanitizedParams);
21
32
  return path || "/";
22
33
  };
23
34
  }
@@ -1,6 +1,7 @@
1
1
  import type { APIContext, AstroConfig, Locales, SSRManifest, ValidRedirectStatus } from '../@types/astro.js';
2
2
  import type { RoutingStrategies } from './utils.js';
3
3
  export declare function requestHasLocale(locales: Locales): (context: APIContext) => boolean;
4
+ export declare function requestIs404Or500(request: Request, base?: string): boolean;
4
5
  export declare function pathHasLocale(path: string, locales: Locales): boolean;
5
6
  type GetLocaleRelativeUrl = GetLocaleOptions & {
6
7
  locale: string;
@@ -9,6 +9,10 @@ function requestHasLocale(locales) {
9
9
  return pathHasLocale(context.url.pathname, locales);
10
10
  };
11
11
  }
12
+ function requestIs404Or500(request, base = "") {
13
+ const url = new URL(request.url);
14
+ return url.pathname.startsWith(`${base}/404`) || url.pathname.startsWith(`${base}/500`);
15
+ }
12
16
  function pathHasLocale(path, locales) {
13
17
  const segments = path.split("/");
14
18
  for (const segment of segments) {
@@ -188,7 +192,7 @@ function notFound({ base, locales }) {
188
192
  if (!(isRoot || pathHasLocale(url.pathname, locales))) {
189
193
  if (response) {
190
194
  response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
191
- return new Response(null, {
195
+ return new Response(response.body, {
192
196
  status: 404,
193
197
  headers: response.headers
194
198
  });
@@ -263,6 +267,7 @@ export {
263
267
  redirectToDefaultLocale,
264
268
  redirectToFallback,
265
269
  requestHasLocale,
270
+ requestIs404Or500,
266
271
  toCodes,
267
272
  toPaths
268
273
  };
@@ -4,7 +4,8 @@ import {
4
4
  notFound,
5
5
  redirectToDefaultLocale,
6
6
  redirectToFallback,
7
- requestHasLocale
7
+ requestHasLocale,
8
+ requestIs404Or500
8
9
  } from "./index.js";
9
10
  function createI18nMiddleware(i18n, base, trailingSlash, format) {
10
11
  if (!i18n) return (_, next) => next();
@@ -50,6 +51,9 @@ function createI18nMiddleware(i18n, base, trailingSlash, format) {
50
51
  if (type !== "page" && type !== "fallback") {
51
52
  return response;
52
53
  }
54
+ if (requestIs404Or500(context.request, base)) {
55
+ return response;
56
+ }
53
57
  const { currentLocale } = context;
54
58
  switch (i18n.strategy) {
55
59
  case "manual": {
@@ -11,8 +11,12 @@ export declare function init(defaultOpts?: InitOptions): void;
11
11
  export interface PrefetchOptions {
12
12
  /**
13
13
  * How the prefetch should prioritize the URL. (default `'link'`)
14
- * - `'link'`: use `<link rel="prefetch">`, has lower loading priority.
15
- * - `'fetch'`: use `fetch()`, has higher loading priority.
14
+ * - `'link'`: use `<link rel="prefetch">`.
15
+ * - `'fetch'`: use `fetch()`.
16
+ *
17
+ * @deprecated It is recommended to not use this option, and let prefetch use `'link'` whenever it's supported,
18
+ * or otherwise fall back to `'fetch'`. `'link'` works better if the URL doesn't set an appropriate cache header,
19
+ * as the browser will continue to cache it as long as it's used subsequently.
16
20
  */
17
21
  with?: 'link' | 'fetch';
18
22
  /**
@@ -24,7 +24,7 @@ function initTapStrategy() {
24
24
  event,
25
25
  (e) => {
26
26
  if (elMatchesStrategy(e.target, "tap")) {
27
- prefetch(e.target.href, { with: "fetch", ignoreSlowConnection: true });
27
+ prefetch(e.target.href, { ignoreSlowConnection: true });
28
28
  }
29
29
  },
30
30
  { passive: true }
@@ -59,7 +59,7 @@ function initHoverStrategy() {
59
59
  clearTimeout(timeout);
60
60
  }
61
61
  timeout = setTimeout(() => {
62
- prefetch(href, { with: "fetch" });
62
+ prefetch(href);
63
63
  }, 80);
64
64
  }
65
65
  function handleHoverOut() {
@@ -97,7 +97,7 @@ function createViewportIntersectionObserver() {
97
97
  setTimeout(() => {
98
98
  observer.unobserve(anchor);
99
99
  timeouts.delete(anchor);
100
- prefetch(anchor.href, { with: "link" });
100
+ prefetch(anchor.href);
101
101
  }, 300)
102
102
  );
103
103
  } else {
@@ -113,7 +113,7 @@ function initLoadStrategy() {
113
113
  onPageLoad(() => {
114
114
  for (const anchor of document.getElementsByTagName("a")) {
115
115
  if (elMatchesStrategy(anchor, "load")) {
116
- prefetch(anchor.href, { with: "link" });
116
+ prefetch(anchor.href);
117
117
  }
118
118
  }
119
119
  });
@@ -122,20 +122,18 @@ function prefetch(url, opts) {
122
122
  const ignoreSlowConnection = opts?.ignoreSlowConnection ?? false;
123
123
  if (!canPrefetchUrl(url, ignoreSlowConnection)) return;
124
124
  prefetchedUrls.add(url);
125
- const priority = opts?.with ?? "link";
126
- debug?.(`[astro] Prefetching ${url} with ${priority}`);
127
- if (clientPrerender && HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules")) {
125
+ if (clientPrerender && HTMLScriptElement.supports?.("speculationrules")) {
126
+ debug?.(`[astro] Prefetching ${url} with <script type="speculationrules">`);
128
127
  appendSpeculationRules(url);
129
- } else if (priority === "link") {
128
+ } else if (document.createElement("link").relList?.supports?.("prefetch") && opts?.with !== "fetch") {
129
+ debug?.(`[astro] Prefetching ${url} with <link rel="prefetch">`);
130
130
  const link = document.createElement("link");
131
131
  link.rel = "prefetch";
132
132
  link.setAttribute("href", url);
133
133
  document.head.append(link);
134
134
  } else {
135
- fetch(url).catch((e) => {
136
- console.log(`[astro] Failed to prefetch ${url}`);
137
- console.error(e);
138
- });
135
+ debug?.(`[astro] Prefetching ${url} with fetch`);
136
+ fetch(url, { priority: "low" });
139
137
  }
140
138
  }
141
139
  function canPrefetchUrl(url, ignoreSlowConnection) {
@@ -128,12 +128,18 @@ async function renderToAsyncIterable(result, componentFactory, props, children,
128
128
  await bufferHeadContent(result);
129
129
  }
130
130
  let error = null;
131
- let next = promiseWithResolvers();
131
+ let next = null;
132
132
  const buffer = [];
133
+ let renderingComplete = false;
133
134
  const iterator = {
134
135
  async next() {
135
136
  if (result.cancelled) return { done: true, value: void 0 };
136
- await next.promise;
137
+ if (next !== null) {
138
+ await next.promise;
139
+ }
140
+ if (!renderingComplete) {
141
+ next = promiseWithResolvers();
142
+ }
137
143
  if (error) {
138
144
  throw error;
139
145
  }
@@ -176,17 +182,18 @@ async function renderToAsyncIterable(result, componentFactory, props, children,
176
182
  const bytes = chunkToByteArray(result, chunk);
177
183
  if (bytes.length > 0) {
178
184
  buffer.push(bytes);
179
- next.resolve();
180
- next = promiseWithResolvers();
185
+ next?.resolve();
181
186
  }
182
187
  }
183
188
  };
184
189
  const renderPromise = templateResult.render(destination);
185
190
  renderPromise.then(() => {
186
- next.resolve();
191
+ renderingComplete = true;
192
+ next?.resolve();
187
193
  }).catch((err) => {
188
194
  error = err;
189
- next.resolve();
195
+ renderingComplete = true;
196
+ next?.resolve();
190
197
  });
191
198
  return {
192
199
  [Symbol.asyncIterator]() {
@@ -3,8 +3,12 @@ async function renderScript(result, id) {
3
3
  if (result._metadata.renderedScripts.has(id)) return;
4
4
  result._metadata.renderedScripts.add(id);
5
5
  const inlined = result.inlinedScripts.get(id);
6
- if (inlined) {
7
- return markHTMLString(`<script type="module">${inlined}</script>`);
6
+ if (inlined != null) {
7
+ if (inlined) {
8
+ return markHTMLString(`<script type="module">${inlined}</script>`);
9
+ } else {
10
+ return "";
11
+ }
8
12
  }
9
13
  const resolved = await result.resolve(id);
10
14
  return markHTMLString(`<script type="module" src="${resolved}"></script>`);
@@ -19,6 +19,6 @@ export declare class DevPipeline extends Pipeline {
19
19
  preload(routeData: RouteData, filePath: URL): Promise<ComponentInstance>;
20
20
  clearRouteCache(): void;
21
21
  getComponentByRoute(routeData: RouteData): Promise<ComponentInstance>;
22
- tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]>;
22
+ tryRewrite(payload: RewritePayload, request: Request): Promise<[RouteData, ComponentInstance]>;
23
23
  setManifestData(manifestData: ManifestData): void;
24
24
  }
@@ -132,7 +132,7 @@ class DevPipeline extends Pipeline {
132
132
  return await this.preload(routeData, filePath);
133
133
  }
134
134
  }
135
- async tryRewrite(payload) {
135
+ async tryRewrite(payload, request) {
136
136
  let foundRoute;
137
137
  if (!this.manifestData) {
138
138
  throw new Error("Missing manifest data. This is an internal error, please file an issue.");
@@ -149,9 +149,12 @@ class DevPipeline extends Pipeline {
149
149
  foundRoute = route;
150
150
  break;
151
151
  }
152
- } else if (route.pattern.test(decodeURI(payload))) {
153
- foundRoute = route;
154
- break;
152
+ } else {
153
+ const newUrl = new URL(payload, new URL(request.url).origin);
154
+ if (route.pattern.test(decodeURI(newUrl.pathname))) {
155
+ foundRoute = route;
156
+ break;
157
+ }
155
158
  }
156
159
  }
157
160
  if (foundRoute) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "4.8.3",
3
+ "version": "4.8.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",
@@ -229,8 +229,10 @@
229
229
  "postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
230
230
  "test": "pnpm run test:node",
231
231
  "test:match": "pnpm run test:node --match",
232
- "test:e2e": "playwright test",
232
+ "test:e2e": "pnpm test:e2e:chrome && pnpm test:e2e:firefox",
233
233
  "test:e2e:match": "playwright test -g",
234
+ "test:e2e:chrome": "playwright test",
235
+ "test:e2e:firefox": "playwright test --config playwright.firefox.config.js",
234
236
  "test:node": "astro-scripts test \"test/**/*.test.js\""
235
237
  }
236
238
  }
@@ -45,6 +45,7 @@ async function actionHandler(clientParam, path) {
45
45
  });
46
46
  }
47
47
  headers.set('Content-Type', 'application/json');
48
+ headers.set('Content-Length', body?.length.toString() ?? '0');
48
49
  }
49
50
  const res = await fetch(path, {
50
51
  method: 'POST',
@@ -54,6 +55,9 @@ async function actionHandler(clientParam, path) {
54
55
  if (!res.ok) {
55
56
  throw await ActionError.fromResponse(res);
56
57
  }
58
+ // Check if response body is empty before parsing.
59
+ if (res.status === 204) return;
60
+
57
61
  const json = await res.json();
58
62
  return json;
59
63
  }