astro 4.1.3 → 4.2.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.
Files changed (46) hide show
  1. package/dist/@types/astro.d.ts +101 -0
  2. package/dist/cli/preferences/index.js +1 -4
  3. package/dist/content/types-generator.js +0 -26
  4. package/dist/content/utils.d.ts +1 -1
  5. package/dist/content/utils.js +3 -10
  6. package/dist/core/app/createOutgoingHttpHeaders.d.ts +9 -0
  7. package/dist/core/app/createOutgoingHttpHeaders.js +19 -0
  8. package/dist/core/app/index.d.ts +38 -1
  9. package/dist/core/app/index.js +38 -8
  10. package/dist/core/app/node.d.ts +42 -10
  11. package/dist/core/app/node.js +87 -46
  12. package/dist/core/app/types.d.ts +2 -1
  13. package/dist/core/build/generate.js +1 -2
  14. package/dist/core/config/schema.d.ts +146 -66
  15. package/dist/core/config/schema.js +24 -6
  16. package/dist/core/config/vite-load.js +1 -1
  17. package/dist/core/constants.js +1 -1
  18. package/dist/core/dev/dev.js +1 -1
  19. package/dist/core/endpoint/index.d.ts +2 -1
  20. package/dist/core/errors/dev/vite.js +5 -11
  21. package/dist/core/errors/overlay.js +2 -2
  22. package/dist/core/logger/node.js +1 -1
  23. package/dist/core/logger/vite.d.ts +0 -1
  24. package/dist/core/logger/vite.js +1 -2
  25. package/dist/core/messages.js +2 -2
  26. package/dist/core/render/context.d.ts +3 -2
  27. package/dist/core/render/context.js +1 -1
  28. package/dist/core/render/result.d.ts +2 -1
  29. package/dist/core/routing/manifest/create.d.ts +1 -1
  30. package/dist/core/routing/manifest/create.js +166 -116
  31. package/dist/core/sync/index.js +1 -1
  32. package/dist/i18n/index.d.ts +3 -2
  33. package/dist/i18n/index.js +4 -4
  34. package/dist/i18n/middleware.js +35 -19
  35. package/dist/prefetch/index.js +17 -1
  36. package/dist/prefetch/vite-plugin-prefetch.js +4 -1
  37. package/dist/runtime/client/dev-toolbar/apps/audit/a11y.js +52 -4
  38. package/dist/runtime/server/render/util.js +1 -1
  39. package/dist/vite-plugin-astro/compile.js +0 -4
  40. package/dist/vite-plugin-astro/hmr.d.ts +3 -2
  41. package/dist/vite-plugin-astro/hmr.js +20 -41
  42. package/dist/vite-plugin-astro/index.js +24 -1
  43. package/dist/vite-plugin-astro/query.d.ts +0 -1
  44. package/dist/vite-plugin-astro/query.js +0 -5
  45. package/dist/vite-plugin-markdown/images.js +46 -19
  46. package/package.json +6 -5
@@ -70,10 +70,6 @@ function getTrailingSlashPattern(addTrailingSlash) {
70
70
  }
71
71
  return "\\/?$";
72
72
  }
