astro 5.0.9 → 5.1.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.
@@ -0,0 +1,385 @@
1
+ import { stringify, unflatten } from "devalue";
2
+ import {
3
+ builtinDrivers,
4
+ createStorage
5
+ } from "unstorage";
6
+ import { SessionStorageInitError, SessionStorageSaveError } from "./errors/errors-data.js";
7
+ import { AstroError } from "./errors/index.js";
8
+ const PERSIST_SYMBOL = Symbol();
9
+ const DEFAULT_COOKIE_NAME = "astro-session";
10
+ const VALID_COOKIE_REGEX = /^[\w-]+$/;
11
+ class AstroSession {
12
+ // The cookies object.
13
+ #cookies;
14
+ // The session configuration.
15
+ #config;
16
+ // The cookie config
17
+ #cookieConfig;
18
+ // The cookie name
19
+ #cookieName;
20
+ // The unstorage object for the session driver.
21
+ #storage;
22
+ #data;
23
+ // The session ID. A v4 UUID.
24
+ #sessionID;
25
+ // Sessions to destroy. Needed because we won't have the old session ID after it's destroyed locally.
26
+ #toDestroy = /* @__PURE__ */ new Set();
27
+ // Session keys to delete. Used for partial data sets to avoid overwriting the deleted value.
28
+ #toDelete = /* @__PURE__ */ new Set();
29
+ // Whether the session is dirty and needs to be saved.
30
+ #dirty = false;
31
+ // Whether the session cookie has been set.
32
+ #cookieSet = false;
33
+ // The local data is "partial" if it has not been loaded from storage yet and only
34
+ // contains values that have been set or deleted in-memory locally.
35
+ // We do this to avoid the need to block on loading data when it is only being set.
36
+ // When we load the data from storage, we need to merge it with the local partial data,
37
+ // preserving in-memory changes and deletions.
38
+ #partial = true;
39
+ constructor(cookies, {
40
+ cookie: cookieConfig = DEFAULT_COOKIE_NAME,
41
+ ...config
42
+ }) {
43
+ this.#cookies = cookies;
44
+ if (typeof cookieConfig === "object") {
45
+ this.#cookieConfig = cookieConfig;
46
+ this.#cookieName = cookieConfig.name || DEFAULT_COOKIE_NAME;
47
+ } else {
48
+ this.#cookieName = cookieConfig || DEFAULT_COOKIE_NAME;
49
+ }
50
+ this.#config = config;
51
+ }
52
+ /**
53
+ * Gets a session value. Returns `undefined` if the session or value does not exist.
54
+ */
55
+ async get(key) {
56
+ return (await this.#ensureData()).get(key)?.data;
57
+ }
58
+ /**
59
+ * Checks if a session value exists.
60
+ */
61
+ async has(key) {
62
+ return (await this.#ensureData()).has(key);
63
+ }
64
+ /**
65
+ * Gets all session values.
66
+ */
67
+ async keys() {
68
+ return (await this.#ensureData()).keys();
69
+ }
70
+ /**
71
+ * Gets all session values.
72
+ */
73
+ async values() {
74
+ return [...(await this.#ensureData()).values()].map((entry) => entry.data);
75
+ }
76
+ /**
77
+ * Gets all session entries.
78
+ */
79
+ async entries() {
80
+ return [...(await this.#ensureData()).entries()].map(([key, entry]) => [key, entry.data]);
81
+ }
82
+ /**
83
+ * Deletes a session value.
84
+ */
85
+ delete(key) {
86
+ this.#data?.delete(key);
87
+ if (this.#partial) {
88
+ this.#toDelete.add(key);
89
+ }
90
+ this.#dirty = true;
91
+ }
92
+ /**
93
+ * Sets a session value. The session is created if it does not exist.
94
+ */
95
+ set(key, value, { ttl } = {}) {
96
+ if (!key) {
97
+ throw new AstroError({
98
+ ...SessionStorageSaveError,
99
+ message: "The session key was not provided."
100
+ });
101
+ }
102
+ try {
103
+ stringify(value);
104
+ } catch (err) {
105
+ throw new AstroError(
106
+ {
107
+ ...SessionStorageSaveError,
108
+ message: `The session data for ${key} could not be serialized.`,
109
+ hint: "See the devalue library for all supported types: https://github.com/rich-harris/devalue"
110
+ },
111
+ { cause: err }
112
+ );
113
+ }
114
+ if (!this.#cookieSet) {
115
+ this.#setCookie();
116
+ this.#cookieSet = true;
117
+ }
118
+ this.#data ??= /* @__PURE__ */ new Map();
119
+ const lifetime = ttl ?? this.#config.ttl;
120
+ const expires = typeof lifetime === "number" ? Date.now() + lifetime * 1e3 : lifetime;
121
+ this.#data.set(key, {
122
+ data: value,
123
+ expires
124
+ });
125
+ this.#dirty = true;
126
+ }
127
+ /**
128
+ * Destroys the session, clearing the cookie and storage if it exists.
129
+ */
130
+ destroy() {
131
+ this.#destroySafe();
132
+ }
133
+ /**
134
+ * Regenerates the session, creating a new session ID. The existing session data is preserved.
135
+ */
136
+ async regenerate() {
137
+ let data = /* @__PURE__ */ new Map();
138
+ try {
139
+ data = await this.#ensureData();
140
+ } catch (err) {
141
+ console.error("Failed to load session data during regeneration:", err);
142
+ }
143
+ const oldSessionId = this.#sessionID;
144
+ this.#sessionID = void 0;
145
+ this.#data = data;
146
+ this.#ensureSessionID();
147
+ await this.#setCookie();
148
+ if (oldSessionId && this.#storage) {
149
+ this.#storage.removeItem(oldSessionId).catch((err) => {
150
+ console.error("Failed to remove old session data:", err);
151
+ });
152
+ }
153
+ }
154
+ // Persists the session data to storage.
155
+ // This is called automatically at the end of the request.
156
+ // Uses a symbol to prevent users from calling it directly.
157
+ async [PERSIST_SYMBOL]() {
158
+ if (!this.#dirty && !this.#toDestroy.size) {
159
+ return;
160
+ }
161
+ const storage = await this.#ensureStorage();
162
+ if (this.#dirty && this.#data) {
163
+ const data = await this.#ensureData();
164
+ this.#toDelete.forEach((key2) => data.delete(key2));
165
+ const key = this.#ensureSessionID();
166
+ let serialized;
167
+ try {
168
+ serialized = stringify(data, {
169
+ // Support URL objects
170
+ URL: (val) => val instanceof URL && val.href
171
+ });
172
+ } catch (err) {
173
+ throw new AstroError(
174
+ {
175
+ ...SessionStorageSaveError,
176
+ message: SessionStorageSaveError.message(
177
+ "The session data could not be serialized.",
178
+ this.#config.driver
179
+ )
180
+ },
181
+ { cause: err }
182
+ );
183
+ }
184
+ await storage.setItem(key, serialized);
185
+ this.#dirty = false;
186
+ }
187
+ if (this.#toDestroy.size > 0) {
188
+ const cleanupPromises = [...this.#toDestroy].map(
189
+ (sessionId) => storage.removeItem(sessionId).catch((err) => {
190
+ console.error(`Failed to clean up session ${sessionId}:`, err);
191
+ })
192
+ );
193
+ await Promise.all(cleanupPromises);
194
+ this.#toDestroy.clear();
195
+ }
196
+ }
197
+ get sessionID() {
198
+ return this.#sessionID;
199
+ }
200
+ /**
201
+ * Sets the session cookie.
202
+ */
203
+ async #setCookie() {
204
+ if (!VALID_COOKIE_REGEX.test(this.#cookieName)) {
205
+ throw new AstroError({
206
+ ...SessionStorageSaveError,
207
+ message: "Invalid cookie name. Cookie names can only contain letters, numbers, and dashes."
208
+ });
209
+ }
210
+ const cookieOptions = {
211
+ sameSite: "lax",
212
+ secure: true,
213
+ path: "/",
214
+ ...this.#cookieConfig,
215
+ httpOnly: true
216
+ };
217
+ const value = this.#ensureSessionID();
218
+ this.#cookies.set(this.#cookieName, value, cookieOptions);
219
+ }
220
+ /**
221
+ * Attempts to load the session data from storage, or creates a new data object if none exists.
222
+ * If there is existing partial data, it will be merged into the new data object.
223
+ */
224
+ async #ensureData() {
225
+ const storage = await this.#ensureStorage();
226
+ if (this.#data && !this.#partial) {
227
+ return this.#data;
228
+ }
229
+ this.#data ??= /* @__PURE__ */ new Map();
230
+ const raw = await storage.get(this.#ensureSessionID());
231
+ if (!raw) {
232
+ return this.#data;
233
+ }
234
+ try {
235
+ const storedMap = unflatten(raw, {
236
+ // Revive URL objects
237
+ URL: (href) => new URL(href)
238
+ });
239
+ if (!(storedMap instanceof Map)) {
240
+ await this.#destroySafe();
241
+ throw new AstroError({
242
+ ...SessionStorageInitError,
243
+ message: SessionStorageInitError.message(
244
+ "The session data was an invalid type.",
245
+ this.#config.driver
246
+ )
247
+ });
248
+ }
249
+ const now = Date.now();
250
+ for (const [key, value] of storedMap) {
251
+ const expired = typeof value.expires === "number" && value.expires < now;
252
+ if (!this.#data.has(key) && !this.#toDelete.has(key) && !expired) {
253
+ this.#data.set(key, value);
254
+ }
255
+ }
256
+ this.#partial = false;
257
+ return this.#data;
258
+ } catch (err) {
259
+ await this.#destroySafe();
260
+ if (err instanceof AstroError) {
261
+ throw err;
262
+ }
263
+ throw new AstroError(
264
+ {
265
+ ...SessionStorageInitError,
266
+ message: SessionStorageInitError.message(
267
+ "The session data could not be parsed.",
268
+ this.#config.driver
269
+ )
270
+ },
271
+ { cause: err }
272
+ );
273
+ }
274
+ }
275
+ /**
276
+ * Safely destroys the session, clearing the cookie and storage if it exists.
277
+ */
278
+ #destroySafe() {
279
+ if (this.#sessionID) {
280
+ this.#toDestroy.add(this.#sessionID);
281
+ }
282
+ if (this.#cookieName) {
283
+ this.#cookies.delete(this.#cookieName);
284
+ }
285
+ this.#sessionID = void 0;
286
+ this.#data = void 0;
287
+ this.#dirty = true;
288
+ }
289
+ /**
290
+ * Returns the session ID, generating a new one if it does not exist.
291
+ */
292
+ #ensureSessionID() {
293
+ this.#sessionID ??= this.#cookies.get(this.#cookieName)?.value ?? crypto.randomUUID();
294
+ return this.#sessionID;
295
+ }
296
+ /**
297
+ * Ensures the storage is initialized.
298
+ * This is called automatically when a storage operation is needed.
299
+ */
300
+ async #ensureStorage() {
301
+ if (this.#storage) {
302
+ return this.#storage;
303
+ }
304
+ if (this.#config.driver === "test") {
305
+ this.#storage = this.#config.options.mockStorage;
306
+ return this.#storage;
307
+ }
308
+ if (this.#config.driver === "fs" || this.#config.driver === "fsLite" || this.#config.driver === "fs-lite") {
309
+ this.#config.options ??= {};
310
+ this.#config.driver = "fs-lite";
311
+ this.#config.options.base ??= ".astro/session";
312
+ }
313
+ if (!this.#config?.driver) {
314
+ throw new AstroError({
315
+ ...SessionStorageInitError,
316
+ message: SessionStorageInitError.message(
317
+ "No driver was defined in the session configuration and the adapter did not provide a default driver."
318
+ )
319
+ });
320
+ }
321
+ let driver = null;
322
+ const driverPackage = await resolveSessionDriver(this.#config.driver);
323
+ try {
324
+ if (this.#config.driverModule) {
325
+ driver = (await this.#config.driverModule()).default;
326
+ } else if (driverPackage) {
327
+ driver = (await import(driverPackage)).default;
328
+ }
329
+ } catch (err) {
330
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
331
+ throw new AstroError(
332
+ {
333
+ ...SessionStorageInitError,
334
+ message: SessionStorageInitError.message(
335
+ err.message.includes(`Cannot find package '${driverPackage}'`) ? "The driver module could not be found." : err.message,
336
+ this.#config.driver
337
+ )
338
+ },
339
+ { cause: err }
340
+ );
341
+ }
342
+ throw err;
343
+ }
344
+ if (!driver) {
345
+ throw new AstroError({
346
+ ...SessionStorageInitError,
347
+ message: SessionStorageInitError.message(
348
+ "The module did not export a driver.",
349
+ this.#config.driver
350
+ )
351
+ });
352
+ }
353
+ try {
354
+ this.#storage = createStorage({
355
+ driver: driver(this.#config.options)
356
+ });
357
+ return this.#storage;
358
+ } catch (err) {
359
+ throw new AstroError(
360
+ {
361
+ ...SessionStorageInitError,
362
+ message: SessionStorageInitError.message("Unknown error", this.#config.driver)
363
+ },
364
+ { cause: err }
365
+ );
366
+ }
367
+ }
368
+ }
369
+ function resolveSessionDriver(driver) {
370
+ if (!driver) {
371
+ return null;
372
+ }
373
+ if (driver === "fs") {
374
+ return import.meta.resolve(builtinDrivers.fsLite);
375
+ }
376
+ if (driver in builtinDrivers) {
377
+ return import.meta.resolve(builtinDrivers[driver]);
378
+ }
379
+ return driver;
380
+ }
381
+ export {
382
+ AstroSession,
383
+ PERSIST_SYMBOL,
384
+ resolveSessionDriver
385
+ };
@@ -20,12 +20,12 @@ function createI18nMiddleware(i18n, base, trailingSlash, format) {
20
20
  const _noFoundForNonLocaleRoute = notFound(payload);
21
21
  const _requestHasLocale = requestHasLocale(payload.locales);
22
22
  const _redirectToFallback = redirectToFallback(payload);
23
- const prefixAlways = (context) => {
23
+ const prefixAlways = (context, response) => {
24
24
  const url = context.url;
25
25
  if (url.pathname === base + "/" || url.pathname === base) {
26
26
  return _redirectToDefaultLocale(context);
27
27
  } else if (!_requestHasLocale(context)) {
28
- return _noFoundForNonLocaleRoute(context);
28
+ return _noFoundForNonLocaleRoute(context, response);
29
29
  }
30
30
  return void 0;
31
31
  };
@@ -96,7 +96,7 @@ function createI18nMiddleware(i18n, base, trailingSlash, format) {
96
96
  break;
97
97
  }
98
98
  case "pathname-prefix-always": {
99
- const result = prefixAlways(context);
99
+ const result = prefixAlways(context, response);
100
100
  if (result) {
101
101
  return result;
102
102
  }
@@ -104,7 +104,7 @@ function createI18nMiddleware(i18n, base, trailingSlash, format) {
104
104
  }
105
105
  case "domains-prefix-always": {
106
106
  if (localeHasntDomain(i18n, currentLocale)) {
107
- const result = prefixAlways(context);
107
+ const result = prefixAlways(context, response);
108
108
  if (result) {
109
109
  return result;
110
110
  }
@@ -67,7 +67,7 @@ export interface Page<T = any> {
67
67
  next: string | undefined;
68
68
  /** url of the first page (if the current page is not the first page) */
69
69
  first: string | undefined;
70
- /** url of the next page (if the current page in not the last page) */
70
+ /** url of the last page (if the current page is not the last page) */
71
71
  last: string | undefined;
72
72
  };
73
73
  }
@@ -1,5 +1,6 @@
1
1
  import type { OutgoingHttpHeaders } from 'node:http';
2
2
  import type { RehypePlugins, RemarkPlugins, RemarkRehype, ShikiConfig } from '@astrojs/markdown-remark';
3
+ import type { BuiltinDriverName, BuiltinDriverOptions, Driver, Storage } from 'unstorage';
3
4
  import type { UserConfig as OriginalViteUserConfig, SSROptions as ViteSSROptions } from 'vite';
4
5
  import type { ImageFit, ImageLayout } from '../../assets/types.js';
5
6
  import type { RemotePattern } from '../../assets/utils/remotePattern.js';
@@ -7,6 +8,7 @@ import type { SvgRenderMode } from '../../assets/utils/svg.js';
7
8
  import type { AssetsPrefix } from '../../core/app/types.js';
8
9
  import type { AstroConfigType } from '../../core/config/schema.js';
9
10
  import type { REDIRECT_STATUS_CODES } from '../../core/constants.js';
11
+ import type { AstroCookieSetOptions } from '../../core/cookies/cookies.js';
10
12
  import type { LoggerLevel } from '../../core/logger/core.js';
11
13
  import type { EnvSchema } from '../../env/schema.js';
12
14
  import type { AstroIntegration } from './integrations.js';
@@ -79,6 +81,41 @@ export type ServerConfig = {
79
81
  */
80
82
  open?: string | boolean;
81
83
  };
84
+ export type SessionDriverName = BuiltinDriverName | 'custom' | 'test';
85
+ interface CommonSessionConfig {
86
+ /**
87
+ * Configures the session cookie. If set to a string, it will be used as the cookie name.
88
+ * Alternatively, you can pass an object with additional options.
89
+ */
90
+ cookie?: string | (Omit<AstroCookieSetOptions, 'httpOnly' | 'expires' | 'encode'> & {
91
+ name?: string;
92
+ });
93
+ /**
94
+ * Default session duration in seconds. If not set, the session will be stored until deleted, or until the cookie expires.
95
+ */
96
+ ttl?: number;
97
+ }
98
+ interface BuiltinSessionConfig<TDriver extends keyof BuiltinDriverOptions> extends CommonSessionConfig {
99
+ driver: TDriver;
100
+ options?: BuiltinDriverOptions[TDriver];
101
+ }
102
+ interface CustomSessionConfig extends CommonSessionConfig {
103
+ /** Entrypoint for a custom session driver */
104
+ driver: string;
105
+ options?: Record<string, unknown>;
106
+ }
107
+ interface TestSessionConfig extends CommonSessionConfig {
108
+ driver: 'test';
109
+ options: {
110
+ mockStorage: Storage;
111
+ };
112
+ }
113
+ export type SessionConfig<TDriver extends SessionDriverName> = TDriver extends keyof BuiltinDriverOptions ? BuiltinSessionConfig<TDriver> : TDriver extends 'test' ? TestSessionConfig : CustomSessionConfig;
114
+ export type ResolvedSessionConfig<TDriver extends SessionDriverName> = SessionConfig<TDriver> & {
115
+ driverModule?: () => Promise<{
116
+ default: () => Driver;
117
+ }>;
118
+ };
82
119
  export interface ViteUserConfig extends OriginalViteUserConfig {
83
120
  ssr?: ViteSSROptions;
84
121
  }
@@ -87,7 +124,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
87
124
  * Docs: https://docs.astro.build/reference/configuration-reference/
88
125
  *
89
126
  * Generics do not follow semver and may change at any time.
90
- */ export interface AstroUserConfig<TLocales extends Locales = never> {
127
+ */ export interface AstroUserConfig<TLocales extends Locales = never, TSession extends SessionDriverName = never> {
91
128
  /**
92
129
  * @docs
93
130
  * @kind heading
@@ -515,8 +552,8 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
515
552
  *
516
553
  * #### Effect on Astro.url
517
554
  * Setting `build.format` controls what `Astro.url` is set to during the build. When it is:
518
- * - `directory` - The `Astro.url.pathname` will include a trailing slash to mimic folder behavior; ie `/foo/`.
519
- * - `file` - The `Astro.url.pathname` will include `.html`; ie `/foo.html`.
555
+ * - `directory` - The `Astro.url.pathname` will include a trailing slash to mimic folder behavior. (e.g. `/foo/`)
556
+ * - `file` - The `Astro.url.pathname` will include `.html`. (e.g. `/foo.html`)
520
557
  *
521
558
  * This means that when you create relative URLs using `new URL('./relative', Astro.url)`, you will get consistent behavior between dev and build.
522
559
  *
@@ -1818,6 +1855,49 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
1818
1855
  * The `densities` attribute is not compatible with responsive images and will be ignored if set.
1819
1856
  */
1820
1857
  responsiveImages?: boolean;
1858
+ /**
1859
+ *
1860
+ * @name experimental.session
1861
+ * @type {SessionConfig}
1862
+ * @version 5.0.0
1863
+ * @description
1864
+ *
1865
+ * Enables support for sessions in Astro. Sessions are used to store user data across requests, such as user authentication state.
1866
+ *
1867
+ * When enabled you can access the `Astro.session` object to read and write data that persists across requests. You can configure the session driver using the [`session` option](#session), or use the default provided by your adapter.
1868
+ *
1869
+ * ```astro title=src/components/CartButton.astro
1870
+ * ---
1871
+ * export const prerender = false; // Not needed in 'server' mode
1872
+ * const cart = await Astro.session.get('cart');
1873
+ * ---
1874
+ *
1875
+ * <a href="/checkout">🛒 {cart?.length ?? 0} items</a>
1876
+ *
1877
+ * ```
1878
+ * The object configures session management for your Astro site by specifying a `driver` as well as any `options` for your data storage.
1879
+ *
1880
+ * You can specify [any driver from Unstorage](https://unstorage.unjs.io/drivers) or provide a custom config which will override your adapter's default.
1881
+ *
1882
+ * ```js title="astro.config.mjs"
1883
+ * {
1884
+ * experimental: {
1885
+ * session: {
1886
+ * // Required: the name of the Unstorage driver
1887
+ * driver: "redis",
1888
+ * // The required options depend on the driver
1889
+ * options: {
1890
+ * url: process.env.REDIS_URL,
1891
+ * }
1892
+ * }
1893
+ * },
1894
+ * }
1895
+ * ```
1896
+ *
1897
+ * For more details, see [the Sessions RFC](https://github.com/withastro/roadmap/blob/sessions/proposals/0054-sessions.md).
1898
+ *
1899
+ */
1900
+ session?: SessionConfig<TSession>;
1821
1901
  /**
1822
1902
  *
1823
1903
  * @name experimental.svg
@@ -2,6 +2,7 @@ import type { z } from 'zod';
2
2
  import type { ActionAccept, ActionClient, ActionReturnType } from '../../actions/runtime/virtual/server.js';
3
3
  import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../core/constants.js';
4
4
  import type { AstroCookies } from '../../core/cookies/cookies.js';
5
+ import type { AstroSession } from '../../core/session.js';
5
6
  import type { AstroComponentFactory } from '../../runtime/server/index.js';
6
7
  import type { Params, RewritePayload } from './common.js';
7
8
  import type { ValidRedirectStatus } from './config.js';
@@ -239,6 +240,10 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
239
240
  * Utility for getting and setting the values of cookies.
240
241
  */
241
242
  cookies: AstroCookies;
243
+ /**
244
+ * Utility for handling sessions.
245
+ */
246
+ session?: AstroSession;
242
247
  /**
243
248
  * Information about the current request. This is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
244
249
  */
@@ -150,7 +150,8 @@ function createDevelopmentManifest(settings) {
150
150
  return {
151
151
  onRequest: NOOP_MIDDLEWARE_FN
152
152
  };
153
- }
153
+ },
154
+ sessionConfig: settings.config.experimental.session
154
155
  };
155
156
  }
156
157
  export {
@@ -14,6 +14,7 @@ import { getProps } from "../core/render/index.js";
14
14
  import { createRequest } from "../core/request.js";
15
15
  import { redirectTemplate } from "../core/routing/3xx.js";
16
16
  import { matchAllRoutes } from "../core/routing/index.js";
17
+ import { PERSIST_SYMBOL } from "../core/session.js";
17
18
  import { getSortedPreloadedMatches } from "../prerender/routing.js";
18
19
  import { writeSSRResult, writeWebResponse } from "./response.js";
19
20
  function isLoggedRequest(url) {
@@ -158,6 +159,8 @@ async function handleRoute({
158
159
  renderContext.props.error = err;
159
160
  response = await renderContext.render(preloaded500Component);
160
161
  statusCode = 500;
162
+ } finally {
163
+ renderContext.session?.[PERSIST_SYMBOL]();
161
164
  }
162
165
  if (isLoggedRequest(pathname)) {
163
166
  const timeEnd = performance.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "5.0.9",
3
+ "version": "5.1.1",
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",
@@ -148,8 +148,9 @@
148
148
  "tsconfck": "^3.1.4",
149
149
  "ultrahtml": "^1.5.3",
150
150
  "unist-util-visit": "^5.0.0",
151
+ "unstorage": "^1.14.0",
151
152
  "vfile": "^6.0.3",
152
- "vite": "^6.0.1",
153
+ "vite": "^6.0.5",
153
154
  "vitefu": "^1.0.4",
154
155
  "which-pm": "^3.0.0",
155
156
  "xxhash-wasm": "^1.1.0",
@@ -159,8 +160,8 @@
159
160
  "zod-to-json-schema": "^3.23.5",
160
161
  "zod-to-ts": "^1.2.0",
161
162
  "@astrojs/internal-helpers": "0.4.2",
162
- "@astrojs/telemetry": "3.2.0",
163
- "@astrojs/markdown-remark": "6.0.1"
163
+ "@astrojs/markdown-remark": "6.0.1",
164
+ "@astrojs/telemetry": "3.2.0"
164
165
  },
165
166
  "optionalDependencies": {
166
167
  "sharp": "^0.33.3"
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ACTION_QUERY_PARAMS,
2
3
  ActionError,
3
4
  appendForwardSlash,
4
5
  deserializeActionResult,
@@ -52,6 +53,17 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') {
52
53
  });
53
54
  }
54
55
 
56
+ const SHOULD_APPEND_TRAILING_SLASH = '/** @TRAILING_SLASH@ **/';
57
+
58
+ /** @param {import('astro:actions').ActionClient<any, any, any>} */
59
+ export function getActionPath(action) {
60
+ let path = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${new URLSearchParams(action.toString()).get(ACTION_QUERY_PARAMS.actionName)}`;
61
+ if (SHOULD_APPEND_TRAILING_SLASH) {
62
+ path = appendForwardSlash(path);
63
+ }
64
+ return path;
65
+ }
66
+
55
67
  /**
56
68
  * @param {*} param argument passed to the action when called server or client-side.
57
69
  * @param {string} path Built path to call action by path name.
@@ -88,19 +100,19 @@ async function handleAction(param, path, context) {
88
100
  headers.set('Content-Length', '0');
89
101
  }
90
102
  }
103
+ const rawResult = await fetch(
104
+ getActionPath({
105
+ toString() {
106
+ return getActionQueryString(path);
107
+ },
108
+ }),
109
+ {
110
+ method: 'POST',
111
+ body,
112
+ headers,
113
+ },
114
+ );
91
115
 
92
- const shouldAppendTrailingSlash = '/** @TRAILING_SLASH@ **/';
93
- let actionPath = import.meta.env.BASE_URL.replace(/\/$/, '') + '/_actions/' + path;
94
-
95
- if (shouldAppendTrailingSlash) {
96
- actionPath = appendForwardSlash(actionPath);
97
- }
98
-
99
- const rawResult = await fetch(actionPath, {
100
- method: 'POST',
101
- body,
102
- headers,
103
- });
104
116
  if (rawResult.status === 204) {
105
117
  return deserializeActionResult({ type: 'empty', status: 204 });
106
118
  }
@@ -1,3 +1,7 @@
1
1
  declare module 'astro:actions' {
2
2
  export * from 'astro/actions/runtime/virtual/server.js';
3
+
4
+ export function getActionPath(
5
+ action: import('astro/actions/runtime/virtual/server.js').ActionClient<any, any, any>,
6
+ ): string;
3
7
  }