astro 4.8.7 → 4.9.1

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.
@@ -676,6 +676,47 @@ export interface AstroUserConfig {
676
676
  * Using `'attribute'` is useful when you are manipulating the `class` attribute of elements and need to avoid conflicts between your own styling logic and Astro's application of styles.
677
677
  */
678
678
  scopedStyleStrategy?: 'where' | 'class' | 'attribute';
679
+ /**
680
+ * @docs
681
+ * @name security
682
+ * @type {boolean}
683
+ * @default `{}`
684
+ * @version 4.9.0
685
+ * @description
686
+ *
687
+ * Enables security measures for an Astro website.
688
+ *
689
+ * These features only exist for pages rendered on demand (SSR) using `server` mode or pages that opt out of prerendering in `hybrid` mode.
690
+ *
691
+ * ```js
692
+ * // astro.config.mjs
693
+ * export default defineConfig({
694
+ * output: "server",
695
+ * security: {
696
+ * checkOrigin: true
697
+ * }
698
+ * })
699
+ * ```
700
+ */
701
+ security?: {
702
+ /**
703
+ * @docs
704
+ * @name security.checkOrigin
705
+ * @kind h4
706
+ * @type {boolean}
707
+ * @default 'false'
708
+ * @version 4.9.0
709
+ * @description
710
+ *
711
+ * When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`. This is used to provide Cross-Site Request Forgery (CSRF) protection.
712
+ *
713
+ * The "origin" check is executed only for pages rendered on demand, and only for the requests `POST`, `PATCH`, `DELETE` and `PUT` with
714
+ * one of the following `content-type` headers: `'application/x-www-form-urlencoded'`, `'multipart/form-data'`, `'text/plain'`.
715
+ *
716
+ * If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page.
717
+ */
718
+ checkOrigin?: boolean;
719
+ };
679
720
  /**
680
721
  * @docs
681
722
  * @name vite
@@ -1820,102 +1861,6 @@ export interface AstroUserConfig {
1820
1861
  * In the event of route collisions, where two routes of equal route priority attempt to build the same URL, Astro will log a warning identifying the conflicting routes.
1821
1862
  */
1822
1863
  globalRoutePriority?: boolean;
1823
- /**
1824
- * @docs
1825
- * @name experimental.i18nDomains
1826
- * @type {boolean}
1827
- * @default `false`
1828
- * @version 4.3.0
1829
- * @description
1830
- *
1831
- * Enables domain support for the [experimental `domains` routing strategy](https://docs.astro.build/en/guides/internationalization/#domains-experimental) which allows you to configure the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
1832
- *
1833
- * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
1834
- *
1835
- * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
1836
- *
1837
- * ```js
1838
- * //astro.config.mjs
1839
- * export default defineConfig({
1840
- * site: "https://example.com",
1841
- * output: "server", // required, with no prerendered pages
1842
- * adapter: node({
1843
- * mode: 'standalone',
1844
- * }),
1845
- * i18n: {
1846
- * defaultLocale: "en",
1847
- * locales: ["en", "fr", "pt-br", "es"],
1848
- * prefixDefaultLocale: false,
1849
- * domains: {
1850
- * fr: "https://fr.example.com",
1851
- * es: "https://example.es",
1852
- * },
1853
- * },
1854
- * experimental: {
1855
- * i18nDomains: true,
1856
- * },
1857
- * });
1858
- * ```
1859
- *
1860
- * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
1861
- *
1862
- * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature.
1863
- */
1864
- i18nDomains?: boolean;
1865
- /**
1866
- * @docs
1867
- * @name experimental.security
1868
- * @type {boolean}
1869
- * @default `false`
1870
- * @version 4.6.0
1871
- * @description
1872
- *
1873
- * Enables CSRF protection for Astro websites.
1874
- *
1875
- * The CSRF protection works only for pages rendered on demand (SSR) using `server` or `hybrid` mode. The pages must opt out of prerendering in `hybrid` mode.
1876
- *
1877
- * ```js
1878
- * // astro.config.mjs
1879
- * export default defineConfig({
1880
- * output: "server",
1881
- * experimental: {
1882
- * security: {
1883
- * csrfProtection: {
1884
- * origin: true
1885
- * }
1886
- * }
1887
- * }
1888
- * })
1889
- * ```
1890
- */
1891
- security?: {
1892
- /**
1893
- * @name security.csrfProtection
1894
- * @type {object}
1895
- * @default '{}'
1896
- * @version 4.6.0
1897
- * @description
1898
- *
1899
- * Allows you to enable security measures to prevent CSRF attacks: https://owasp.org/www-community/attacks/csrf
1900
- */
1901
- csrfProtection?: {
1902
- /**
1903
- * @name security.csrfProtection.origin
1904
- * @type {boolean}
1905
- * @default 'false'
1906
- * @version 4.6.0
1907
- * @description
1908
- *
1909
- * When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`.
1910
- *
1911
- * The "origin" check is executed only for pages rendered on demand, and only for the requests `POST, `PATCH`, `DELETE` and `PUT` with
1912
- * the following `content-type` header: 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'.
1913
- *
1914
- * If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page.
1915
- */
1916
- origin?: boolean;
1917
- };
1918
- };
1919
1864
  /**
1920
1865
  * @docs
1921
1866
  * @name experimental.rewriting
@@ -2848,6 +2793,8 @@ export interface SSRResult {
2848
2793
  createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
2849
2794
  resolve: (s: string) => Promise<string>;
2850
2795
  response: AstroGlobal['response'];
2796
+ request: AstroGlobal['request'];
2797
+ actionResult?: ReturnType<AstroGlobal['getActionResult']>;
2851
2798
  renderers: SSRLoadedRenderer[];
2852
2799
  /**
2853
2800
  * Map of directive name (e.g. `load`) to the directive script code
@@ -2,6 +2,7 @@ import type { APIContext } from '../../@types/astro.js';
2
2
  export type Locals = {
3
3
  _actionsInternal: {
4
4
  getActionResult: APIContext['getActionResult'];
5
+ actionResult?: ReturnType<APIContext['getActionResult']>;
5
6
  };
6
7
  };
7
8
  export declare const onRequest: import("../../@types/astro.js").MiddlewareHandler;
@@ -5,45 +5,47 @@ import { formContentTypes, getAction, hasContentType } from "./utils.js";
5
5
  import { callSafely } from "./virtual/shared.js";
6
6
  const onRequest = defineMiddleware(async (context, next) => {
7
7
  const locals = context.locals;
8
+ if (locals._actionsInternal) return ApiContextStorage.run(context, () => next());
8
9
  if (context.request.method === "GET") {
9
- return nextWithLocalsStub(next, locals);
10
+ return nextWithLocalsStub(next, context);
10
11
  }
11
12
  if (context.request.method === "POST" && context.request.body === null) {
12
- return nextWithStaticStub(next, locals);
13
+ return nextWithStaticStub(next, context);
13
14
  }
14
- if (locals._actionsInternal) return next();
15
15
  const { request, url } = context;
16
16
  const contentType = request.headers.get("Content-Type");
17
- if (url.pathname.startsWith("/_actions")) return nextWithLocalsStub(next, locals);
17
+ if (url.pathname.startsWith("/_actions")) return nextWithLocalsStub(next, context);
18
18
  if (!contentType || !hasContentType(contentType, formContentTypes)) {
19
- return nextWithLocalsStub(next, locals);
19
+ return nextWithLocalsStub(next, context);
20
20
  }
21
21
  const formData = await request.clone().formData();
22
22
  const actionPath = formData.get("_astroAction");
23
- if (typeof actionPath !== "string") return nextWithLocalsStub(next, locals);
24
- const actionPathKeys = actionPath.replace("/_actions/", "").split(".");
25
- const action = await getAction(actionPathKeys);
26
- if (!action) return nextWithLocalsStub(next, locals);
23
+ if (typeof actionPath !== "string") return nextWithLocalsStub(next, context);
24
+ const action = await getAction(actionPath);
25
+ if (!action) return nextWithLocalsStub(next, context);
27
26
  const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));
28
27
  const actionsInternal = {
29
28
  getActionResult: (actionFn) => {
30
29
  if (actionFn.toString() !== actionPath) return Promise.resolve(void 0);
31
30
  return result;
32
- }
31
+ },
32
+ actionResult: result
33
33
  };
34
34
  Object.defineProperty(locals, "_actionsInternal", { writable: false, value: actionsInternal });
35
- const response = await next();
36
- if (result.error) {
37
- return new Response(response.body, {
38
- status: result.error.status,
39
- statusText: result.error.name,
40
- headers: response.headers
41
- });
42
- }
43
- return response;
35
+ return ApiContextStorage.run(context, async () => {
36
+ const response = await next();
37
+ if (result.error) {
38
+ return new Response(response.body, {
39
+ status: result.error.status,
40
+ statusText: result.error.name,
41
+ headers: response.headers
42
+ });
43
+ }
44
+ return response;
45
+ });
44
46
  });
45
- function nextWithStaticStub(next, locals) {
46
- Object.defineProperty(locals, "_actionsInternal", {
47
+ function nextWithStaticStub(next, context) {
48
+ Object.defineProperty(context.locals, "_actionsInternal", {
47
49
  writable: false,
48
50
  value: {
49
51
  getActionResult: () => {
@@ -55,16 +57,16 @@ function nextWithStaticStub(next, locals) {
55
57
  }
56
58
  }
57
59
  });
58
- return next();
60
+ return ApiContextStorage.run(context, () => next());
59
61
  }
60
- function nextWithLocalsStub(next, locals) {
61
- Object.defineProperty(locals, "_actionsInternal", {
62
+ function nextWithLocalsStub(next, context) {
63
+ Object.defineProperty(context.locals, "_actionsInternal", {
62
64
  writable: false,
63
65
  value: {
64
66
  getActionResult: () => void 0
65
67
  }
66
68
  });
67
- return next();
69
+ return ApiContextStorage.run(context, () => next());
68
70
  }
69
71
  export {
70
72
  onRequest
@@ -3,8 +3,7 @@ import { formContentTypes, getAction, hasContentType } from "./utils.js";
3
3
  import { callSafely } from "./virtual/shared.js";
4
4
  const POST = async (context) => {
5
5
  const { request, url } = context;
6
- const actionPathKeys = url.pathname.replace("/_actions/", "").split(".");
7
- const action = await getAction(actionPathKeys);
6
+ const action = await getAction(url.pathname);
8
7
  if (!action) {
9
8
  return new Response(null, { status: 404 });
10
9
  }
@@ -1,4 +1,9 @@
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>) | undefined>;
4
+ /**
5
+ * Get server-side action based on the route path.
6
+ * Imports from `import.meta.env.ACTIONS_PATH`, which maps to
7
+ * the user's `src/actions/index.ts` file at build-time.
8
+ */
9
+ export declare function getAction(path: string): Promise<((param: unknown) => MaybePromise<unknown>) | undefined>;
@@ -3,7 +3,8 @@ function hasContentType(contentType, expected) {
3
3
  const type = contentType.split(";")[0].toLowerCase();
4
4
  return expected.some((t) => type === t);
5
5
  }
6
- async function getAction(pathKeys) {
6
+ async function getAction(path) {
7
+ const pathKeys = path.replace("/_actions/", "").split(".");
7
8
  let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH);
8
9
  for (const key of pathKeys) {
9
10
  if (!(key in actionLookup)) {
@@ -1,12 +1,14 @@
1
1
  import { z } from 'zod';
2
+ import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
2
3
  import { type MaybePromise } from '../utils.js';
3
4
  import { type ErrorInferenceObject, type SafeResult } from './shared.js';
4
5
  export * from './shared.js';
5
6
  export { z } from 'zod';
6
- export { getApiContext } from '../store.js';
7
+ /** @deprecated Access context from the second `handler()` parameter. */
8
+ export declare const getApiContext: typeof _getApiContext;
7
9
  export type Accept = 'form' | 'json';
8
10
  export type InputSchema<T extends Accept> = T extends 'form' ? z.AnyZodObject | z.ZodType<FormData> : z.ZodType;
9
- type Handler<TInputSchema, TOutput> = TInputSchema extends z.ZodType ? (input: z.infer<TInputSchema>) => MaybePromise<TOutput> : (input?: any) => MaybePromise<TOutput>;
11
+ type Handler<TInputSchema, TOutput> = TInputSchema extends z.ZodType ? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput> : (input: any, context: ActionAPIContext) => MaybePromise<TOutput>;
10
12
  export type ActionClient<TOutput, TAccept extends Accept, TInputSchema extends InputSchema<TAccept> | undefined> = TInputSchema extends z.ZodType ? ((input: TAccept extends 'form' ? FormData : z.input<TInputSchema>) => Promise<Awaited<TOutput>>) & {
11
13
  safe: (input: TAccept extends 'form' ? FormData : z.input<TInputSchema>) => Promise<SafeResult<z.input<TInputSchema> extends ErrorInferenceObject ? z.input<TInputSchema> : ErrorInferenceObject, Awaited<TOutput>>>;
12
14
  } : ((input?: any) => Promise<Awaited<TOutput>>) & {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- import { getApiContext } from "../store.js";
3
- import { hasContentType } from "../utils.js";
2
+ import { getApiContext as _getApiContext } from "../store.js";
3
+ import {} from "../utils.js";
4
4
  import {
5
5
  ActionError,
6
6
  ActionInputError,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./shared.js";
9
9
  export * from "./shared.js";
10
10
  import { z as z2 } from "zod";
11
- import { getApiContext as getApiContext2 } from "../store.js";
11
+ const getApiContext = _getApiContext;
12
12
  function defineAction({
13
13
  accept,
14
14
  input: inputSchema,
@@ -30,30 +30,28 @@ function getFormServerHandler(handler, inputSchema) {
30
30
  message: "This action only accepts FormData."
31
31
  });
32
32
  }
33
- if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput);
33
+ if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput, getApiContext());
34
34
  const parsed = await inputSchema.safeParseAsync(formDataToObject(unparsedInput, inputSchema));
35
35
  if (!parsed.success) {
36
36
  throw new ActionInputError(parsed.error.issues);
37
37
  }
38
- return await handler(parsed.data);
38
+ return await handler(parsed.data, getApiContext());
39
39
  };
40
40
  }
41
41
  function getJsonServerHandler(handler, inputSchema) {
42
42
  return async (unparsedInput) => {
43
- const context = getApiContext();
44
- const contentType = context.request.headers.get("content-type");
45
- if (!contentType || !hasContentType(contentType, ["application/json"])) {
43
+ if (unparsedInput instanceof FormData) {
46
44
  throw new ActionError({
47
45
  code: "UNSUPPORTED_MEDIA_TYPE",
48
46
  message: "This action only accepts JSON."
49
47
  });
50
48
  }
51
- if (!inputSchema) return await handler(unparsedInput);
49
+ if (!inputSchema) return await handler(unparsedInput, getApiContext());
52
50
  const parsed = await inputSchema.safeParseAsync(unparsedInput);
53
51
  if (!parsed.success) {
54
52
  throw new ActionInputError(parsed.error.issues);
55
53
  }
56
- return await handler(parsed.data);
54
+ return await handler(parsed.data, getApiContext());
57
55
  };
58
56
  }
59
57
  function formDataToObject(formData, schema) {
@@ -93,6 +91,6 @@ function handleFormDataGet(key, formData, validator, baseValidator) {
93
91
  export {
94
92
  defineAction,
95
93
  formDataToObject,
96
- getApiContext2 as getApiContext,
94
+ getApiContext,
97
95
  z2 as z
98
96
  };
@@ -1,2 +1,4 @@
1
1
  import type { APIContext } from '../@types/astro.js';
2
+ import type { Locals } from './runtime/middleware.js';
3
+ export declare function hasActionsInternal(locals: APIContext['locals']): locals is Locals;
2
4
  export declare function createGetActionResult(locals: APIContext['locals']): APIContext['getActionResult'];
@@ -14,5 +14,6 @@ function createGetActionResult(locals) {
14
14
  };
15
15
  }
16
16
  export {
17
- createGetActionResult
17
+ createGetActionResult,
18
+ hasActionsInternal
18
19
  };
@@ -0,0 +1,162 @@
1
+ import type { AstroRenderer, AstroUserConfig, RouteType } from '../@types/astro.js';
2
+ import type { AstroComponentFactory } from '../runtime/server/index.js';
3
+ /**
4
+ * Options to be passed when rendering a route
5
+ */
6
+ export type ContainerRenderOptions = {
7
+ /**
8
+ * If your component renders slots, that's where you want to fill the slots.
9
+ * A single slot should have the `default` field:
10
+ *
11
+ * ## Examples
12
+ *
13
+ * **Default slot**
14
+ *
15
+ * ```js
16
+ * container.renderToString(Component, { slots: { default: "Some value"}});
17
+ * ```
18
+ *
19
+ * **Named slots**
20
+ *
21
+ * ```js
22
+ * container.renderToString(Component, { slots: { "foo": "Some value", "bar": "Lorem Ipsum" }});
23
+ * ```
24
+ */
25
+ slots?: Record<string, any>;
26
+ /**
27
+ * The request is used to understand which path/URL the component is about to render.
28
+ *
29
+ * Use this option in case your component or middleware needs to read information like `Astro.url` or `Astro.request`.
30
+ */
31
+ request?: Request;
32
+ /**
33
+ * Useful for dynamic routes. If your component is something like `src/pages/blog/[id]/[...slug]`, you'll want to provide:
34
+ * ```js
35
+ * container.renderToString(Component, { params: ["id", "...slug"] });
36
+ * ```
37
+ */
38
+ params?: Record<string, string | undefined>;
39
+ /**
40
+ * Useful if your component needs to access some locals without the use a middleware.
41
+ * ```js
42
+ * container.renderToString(Component, { locals: { getSomeValue() {} } });
43
+ * ```
44
+ */
45
+ locals?: App.Locals;
46
+ /**
47
+ * Useful in case you're attempting to render an endpoint:
48
+ * ```js
49
+ * container.renderToString(Endpoint, { routeType: "endpoint" });
50
+ * ```
51
+ */
52
+ routeType?: RouteType;
53
+ };
54
+ export type AstroContainerUserConfig = Omit<AstroUserConfig, 'integrations' | 'adapter'>;
55
+ /**
56
+ * Options that are used for the entire lifecycle of the current instance of the container.
57
+ */
58
+ export type AstroContainerOptions = {
59
+ /**
60
+ * @default false
61
+ *
62
+ * @description
63
+ *
64
+ * Enables streaming during rendering
65
+ *
66
+ * ## Example
67
+ *
68
+ * ```js
69
+ * const container = await AstroContainer.create({
70
+ * streaming: true
71
+ * });
72
+ * ```
73
+ */
74
+ streaming?: boolean;
75
+ /**
76
+ * @default []
77
+ * @description
78
+ *
79
+ * List or renderers to use when rendering components. Usually they are entry points
80
+ *
81
+ * ## Example
82
+ *
83
+ * ```js
84
+ * const container = await AstroContainer.create({
85
+ * renderers: [{
86
+ * name: "@astrojs/react"
87
+ * client: "@astrojs/react/client.js"
88
+ * server: "@astrojs/react/server.js"
89
+ * }]
90
+ * });
91
+ * ```
92
+ */
93
+ renderers?: AstroRenderer[];
94
+ /**
95
+ * @default {}
96
+ * @description
97
+ *
98
+ * A subset of the astro configuration object.
99
+ *
100
+ * ## Example
101
+ *
102
+ * ```js
103
+ * const container = await AstroContainer.create({
104
+ * astroConfig: {
105
+ * trailingSlash: "never"
106
+ * }
107
+ * });
108
+ * ```
109
+ */
110
+ astroConfig?: AstroContainerUserConfig;
111
+ };
112
+ export declare class experimental_AstroContainer {
113
+ #private;
114
+ private constructor();
115
+ /**
116
+ * Creates a new instance of a container.
117
+ *
118
+ * @param {AstroContainerOptions=} containerOptions
119
+ */
120
+ static create(containerOptions?: AstroContainerOptions): Promise<experimental_AstroContainer>;
121
+ private static createFromManifest;
122
+ /**
123
+ * @description
124
+ * It renders a component and returns the result as a string.
125
+ *
126
+ * ## Example
127
+ *
128
+ * ```js
129
+ * import Card from "../src/components/Card.astro";
130
+ *
131
+ * const container = await AstroContainer.create();
132
+ * const result = await container.renderToString(Card);
133
+ *
134
+ * console.log(result); // it's a string
135
+ * ```
136
+ *
137
+ *
138
+ * @param {AstroComponentFactory} component The instance of the component.
139
+ * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
140
+ */
141
+ renderToString(component: AstroComponentFactory, options?: ContainerRenderOptions): Promise<string>;
142
+ /**
143
+ * @description
144
+ * It renders a component and returns the `Response` as result of the rendering phase.
145
+ *
146
+ * ## Example
147
+ *
148
+ * ```js
149
+ * import Card from "../src/components/Card.astro";
150
+ *
151
+ * const container = await AstroContainer.create();
152
+ * const response = await container.renderToResponse(Card);
153
+ *
154
+ * console.log(response.status); // it's a number
155
+ * ```
156
+ *
157
+ *
158
+ * @param {AstroComponentFactory} component The instance of the component.
159
+ * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
160
+ */
161
+ renderToResponse(component: AstroComponentFactory, options?: ContainerRenderOptions): Promise<Response>;
162
+ }