73
- function isSpread(str) {
74
- const spreadPattern = /\[\.{3}/g;
75
- return spreadPattern.test(str);
76
- }
77
73
  function validateSegment(segment, file = "") {
78
74
  if (!file)
79
75
  file = segment;
@@ -87,59 +83,50 @@ function validateSegment(segment, file = "") {
87
83
  throw new Error(`Invalid route ${file} \u2014 rest parameter must be a standalone segment`);
88
84
  }
89
85
  }
90
- function comparator(a, b) {
91
- if (a.isIndex !== b.isIndex) {
92
- if (a.isIndex)
93
- return isSpread(a.file) ? 1 : -1;
94
- return isSpread(b.file) ? -1 : 1;
86
+ function isSemanticallyEqualSegment(segmentA, segmentB) {
87
+ if (segmentA.length !== segmentB.length) {
88
+ return false;
95
89
  }
96
- const max = Math.max(a.parts.length, b.parts.length);
97
- for (let i = 0; i < max; i += 1) {
98
- const aSubPart = a.parts[i];
99
- const bSubPart = b.parts[i];
100
- if (!aSubPart)
101
- return 1;
102
- if (!bSubPart)
103
- return -1;
104
- if (aSubPart.spread && bSubPart.spread) {
105
- return a.isIndex ? 1 : -1;
106
- }
107
- if (aSubPart.spread !== bSubPart.spread)
108
- return aSubPart.spread ? 1 : -1;
109
- if (aSubPart.dynamic !== bSubPart.dynamic) {
110
- return aSubPart.dynamic ? 1 : -1;
90
+ for (const [index, partA] of segmentA.entries()) {
91
+ const partB = segmentB[index];
92
+ if (partA.dynamic !== partB.dynamic || partA.spread !== partB.spread) {
93
+ return false;
111
94
  }
112
- if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
113
- return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1);
95
+ if (!partA.dynamic && partA.content !== partB.content) {
96
+ return false;
114
97
  }
115
98
  }
116
- if (a.isPage !== b.isPage) {
117
- return a.isPage ? 1 : -1;
118
- }
119
- return a.file < b.file ? -1 : 1;
99
+ return true;
120
100
  }
121
- function injectedRouteToItem({ config, cwd }, { pattern, entrypoint }) {
122
- let resolved;
123
- try {
124
- resolved = require2.resolve(entrypoint, { paths: [cwd || fileURLToPath(config.root)] });
125
- } catch (e) {
126
- resolved = fileURLToPath(new URL(entrypoint, config.root));
101
+ function routeComparator(a, b) {
102
+ const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
103
+ const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
104
+ if (aLength !== bLength) {
105
+ return aLength > bLength ? -1 : 1;
127
106
  }
128
- const ext = path.extname(pattern);
129
- const type = resolved.endsWith(".astro") ? "page" : "endpoint";
130
- const isPage = type === "page";
131
- return {
132
- basename: pattern,
133
- ext,
134
- parts: getParts(pattern, resolved),
135
- file: resolved,
136
- isDir: false,
137
- isIndex: true,
138
- isPage,
139
- routeSuffix: pattern.slice(pattern.indexOf("."), -ext.length)
140
- };
107
+ const aIsStatic = a.segments.every(
108
+ (segment) => segment.every((part) => !part.dynamic && !part.spread)
109
+ );
110
+ const bIsStatic = b.segments.every(
111
+ (segment) => segment.every((part) => !part.dynamic && !part.spread)
112
+ );
113
+ if (aIsStatic !== bIsStatic) {
114
+ return aIsStatic ? -1 : 1;
115
+ }
116
+ const aHasSpread = a.segments.some((segment) => segment.some((part) => part.spread));
117
+ const bHasSpread = b.segments.some((segment) => segment.some((part) => part.spread));
118
+ if (aHasSpread !== bHasSpread) {
119
+ return aHasSpread ? 1 : -1;
120
+ }
121
+ if (a.prerender !== b.prerender) {
122
+ return a.prerender ? -1 : 1;
123
+ }
124
+ if (a.type === "endpoint" !== (b.type === "endpoint")) {
125
+ return a.type === "endpoint" ? -1 : 1;
126
+ }
127
+ return a.route.localeCompare(b.route);
141
128
  }
142
- function createRouteManifest({ settings, cwd, fsMod }, logger) {
129
+ function createFileBasedRoutes({ settings, cwd, fsMod }, logger) {
143
130
  const components = [];
144
131
  const routes = [];
145
132
  const validPageExtensions = /* @__PURE__ */ new Set([
@@ -152,17 +139,18 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
152
139
  const prerender = getPrerenderDefault(settings.config);
153
140
  function walk(fs, dir, parentSegments, parentParams) {
154
141
  let items = [];
155
- fs.readdirSync(dir).forEach((basename) => {
142
+ const files = fs.readdirSync(dir);
143
+ for (const basename of files) {
156
144
  const resolved = path.join(dir, basename);
157
145
  const file = slash(path.relative(cwd || fileURLToPath(settings.config.root), resolved));
158
146
  const isDir = fs.statSync(resolved).isDirectory();
159
147
  const ext = path.extname(basename);
160
148
  const name = ext ? basename.slice(0, -ext.length) : basename;
161
149
  if (name[0] === "_") {
162
- return;
150
+ continue;
163
151
  }
164
152
  if (basename[0] === "." && basename !== ".well-known") {
165
- return;
153
+ continue;
166
154
  }
167
155
  if (!isDir && !validPageExtensions.has(ext) && !validEndpointExtensions.has(ext)) {
168
156
  logger.warn(
@@ -171,7 +159,7 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
171
159
  resolved
172
160
  )} found. Prefix filename with an underscore (\`_\`) to ignore.`
173
161
  );
174
- return;
162
+ continue;
175
163
  }
176
164
  const segment = isDir ? basename : name;
177
165
  validateSegment(segment, file);
@@ -189,9 +177,8 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
189
177
  isPage,
190
178
  routeSuffix
191
179
  });
192
- });
193
- items = items.sort(comparator);
194
- items.forEach((item) => {
180
+ }
181
+ for (const item of items) {
195
182
  const segments = parentSegments.slice();
196
183
  if (item.isIndex) {
197
184
  if (item.routeSuffix) {
@@ -233,6 +220,7 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
233
220
  const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
234
221
  routes.push({
235
222
  route,
223
+ isIndex: item.isIndex,
236
224
  type: item.isPage ? "page" : "endpoint",
237
225
  pattern,
238
226
  segments,
@@ -244,7 +232,7 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
244
232
  fallbackRoutes: []
245
233
  });
246
234
  }
247
- });
235
+ }
248
236
  }
249
237
  const { config } = settings;
250
238
  const pages = resolvePages(config);
@@ -254,12 +242,18 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
254
242
  const pagesDirRootRelative = pages.href.slice(settings.config.root.href.length);
255
243
  logger.warn(null, `Missing pages directory: ${pagesDirRootRelative}`);
256
244
  }
257
- settings.injectedRoutes?.sort(
258
- (a, b) => (
259
- // sort injected routes in the same way as user-defined routes
260
- comparator(injectedRouteToItem({ config, cwd }, a), injectedRouteToItem({ config, cwd }, b))
261
- )
262
- ).reverse().forEach(({ pattern: name, entrypoint, prerender: prerenderInjected }) => {
245
+ return routes;
246
+ }
247
+ function createInjectedRoutes({ settings, cwd }) {
248
+ const { config } = settings;
249
+ const prerender = getPrerenderDefault(config);
250
+ const routes = {
251
+ normal: [],
252
+ legacy: []
253
+ };
254
+ const priority = computeRoutePriority(config);
255
+ for (const injectedRoute of settings.injectedRoutes) {
256
+ const { pattern: name, entrypoint, prerender: prerenderInjected } = injectedRoute;
263
257
  let resolved;
264
258
  try {
265
259
  resolved = require2.resolve(entrypoint, { paths: [cwd || fileURLToPath(config.root)] });
@@ -279,15 +273,10 @@ function createRouteManifest({ settings, cwd, fsMod }, logger) {
279
273
  const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
280
274
  const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
281
275
  const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
282
- const collision = routes.find(({ route: r }) => r === route);
283
- if (collision) {
284
- throw new Error(
285
- `An integration attempted to inject a route that is already used in your project: "${route}" at "${component}".
286
- This route collides with: "${collision.component}".`
287
- );
288
- }
289
- routes.unshift({
276
+ routes[priority].push({
290
277
  type,
278
+ // For backwards compatibility, an injected route is never considered an index route.
279
+ isIndex: false,
291
280
  route,
292
281
  pattern,
293
282
  segments,
@@ -298,9 +287,18 @@ This route collides with: "${collision.component}".`
298
287
  prerender: prerenderInjected ?? prerender,
299
288
  fallbackRoutes: []
300
289
  });
301
- });
302
- Object.entries(settings.config.redirects).forEach(([from, to]) => {
303
- const trailingSlash = config.trailingSlash;
290
+ }
291
+ return routes;
292
+ }
293
+ function createRedirectRoutes({ settings }, routeMap, logger) {
294
+ const { config } = settings;
295
+ const trailingSlash = config.trailingSlash;
296
+ const routes = {
297
+ normal: [],
298
+ legacy: []
299
+ };
300
+ const priority = computeRoutePriority(settings.config);
301
+ for (const [from, to] of Object.entries(settings.config.redirects)) {
304
302
  const segments = removeLeadingForwardSlash(from).split(path.posix.sep).filter(Boolean).map((s) => {
305
303
  validateSegment(s);
306
304
  return getParts(s, from);
@@ -310,22 +308,22 @@ This route collides with: "${collision.component}".`
310
308
  const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
311
309
  const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
312
310
  const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
313
- {
314
- let destination;
315
- if (typeof to === "string") {
316
- destination = to;
317
- } else {
318
- destination = to.destination;
319
- }
320
- if (/^https?:\/\//.test(destination)) {
321
- logger.warn(
322
- "redirects",
323
- `Redirecting to an external URL is not officially supported: ${from} -> ${destination}`
324
- );
325
- }
311
+ let destination;
312
+ if (typeof to === "string") {
313
+ destination = to;
314
+ } else {
315
+ destination = to.destination;
316
+ }
317
+ if (/^https?:\/\//.test(destination)) {
318
+ logger.warn(
319
+ "redirects",
320
+ `Redirecting to an external URL is not officially supported: ${from} -> ${destination}`
321
+ );
326
322
  }
327
- const routeData = {
323
+ routes[priority].push({
328
324
  type: "redirect",
325
+ // For backwards compatibility, a redirect is never considered an index route.
326
+ isIndex: false,
329
327
  route,
330
328
  pattern,
331
329
  segments,
@@ -335,36 +333,82 @@ This route collides with: "${collision.component}".`
335
333
  pathname: pathname || void 0,
336
334
  prerender: false,
337
335
  redirect: to,
338
- redirectRoute: routes.find((r) => {
339
- if (typeof to === "object") {
340
- return r.route === to.destination;
341
- } else {
342
- return r.route === to;
343
- }
344
- }),
336
+ redirectRoute: routeMap.get(destination),
345
337
  fallbackRoutes: []
346
- };
347
- const lastSegmentIsDynamic = (r) => !!r.segments.at(-1)?.at(-1)?.dynamic;
348
- const redirBase = path.posix.dirname(route);
349
- const dynamicRedir = lastSegmentIsDynamic(routeData);
350
- let i = 0;
351
- for (const existingRoute of routes) {
352
- if (existingRoute.route === route) {
353
- routes.splice(i + 1, 0, routeData);
354
- return;
355
- }
356
- const base = path.posix.dirname(existingRoute.route);
357
- if (base === redirBase && !dynamicRedir && lastSegmentIsDynamic(existingRoute)) {
358
- routes.splice(i, 0, routeData);
359
- return;
338
+ });
339
+ }
340
+ return routes;
341
+ }
342
+ function isStaticSegment(segment) {
343
+ return segment.every((part) => !part.dynamic && !part.spread);
344
+ }
345
+ function detectRouteCollision(a, b, config, logger) {
346
+ if (a.type === "fallback" || b.type === "fallback") {
347
+ return;
348
+ }
349
+ if (a.route === b.route && a.segments.every(isStaticSegment) && b.segments.every(isStaticSegment)) {
350
+ logger.warn(
351
+ "router",
352
+ `The route "${a.route}" is defined in both "${a.component}" and "${b.component}". A static route cannot be defined more than once.`
353
+ );
354
+ logger.warn(
355
+ "router",
356
+ "A collision will result in an hard error in following versions of Astro."
357
+ );
358
+ return;
359
+ }
360
+ if (a.prerender || b.prerender) {
361
+ return;
362
+ }
363
+ if (a.segments.length !== b.segments.length) {
364
+ return;
365
+ }
366
+ const segmentCount = a.segments.length;
367
+ for (let index = 0; index < segmentCount; index++) {
368
+ const segmentA = a.segments[index];
369
+ const segmentB = b.segments[index];
370
+ if (!isSemanticallyEqualSegment(segmentA, segmentB)) {
371
+ return;
372
+ }
373
+ }
374
+ logger.warn(
375
+ "router",
376
+ `The route "${a.route}" is defined in both "${a.component}" and "${b.component}" using SSR mode. A dynamic SSR route cannot be defined more than once.`
377
+ );
378
+ logger.warn("router", "A collision will result in an hard error in following versions of Astro.");
379
+ }
380
+ function createRouteManifest(params, logger) {
381
+ const { settings } = params;
382
+ const { config } = settings;
383
+ const routeMap = /* @__PURE__ */ new Map();
384
+ const fileBasedRoutes = createFileBasedRoutes(params, logger);
385
+ for (const route of fileBasedRoutes) {
386
+ routeMap.set(route.route, route);
387
+ }
388
+ const injectedRoutes = createInjectedRoutes(params);
389
+ for (const [, routes2] of Object.entries(injectedRoutes)) {
390
+ for (const route of routes2) {
391
+ routeMap.set(route.route, route);
392
+ }
393
+ }
394
+ const redirectRoutes = createRedirectRoutes(params, routeMap, logger);
395
+ const routes = [
396
+ ...injectedRoutes["legacy"].sort(routeComparator),
397
+ ...[...fileBasedRoutes, ...injectedRoutes["normal"], ...redirectRoutes["normal"]].sort(
398
+ routeComparator
399
+ ),
400
+ ...redirectRoutes["legacy"].sort(routeComparator)
401
+ ];
402
+ if (config.experimental.globalRoutePriority) {
403
+ for (const [index, higherRoute] of routes.entries()) {
404
+ for (const lowerRoute of routes.slice(index + 1)) {
405
+ detectRouteCollision(higherRoute, lowerRoute, config, logger);
360
406
  }
361
- i++;
362
407
  }
363
- routes.push(routeData);
364
- });
408
+ }
365
409
  const i18n = settings.config.i18n;
366
410
  if (i18n) {
367
- if (i18n.routing === "prefix-always") {
411
+ if (i18n.routing === "pathname-prefix-always") {
368
412
  let index = routes.find((route) => route.route === "/");
369
413
  if (!index) {
370
414
  let relativePath = path.relative(
@@ -415,7 +459,7 @@ This route collides with: "${collision.component}".`
415
459
  }
416
460
  setRoutes.delete(route);
417
461
  }
418
- if (i18n.routing === "prefix-always") {
462
+ if (i18n.routing === "pathname-prefix-always") {
419
463
  const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
420
464
  if (defaultLocaleRoutes) {
421
465
  const indexDefaultRoute = defaultLocaleRoutes.find((routeData) => {
@@ -465,7 +509,7 @@ This route collides with: "${collision.component}".`
465
509
  if (!hasRoute) {
466
510
  let pathname;
467
511
  let route;
468
- if (fallbackToLocale === i18n.defaultLocale && i18n.routing === "prefix-other-locales") {
512
+ if (fallbackToLocale === i18n.defaultLocale && i18n.routing === "pathname-prefix-other-locales") {
469
513
  if (fallbackToRoute.pathname) {
470
514
  pathname = `/${fallbackFromLocale}${fallbackToRoute.pathname}`;
471
515
  }
@@ -504,6 +548,12 @@ This route collides with: "${collision.component}".`
504
548
  routes
505
549
  };
506
550
  }
551
+ function computeRoutePriority(config) {
552
+ if (config.experimental.globalRoutePriority) {
553
+ return "normal";
554
+ }
555
+ return "legacy";
556
+ }
507
557
  export {
508
558
  createRouteManifest
509
559
  };
@@ -35,7 +35,7 @@ async function syncInternal(settings, { logger, fs }) {
35
35
  await createVite(
36
36
  {
37
37
  server: { middlewareMode: true, hmr: false, watch: null },
38
- optimizeDeps: { disabled: true },
38
+ optimizeDeps: { noDiscovery: true },
39
39
  ssr: { external: [] },
40
40
  logLevel: "silent"
41
41
  },
@@ -1,11 +1,12 @@
1
1
  import type { AstroConfig, Locales } from '../@types/astro.js';
2
+ import type { RoutingStrategies } from '../core/config/schema.js';
2
3
  type GetLocaleRelativeUrl = GetLocaleOptions & {
3
4
  locale: string;
4
5
  base: string;
5
6
  locales: Locales;
6
7
  trailingSlash: AstroConfig['trailingSlash'];
7
8
  format: AstroConfig['build']['format'];
8
- routing?: 'prefix-always' | 'prefix-other-locales';
9
+ routing?: RoutingStrategies;
9
10
  defaultLocale: string;
10
11
  };
11
12
  export type GetLocaleOptions = {
@@ -39,7 +40,7 @@ type GetLocalesBaseUrl = GetLocaleOptions & {
39
40
  locales: Locales;
40
41
  trailingSlash: AstroConfig['trailingSlash'];
41
42
  format: AstroConfig['build']['format'];
42
- routing?: 'prefix-always' | 'prefix-other-locales';
43
+ routing?: RoutingStrategies;
43
44
  defaultLocale: string;
44
45
  };
45
46
  export declare function getLocaleRelativeUrlList({ base, locales: _locales, trailingSlash, format, path, prependWith, normalizeLocale, routing, defaultLocale, }: GetLocalesBaseUrl): string[];
@@ -11,7 +11,7 @@ function getLocaleRelativeUrl({
11
11
  path,
12
12
  prependWith,
13
13
  normalizeLocale = true,
14
- routing = "prefix-other-locales",
14
+ routing = "pathname-prefix-other-locales",
15
15
  defaultLocale
16
16
  }) {
17
17
  const codeToUse = peekCodePathToUse(_locales, locale);
@@ -23,7 +23,7 @@ function getLocaleRelativeUrl({
23
23
  }
24
24
  const pathsToJoin = [base, prependWith];
25
25
  const normalizedLocale = normalizeLocale ? normalizeTheLocale(codeToUse) : codeToUse;
26
- if (routing === "prefix-always") {
26
+ if (routing === "pathname-prefix-always") {
27
27
  pathsToJoin.push(normalizedLocale);
28
28
  } else if (locale !== defaultLocale) {
29
29
  pathsToJoin.push(normalizedLocale);
@@ -51,14 +51,14 @@ function getLocaleRelativeUrlList({
51
51
  path,
52
52
  prependWith,
53
53
  normalizeLocale = false,
54
- routing = "prefix-other-locales",
54
+ routing = "pathname-prefix-other-locales",
55
55
  defaultLocale
56
56
  }) {
57
57
  const locales = toPaths(_locales);
58
58
  return locales.map((locale) => {
59
59
  const pathsToJoin = [base, prependWith];
60
60
  const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;
61
- if (routing === "prefix-always") {
61
+ if (routing === "pathname-prefix-always") {
62
62
  pathsToJoin.push(normalizedLocale);
63
63
  } else if (locale !== defaultLocale) {
64
64
  pathsToJoin.push(normalizedLocale);
@@ -35,25 +35,41 @@ function createI18nMiddleware(i18n, base, trailingSlash) {
35
35
  const response = await next();
36
36
  if (response instanceof Response) {
37
37
  const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`);
38
- if (i18n.routing === "prefix-other-locales" && pathnameContainsDefaultLocale) {
39
- const newLocation = url.pathname.replace(`/${defaultLocale}`, "");
40
- response.headers.set("Location", newLocation);
41
- return new Response(null, {
42
- status: 404,
43
- headers: response.headers
44
- });
45
- } else if (i18n.routing === "prefix-always") {
46
- if (url.pathname === base + "/" || url.pathname === base) {
47
- if (trailingSlash === "always") {
48
- return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
49
- } else {
50
- return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
38
+ switch (i18n.routing) {
39
+ case "pathname-prefix-other-locales": {
40
+ if (pathnameContainsDefaultLocale) {
41
+ const newLocation = url.pathname.replace(`/${defaultLocale}`, "");
42
+ response.headers.set("Location", newLocation);
43
+ return new Response(null, {
44
+ status: 404,
45
+ headers: response.headers
46
+ });
47
+ }
48
+ break;
49
+ }
50
+ case "pathname-prefix-always-no-redirect": {
51
+ const isRoot = url.pathname === base + "/" || url.pathname === base;
52
+ if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) {
53
+ return new Response(null, {
54
+ status: 404,
55
+ headers: response.headers
56
+ });
57
+ }
58
+ break;
59
+ }
60
+ case "pathname-prefix-always": {
61
+ if (url.pathname === base + "/" || url.pathname === base) {
62
+ if (trailingSlash === "always") {
63
+ return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
64
+ } else {
65
+ return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
66
+ }
67
+ } else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
68
+ return new Response(null, {
69
+ status: 404,
70
+ headers: response.headers
71
+ });
51
72
  }
52
- } else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
53
- return new Response(null, {
54
- status: 404,
55
- headers: response.headers
56
- });
57
73
  }
58
74
  }
59
75
  if (response.status >= 300 && fallback) {
@@ -75,7 +91,7 @@ function createI18nMiddleware(i18n, base, trailingSlash) {
75
91
  const fallbackLocale = fallback[urlLocale];
76
92
  const pathFallbackLocale = getPathByLocale(fallbackLocale, locales);
77
93
  let newPathname;
78
- if (pathFallbackLocale === defaultLocale && routing === "prefix-other-locales") {
94
+ if (pathFallbackLocale === defaultLocale && routing === "pathname-prefix-other-locales") {
79
95
  newPathname = url.pathname.replace(`/${urlLocale}`, ``);
80
96
  } else {
81
97
  newPathname = url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
@@ -4,6 +4,7 @@ const prefetchedUrls = /* @__PURE__ */ new Set();
4
4
  const listenedAnchors = /* @__PURE__ */ new WeakSet();
5
5
  let prefetchAll = __PREFETCH_PREFETCH_ALL__;
6
6
  let defaultStrategy = __PREFETCH_DEFAULT_STRATEGY__;
7
+ let clientPrerender = __EXPERIMENTAL_CLIENT_PRERENDER__;
7
8
  let inited = false;
8
9
  function init(defaultOpts) {
9
10
  if (!inBrowser)
@@ -128,7 +129,9 @@ function prefetch(url, opts) {
128
129
  prefetchedUrls.add(url);
129
130
  const priority = opts?.with ?? "link";
130
131
  debug?.(`[astro] Prefetching ${url} with ${priority}`);
131
- if (priority === "link") {
132
+ if (clientPrerender && HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules")) {
133
+ appendSpeculationRules(url);
134
+ } else if (priority === "link") {
132
135
  const link = document.createElement("link");
133
136
  link.rel = "prefetch";
134
137
  link.setAttribute("href", url);
@@ -188,6 +191,19 @@ function onPageLoad(cb) {
188
191
  cb();
189
192
  });
190
193
  }
194
+ function appendSpeculationRules(url) {
195
+ const script = document.createElement("script");
196
+ script.type = "speculationrules";
197
+ script.textContent = JSON.stringify({
198
+ prerender: [
199
+ {
200
+ source: "list",
201
+ urls: [url]
202
+ }
203
+ ]
204
+ });
205
+ document.head.append(script);
206
+ }
191
207
  export {
192
208
  init,
193
209
  prefetch
@@ -32,7 +32,10 @@ function astroPrefetch({ settings }) {
32
32
  },
33
33
  transform(code, id) {
34
34
  if (id.includes(prefetchInternalModuleFsSubpath)) {
35
- return code.replace("__PREFETCH_PREFETCH_ALL__", JSON.stringify(prefetch?.prefetchAll)).replace("__PREFETCH_DEFAULT_STRATEGY__", JSON.stringify(prefetch?.defaultStrategy));
35
+ return code.replace("__PREFETCH_PREFETCH_ALL__", JSON.stringify(prefetch?.prefetchAll)).replace("__PREFETCH_DEFAULT_STRATEGY__", JSON.stringify(prefetch?.defaultStrategy)).replace(
36
+ "__EXPERIMENTAL_CLIENT_PRERENDER__",
37
+ JSON.stringify(settings.config.experimental.clientPrerender)
38
+ );
36
39
  }
37
40
  }
38
41
  };
@@ -330,13 +330,61 @@ const a11y = [
330
330
  },
331
331
  {
332
332
  code: "a11y-missing-content",
333
- title: "Missing content on element important for accessibility",
334
- message: "Headings and anchors must have content to be accessible.",
333
+ title: "Missing content",
334
+ message: "Headings and anchors must have an accessible name, which can come from: inner text, aria-label, aria-labelledby, an img with alt property, or an svg with a tag <title></title>.",
335
335
  selector: a11y_required_content.join(","),
336
336
  match(element) {
337
337
  const innerText = element.innerText.trim();
338
- if (innerText === "")
339
- return true;
338
+ if (innerText !== "")
339
+ return false;
340
+ const ariaLabel = element.getAttribute("aria-label")?.trim();
341
+ if (ariaLabel && ariaLabel !== "")
342
+ return false;
343
+ const ariaLabelledby = element.getAttribute("aria-labelledby")?.trim();
344
+ if (ariaLabelledby) {
345
+ const ids = ariaLabelledby.split(" ");
346
+ for (const id of ids) {
347
+ const referencedElement = document.getElementById(id);
348
+ if (referencedElement && referencedElement.innerText.trim() !== "")
349
+ return false;
350
+ }
351
+ }
352
+ const imgElements = element.querySelectorAll("img");
353
+ for (const img of imgElements) {
354
+ const altAttribute = img.getAttribute("alt");
355
+ if (altAttribute && altAttribute.trim() !== "")
356
+ return false;
357
+ }
358
+ const svgElements = element.querySelectorAll("svg");
359
+ for (const svg of svgElements) {
360
+ const titleText = svg.querySelector("title");
361
+ if (titleText && titleText.textContent && titleText.textContent.trim() !== "")
362
+ return false;
363
+ }
364
+ const inputElements = element.querySelectorAll("input");
365
+ for (const input of inputElements) {
366
+ if (input.type === "image") {
367
+ const altAttribute = input.getAttribute("alt");
368
+ if (altAttribute && altAttribute.trim() !== "")
369
+ return false;
370
+ }
371
+ const inputAriaLabel = input.getAttribute("aria-label")?.trim();
372
+ if (inputAriaLabel && inputAriaLabel !== "")
373
+ return false;
374
+ const inputAriaLabelledby = input.getAttribute("aria-labelledby")?.trim();
375
+ if (inputAriaLabelledby) {
376
+ const ids = inputAriaLabelledby.split(" ");
377
+ for (const id of ids) {
378
+ const referencedElement = document.getElementById(id);
379
+ if (referencedElement && referencedElement.innerText.trim() !== "")
380
+ return false;
381
+ }
382
+ }
383
+ const title = input.getAttribute("title")?.trim();
384
+ if (title && title !== "")
385
+ return false;
386
+ }
387
+ return true;
340
388
  }
341
389
  },
342
390
  {