astro 3.5.5 → 3.5.7

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.
@@ -30,6 +30,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
30
30
  import type { Options } from 'astro:transitions/client';
31
31
  import { supportsViewTransitions, navigate } from 'astro:transitions/client';
32
32
  // NOTE: import from `astro/prefetch` as `astro:prefetch` requires the `prefetch` config to be enabled
33
+ // @ts-ignore
33
34
  import { init } from 'astro/prefetch';
34
35
 
35
36
  export type Fallback = 'none' | 'animate' | 'swap';
@@ -42,27 +43,34 @@ const { fallback = 'animate', handleForms } = Astro.props;
42
43
  return 'animate';
43
44
  }
44
45
 
45
- function isReloadEl(el: HTMLElement): boolean {
46
+ function isReloadEl(el: HTMLElement | SVGAElement): boolean {
46
47
  return el.dataset.astroReload !== undefined;
47
48
  }
48
49
 
49
50
  if (supportsViewTransitions || getFallback() !== 'none') {
50
51
  document.addEventListener('click', (ev) => {
51
52
  let link = ev.target;
52
- if (link instanceof Element && link.tagName !== 'A') {
53
- link = link.closest('a');
53
+ if (link instanceof Element) {
54
+ link = link.closest('a, area');
54
55
  }
56
+ if (
57
+ !(link instanceof HTMLAnchorElement) &&
58
+ !(link instanceof SVGAElement) &&
59
+ !(link instanceof HTMLAreaElement)
60
+ )
61
+ return;
55
62
  // This check verifies that the click is happening on an anchor
56
63
  // that is going to another page within the same origin. Basically it determines
57
64
  // same-origin navigation, but omits special key combos for new tabs, etc.
65
+ const linkTarget = link instanceof HTMLElement ? link.target : link.target.baseVal;
66
+ const href = link instanceof HTMLElement ? link.href : link.href.baseVal;
67
+ const origin = new URL(href, location.href).origin;
58
68
  if (
59
- !link ||
60
- !(link instanceof HTMLAnchorElement) ||
61
69
  isReloadEl(link) ||
62
70
  link.hasAttribute('download') ||
63
71
  !link.href ||
64
- (link.target && link.target !== '_self') ||
65
- link.origin !== location.origin ||
72
+ (linkTarget && linkTarget !== '_self') ||
73
+ origin !== location.origin ||
66
74
  ev.button !== 0 || // left clicks only
67
75
  ev.metaKey || // new tab (mac)
68
76
  ev.ctrlKey || // new tab (windows)
@@ -75,7 +83,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
75
83
  return;
76
84
  }
77
85
  ev.preventDefault();
78
- navigate(link.href, {
86
+ navigate(href, {
79
87
  history: link.dataset.astroHistory === 'replace' ? 'replace' : 'auto',
80
88
  });
81
89
  });
@@ -1903,6 +1903,10 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
1903
1903
  * The list of locales computed from the `Accept-Language` header of the browser, sorted by quality value (**SSR Only**).
1904
1904
  */
1905
1905
  preferredLocaleList: string[] | undefined;
1906
+ /**
1907
+ * The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise.
1908
+ */
1909
+ currentLocale: string | undefined;
1906
1910
  }
1907
1911
  export interface APIContext<Props extends Record<string, any> = Record<string, any>, APIParams extends Record<string, string | undefined> = Record<string, string | undefined>> extends AstroSharedContext<Props, Params> {
1908
1912
  site: URL | undefined;
@@ -2025,6 +2029,10 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
2025
2029
  * [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
2026
2030
  */
2027
2031
  preferredLocaleList: string[] | undefined;
2032
+ /**
2033
+ * The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise.
2034
+ */
2035
+ currentLocale: string | undefined;
2028
2036
  }
2029
2037
  export type EndpointOutput = {
2030
2038
  body: Body;
@@ -2178,14 +2186,16 @@ export interface RouteData {
2178
2186
  prerender: boolean;
2179
2187
  redirect?: RedirectConfig;
2180
2188
  redirectRoute?: RouteData;
2189
+ fallbackRoutes: RouteData[];
2181
2190
  }
2182
2191
  export type RedirectRouteData = RouteData & {
2183
2192
  redirect: string;
2184
2193
  };
2185
- export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern' | 'redirectRoute'> & {
2194
+ export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern' | 'redirectRoute' | 'fallbackRoutes'> & {
2186
2195
  generate: undefined;
2187
2196
  pattern: string;
2188
2197
  redirectRoute: SerializedRouteData | undefined;
2198
+ fallbackRoutes: SerializedRouteData[];
2189
2199
  _meta: {
2190
2200
  trailingSlash: AstroConfig['trailingSlash'];
2191
2201
  };
@@ -608,8 +608,10 @@ async function fetchPackageJson(scope, name, tag) {
608
608
  const res = await fetch(`${registry}/${packageName}/${tag}`);
609
609
  if (res.status >= 200 && res.status < 300) {
610
610
  return await res.json();
611
- } else {
611
+ } else if (res.status === 404) {
612
612
  return new Error();
613
+ } else {
614
+ return new Error(`Failed to fetch ${registry}/${packageName}/${tag} - GET ${res.status}`);
613
615
  }
614
616
  }
615
617
  async function validateIntegrations(integrations) {
@@ -629,6 +631,9 @@ async function validateIntegrations(integrations) {
629
631
  } else {
630
632
  const firstPartyPkgCheck = await fetchPackageJson("@astrojs", name, tag);
631
633
  if (firstPartyPkgCheck instanceof Error) {
634
+ if (firstPartyPkgCheck.message) {
635
+ spinner.warn(yellow(firstPartyPkgCheck.message));
636
+ }
632
637
  spinner.warn(
633
638
  yellow(`${bold(integration)} is not an official Astro package. Use at your own risk!`)
634
639
  );
@@ -655,6 +660,9 @@ async function validateIntegrations(integrations) {
655
660
  if (pkgType === "third-party") {
656
661
  const thirdPartyPkgCheck = await fetchPackageJson(scope, name, tag);
657
662
  if (thirdPartyPkgCheck instanceof Error) {
663
+ if (thirdPartyPkgCheck.message) {
664
+ spinner.warn(yellow(thirdPartyPkgCheck.message));
665
+ }
658
666
  throw new Error(`Unable to fetch ${bold(integration)}. Does the package exist?`);
659
667
  } else {
660
668
  pkgJson = thirdPartyPkgCheck;
@@ -96,6 +96,11 @@ class App {
96
96
  }
97
97
  return pathname;
98
98
  }
99
+ #getPathnameFromRequest(request) {
100
+ const url = new URL(request.url);
101
+ const pathname = prependForwardSlash(this.removeBase(url.pathname));
102
+ return pathname;
103
+ }
99
104
  match(request, _opts = {}) {
100
105
  const url = new URL(request.url);
101
106
  if (this.#manifest.assets.has(url.pathname))
@@ -117,7 +122,8 @@ class App {
117
122
  return this.#renderError(request, { status: 404 });
118
123
  }
119
124
  Reflect.set(request, clientLocalsSymbol, locals ?? {});
120
- const defaultStatus = this.#getDefaultStatusCode(routeData.route);
125
+ const pathname = this.#getPathnameFromRequest(request);
126
+ const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
121
127
  const mod = await this.#getModuleForRoute(routeData);
122
128
  const pageModule = await mod.page();
123
129
  const url = new URL(request.url);
@@ -188,7 +194,9 @@ class App {
188
194
  status,
189
195
  env: this.#pipeline.env,
190
196
  mod: handler,
191
- locales: this.#manifest.i18n ? this.#manifest.i18n.locales : void 0
197
+ locales: this.#manifest.i18n?.locales,
198
+ routingStrategy: this.#manifest.i18n?.routingStrategy,
199
+ defaultLocale: this.#manifest.i18n?.defaultLocale
192
200
  });
193
201
  } else {
194
202
  const pathname = prependForwardSlash(this.removeBase(url.pathname));
@@ -220,7 +228,9 @@ class App {
220
228
  status,
221
229
  mod,
222
230
  env: this.#pipeline.env,
223
- locales: this.#manifest.i18n ? this.#manifest.i18n.locales : void 0
231
+ locales: this.#manifest.i18n?.locales,
232
+ routingStrategy: this.#manifest.i18n?.routingStrategy,
233
+ defaultLocale: this.#manifest.i18n?.defaultLocale
224
234
  });
225
235
  }
226
236
  }
@@ -293,8 +303,15 @@ class App {
293
303
  headers: new Headers(Array.from(headers))
294
304
  });
295
305
  }
296
- #getDefaultStatusCode(route) {
297
- route = removeTrailingForwardSlash(route);
306
+ #getDefaultStatusCode(routeData, pathname) {
307
+ if (!routeData.pattern.exec(pathname)) {
308
+ for (const fallbackRoute of routeData.fallbackRoutes) {
309
+ if (fallbackRoute.pattern.test(pathname)) {
310
+ return 302;
311
+ }
312
+ }
313
+ }
314
+ const route = removeTrailingForwardSlash(routeData.route);
298
315
  if (route.endsWith("/404"))
299
316
  return 404;
300
317
  if (route.endsWith("/500"))
@@ -128,13 +128,11 @@ class BuildPipeline extends Pipeline {
128
128
  pages.set(pageData, filePath);
129
129
  }
130
130
  }
131
- for (const [path, pageDataList] of this.#internals.pagesByComponents.entries()) {
132
- for (const pageData of pageDataList) {
133
- if (routeIsRedirect(pageData.route)) {
134
- pages.set(pageData, path);
135
- } else if (routeIsFallback(pageData.route) && (i18nHasFallback(this.getConfig()) || routeIsFallback(pageData.route) && pageData.route.route === "/")) {
136
- pages.set(pageData, path);
137
- }
131
+ for (const [path, pageData] of this.#internals.pagesByComponent.entries()) {
132
+ if (routeIsRedirect(pageData.route)) {
133
+ pages.set(pageData, path);
134
+ } else if (routeIsFallback(pageData.route) && (i18nHasFallback(this.getConfig()) || routeIsFallback(pageData.route) && pageData.route.route === "/")) {
135
+ pages.set(pageData, path);
138
136
  }
139
137
  }
140
138
  return pages;
@@ -244,6 +244,9 @@ async function generatePage(pageData, ssrEntry, builtPaths, pipeline) {
244
244
  const icon = pageData.route.type === "page" || pageData.route.type === "redirect" || pageData.route.type === "fallback" ? green("\u25B6") : magenta("\u03BB");
245
245
  if (isRelativePath(pageData.route.component)) {
246
246
  logger.info(null, `${icon} ${pageData.route.route}`);
247
+ for (const fallbackRoute of pageData.route.fallbackRoutes) {
248
+ logger.info(null, `${icon} ${fallbackRoute.route}`);
249
+ }
247
250
  } else {
248
251
  logger.info(null, `${icon} ${pageData.route.component}`);
249
252
  }
@@ -261,6 +264,12 @@ async function generatePage(pageData, ssrEntry, builtPaths, pipeline) {
261
264
  prevTimeEnd = timeEnd;
262
265
  }
263
266
  }
267
+ function* eachRouteInRouteData(data) {
268
+ yield data.route;
269
+ for (const fallbackRoute of data.route.fallbackRoutes) {
270
+ yield fallbackRoute;
271
+ }
272
+ }
264
273
  async function getPathsForRoute(pageData, mod, pipeline, builtPaths) {
265
274
  const opts = pipeline.getStaticBuildOptions();
266
275
  const logger = pipeline.getLogger();
@@ -268,43 +277,52 @@ async function getPathsForRoute(pageData, mod, pipeline, builtPaths) {
268
277
  if (pageData.route.pathname) {
269
278
  paths.push(pageData.route.pathname);
270
279
  builtPaths.add(pageData.route.pathname);
271
- } else {
272
- const route = pageData.route;
273
- const staticPaths = await callGetStaticPaths({
274
- mod,
275
- route,
276
- routeCache: opts.routeCache,
277
- logger,
278
- ssr: isServerLikeOutput(opts.settings.config)
279
- }).catch((err) => {
280
- logger.debug("build", `\u251C\u2500\u2500 ${colors.bold(colors.red("\u2717"))} ${route.component}`);
281
- throw err;
282
- });
283
- const label = staticPaths.length === 1 ? "page" : "pages";
284
- logger.debug(
285
- "build",
286
- `\u251C\u2500\u2500 ${colors.bold(colors.green("\u2714"))} ${route.component} \u2192 ${colors.magenta(
287
- `[${staticPaths.length} ${label}]`
288
- )}`
289
- );
290
- paths = staticPaths.map((staticPath) => {
291
- try {
292
- return route.generate(staticPath.params);
293
- } catch (e) {
294
- if (e instanceof TypeError) {
295
- throw getInvalidRouteSegmentError(e, route, staticPath);
296
- }
297
- throw e;
280
+ for (const virtualRoute of pageData.route.fallbackRoutes) {
281
+ if (virtualRoute.pathname) {
282
+ paths.push(virtualRoute.pathname);
283
+ builtPaths.add(virtualRoute.pathname);
298
284
  }
299
- }).filter((staticPath) => {
300
- if (!builtPaths.has(removeTrailingForwardSlash(staticPath))) {
301
- return true;
285
+ }
286
+ } else {
287
+ for (const route of eachRouteInRouteData(pageData)) {
288
+ const staticPaths = await callGetStaticPaths({
289
+ mod,
290
+ route,
291
+ routeCache: opts.routeCache,
292
+ logger,
293
+ ssr: isServerLikeOutput(opts.settings.config)
294
+ }).catch((err) => {
295
+ logger.debug("build", `\u251C\u2500\u2500 ${colors.bold(colors.red("\u2717"))} ${route.component}`);
296
+ throw err;
297
+ });
298
+ const label = staticPaths.length === 1 ? "page" : "pages";
299
+ logger.debug(
300
+ "build",
301
+ `\u251C\u2500\u2500 ${colors.bold(colors.green("\u2714"))} ${route.component} \u2192 ${colors.magenta(
302
+ `[${staticPaths.length} ${label}]`
303
+ )}`
304
+ );
305
+ paths.push(
306
+ ...staticPaths.map((staticPath) => {
307
+ try {
308
+ return route.generate(staticPath.params);
309
+ } catch (e) {
310
+ if (e instanceof TypeError) {
311
+ throw getInvalidRouteSegmentError(e, route, staticPath);
312
+ }
313
+ throw e;
314
+ }
315
+ }).filter((staticPath) => {
316
+ if (!builtPaths.has(removeTrailingForwardSlash(staticPath))) {
317
+ return true;
318
+ }
319
+ const matchedRoute = matchRoute(staticPath, opts.manifest);
320
+ return matchedRoute === route;
321
+ })
322
+ );
323
+ for (const staticPath of paths) {
324
+ builtPaths.add(removeTrailingForwardSlash(staticPath));
302
325
  }
303
- const matchedRoute = matchRoute(staticPath, opts.manifest);
304
- return matchedRoute === route;
305
- });
306
- for (const staticPath of paths) {
307
- builtPaths.add(removeTrailingForwardSlash(staticPath));
308
326
  }
309
327
  }
310
328
  return paths;
@@ -356,84 +374,87 @@ function getUrlForPath(pathname, base, origin, format, routeType) {
356
374
  async function generatePath(pathname, gopts, pipeline) {
357
375
  const manifest = pipeline.getManifest();
358
376
  const { mod, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
359
- if (pageData.route.type === "page") {
360
- addPageName(pathname, pipeline.getStaticBuildOptions());
361
- }
362
- pipeline.getEnvironment().logger.debug("build", `Generating: ${pathname}`);
363
- const links = /* @__PURE__ */ new Set();
364
- const scripts = createModuleScriptsSet(
365
- hoistedScripts ? [hoistedScripts] : [],
366
- manifest.base,
367
- manifest.assetsPrefix
368
- );
369
- const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
370
- if (pipeline.getSettings().scripts.some((script) => script.stage === "page")) {
371
- const hashedFilePath = pipeline.getInternals().entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
372
- if (typeof hashedFilePath !== "string") {
373
- throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
377
+ for (const route of eachRouteInRouteData(pageData)) {
378
+ if (route.type === "page") {
379
+ addPageName(pathname, pipeline.getStaticBuildOptions());
374
380
  }
375
- const src = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
376
- scripts.add({
377
- props: { type: "module", src },
378
- children: ""
379
- });
380
- }
381
- for (const script of pipeline.getSettings().scripts) {
382
- if (script.stage === "head-inline") {
381
+ pipeline.getEnvironment().logger.debug("build", `Generating: ${pathname}`);
382
+ const links = /* @__PURE__ */ new Set();
383
+ const scripts = createModuleScriptsSet(
384
+ hoistedScripts ? [hoistedScripts] : [],
385
+ manifest.base,
386
+ manifest.assetsPrefix
387
+ );
388
+ const styles = createStylesheetElementSet(_styles, manifest.base, manifest.assetsPrefix);
389
+ if (pipeline.getSettings().scripts.some((script) => script.stage === "page")) {
390
+ const hashedFilePath = pipeline.getInternals().entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
391
+ if (typeof hashedFilePath !== "string") {
392
+ throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
393
+ }
394
+ const src = createAssetLink(hashedFilePath, manifest.base, manifest.assetsPrefix);
383
395
  scripts.add({
384
- props: {},
385
- children: script.content
396
+ props: { type: "module", src },
397
+ children: ""
386
398
  });
387
399
  }
388
- }
389
- const ssr = isServerLikeOutput(pipeline.getConfig());
390
- const url = getUrlForPath(
391
- pathname,
392
- pipeline.getConfig().base,
393
- pipeline.getStaticBuildOptions().origin,
394
- pipeline.getConfig().build.format,
395
- pageData.route.type
396
- );
397
- const request = createRequest({
398
- url,
399
- headers: new Headers(),
400
- logger: pipeline.getLogger(),
401
- ssr
402
- });
403
- const i18n = pipeline.getConfig().experimental.i18n;
404
- const renderContext = await createRenderContext({
405
- pathname,
406
- request,
407
- componentMetadata: manifest.componentMetadata,
408
- scripts,
409
- styles,
410
- links,
411
- route: pageData.route,
412
- env: pipeline.getEnvironment(),
413
- mod,
414
- locales: i18n ? i18n.locales : void 0
415
- });
416
- let body;
417
- let encoding;
418
- let response;
419
- try {
420
- response = await pipeline.renderRoute(renderContext, mod);
421
- } catch (err) {
422
- if (!AstroError.is(err) && !err.id && typeof err === "object") {
423
- err.id = pageData.component;
400
+ for (const script of pipeline.getSettings().scripts) {
401
+ if (script.stage === "head-inline") {
402
+ scripts.add({
403
+ props: {},
404
+ children: script.content
405
+ });
406
+ }
424
407
  }
425
- throw err;
426
- }
427
- if (response.status >= 300 && response.status < 400) {
428
- if (!pipeline.getConfig().build.redirects) {
429
- return;
408
+ const ssr = isServerLikeOutput(pipeline.getConfig());
409
+ const url = getUrlForPath(
410
+ pathname,
411
+ pipeline.getConfig().base,
412
+ pipeline.getStaticBuildOptions().origin,
413
+ pipeline.getConfig().build.format,
414
+ route.type
415
+ );
416
+ const request = createRequest({
417
+ url,
418
+ headers: new Headers(),
419
+ logger: pipeline.getLogger(),
420
+ ssr
421
+ });
422
+ const i18n = pipeline.getConfig().experimental.i18n;
423
+ const renderContext = await createRenderContext({
424
+ pathname,
425
+ request,
426
+ componentMetadata: manifest.componentMetadata,
427
+ scripts,
428
+ styles,
429
+ links,
430
+ route,
431
+ env: pipeline.getEnvironment(),
432
+ mod,
433
+ locales: i18n?.locales,
434
+ routingStrategy: i18n?.routingStrategy,
435
+ defaultLocale: i18n?.defaultLocale
436
+ });
437
+ let body;
438
+ let encoding;
439
+ let response;
440
+ try {
441
+ response = await pipeline.renderRoute(renderContext, mod);
442
+ } catch (err) {
443
+ if (!AstroError.is(err) && !err.id && typeof err === "object") {
444
+ err.id = pageData.component;
445
+ }
446
+ throw err;
430
447
  }
431
- const locationSite = getRedirectLocationOrThrow(response.headers);
432
- const siteURL = pipeline.getConfig().site;
433
- const location = siteURL ? new URL(locationSite, siteURL) : locationSite;
434
- const fromPath = new URL(renderContext.request.url).pathname;
435
- const delay = response.status === 302 ? 2 : 0;
436
- body = `<!doctype html>
448
+ if (response.status >= 300 && response.status < 400) {
449
+ if (!pipeline.getConfig().build.redirects) {
450
+ return;
451
+ }
452
+ const locationSite = getRedirectLocationOrThrow(response.headers);
453
+ const siteURL = pipeline.getConfig().site;
454
+ const location = siteURL ? new URL(locationSite, siteURL) : locationSite;
455
+ const fromPath = new URL(renderContext.request.url).pathname;
456
+ const delay = response.status === 302 ? 2 : 0;
457
+ body = `<!doctype html>
437
458
  <title>Redirecting to: ${location}</title>
438
459
  <meta http-equiv="refresh" content="${delay};url=${location}">
439
460
  <meta name="robots" content="noindex">
@@ -441,23 +462,24 @@ async function generatePath(pathname, gopts, pipeline) {
441
462
  <body>
442
463
  <a href="${location}">Redirecting from <code>${fromPath}</code> to <code>${location}</code></a>
443
464
  </body>`;
444
- if (pipeline.getConfig().compressHTML === true) {
445
- body = body.replaceAll("\n", "");
446
- }
447
- if (pageData.route.type !== "redirect") {
448
- pageData.route.redirect = location.toString();
465
+ if (pipeline.getConfig().compressHTML === true) {
466
+ body = body.replaceAll("\n", "");
467
+ }
468
+ if (route.type !== "redirect") {
469
+ route.redirect = location.toString();
470
+ }
471
+ } else {
472
+ if (!response.body)
473
+ return;
474
+ body = Buffer.from(await response.arrayBuffer());
475
+ encoding = response.headers.get("X-Astro-Encoding") ?? "utf-8";
449
476
  }
450
- } else {
451
- if (!response.body)
452
- return;
453
- body = Buffer.from(await response.arrayBuffer());
454
- encoding = response.headers.get("X-Astro-Encoding") ?? "utf-8";
477
+ const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
478
+ const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
479
+ route.distURL = outFile;
480
+ await fs.promises.mkdir(outFolder, { recursive: true });
481
+ await fs.promises.writeFile(outFile, body, encoding);
455
482
  }
456
- const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
457
- const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
458
- pageData.route.distURL = outFile;
459
- await fs.promises.mkdir(outFolder, { recursive: true });
460
- await fs.promises.writeFile(outFile, body, encoding);
461
483
  }
462
484
  function createBuildManifest(settings, internals, renderers) {
463
485
  let i18nManifest = void 0;
@@ -20,14 +20,8 @@ export interface BuildInternals {
20
20
  pageToBundleMap: Map<string, string>;
21
21
  /**
22
22
  * A map for page-specific information.
23
- * // TODO: Remove in Astro 4.0
24
- * @deprecated
25
23
  */
26
24
  pagesByComponent: Map<string, PageBuildData>;
27
- /**
28
- * TODO: Use this in Astro 4.0
29
- */
30
- pagesByComponents: Map<string, PageBuildData[]>;
31
25
  /**
32
26
  * A map for page-specific output.
33
27
  */
@@ -1,5 +1,4 @@
1
1
  import { prependForwardSlash, removeFileExtension } from "../path.js";
2
- import { routeIsFallback } from "../redirects/helpers.js";
3
2
  import { viteID } from "../util.js";
4
3
  import {
5
4
  ASTRO_PAGE_RESOLVED_MODULE_ID,
@@ -18,7 +17,6 @@ function createBuildInternals() {
18
17
  entrySpecifierToBundleMap: /* @__PURE__ */ new Map(),
19
18
  pageToBundleMap: /* @__PURE__ */ new Map(),
20
19
  pagesByComponent: /* @__PURE__ */ new Map(),
21
- pagesByComponents: /* @__PURE__ */ new Map(),
22
20
  pageOptionsByPage: /* @__PURE__ */ new Map(),
23
21
  pagesByViteID: /* @__PURE__ */ new Map(),
24
22
  pagesByClientOnly: /* @__PURE__ */ new Map(),
@@ -35,16 +33,7 @@ function createBuildInternals() {
35
33
  }
36
34
  function trackPageData(internals, component, pageData, componentModuleId, componentURL) {
37
35
  pageData.moduleSpecifier = componentModuleId;
38
- if (!routeIsFallback(pageData.route)) {
39
- internals.pagesByComponent.set(component, pageData);
40
- }
41
- const list = internals.pagesByComponents.get(component);
42
- if (list) {
43
- list.push(pageData);
44
- internals.pagesByComponents.set(component, list);
45
- } else {
46
- internals.pagesByComponents.set(component, [pageData]);
47
- }
36
+ internals.pagesByComponent.set(component, pageData);
48
37
  internals.pagesByViteID.set(viteID(componentURL), pageData);
49
38
  }
50
39
  function trackClientOnlyPageDatas(internals, pageData, clientOnlys) {
@@ -105,10 +94,8 @@ function* eachPageData(internals) {
105
94
  yield* internals.pagesByComponent.values();
106
95
  }
107
96
  function* eachPageFromAllPages(allPages) {
108
- for (const [path, list] of Object.entries(allPages)) {
109
- for (const pageData of list) {
110
- yield [path, pageData];
111
- }
97
+ for (const [path, pageData] of Object.entries(allPages)) {
98
+ yield [path, pageData];
112
99
  }
113
100
  }
114
101
  function* eachPageDataFromEntryPoint(internals) {
@@ -21,29 +21,15 @@ async function collectPagesData(opts) {
21
21
  clearInterval(routeCollectionLogTimeout);
22
22
  }, 1e4);
23
23
  builtPaths.add(route.pathname);
24
- if (allPages[route.component]) {
25
- allPages[route.component].push({
26
- component: route.component,
27
- route,
28
- moduleSpecifier: "",
29
- styles: [],
30
- propagatedStyles: /* @__PURE__ */ new Map(),
31
- propagatedScripts: /* @__PURE__ */ new Map(),
32
- hoistedScript: void 0
33
- });
34
- } else {
35
- allPages[route.component] = [
36
- {
37
- component: route.component,
38
- route,
39
- moduleSpecifier: "",
40
- styles: [],
41
- propagatedStyles: /* @__PURE__ */ new Map(),
42
- propagatedScripts: /* @__PURE__ */ new Map(),
43
- hoistedScript: void 0
44
- }
45
- ];
46
- }
24
+ allPages[route.component] = {
25
+ component: route.component,
26
+ route,
27
+ moduleSpecifier: "",
28
+ styles: [],
29
+ propagatedStyles: /* @__PURE__ */ new Map(),
30
+ propagatedScripts: /* @__PURE__ */ new Map(),
31
+ hoistedScript: void 0
32
+ };
47
33
  clearInterval(routeCollectionLogTimeout);
48
34
  if (settings.config.output === "static") {
49
35
  const html = `${route.pathname}`.replace(/\/?$/, "/index.html");
@@ -56,29 +42,15 @@ async function collectPagesData(opts) {
56
42
  }
57
43
  continue;
58
44
  }
59
- if (allPages[route.component]) {
60
- allPages[route.component].push({
61
- component: route.component,
62
- route,
63
- moduleSpecifier: "",
64
- styles: [],
65
- propagatedStyles: /* @__PURE__ */ new Map(),
66
- propagatedScripts: /* @__PURE__ */ new Map(),
67
- hoistedScript: void 0
68
- });
69
- } else {
70
- allPages[route.component] = [
71
- {
72
- component: route.component,
73
- route,
74
- moduleSpecifier: "",
75
- styles: [],
76
- propagatedStyles: /* @__PURE__ */ new Map(),
77
- propagatedScripts: /* @__PURE__ */ new Map(),
78
- hoistedScript: void 0
79
- }
80
- ];
81
- }
45
+ allPages[route.component] = {
46
+ component: route.component,
47
+ route,
48
+ moduleSpecifier: "",
49
+ styles: [],
50
+ propagatedStyles: /* @__PURE__ */ new Map(),
51
+ propagatedScripts: /* @__PURE__ */ new Map(),
52
+ hoistedScript: void 0
53
+ };
82
54
  }
83
55
  clearInterval(dataCollectionLogTimeout);
84
56
  return { assets, allPages };
@@ -39,14 +39,12 @@ async function viteBuild(opts) {
39
39
  settings.timer.start("SSR build");
40
40
  const pageInput = /* @__PURE__ */ new Set();
41
41
  const internals = createBuildInternals();
42
- for (const [component, pageDataList] of Object.entries(allPages)) {
43
- for (const pageData of pageDataList) {
44
- const astroModuleURL = new URL("./" + component, settings.config.root);
45
- const astroModuleId = prependForwardSlash(component);
46
- trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
47
- if (!routeIsRedirect(pageData.route)) {
48
- pageInput.add(astroModuleId);
49
- }
42
+ for (const [component, pageData] of Object.entries(allPages)) {
43
+ const astroModuleURL = new URL("./" + component, settings.config.root);
44
+ const astroModuleId = prependForwardSlash(component);
45
+ trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
46
+ if (!routeIsRedirect(pageData.route)) {
47
+ pageInput.add(astroModuleId);
50
48
  }
51
49
  }
52
50
  if (settings.config?.vite?.build?.emptyOutDir !== false) {
@@ -108,7 +106,7 @@ async function ssrBuild(opts, internals, input, container) {
108
106
  const { allPages, settings, viteConfig } = opts;
109
107
  const ssr = isServerLikeOutput(settings.config);
110
108
  const out = getOutputDirectory(settings.config);
111
- const routes = Object.values(allPages).flat().map((pageData) => pageData.route);
109
+ const routes = Object.values(allPages).flatMap((pageData) => pageData.route);
112
110
  const isContentCache = !ssr && settings.config.experimental.contentCollectionCache;
113
111
  const { lastVitePlugins, vitePlugins } = await container.runBeforeHook("server", input);
114
112
  const viteBuildConfig = {
@@ -29,7 +29,7 @@ export interface PageBuildData {
29
29
  sheet: StylesheetAsset;
30
30
  }>;
31
31
  }
32
- export type AllPagesData = Record<ComponentPath, PageBuildData[]>;
32
+ export type AllPagesData = Record<ComponentPath, PageBuildData>;
33
33
  /** Options for the static build */
34
34
  export interface StaticBuildOptions {
35
35
  allPages: AllPagesData;
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "3.5.5";
1
+ const ASTRO_VERSION = "3.5.7";
2
2
  const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
3
3
  ".markdown",
4
4
  ".mdown",
@@ -20,7 +20,7 @@ async function dev(inlineConfig) {
20
20
  base: restart.container.settings.config.base
21
21
  })
22
22
  );
23
- const currentVersion = "3.5.5";
23
+ const currentVersion = "3.5.7";
24
24
  if (currentVersion.includes("-")) {
25
25
  logger.warn(null, msg.prerelease({ currentVersion }));
26
26
  }
@@ -8,16 +8,18 @@ type CreateAPIContext = {
8
8
  props: Record<string, any>;
9
9
  adapterName?: string;
10
10
  locales: string[] | undefined;
11
+ routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
12
+ defaultLocale: string | undefined;
11
13
  };
12
14
  /**
13
15
  * Creates a context that holds all the information needed to handle an Astro endpoint.
14
16
  *
15
17
  * @param {CreateAPIContext} payload
16
18
  */
17
- export declare function createAPIContext({ request, params, site, props, adapterName, locales, }: CreateAPIContext): APIContext;
19
+ export declare function createAPIContext({ request, params, site, props, adapterName, locales, routingStrategy, defaultLocale, }: CreateAPIContext): APIContext;
18
20
  type ResponseParameters = ConstructorParameters<typeof Response>;
19
21
  export declare class ResponseWithEncoding extends Response {
20
22
  constructor(body: ResponseParameters[0], init: ResponseParameters[1], encoding?: BufferEncoding);
21
23
  }
22
- export declare function callEndpoint<MiddlewareResult = Response | EndpointOutput>(mod: EndpointHandler, env: Environment, ctx: RenderContext, onRequest: MiddlewareHandler<MiddlewareResult> | undefined, locales: undefined | string[]): Promise<Response>;
24
+ export declare function callEndpoint<MiddlewareResult = Response | EndpointOutput>(mod: EndpointHandler, env: Environment, ctx: RenderContext, onRequest: MiddlewareHandler<MiddlewareResult> | undefined): Promise<Response>;
23
25
  export {};
@@ -4,7 +4,11 @@ import { ASTRO_VERSION } from "../constants.js";
4
4
  import { AstroCookies, attachCookiesToResponse } from "../cookies/index.js";
5
5
  import { AstroError, AstroErrorData } from "../errors/index.js";
6
6
  import { callMiddleware } from "../middleware/callMiddleware.js";
7
- import { computePreferredLocale, computePreferredLocaleList } from "../render/context.js";
7
+ import {
8
+ computeCurrentLocale,
9
+ computePreferredLocale,
10
+ computePreferredLocaleList
11
+ } from "../render/context.js";
8
12
  import {} from "../render/index.js";
9
13
  const encoder = new TextEncoder();
10
14
  const clientAddressSymbol = Symbol.for("astro.clientAddress");
@@ -15,10 +19,13 @@ function createAPIContext({
15
19
  site,
16
20
  props,
17
21
  adapterName,
18
- locales
22
+ locales,
23
+ routingStrategy,
24
+ defaultLocale
19
25
  }) {
20
26
  let preferredLocale = void 0;
21
27
  let preferredLocaleList = void 0;
28
+ let currentLocale = void 0;
22
29
  const context = {
23
30
  cookies: new AstroCookies(request),
24
31
  request,
@@ -55,6 +62,15 @@ function createAPIContext({
55
62
  }
56
63
  return void 0;
57
64
  },
65
+ get currentLocale() {
66
+ if (currentLocale) {
67
+ return currentLocale;
68
+ }
69
+ if (locales) {
70
+ currentLocale = computeCurrentLocale(request, locales, routingStrategy, defaultLocale);
71
+ }
72
+ return currentLocale;
73
+ },
58
74
  url: new URL(request.url),
59
75
  get clientAddress() {
60
76
  if (clientAddressSymbol in request) {
@@ -106,14 +122,16 @@ class ResponseWithEncoding extends Response {
106
122
  }
107
123
  }
108
124
  }
109
- async function callEndpoint(mod, env, ctx, onRequest, locales) {
125
+ async function callEndpoint(mod, env, ctx, onRequest) {
110
126
  const context = createAPIContext({
111
127
  request: ctx.request,
112
128
  params: ctx.params,
113
129
  props: ctx.props,
114
130
  site: env.site,
115
131
  adapterName: env.adapterName,
116
- locales
132
+ routingStrategy: ctx.routingStrategy,
133
+ defaultLocale: ctx.defaultLocale,
134
+ locales: ctx.locales
117
135
  });
118
136
  let response;
119
137
  if (onRequest) {
@@ -50,7 +50,7 @@ function serverStart({
50
50
  base,
51
51
  isRestart = false
52
52
  }) {
53
- const version = "3.5.5";
53
+ const version = "3.5.7";
54
54
  const localPrefix = `${dim("\u2503")} Local `;
55
55
  const networkPrefix = `${dim("\u2503")} Network `;
56
56
  const emptyPrefix = " ".repeat(11);
@@ -235,7 +235,7 @@ function printHelp({
235
235
  message.push(
236
236
  linebreak(),
237
237
  ` ${bgGreen(black(` ${commandName} `))} ${green(
238
- `v${"3.5.5"}`
238
+ `v${"3.5.7"}`
239
239
  )} ${headline}`
240
240
  );
241
241
  }
@@ -9,7 +9,9 @@ function createContext({ request, params, userDefinedLocales = [] }) {
9
9
  params: params ?? {},
10
10
  props: {},
11
11
  site: void 0,
12
- locales: userDefinedLocales
12
+ locales: userDefinedLocales,
13
+ defaultLocale: void 0,
14
+ routingStrategy: void 0
13
15
  });
14
16
  }
15
17
  function isLocalsSerializable(value) {
@@ -89,7 +89,9 @@ class Pipeline {
89
89
  props: renderContext.props,
90
90
  site: env.site,
91
91
  adapterName: env.adapterName,
92
- locales: renderContext.locales
92
+ locales: renderContext.locales,
93
+ routingStrategy: renderContext.routingStrategy,
94
+ defaultLocale: renderContext.defaultLocale
93
95
  });
94
96
  switch (renderContext.route.type) {
95
97
  case "page":
@@ -119,13 +121,7 @@ class Pipeline {
119
121
  }
120
122
  }
121
123
  case "endpoint": {
122
- return await callEndpoint(
123
- mod,
124
- env,
125
- renderContext,
126
- onRequest,
127
- renderContext.locales
128
- );
124
+ return await callEndpoint(mod, env, renderContext, onRequest);
129
125
  }
130
126
  default:
131
127
  throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
@@ -16,6 +16,8 @@ export interface RenderContext {
16
16
  props: Props;
17
17
  locals?: object;
18
18
  locales: string[] | undefined;
19
+ defaultLocale: string | undefined;
20
+ routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
19
21
  }
20
22
  export type CreateRenderContextArgs = Partial<Omit<RenderContext, 'params' | 'props' | 'locals'>> & {
21
23
  route: RouteData;
@@ -45,4 +47,5 @@ export declare function parseLocale(header: string): BrowserLocale[];
45
47
  */
46
48
  export declare function computePreferredLocale(request: Request, locales: string[]): string | undefined;
47
49
  export declare function computePreferredLocaleList(request: Request, locales: string[]): string[];
50
+ export declare function computeCurrentLocale(request: Request, locales: string[], routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined, defaultLocale: string | undefined): undefined | string;
48
51
  export {};
@@ -18,7 +18,9 @@ async function createRenderContext(options) {
18
18
  pathname,
19
19
  params,
20
20
  props,
21
- locales: options.locales
21
+ locales: options.locales,
22
+ routingStrategy: options.routingStrategy,
23
+ defaultLocale: options.defaultLocale
22
24
  };
23
25
  Object.defineProperty(context, "locals", {
24
26
  enumerable: true,
@@ -124,7 +126,22 @@ function computePreferredLocaleList(request, locales) {
124
126
  }
125
127
  return result;
126
128
  }
129
+ function computeCurrentLocale(request, locales, routingStrategy, defaultLocale) {
130
+ const requestUrl = new URL(request.url);
131
+ for (const segment of requestUrl.pathname.split("/")) {
132
+ for (const locale of locales) {
133
+ if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
134
+ return locale;
135
+ }
136
+ }
137
+ }
138
+ if (routingStrategy === "prefix-other-locales") {
139
+ return defaultLocale;
140
+ }
141
+ return void 0;
142
+ }
127
143
  export {
144
+ computeCurrentLocale,
128
145
  computePreferredLocale,
129
146
  computePreferredLocaleList,
130
147
  createRenderContext,
@@ -43,7 +43,9 @@ async function renderPage({ mod, renderContext, env, cookies }) {
43
43
  status: renderContext.status ?? 200,
44
44
  cookies,
45
45
  locals: renderContext.locals ?? {},
46
- locales: renderContext.locales
46
+ locales: renderContext.locales,
47
+ defaultLocale: renderContext.defaultLocale,
48
+ routingStrategy: renderContext.routingStrategy
47
49
  });
48
50
  if (mod.frontmatter && typeof mod.frontmatter === "object" && "draft" in mod.frontmatter) {
49
51
  env.logger.warn(
@@ -31,5 +31,7 @@ export interface CreateResultArgs {
31
31
  locals: App.Locals;
32
32
  cookies?: AstroCookies;
33
33
  locales: string[] | undefined;
34
+ defaultLocale: string | undefined;
35
+ routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
34
36
  }
35
37
  export declare function createResult(args: CreateResultArgs): SSRResult;
@@ -3,7 +3,11 @@ import { renderJSX } from "../../runtime/server/jsx.js";
3
3
  import { chunkToString } from "../../runtime/server/render/index.js";
4
4
  import { AstroCookies } from "../cookies/index.js";
5
5
  import { AstroError, AstroErrorData } from "../errors/index.js";
6
- import { computePreferredLocale, computePreferredLocaleList } from "./context.js";
6
+ import {
7
+ computeCurrentLocale,
8
+ computePreferredLocale,
9
+ computePreferredLocaleList
10
+ } from "./context.js";
7
11
  const clientAddressSymbol = Symbol.for("astro.clientAddress");
8
12
  const responseSentSymbol = Symbol.for("astro.responseSent");
9
13
  function getFunctionExpression(slot) {
@@ -91,6 +95,7 @@ function createResult(args) {
91
95
  let cookies = args.cookies;
92
96
  let preferredLocale = void 0;
93
97
  let preferredLocaleList = void 0;
98
+ let currentLocale = void 0;
94
99
  const result = {
95
100
  styles: args.styles ?? /* @__PURE__ */ new Set(),
96
101
  scripts: args.scripts ?? /* @__PURE__ */ new Set(),
@@ -149,6 +154,23 @@ function createResult(args) {
149
154
  }
150
155
  return void 0;
151
156
  },
157
+ get currentLocale() {
158
+ if (currentLocale) {
159
+ return currentLocale;
160
+ }
161
+ if (args.locales) {
162
+ currentLocale = computeCurrentLocale(
163
+ request,
164
+ args.locales,
165
+ args.routingStrategy,
166
+ args.defaultLocale
167
+ );
168
+ if (currentLocale) {
169
+ return currentLocale;
170
+ }
171
+ }
172
+ return void 0;
173
+ },
152
174
  params,
153
175
  props,
154
176
  locals,
@@ -236,7 +236,8 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
236
236
  component,
237
237
  generate,
238
238
  pathname: pathname || void 0,
239
- prerender
239
+ prerender,
240
+ fallbackRoutes: []
240
241
  });
241
242
  }
242
243
  });
@@ -290,7 +291,8 @@ This route collides with: "${collision.component}".`
290
291
  component,
291
292
  generate,
292
293
  pathname: pathname || void 0,
293
- prerender: prerenderInjected ?? prerender
294
+ prerender: prerenderInjected ?? prerender,
295
+ fallbackRoutes: []
294
296
  });
295
297
  });
296
298
  Object.entries(settings.config.redirects).forEach(([from, to]) => {
@@ -315,7 +317,8 @@ This route collides with: "${collision.component}".`
315
317
  pathname: pathname || void 0,
316
318
  prerender: false,
317
319
  redirect: to,
318
- redirectRoute: routes.find((r) => r.route === to)
320
+ redirectRoute: routes.find((r) => r.route === to),
321
+ fallbackRoutes: []
319
322
  };
320
323
  const lastSegmentIsDynamic = (r) => !!r.segments.at(-1)?.at(-1)?.dynamic;
321
324
  const redirBase = path.posix.dirname(route);
@@ -433,14 +436,20 @@ This route collides with: "${collision.component}".`
433
436
  validateSegment(s);
434
437
  return getParts(s, route);
435
438
  });
436
- routes.push({
437
- ...fallbackToRoute,
438
- pathname,
439
- route,
440
- segments,
441
- pattern: getPattern(segments, config, config.trailingSlash),
442
- type: "fallback"
443
- });
439
+ const index = routes.findIndex((r) => r === fallbackToRoute);
440
+ if (index) {
441
+ const fallbackRoute = {
442
+ ...fallbackToRoute,
443
+ pathname,
444
+ route,
445
+ segments,
446
+ pattern: getPattern(segments, config, config.trailingSlash),
447
+ type: "fallback",
448
+ fallbackRoutes: []
449
+ };
450
+ const routeData = routes[index];
451
+ routeData.fallbackRoutes.push(fallbackRoute);
452
+ }
444
453
  }
445
454
  }
446
455
  }
@@ -5,6 +5,9 @@ function serializeRouteData(routeData, trailingSlash) {
5
5
  generate: void 0,
6
6
  pattern: routeData.pattern.source,
7
7
  redirectRoute: routeData.redirectRoute ? serializeRouteData(routeData.redirectRoute, trailingSlash) : void 0,
8
+ fallbackRoutes: routeData.fallbackRoutes.map((fallbackRoute) => {
9
+ return serializeRouteData(fallbackRoute, trailingSlash);
10
+ }),
8
11
  _meta: { trailingSlash }
9
12
  };
10
13
  }
@@ -20,7 +23,10 @@ function deserializeRouteData(rawRouteData) {
20
23
  segments: rawRouteData.segments,
21
24
  prerender: rawRouteData.prerender,
22
25
  redirect: rawRouteData.redirect,
23
- redirectRoute: rawRouteData.redirectRoute ? deserializeRouteData(rawRouteData.redirectRoute) : void 0
26
+ redirectRoute: rawRouteData.redirectRoute ? deserializeRouteData(rawRouteData.redirectRoute) : void 0,
27
+ fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
28
+ return deserializeRouteData(fallback);
29
+ })
24
30
  };
25
31
  }
26
32
  export {
@@ -1,5 +1,8 @@
1
1
  function matchRoute(pathname, manifest) {
2
- return manifest.routes.find((route) => route.pattern.test(decodeURI(pathname)));
2
+ const decodedPathname = decodeURI(pathname);
3
+ return manifest.routes.find((route) => {
4
+ return route.pattern.test(decodedPathname) || route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(decodedPathname));
5
+ });
3
6
  }
4
7
  function matchAllRoutes(pathname, manifest) {
5
8
  return manifest.routes.filter((route) => route.pattern.test(pathname));
@@ -139,14 +139,18 @@ async function handleRoute({
139
139
  prerender: false,
140
140
  segments: [],
141
141
  type: "fallback",
142
- route: ""
142
+ route: "",
143
+ fallbackRoutes: []
143
144
  };
144
145
  renderContext = await createRenderContext({
145
146
  request,
146
147
  pathname,
147
148
  env,
148
149
  mod,
149
- route
150
+ route,
151
+ locales: manifest.i18n?.locales,
152
+ routingStrategy: manifest.i18n?.routingStrategy,
153
+ defaultLocale: manifest.i18n?.defaultLocale
150
154
  });
151
155
  } else {
152
156
  return handle404Response(origin, incomingRequest, incomingResponse);
@@ -197,7 +201,9 @@ async function handleRoute({
197
201
  route: options.route,
198
202
  mod,
199
203
  env,
200
- locales: i18n ? i18n.locales : void 0
204
+ locales: i18n?.locales,
205
+ routingStrategy: i18n?.routingStrategy,
206
+ defaultLocale: i18n?.defaultLocale
201
207
  });
202
208
  }
203
209
  const onRequest = middleware?.onRequest;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "3.5.5",
3
+ "version": "3.5.7",
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",