astro 5.0.8 → 5.1.0

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/client.d.ts CHANGED
@@ -535,3 +535,13 @@ declare module '*?inline' {
535
535
  const src: string;
536
536
  export default src;
537
537
  }
538
+
539
+ declare module '*?url&inline' {
540
+ const src: string;
541
+ export default src;
542
+ }
543
+
544
+ declare module '*?url&no-inline' {
545
+ const src: string;
546
+ export default src;
547
+ }
@@ -70,13 +70,13 @@ export * from 'astro/actions/runtime/virtual/server.js';`;
70
70
  } else {
71
71
  code += `
72
72
  export * from 'astro/actions/runtime/virtual/client.js';`;
73
- code = code.replace(
74
- "'/** @TRAILING_SLASH@ **/'",
75
- JSON.stringify(
76
- shouldAppendForwardSlash(settings.config.trailingSlash, settings.config.build.format)
77
- )
78
- );
79
73
  }
74
+ code = code.replace(
75
+ "'/** @TRAILING_SLASH@ **/'",
76
+ JSON.stringify(
77
+ shouldAppendForwardSlash(settings.config.trailingSlash, settings.config.build.format)
78
+ )
79
+ );
80
80
  return code;
81
81
  }
82
82
  };
@@ -8,7 +8,7 @@ import { AstroErrorData } from "../../core/errors/index.js";
8
8
  import { isRemotePath, removeLeadingForwardSlash } from "../../core/path.js";
9
9
  import { getConfiguredImageService } from "../internal.js";
10
10
  import { isESMImportedImage } from "../utils/imageKind.js";
11
- import { loadRemoteImage } from "./remote.js";
11
+ import { loadRemoteImage, revalidateRemoteImage } from "./remote.js";
12
12
  async function prepareAssetsGenerationEnv(pipeline, totalCount) {
13
13
  const { config, logger, settings } = pipeline;
14
14
  let useCache = true;
@@ -72,7 +72,7 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
72
72
  const timeEnd = performance.now();
73
73
  const timeChange = getTimeStat(timeStart, timeEnd);
74
74
  const timeIncrease = `(+${timeChange})`;
75
- const statsText = generationData.cached ? `(reused cache entry)` : `(before: ${generationData.weight.before}kB, after: ${generationData.weight.after}kB)`;
75
+ const statsText = generationData.cached !== "miss" ? generationData.cached === "hit" ? `(reused cache entry)` : `(revalidated cache entry)` : `(before: ${generationData.weight.before}kB, after: ${generationData.weight.after}kB)`;
76
76
  const count = `(${env.count.current}/${env.count.total})`;
77
77
  env.logger.info(
78
78
  null,
@@ -91,7 +91,7 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
91
91
  if (isLocalImage) {
92
92
  await fs.promises.copyFile(cachedFileURL, finalFileURL, fs.constants.COPYFILE_FICLONE);
93
93
  return {
94
- cached: true
94
+ cached: "hit"
95
95
  };
96
96
  } else {
97
97
  const JSONData = JSON.parse(readFileSync(cachedFileURL, "utf-8"));
@@ -104,11 +104,33 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
104
104
  if (JSONData.expires > Date.now()) {
105
105
  await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, "base64"));
106
106
  return {
107
- cached: true
107
+ cached: "hit"
108
108
  };
109
- } else {
110
- await fs.promises.unlink(cachedFileURL);
111
109
  }
110
+ if (JSONData.etag || JSONData.lastModified) {
111
+ try {
112
+ const revalidatedData = await revalidateRemoteImage(options.src, {
113
+ etag: JSONData.etag,
114
+ lastModified: JSONData.lastModified
115
+ });
116
+ if (revalidatedData.data.length) {
117
+ originalImage = revalidatedData;
118
+ } else {
119
+ revalidatedData.data = Buffer.from(JSONData.data, "base64");
120
+ await writeRemoteCacheFile(cachedFileURL, revalidatedData, env);
121
+ await fs.promises.writeFile(finalFileURL, revalidatedData.data);
122
+ return { cached: "revalidated" };
123
+ }
124
+ } catch (e) {
125
+ env.logger.warn(
126
+ null,
127
+ `An error was encountered while revalidating a cached remote asset. Proceeding with stale cache. ${e}`
128
+ );
129
+ await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, "base64"));
130
+ return { cached: "hit" };
131
+ }
132
+ }
133
+ await fs.promises.unlink(cachedFileURL);
112
134
  }
113
135
  } catch (e) {
114
136
  if (e.code !== "ENOENT") {
@@ -121,7 +143,9 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
121
143
  }
122
144
  let resultData = {
123
145
  data: void 0,
124
- expires: originalImage.expires
146
+ expires: originalImage.expires,
147
+ etag: originalImage.etag,
148
+ lastModified: originalImage.lastModified
125
149
  };
126
150
  const imageService = await getConfiguredImageService();
127
151
  try {
@@ -145,13 +169,7 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
145
169
  if (isLocalImage) {
146
170
  await fs.promises.writeFile(cachedFileURL, resultData.data);
147
171
  } else {
148
- await fs.promises.writeFile(
149
- cachedFileURL,
150
- JSON.stringify({
151
- data: Buffer.from(resultData.data).toString("base64"),
152
- expires: resultData.expires
153
- })
154
- );
172
+ await writeRemoteCacheFile(cachedFileURL, resultData, env);
155
173
  }
156
174
  }
157
175
  } catch (e) {
@@ -163,7 +181,7 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
163
181
  await fs.promises.writeFile(finalFileURL, resultData.data);
164
182
  }
165
183
  return {
166
- cached: false,
184
+ cached: "miss",
167
185
  weight: {
168
186
  // Divide by 1024 to get size in kilobytes
169
187
  before: Math.trunc(originalImage.data.byteLength / 1024),
@@ -172,6 +190,24 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
172
190
  };
173
191
  }
174
192
  }
193
+ async function writeRemoteCacheFile(cachedFileURL, resultData, env) {
194
+ try {
195
+ return await fs.promises.writeFile(
196
+ cachedFileURL,
197
+ JSON.stringify({
198
+ data: Buffer.from(resultData.data).toString("base64"),
199
+ expires: resultData.expires,
200
+ etag: resultData.etag,
201
+ lastModified: resultData.lastModified
202
+ })
203
+ );
204
+ } catch (e) {
205
+ env.logger.warn(
206
+ null,
207
+ `An error was encountered while writing the cache file for a remote asset. Proceeding without caching this asset. Error: ${e}`
208
+ );
209
+ }
210
+ }
175
211
  function getStaticImageList() {
176
212
  if (!globalThis?.astroAsset?.staticImages) {
177
213
  return /* @__PURE__ */ new Map();
@@ -180,11 +216,7 @@ function getStaticImageList() {
180
216
  }
181
217
  async function loadImage(path, env) {
182
218
  if (isRemotePath(path)) {
183
- const remoteImage = await loadRemoteImage(path);
184
- return {
185
- data: remoteImage.data,
186
- expires: remoteImage.expires
187
- };
219
+ return await loadRemoteImage(path);
188
220
  }
189
221
  return {
190
222
  data: await fs.promises.readFile(getFullImagePath(path, env)),
@@ -1,8 +1,30 @@
1
1
  export type RemoteCacheEntry = {
2
2
  data: string;
3
3
  expires: number;
4
+ etag?: string;
5
+ lastModified?: string;
4
6
  };
5
7
  export declare function loadRemoteImage(src: string): Promise<{
6
8
  data: Buffer;
7
9
  expires: number;
10
+ etag: string | undefined;
11
+ lastModified: string | undefined;
12
+ }>;
13
+ /**
14
+ * Revalidate a cached remote asset using its entity-tag or modified date.
15
+ * Uses the [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) and [If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
16
+ * headers to check with the remote server if the cached version of a remote asset is still up to date.
17
+ * The remote server may respond that the cached asset is still up-to-date if the entity-tag or modification time matches (304 Not Modified), or respond with an updated asset (200 OK)
18
+ * @param src - url to remote asset
19
+ * @param revalidationData - an object containing the stored Entity-Tag of the cached asset and/or the Last Modified time
20
+ * @returns An ImageData object containing the asset data, a new expiry time, and the asset's etag. The data buffer will be empty if the asset was not modified.
21
+ */
22
+ export declare function revalidateRemoteImage(src: string, revalidationData: {
23
+ etag?: string;
24
+ lastModified?: string;
25
+ }): Promise<{
26
+ data: Buffer;
27
+ expires: number;
28
+ etag: string | undefined;
29
+ lastModified: string | undefined;
8
30
  }>;
@@ -11,7 +11,41 @@ async function loadRemoteImage(src) {
11
11
  const expires = policy.storable() ? policy.timeToLive() : 0;
12
12
  return {
13
13
  data: Buffer.from(await res.arrayBuffer()),
14
- expires: Date.now() + expires
14
+ expires: Date.now() + expires,
15
+ etag: res.headers.get("Etag") ?? void 0,
16
+ lastModified: res.headers.get("Last-Modified") ?? void 0
17
+ };
18
+ }
19
+ async function revalidateRemoteImage(src, revalidationData) {
20
+ const headers = {
21
+ ...revalidationData.etag && { "If-None-Match": revalidationData.etag },
22
+ ...revalidationData.lastModified && { "If-Modified-Since": revalidationData.lastModified }
23
+ };
24
+ const req = new Request(src, { headers });
25
+ const res = await fetch(req);
26
+ if (!res.ok && res.status !== 304) {
27
+ throw new Error(
28
+ `Failed to revalidate cached remote image ${src}. The request did not return a 200 OK / 304 NOT MODIFIED response. (received ${res.status} ${res.statusText})`
29
+ );
30
+ }
31
+ const data = Buffer.from(await res.arrayBuffer());
32
+ if (res.ok && !data.length) {
33
+ return await loadRemoteImage(src);
34
+ }
35
+ const policy = new CachePolicy(
36
+ webToCachePolicyRequest(req),
37
+ webToCachePolicyResponse(
38
+ res.ok ? res : new Response(null, { status: 200, headers: res.headers })
39
+ )
40
+ // 304 responses themselves are not cachable, so just pretend to get the refreshed TTL
41
+ );
42
+ const expires = policy.storable() ? policy.timeToLive() : 0;
43
+ return {
44
+ data,
45
+ expires: Date.now() + expires,
46
+ // While servers should respond with the same headers as a 200 response, if they don't we should reuse the stored value
47
+ etag: res.headers.get("Etag") ?? (res.ok ? void 0 : revalidationData.etag),
48
+ lastModified: res.headers.get("Last-Modified") ?? (res.ok ? void 0 : revalidationData.lastModified)
15
49
  };
16
50
  }
17
51
  function webToCachePolicyRequest({ url, method, headers: _headers }) {
@@ -38,5 +72,6 @@ function webToCachePolicyResponse({ status, headers: _headers }) {
38
72
  };
39
73
  }
40
74
  export {
41
- loadRemoteImage
75
+ loadRemoteImage,
76
+ revalidateRemoteImage
42
77
  };
@@ -1,10 +1,10 @@
1
1
  import type { UserConfig as ViteUserConfig, UserConfigFn as ViteUserConfigFn } from 'vite';
2
- import type { AstroInlineConfig, AstroUserConfig, Locales } from '../types/public/config.js';
2
+ import type { AstroInlineConfig, AstroUserConfig, Locales, SessionDriverName } from '../types/public/config.js';
3
3
  /**
4
4
  * See the full Astro Configuration API Documentation
5
5
  * https://astro.build/config
6
6
  */
7
- export declare function defineConfig<const TLocales extends Locales = never>(config: AstroUserConfig<TLocales>): AstroUserConfig<TLocales>;
7
+ export declare function defineConfig<const TLocales extends Locales = never, const TDriver extends SessionDriverName = never>(config: AstroUserConfig<TLocales, TDriver>): AstroUserConfig<TLocales, TDriver>;
8
8
  /**
9
9
  * Use Astro to generate a fully resolved Vite config
10
10
  */
@@ -12,7 +12,8 @@ import {
12
12
  import {
13
13
  getEntryConfigByExtMap,
14
14
  getEntryDataAndImages,
15
- globalContentConfigObserver
15
+ globalContentConfigObserver,
16
+ safeStringify
16
17
  } from "./utils.js";
17
18
  class ContentLayer {
18
19
  #logger;
@@ -105,16 +106,28 @@ class ContentLayer {
105
106
  return;
106
107
  }
107
108
  logger.info("Syncing content");
109
+ const {
110
+ vite: _vite,
111
+ integrations: _integrations,
112
+ adapter: _adapter,
113
+ ...hashableConfig
114
+ } = this.#settings.config;
115
+ const astroConfigDigest = safeStringify(hashableConfig);
108
116
  const { digest: currentConfigDigest } = contentConfig.config;
109
117
  this.#lastConfigDigest = currentConfigDigest;
110
118
  let shouldClear = false;
111
- const previousConfigDigest = await this.#store.metaStore().get("config-digest");
119
+ const previousConfigDigest = await this.#store.metaStore().get("content-config-digest");
120
+ const previousAstroConfigDigest = await this.#store.metaStore().get("astro-config-digest");
112
121
  const previousAstroVersion = await this.#store.metaStore().get("astro-version");
122
+ if (previousAstroConfigDigest && previousAstroConfigDigest !== astroConfigDigest) {
123
+ logger.info("Astro config changed");
124
+ shouldClear = true;
125
+ }
113
126
  if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) {
114
127
  logger.info("Content config changed");
115
128
  shouldClear = true;
116
129
  }
117
- if (previousAstroVersion !== "5.0.8") {
130
+ if (previousAstroVersion !== "5.1.0") {
118
131
  logger.info("Astro version changed");
119
132
  shouldClear = true;
120
133
  }
@@ -122,11 +135,14 @@ class ContentLayer {
122
135
  logger.info("Clearing content store");
123
136
  this.#store.clearAll();
124
137
  }
125
- if ("5.0.8") {
126
- await this.#store.metaStore().set("astro-version", "5.0.8");
138
+ if ("5.1.0") {
139
+ await this.#store.metaStore().set("astro-version", "5.1.0");
127
140
  }
128
141
  if (currentConfigDigest) {
129
- await this.#store.metaStore().set("config-digest", currentConfigDigest);
142
+ await this.#store.metaStore().set("content-config-digest", currentConfigDigest);
143
+ }
144
+ if (astroConfigDigest) {
145
+ await this.#store.metaStore().set("astro-config-digest", astroConfigDigest);
130
146
  }
131
147
  await Promise.all(
132
148
  Object.entries(contentConfig.config.collections).map(async ([name, collection]) => {
@@ -583,4 +583,5 @@ export declare function posixifyPath(filePath: string): string;
583
583
  */
584
584
  export declare function posixRelative(from: string, to: string): string;
585
585
  export declare function contentModuleToId(fileName: string): string;
586
+ export declare function safeStringify(value: unknown): string;
586
587
  export {};
@@ -589,6 +589,27 @@ function contentModuleToId(fileName) {
589
589
  params.set(CONTENT_MODULE_FLAG, "true");
590
590
  return `${DEFERRED_MODULE}?${params.toString()}`;
591
591
  }
592
+ function safeStringifyReplacer(seen) {
593
+ return function(_key, value) {
594
+ if (!(value !== null && typeof value === "object")) {
595
+ return value;
596
+ }
597
+ if (seen.has(value)) {
598
+ return "[Circular]";
599
+ }
600
+ seen.add(value);
601
+ const newValue = Array.isArray(value) ? [] : {};
602
+ for (const [key2, value2] of Object.entries(value)) {
603
+ newValue[key2] = safeStringifyReplacer(seen)(key2, value2);
604
+ }
605
+ seen.delete(value);
606
+ return newValue;
607
+ };
608
+ }
609
+ function safeStringify(value) {
610
+ const seen = /* @__PURE__ */ new WeakSet();
611
+ return JSON.stringify(value, safeStringifyReplacer(seen));
612
+ }
592
613
  export {
593
614
  autogenerateCollections,
594
615
  contentModuleToId,
@@ -617,5 +638,6 @@ export {
617
638
  posixifyPath,
618
639
  reloadContentConfigObserver,
619
640
  reverseSymlink,
620
- safeParseFrontmatter
641
+ safeParseFrontmatter,
642
+ safeStringify
621
643
  };
@@ -21,6 +21,7 @@ import { createAssetLink } from "../render/ssr-element.js";
21
21
  import { ensure404Route } from "../routing/astro-designed-error-pages.js";
22
22
  import { createDefaultRoutes } from "../routing/default.js";
23
23
  import { matchRoute } from "../routing/match.js";
24
+ import { PERSIST_SYMBOL } from "../session.js";
24
25
  import { AppPipeline } from "./pipeline.js";
25
26
  import { deserializeManifest } from "./common.js";
26
27
  class App {
@@ -188,6 +189,7 @@ class App {
188
189
  const pathname = this.#getPathnameFromRequest(request);
189
190
  const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
190
191
  let response;
192
+ let session;
191
193
  try {
192
194
  const mod = await this.#pipeline.getModuleForRoute(routeData);
193
195
  const renderContext = await RenderContext.create({
@@ -199,10 +201,13 @@ class App {
199
201
  status: defaultStatus,
200
202
  clientAddress
201
203
  });
204
+ session = renderContext.session;
202
205
  response = await renderContext.render(await mod.page());
203
206
  } catch (err) {
204
207
  this.#logger.error(null, err.stack || err.message || String(err));
205
208
  return this.#renderError(request, { locals, status: 500, error: err, clientAddress });
209
+ } finally {
210
+ session?.[PERSIST_SYMBOL]();
206
211
  }
207
212
  if (REROUTABLE_STATUS_CODES.includes(response.status) && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
208
213
  return this.#renderError(request, {
@@ -270,6 +275,7 @@ class App {
270
275
  }
271
276
  }
272
277
  const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
278
+ let session;
273
279
  try {
274
280
  const renderContext = await RenderContext.create({
275
281
  locals,
@@ -282,6 +288,7 @@ class App {
282
288
  props: { error },
283
289
  clientAddress
284
290
  });
291
+ session = renderContext.session;
285
292
  const response2 = await renderContext.render(await mod.page());
286
293
  return this.#mergeResponses(response2, originalResponse);
287
294
  } catch {
@@ -294,6 +301,8 @@ class App {
294
301
  clientAddress
295
302
  });
296
303
  }
304
+ } finally {
305
+ session?.[PERSIST_SYMBOL]();
297
306
  }
298
307
  }
299
308
  const response = this.#mergeResponses(new Response(null, { status }), originalResponse);
@@ -1,7 +1,7 @@
1
1
  import type { RoutingStrategies } from '../../i18n/utils.js';
2
2
  import type { ComponentInstance, SerializedRouteData } from '../../types/astro.js';
3
3
  import type { AstroMiddlewareInstance } from '../../types/public/common.js';
4
- import type { Locales } from '../../types/public/config.js';
4
+ import type { Locales, ResolvedSessionConfig } from '../../types/public/config.js';
5
5
  import type { RouteData, SSRComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../types/public/internal.js';
6
6
  import type { SinglePageBuiltModule } from '../build/types.js';
7
7
  export type ComponentPath = string;
@@ -59,6 +59,7 @@ export type SSRManifest = {
59
59
  i18n: SSRManifestI18n | undefined;
60
60
  middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
61
61
  checkOrigin: boolean;
62
+ sessionConfig?: ResolvedSessionConfig<any>;
62
63
  };
63
64
  export type SSRManifestI18n = {
64
65
  fallback: Record<string, string> | undefined;
@@ -9,6 +9,7 @@ import { encodeKey } from "../../encryption.js";
9
9
  import { fileExtension, joinPaths, prependForwardSlash } from "../../path.js";
10
10
  import { DEFAULT_COMPONENTS } from "../../routing/default.js";
11
11
  import { serializeRouteData } from "../../routing/index.js";
12
+ import { resolveSessionDriver } from "../../session.js";
12
13
  import { addRollupInput } from "../add-rollup-input.js";
13
14
  import { getOutFile, getOutFolder } from "../common.js";
14
15
  import { cssOrder, mergeInlineCss } from "../internal.js";
@@ -17,7 +18,7 @@ const manifestReplace = "@@ASTRO_MANIFEST_REPLACE@@";
17
18
  const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, "g");
18
19
  const SSR_MANIFEST_VIRTUAL_MODULE_ID = "@astrojs-manifest";
19
20
  const RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID = "\0" + SSR_MANIFEST_VIRTUAL_MODULE_ID;
20
- function vitePluginManifest(_options, internals) {
21
+ function vitePluginManifest(options, internals) {
21
22
  return {
22
23
  name: "@astro/plugin-build-manifest",
23
24
  enforce: "post",
@@ -40,8 +41,12 @@ function vitePluginManifest(_options, internals) {
40
41
  `import { deserializeManifest as _deserializeManifest } from 'astro/app'`,
41
42
  `import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`
42
43
  ];
44
+ const resolvedDriver = await resolveSessionDriver(
45
+ options.settings.config.experimental?.session?.driver
46
+ );
43
47
  const contents = [
44
48
  `const manifest = _deserializeManifest('${manifestReplace}');`,
49
+ `if (manifest.sessionConfig) manifest.sessionConfig.driverModule = ${resolvedDriver ? `() => import(${JSON.stringify(resolvedDriver)})` : "null"};`,
45
50
  `_privateSetManifestDontUseThis(manifest);`
46
51
  ];
47
52
  const exports = [`export { manifest }`];
@@ -220,7 +225,8 @@ function buildManifest(opts, internals, staticFiles, encodedKey) {
220
225
  buildFormat: settings.config.build.format,
221
226
  checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === "server") ?? false,
222
227
  serverIslandNameMap: Array.from(settings.serverIslandNameMap),
223
- key: encodedKey
228
+ key: encodedKey,
229
+ sessionConfig: settings.config.experimental.session
224
230
  };
225
231
  }
226
232
  export {
@@ -218,14 +218,14 @@ ${bgGreen(black(" building client (vite) "))}`);
218
218
  envPrefix: viteConfig.envPrefix ?? "PUBLIC_",
219
219
  base: settings.config.base
220
220
  };
221
- await runHookBuildSetup({
221
+ const updatedViteBuildConfig = await runHookBuildSetup({
222
222
  config: settings.config,
223
223
  pages: internals.pagesByKeys,
224
224
  vite: viteBuildConfig,
225
225
  target: "client",
226
226
  logger: opts.logger
227
227
  });
228
- const buildResult = await vite.build(viteBuildConfig);
228
+ const buildResult = await vite.build(updatedViteBuildConfig);
229
229
  return buildResult;
230
230
  }
231
231
  async function runPostBuildHooks(container, ssrOutputs, clientOutputs) {
@@ -316,7 +316,6 @@ async function ssrMoveAssets(opts) {
316
316
  const files = await glob(`**/*`, {
317
317
  cwd: fileURLToPath(serverAssets)
318
318
  });
319
- console.log("FILES2", files);
320
319
  if (files.length > 0) {
321
320
  await Promise.all(
322
321
  files.map(async function moveAsset(filename) {