astro 4.8.2 → 4.8.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.
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
@@ -1596,14 +1596,14 @@ export interface AstroUserConfig {
1596
1596
  * @name experimental.actions
1597
1597
  * @type {boolean}
1598
1598
  * @default `false`
1599
- * @version 4.7.0
1599
+ * @version 4.8.0
1600
1600
  * @description
1601
1601
  *
1602
1602
  * Actions help you write type-safe backend functions you can call from anywhere. Enable server rendering [using the `output` property](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered) and add the `actions` flag to the `experimental` object:
1603
1603
  *
1604
1604
  * ```js
1605
1605
  * {
1606
- * output: 'hybrid', // or 'server'
1606
+ * output: 'hybrid', // or 'server'
1607
1607
  * experimental: {
1608
1608
  * actions: true,
1609
1609
  * },
@@ -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
  *
@@ -1623,20 +1623,20 @@ export interface AstroUserConfig {
1623
1623
  * export const server = {
1624
1624
  * like: defineAction({
1625
1625
  * input: z.object({ postId: z.string() }),
1626
- * handler: async ({ postId }, context) => {
1626
+ * handler: async ({ postId }) => {
1627
1627
  * // update likes in db
1628
1628
  *
1629
1629
  * return likes;
1630
1630
  * },
1631
1631
  * }),
1632
1632
  * comment: defineAction({
1633
- * accept: 'form',
1633
+ * accept: 'form',
1634
1634
  * input: z.object({
1635
1635
  * postId: z.string(),
1636
1636
  * author: z.string(),
1637
1637
  * body: z.string(),
1638
1638
  * }),
1639
- * handler: async ({ postId }, context) => {
1639
+ * handler: async ({ postId }) => {
1640
1640
  * // insert comments in db
1641
1641
  *
1642
1642
  * return comment;
@@ -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
  );
@@ -24,7 +31,7 @@ function astroActions() {
24
31
  });
25
32
  params.addMiddleware({
26
33
  entrypoint: "astro/actions/runtime/middleware.js",
27
- order: "pre"
34
+ order: "post"
28
35
  });
29
36
  await typegen({
30
37
  stringifiedActionsImport,
@@ -14,6 +14,7 @@ const onRequest = defineMiddleware(async (context, next) => {
14
14
  if (typeof actionPath !== "string") return nextWithLocalsStub(next, locals);
15
15
  const actionPathKeys = actionPath.replace("/_actions/", "").split(".");
16
16
  const action = await getAction(actionPathKeys);
17
+ if (!action) return nextWithLocalsStub(next, locals);
17
18
  const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));
18
19
  const actionsInternal = {
19
20
  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) {
@@ -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
  };
@@ -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.2";
1
+ const ASTRO_VERSION = "4.8.4";
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.2";
22
+ const currentVersion = "4.8.4";
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.2";
40
+ const version = "4.8.4";
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.2"}`
272
+ `v${"4.8.4"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -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) {
@@ -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>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "4.8.2",
3
+ "version": "4.8.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",
@@ -131,7 +131,7 @@
131
131
  "dlv": "^1.1.3",
132
132
  "dset": "^3.1.3",
133
133
  "es-module-lexer": "^1.5.2",
134
- "esbuild": "^0.21.1",
134
+ "esbuild": "^0.21.2",
135
135
  "estree-walker": "^3.0.3",
136
136
  "execa": "^8.0.1",
137
137
  "fast-glob": "^3.3.2",
@@ -152,8 +152,8 @@
152
152
  "prompts": "^2.4.2",
153
153
  "rehype": "^13.0.1",
154
154
  "resolve": "^1.22.8",
155
- "semver": "^7.6.1",
156
- "shiki": "^1.5.0",
155
+ "semver": "^7.6.2",
156
+ "shiki": "^1.5.1",
157
157
  "string-width": "^7.1.0",
158
158
  "strip-ansi": "^7.1.0",
159
159
  "tsconfck": "^3.0.3",
@@ -208,7 +208,7 @@
208
208
  "rehype-toc": "^3.0.2",
209
209
  "remark-code-titles": "^0.1.2",
210
210
  "rollup": "^4.17.2",
211
- "sass": "^1.77.0",
211
+ "sass": "^1.77.1",
212
212
  "srcset-parse": "^1.1.0",
213
213
  "unified": "^11.0.4",
214
214
  "astro-scripts": "0.0.14"
@@ -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
  }