astro 3.1.4 → 3.2.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
@@ -120,6 +120,13 @@ declare module 'astro:transitions' {
120
120
  export const ViewTransitions: ViewTransitionsModule['default'];
121
121
  }
122
122
 
123
+ declare module 'astro:transitions/client' {
124
+ type TransitionRouterModule = typeof import('./dist/transitions/router.js');
125
+ export const supportsViewTransitions: TransitionRouterModule['supportsViewTransitions'];
126
+ export const transitionEnabledOnThisPage: TransitionRouterModule['transitionEnabledOnThisPage'];
127
+ export const navigate: TransitionRouterModule['navigate'];
128
+ }
129
+
123
130
  declare module 'astro:middleware' {
124
131
  export * from 'astro/middleware/namespace';
125
132
  }
@@ -11,91 +11,12 @@ const { fallback = 'animate' } = Astro.props as Props;
11
11
  <meta name="astro-view-transitions-enabled" content="true" />
12
12
  <meta name="astro-view-transitions-fallback" content={fallback} />
13
13
  <script>
14
- type Fallback = 'none' | 'animate' | 'swap';
15
- type Direction = 'forward' | 'back';
16
- type State = {
17
- index: number;
18
- scrollX: number;
19
- scrollY: number;
20
- intraPage?: boolean;
21
- };
22
- type Events = 'astro:page-load' | 'astro:after-swap';
23
-
24
- // only update history entries that are managed by us
25
- // leave other entries alone and do not accidently add state.
26
- const persistState = (state: State) => history.state && history.replaceState(state, '');
27
- // @ts-expect-error: startViewTransition might exist
28
- const supportsViewTransitions = !!document.startViewTransition;
29
- const transitionEnabledOnThisPage = () =>
30
- !!document.querySelector('[name="astro-view-transitions-enabled"]');
31
- const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
32
- const onPageLoad = () => triggerEvent('astro:page-load');
33
- const PERSIST_ATTR = 'data-astro-transition-persist';
34
- const parser = new DOMParser();
35
- // explained at its usage
36
- let noopEl: HTMLDivElement;
37
- if (import.meta.env.DEV) {
38
- noopEl = document.createElement('div');
39
- }
40
-
41
- // The History API does not tell you if navigation is forward or back, so
42
- // you can figure it using an index. On pushState the index is incremented so you
43
- // can use that to determine popstate if going forward or back.
44
- let currentHistoryIndex = 0;
45
- if (history.state) {
46
- // we reloaded a page with history state
47
- // (e.g. history navigation from non-transition page or browser reload)
48
- currentHistoryIndex = history.state.index;
49
- scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
50
- } else if (transitionEnabledOnThisPage()) {
51
- history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
52
- }
53
- const throttle = (cb: (...args: any[]) => any, delay: number) => {
54
- let wait = false;
55
- // During the waiting time additional events are lost.
56
- // So repeat the callback at the end if we have swallowed events.
57
- let onceMore = false;
58
- return (...args: any[]) => {
59
- if (wait) {
60
- onceMore = true;
61
- return;
62
- }
63
- cb(...args);
64
- wait = true;
65
- setTimeout(() => {
66
- if (onceMore) {
67
- onceMore = false;
68
- cb(...args);
69
- }
70
- wait = false;
71
- }, delay);
72
- };
73
- };
74
-
75
- // returns the contents of the page or null if the router can't deal with it.
76
- async function fetchHTML(
77
- href: string
78
- ): Promise<null | { html: string; redirected?: string; mediaType: DOMParserSupportedType }> {
79
- try {
80
- const res = await fetch(href);
81
- // drop potential charset (+ other name/value pairs) as parser needs the mediaType
82
- const mediaType = res.headers.get('content-type')?.replace(/;.*$/, '');
83
- // the DOMParser can handle two types of HTML
84
- if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml') {
85
- // everything else (e.g. audio/mp3) will be handled by the browser but not by us
86
- return null;
87
- }
88
- const html = await res.text();
89
- return {
90
- html,
91
- redirected: res.redirected ? res.url : undefined,
92
- mediaType,
93
- };
94
- } catch (err) {
95
- // can't fetch, let someone else deal with it.
96
- return null;
97
- }
98
- }
14
+ import {
15
+ supportsViewTransitions,
16
+ transitionEnabledOnThisPage,
17
+ navigate,
18
+ } from 'astro:transitions/client';
19
+ export type Fallback = 'none' | 'animate' | 'swap';
99
20
 
100
21
  function getFallback(): Fallback {
101
22
  const el = document.querySelector('[name="astro-view-transitions-fallback"]');
@@ -105,263 +26,6 @@ const { fallback = 'animate' } = Astro.props as Props;
105
26
  return 'animate';
106
27
  }
107
28
 
108
- function markScriptsExec() {
109
- for (const script of document.scripts) {
110
- script.dataset.astroExec = '';
111
- }
112
- }
113
-
114
- function runScripts() {
115
- let wait = Promise.resolve();
116
- for (const script of Array.from(document.scripts)) {
117
- if (script.dataset.astroExec === '') continue;
118
- const newScript = document.createElement('script');
119
- newScript.innerHTML = script.innerHTML;
120
- for (const attr of script.attributes) {
121
- if (attr.name === 'src') {
122
- const p = new Promise((r) => {
123
- newScript.onload = r;
124
- });
125
- wait = wait.then(() => p as any);
126
- }
127
- newScript.setAttribute(attr.name, attr.value);
128
- }
129
- newScript.dataset.astroExec = '';
130
- script.replaceWith(newScript);
131
- }
132
- return wait;
133
- }
134
-
135
- function isInfinite(animation: Animation) {
136
- const effect = animation.effect;
137
- if (!effect || !(effect instanceof KeyframeEffect) || !effect.target) return false;
138
- const style = window.getComputedStyle(effect.target, effect.pseudoElement);
139
- return style.animationIterationCount === 'infinite';
140
- }
141
-
142
- const updateHistoryAndScrollPosition = (toLocation) => {
143
- if (toLocation.href !== location.href) {
144
- history.pushState(
145
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
146
- '',
147
- toLocation.href
148
- );
149
- // now we are on the new page for non-history navigations!
150
- // (with history navigation page change happens before popstate is fired)
151
- }
152
- // freshly loaded pages start from the top
153
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
154
-
155
- if (toLocation.hash) {
156
- // because we are already on the target page ...
157
- // ... what comes next is a intra-page navigation
158
- // that won't reload the page but instead scroll to the fragment
159
- location.href = toLocation.href;
160
- }
161
- };
162
-
163
- // replace head and body of the windows document with contents from newDocument
164
- // if !popstate, update the history entry and scroll position according to toLocation
165
- // if popState is given, this holds the scroll position for history navigation
166
- // if fallback === "animate" then simulate view transitions
167
- async function updateDOM(
168
- newDocument: Document,
169
- toLocation: URL,
170
- popState?: State,
171
- fallback?: Fallback
172
- ) {
173
- // Check for a head element that should persist and returns it,
174
- // either because it has the data attribute or is a link el.
175
- const persistedHeadElement = (el: HTMLElement): Element | null => {
176
- const id = el.getAttribute(PERSIST_ATTR);
177
- const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
178
- if (newEl) {
179
- return newEl;
180
- }
181
- if (el.matches('link[rel=stylesheet]')) {
182
- const href = el.getAttribute('href');
183
- return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
184
- }
185
- // What follows is a fix for an issue (#8472) with missing client:only styles after transition.
186
- // That problem exists only in dev mode where styles are injected into the page by Vite.
187
- // Returning a noop element ensures that the styles are not removed from the old document.
188
- // Guarding the code below with the dev mode check
189
- // allows tree shaking to remove this code in production.
190
- if (import.meta.env.DEV) {
191
- if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
192
- const devId = el.dataset.viteDevId;
193
- // If this same style tag exists, remove it from the new page
194
- return (
195
- newDocument.querySelector(`style[data-astro-dev-id="${devId}"]`) ||
196
- // Otherwise, keep it anyways. This is client:only styles.
197
- noopEl
198
- );
199
- }
200
- }
201
- return null;
202
- };
203
-
204
- const swap = () => {
205
- // swap attributes of the html element
206
- // - delete all attributes from the current document
207
- // - insert all attributes from doc
208
- // - reinsert all original attributes that are named 'data-astro-*'
209
- const html = document.documentElement;
210
- const astro = [...html.attributes].filter(
211
- ({ name }) => (html.removeAttribute(name), name.startsWith('data-astro-'))
212
- );
213
- [...newDocument.documentElement.attributes, ...astro].forEach(({ name, value }) =>
214
- html.setAttribute(name, value)
215
- );
216
-
217
- // Replace scripts in both the head and body.
218
- for (const s1 of document.scripts) {
219
- for (const s2 of newDocument.scripts) {
220
- if (
221
- // Inline
222
- (!s1.src && s1.textContent === s2.textContent) ||
223
- // External
224
- (s1.src && s1.type === s2.type && s1.src === s2.src)
225
- ) {
226
- // the old script is in the new document: we mark it as executed to prevent re-execution
227
- s2.dataset.astroExec = '';
228
- break;
229
- }
230
- }
231
- }
232
-
233
- // Swap head
234
- for (const el of Array.from(document.head.children)) {
235
- const newEl = persistedHeadElement(el as HTMLElement);
236
- // If the element exists in the document already, remove it
237
- // from the new document and leave the current node alone
238
- if (newEl) {
239
- newEl.remove();
240
- } else {
241
- // Otherwise remove the element in the head. It doesn't exist in the new page.
242
- el.remove();
243
- }
244
- }
245
-
246
- // Everything left in the new head is new, append it all.
247
- document.head.append(...newDocument.head.children);
248
-
249
- // Persist elements in the existing body
250
- const oldBody = document.body;
251
-
252
- // this will reset scroll Position
253
- document.body.replaceWith(newDocument.body);
254
- for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
255
- const id = el.getAttribute(PERSIST_ATTR);
256
- const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
257
- if (newEl) {
258
- // The element exists in the new page, replace it with the element
259
- // from the old page so that state is preserved.
260
- newEl.replaceWith(el);
261
- }
262
- }
263
-
264
- if (popState) {
265
- scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
266
- } else {
267
- updateHistoryAndScrollPosition(toLocation);
268
- }
269
-
270
- triggerEvent('astro:after-swap');
271
- };
272
-
273
- // Wait on links to finish, to prevent FOUC
274
- const links: Promise<any>[] = [];
275
- for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) {
276
- // Do not preload links that are already on the page.
277
- if (
278
- !document.querySelector(
279
- `[${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], link[rel=stylesheet]`
280
- )
281
- ) {
282
- const c = document.createElement('link');
283
- c.setAttribute('rel', 'preload');
284
- c.setAttribute('as', 'style');
285
- c.setAttribute('href', el.getAttribute('href')!);
286
- links.push(
287
- new Promise<any>((resolve) => {
288
- ['load', 'error'].forEach((evName) => c.addEventListener(evName, resolve));
289
- document.head.append(c);
290
- })
291
- );
292
- }
293
- }
294
- links.length && (await Promise.all(links));
295
-
296
- if (fallback === 'animate') {
297
- // Trigger the animations
298
- const currentAnimations = document.getAnimations();
299
- document.documentElement.dataset.astroTransitionFallback = 'old';
300
- const newAnimations = document
301
- .getAnimations()
302
- .filter((a) => !currentAnimations.includes(a) && !isInfinite(a));
303
- const finished = Promise.all(newAnimations.map((a) => a.finished));
304
- const fallbackSwap = () => {
305
- swap();
306
- document.documentElement.dataset.astroTransitionFallback = 'new';
307
- };
308
- await finished;
309
- fallbackSwap();
310
- } else {
311
- swap();
312
- }
313
- }
314
-
315
- async function transition(direction: Direction, toLocation: URL, popState?: State) {
316
- let finished: Promise<void>;
317
- const href = toLocation.href;
318
- const response = await fetchHTML(href);
319
- // If there is a problem fetching the new page, just do an MPA navigation to it.
320
- if (response === null) {
321
- location.href = href;
322
- return;
323
- }
324
- // if there was a redirection, show the final URL in the browser's address bar
325
- if (response.redirected) {
326
- toLocation = new URL(response.redirected);
327
- }
328
-
329
- const newDocument = parser.parseFromString(response.html, response.mediaType);
330
- // The next line might look like a hack,
331
- // but it is actually necessary as noscript elements
332
- // and their contents are returned as markup by the parser,
333
- // see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString
334
- newDocument.querySelectorAll('noscript').forEach((el) => el.remove());
335
-
336
- if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
337
- location.href = href;
338
- return;
339
- }
340
-
341
- if (!popState) {
342
- // save the current scroll position before we change the DOM and transition to the new page
343
- history.replaceState({ ...history.state, scrollX, scrollY }, '');
344
- }
345
- document.documentElement.dataset.astroTransition = direction;
346
- if (supportsViewTransitions) {
347
- // @ts-expect-error: startViewTransition exist
348
- finished = document.startViewTransition(() =>
349
- updateDOM(newDocument, toLocation, popState)
350
- ).finished;
351
- } else {
352
- finished = updateDOM(newDocument, toLocation, popState, getFallback());
353
- }
354
- try {
355
- await finished;
356
- } finally {
357
- // skip this for the moment as it tends to stop fallback animations
358
- // document.documentElement.removeAttribute('data-astro-transition');
359
- await runScripts();
360
- markScriptsExec();
361
- onPageLoad();
362
- }
363
- }
364
-
365
29
  // Prefetching
366
30
  function maybePrefetch(pathname: string) {
367
31
  if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
@@ -406,83 +70,9 @@ const { fallback = 'animate' } = Astro.props as Props;
406
70
  return;
407
71
  }
408
72
  ev.preventDefault();
409
- navigate(link.href);
410
- });
411
-
412
- function navigate(href) {
413
- // not ours
414
- if (!transitionEnabledOnThisPage()) {
415
- location.href = href;
416
- return;
417
- }
418
- const toLocation = new URL(href, location.href);
419
- // We do not have page transitions on navigations to the same page (intra-page navigation)
420
- // but we want to handle prevent reload on navigation to the same page
421
- // Same page means same origin, path and query params (but maybe different hash)
422
- if (
423
- location.origin === toLocation.origin &&
424
- location.pathname === toLocation.pathname &&
425
- location.search === toLocation.search
426
- ) {
427
- // mark current position as non transition intra-page scrolling
428
- if (location.href !== toLocation.href) {
429
- history.replaceState({ ...history.state, intraPage: true }, '');
430
- history.pushState(
431
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
432
- '',
433
- toLocation.href
434
- );
435
- }
436
- if (toLocation.hash) {
437
- location.href = toLocation.href;
438
- } else {
439
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
440
- }
441
- } else {
442
- transition('forward', toLocation);
443
- }
444
- }
445
-
446
- addEventListener('popstate', (ev) => {
447
- if (!transitionEnabledOnThisPage() && ev.state) {
448
- // The current page doesn't have View Transitions enabled
449
- // but the page we navigate to does (because it set the state).
450
- // Do a full page refresh to reload the client-side router from the new page.
451
- // Scroll restauration will then happen during the reload when the router's code is re-executed
452
- if (history.scrollRestoration) {
453
- history.scrollRestoration = 'manual';
454
- }
455
- location.reload();
456
- return;
457
- }
458
-
459
- // History entries without state are created by the browser (e.g. for hash links)
460
- // Our view transition entries always have state.
461
- // Just ignore stateless entries.
462
- // The browser will handle navigation fine without our help
463
- if (ev.state === null) {
464
- if (history.scrollRestoration) {
465
- history.scrollRestoration = 'auto';
466
- }
467
- return;
468
- }
469
-
470
- // With the default "auto", the browser will jump to the old scroll position
471
- // before the ViewTransition is complete.
472
- if (history.scrollRestoration) {
473
- history.scrollRestoration = 'manual';
474
- }
475
-
476
- const state: State = history.state;
477
- if (state.intraPage) {
478
- // this is non transition intra-page scrolling
479
- scrollTo(state.scrollX, state.scrollY);
480
- } else {
481
- const nextIndex = state.index;
482
- const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
483
- currentHistoryIndex = nextIndex;
484
- transition(direction, new URL(location.href), state);
485
- }
73
+ navigate(link.href, {
74
+ history: link.dataset.astroHistory === 'replace' ? 'replace' : 'auto',
75
+ });
486
76
  });
487
77
 
488
78
  ['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
@@ -503,17 +93,5 @@ const { fallback = 'animate' } = Astro.props as Props;
503
93
  { passive: true, capture: true }
504
94
  );
505
95
  });
506
-
507
- addEventListener('load', onPageLoad);
508
- // There's not a good way to record scroll position before a back button.
509
- // So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
510
- const updateState = () => {
511
- persistState({ ...history.state, scrollX, scrollY });
512
- };
513
-
514
- if ('onscrollend' in window) addEventListener('scrollend', updateState);
515
- else addEventListener('scroll', throttle(updateState, 300));
516
-
517
- markScriptsExec();
518
96
  }
519
97
  </script>
@@ -1,5 +1,6 @@
1
1
  import fs, { readFileSync } from "node:fs";
2
2
  import { basename, join } from "node:path/posix";
3
+ import { getOutDirWithinCwd } from "../../core/build/common.js";
3
4
  import { prependForwardSlash } from "../../core/path.js";
4
5
  import { isServerLikeOutput } from "../../prerender/utils.js";
5
6
  import { getConfiguredImageService, isESMImportedImage } from "../internal.js";
@@ -23,7 +24,7 @@ async function generateImage(pipeline, options, filepath) {
23
24
  serverRoot = config.build.server;
24
25
  clientRoot = config.build.client;
25
26
  } else {
26
- serverRoot = config.outDir;
27
+ serverRoot = getOutDirWithinCwd(config.outDir);
27
28
  clientRoot = config.outDir;
28
29
  }
29
30
  const isLocalImage = isESMImportedImage(options.src);
@@ -53,7 +53,8 @@ async function createContentTypesGenerator({
53
53
  objectMode: true
54
54
  });
55
55
  for (const entry of globResult) {
56
- const entryURL = new URL(entry.path, contentPaths.contentDir);
56
+ const fullPath = path.join(fileURLToPath(contentPaths.contentDir), entry.path);
57
+ const entryURL = pathToFileURL(fullPath);
57
58
  if (entryURL.href.startsWith(contentPaths.config.url.href))
58
59
  continue;
59
60
  if (entry.dirent.isFile()) {
@@ -4,7 +4,7 @@ import glob from "fast-glob";
4
4
  import { bgGreen, bgMagenta, black, dim } from "kleur/colors";
5
5
  import fs from "node:fs";
6
6
  import path, { extname } from "node:path";
7
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import * as vite from "vite";
9
9
  import {
10
10
  createBuildInternals,
@@ -109,7 +109,8 @@ async function ssrBuild(opts, internals, input, container) {
109
109
  const viteBuildConfig = {
110
110
  ...viteConfig,
111
111
  mode: viteConfig.mode || "production",
112
- logLevel: opts.viteConfig.logLevel ?? "error",
112
+ // Check using `settings...` as `viteConfig` always defaults to `warn` by Astro
113
+ logLevel: settings.config.vite.logLevel ?? "error",
113
114
  build: {
114
115
  target: "esnext",
115
116
  // Vite defaults cssMinify to false in SSR by default, but we want to minify it
@@ -137,6 +138,14 @@ async function ssrBuild(opts, internals, input, container) {
137
138
  const sanitizedName = name.split(".")[0];
138
139
  return `chunks/${sanitizedName}_[hash].mjs`;
139
140
  }
141
+ for (let i = 0; i < name.length; i++) {
142
+ if (name[i] === "%") {
143
+ const third = name.codePointAt(i + 2) | 32;
144
+ if (name[i + 1] !== "2" || third !== 102) {
145
+ return `chunks/${name.replace(/%/g, "_percent_")}_[hash].mjs`;
146
+ }
147
+ }
148
+ }
140
149
  return `chunks/[name]_[hash].mjs`;
141
150
  },
142
151
  assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`,
@@ -199,7 +208,8 @@ ${bgGreen(black(" building client "))}`);
199
208
  const viteBuildConfig = {
200
209
  ...viteConfig,
201
210
  mode: viteConfig.mode || "production",
202
- logLevel: "info",
211
+ // Check using `settings...` as `viteConfig` always defaults to `warn` by Astro
212
+ logLevel: settings.config.vite.logLevel ?? "info",
203
213
  build: {
204
214
  target: "esnext",
205
215
  ...viteConfig.build,
@@ -240,7 +250,8 @@ async function runPostBuildHooks(container, ssrReturn, clientReturn) {
240
250
  const build = container.options.settings.config.build;
241
251
  for (const [fileName, mutation] of mutations) {
242
252
  const root = isServerLikeOutput(config) ? mutation.build === "server" ? build.server : build.client : config.outDir;
243
- const fileURL = new URL(fileName, root);
253
+ const fullPath = path.join(fileURLToPath(root), fileName);
254
+ const fileURL = pathToFileURL(fullPath);
244
255
  await fs.promises.mkdir(new URL("./", fileURL), { recursive: true });
245
256
  await fs.promises.writeFile(fileURL, mutation.code, "utf-8");
246
257
  }
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "3.1.4";
1
+ const ASTRO_VERSION = "3.2.0";
2
2
  const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
3
3
  ".markdown",
4
4
  ".mdown",
@@ -188,7 +188,6 @@ async function createVite(commandConfig, { settings, logger, mode, command, fs =
188
188
  result = vite.mergeConfig(result, settings.config.vite || {});
189
189
  }
190
190
  result = vite.mergeConfig(result, commandConfig);
191
- result.customLogger = vite.createLogger(result.logLevel ?? "warn");
192
191
  return result;
193
192
  }
194
193
  const COMMON_DEPENDENCIES_NOT_ASTRO = [
@@ -20,7 +20,7 @@ async function dev(inlineConfig) {
20
20
  base: restart.container.settings.config.base
21
21
  })
22
22
  );
23
- const currentVersion = "3.1.4";
23
+ const currentVersion = "3.2.0";
24
24
  if (currentVersion.includes("-")) {
25
25
  logger.warn(null, msg.prerelease({ currentVersion }));
26
26
  }
@@ -203,7 +203,7 @@ const ImageNotFound = {
203
203
  const NoImageMetadata = {
204
204
  name: "NoImageMetadata",
205
205
  title: "Could not process image metadata.",
206
- message: (imagePath) => `Could not process image metadata${imagePath ? " for `${imagePath}`" : ""}.`,
206
+ message: (imagePath) => `Could not process image metadata${imagePath ? ` for \`${imagePath}\`` : ""}.`,
207
207
  hint: "This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue."
208
208
  };
209
209
  const MarkdownImageNotFound = {
@@ -50,7 +50,7 @@ function serverStart({
50
50
  base,
51
51
  isRestart = false
52
52
  }) {
53
- const version = "3.1.4";
53
+ const version = "3.2.0";
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.1.4"}`
238
+ `v${"3.2.0"}`
239
239
  )} ${headline}`
240
240
  );
241
241
  }
@@ -1,4 +1,5 @@
1
1
  import { AstroError, AstroErrorData } from "../errors/index.js";
2
+ import { routeIsRedirect } from "../redirects/index.js";
2
3
  import { getParams } from "../routing/params.js";
3
4
  import { RouteCache, callGetStaticPaths, findPathItemByKey } from "./route-cache.js";
4
5
  async function getParamsAndProps(opts) {
@@ -7,6 +8,9 @@ async function getParamsAndProps(opts) {
7
8
  return [{}, {}];
8
9
  }
9
10
  const params = getRouteParams(route, pathname) ?? {};
11
+ if (routeIsRedirect(route)) {
12
+ return [params, {}];
13
+ }
10
14
  validatePrerenderEndpointCollision(route, mod, params);
11
15
  const staticPaths = await callGetStaticPaths({
12
16
  mod,
@@ -41,7 +41,8 @@ async function runHookConfigSetup({
41
41
  let updatedSettings = { ...settings, config: updatedConfig };
42
42
  let addedClientDirectives = /* @__PURE__ */ new Map();
43
43
  let astroJSXRenderer = null;
44
- for (const integration of settings.config.integrations) {
44
+ for (let i = 0; i < updatedConfig.integrations.length; i++) {
45
+ const integration = updatedConfig.integrations[i];
45
46
  if (integration.hooks?.["astro:config:setup"]) {
46
47
  let addPageExtension2 = function(...input) {
47
48
  const exts = input.flat(Infinity).map((ext) => `.${ext.replace(/^\./, "")}`);
@@ -0,0 +1,8 @@
1
+ export type Fallback = 'none' | 'animate' | 'swap';
2
+ export type Direction = 'forward' | 'back';
3
+ export type Options = {
4
+ history?: 'auto' | 'push' | 'replace';
5
+ };
6
+ export declare const supportsViewTransitions = true;
7
+ export declare const transitionEnabledOnThisPage: () => boolean;
8
+ export declare function navigate(href: string, options?: Options): void;
@@ -0,0 +1,324 @@
1
+ const persistState = (state) => history.state && history.replaceState(state, "");
2
+ const supportsViewTransitions = !!document.startViewTransition;
3
+ const transitionEnabledOnThisPage = () => !!document.querySelector('[name="astro-view-transitions-enabled"]');
4
+ const samePage = (otherLocation) => location.pathname === otherLocation.pathname && location.search === otherLocation.search;
5
+ const triggerEvent = (name) => document.dispatchEvent(new Event(name));
6
+ const onPageLoad = () => triggerEvent("astro:page-load");
7
+ const announce = () => {
8
+ let div = document.createElement("div");
9
+ div.setAttribute("aria-live", "assertive");
10
+ div.setAttribute("aria-atomic", "true");
11
+ div.setAttribute(
12
+ "style",
13
+ "position:absolute;left:0;top:0;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden;white-space:nowrap;width:1px;height:1px"
14
+ );
15
+ document.body.append(div);
16
+ setTimeout(
17
+ () => {
18
+ let title = document.title || document.querySelector("h1")?.textContent || location.pathname;
19
+ div.textContent = title;
20
+ },
21
+ // Much thought went into this magic number; the gist is that screen readers
22
+ // need to see that the element changed and might not do so if it happens
23
+ // too quickly.
24
+ 60
25
+ );
26
+ };
27
+ const PERSIST_ATTR = "data-astro-transition-persist";
28
+ const parser = new DOMParser();
29
+ let noopEl;
30
+ if (import.meta.env.DEV) {
31
+ noopEl = document.createElement("div");
32
+ }
33
+ let currentHistoryIndex = 0;
34
+ if (history.state) {
35
+ currentHistoryIndex = history.state.index;
36
+ scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
37
+ } else if (transitionEnabledOnThisPage()) {
38
+ history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, "");
39
+ }
40
+ const throttle = (cb, delay) => {
41
+ let wait = false;
42
+ let onceMore = false;
43
+ return (...args) => {
44
+ if (wait) {
45
+ onceMore = true;
46
+ return;
47
+ }
48
+ cb(...args);
49
+ wait = true;
50
+ setTimeout(() => {
51
+ if (onceMore) {
52
+ onceMore = false;
53
+ cb(...args);
54
+ }
55
+ wait = false;
56
+ }, delay);
57
+ };
58
+ };
59
+ async function fetchHTML(href) {
60
+ try {
61
+ const res = await fetch(href);
62
+ const mediaType = res.headers.get("content-type")?.replace(/;.*$/, "");
63
+ if (mediaType !== "text/html" && mediaType !== "application/xhtml+xml") {
64
+ return null;
65
+ }
66
+ const html = await res.text();
67
+ return {
68
+ html,
69
+ redirected: res.redirected ? res.url : void 0,
70
+ mediaType
71
+ };
72
+ } catch (err) {
73
+ return null;
74
+ }
75
+ }
76
+ function getFallback() {
77
+ const el = document.querySelector('[name="astro-view-transitions-fallback"]');
78
+ if (el) {
79
+ return el.getAttribute("content");
80
+ }
81
+ return "animate";
82
+ }
83
+ function markScriptsExec() {
84
+ for (const script of document.scripts) {
85
+ script.dataset.astroExec = "";
86
+ }
87
+ }
88
+ function runScripts() {
89
+ let wait = Promise.resolve();
90
+ for (const script of Array.from(document.scripts)) {
91
+ if (script.dataset.astroExec === "")
92
+ continue;
93
+ const newScript = document.createElement("script");
94
+ newScript.innerHTML = script.innerHTML;
95
+ for (const attr of script.attributes) {
96
+ if (attr.name === "src") {
97
+ const p = new Promise((r) => {
98
+ newScript.onload = r;
99
+ });
100
+ wait = wait.then(() => p);
101
+ }
102
+ newScript.setAttribute(attr.name, attr.value);
103
+ }
104
+ newScript.dataset.astroExec = "";
105
+ script.replaceWith(newScript);
106
+ }
107
+ return wait;
108
+ }
109
+ function isInfinite(animation) {
110
+ const effect = animation.effect;
111
+ if (!effect || !(effect instanceof KeyframeEffect) || !effect.target)
112
+ return false;
113
+ const style = window.getComputedStyle(effect.target, effect.pseudoElement);
114
+ return style.animationIterationCount === "infinite";
115
+ }
116
+ const updateHistoryAndScrollPosition = (toLocation, replace, intraPage) => {
117
+ const fresh = !samePage(toLocation);
118
+ if (toLocation.href !== location.href) {
119
+ if (replace) {
120
+ history.replaceState({ ...history.state }, "", toLocation.href);
121
+ } else {
122
+ history.replaceState({ ...history.state, intraPage }, "");
123
+ history.pushState({ index: ++currentHistoryIndex, scrollX, scrollY }, "", toLocation.href);
124
+ }
125
+ if (fresh) {
126
+ scrollTo({ left: 0, top: 0, behavior: "instant" });
127
+ }
128
+ }
129
+ if (toLocation.hash) {
130
+ location.href = toLocation.href;
131
+ } else {
132
+ scrollTo({ left: 0, top: 0, behavior: "instant" });
133
+ }
134
+ };
135
+ async function updateDOM(newDocument, toLocation, options, popState, fallback) {
136
+ const persistedHeadElement = (el) => {
137
+ const id = el.getAttribute(PERSIST_ATTR);
138
+ const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
139
+ if (newEl) {
140
+ return newEl;
141
+ }
142
+ if (el.matches("link[rel=stylesheet]")) {
143
+ const href = el.getAttribute("href");
144
+ return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
145
+ }
146
+ if (import.meta.env.DEV) {
147
+ if (el.tagName === "STYLE" && el.dataset.viteDevId) {
148
+ const devId = el.dataset.viteDevId;
149
+ return newDocument.querySelector(`style[data-astro-dev-id="${devId}"]`) || // Otherwise, keep it anyways. This is client:only styles.
150
+ noopEl;
151
+ }
152
+ }
153
+ return null;
154
+ };
155
+ const swap = () => {
156
+ const html = document.documentElement;
157
+ const astro = [...html.attributes].filter(
158
+ ({ name }) => (html.removeAttribute(name), name.startsWith("data-astro-"))
159
+ );
160
+ [...newDocument.documentElement.attributes, ...astro].forEach(
161
+ ({ name, value }) => html.setAttribute(name, value)
162
+ );
163
+ for (const s1 of document.scripts) {
164
+ for (const s2 of newDocument.scripts) {
165
+ if (
166
+ // Inline
167
+ !s1.src && s1.textContent === s2.textContent || // External
168
+ s1.src && s1.type === s2.type && s1.src === s2.src
169
+ ) {
170
+ s2.dataset.astroExec = "";
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ for (const el of Array.from(document.head.children)) {
176
+ const newEl = persistedHeadElement(el);
177
+ if (newEl) {
178
+ newEl.remove();
179
+ } else {
180
+ el.remove();
181
+ }
182
+ }
183
+ document.head.append(...newDocument.head.children);
184
+ const oldBody = document.body;
185
+ document.body.replaceWith(newDocument.body);
186
+ for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
187
+ const id = el.getAttribute(PERSIST_ATTR);
188
+ const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
189
+ if (newEl) {
190
+ newEl.replaceWith(el);
191
+ }
192
+ }
193
+ if (popState) {
194
+ scrollTo(popState.scrollX, popState.scrollY);
195
+ } else {
196
+ updateHistoryAndScrollPosition(toLocation, options.history === "replace", false);
197
+ }
198
+ triggerEvent("astro:after-swap");
199
+ };
200
+ const links = [];
201
+ for (const el of newDocument.querySelectorAll("head link[rel=stylesheet]")) {
202
+ if (!document.querySelector(
203
+ `[${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], link[rel=stylesheet]`
204
+ )) {
205
+ const c = document.createElement("link");
206
+ c.setAttribute("rel", "preload");
207
+ c.setAttribute("as", "style");
208
+ c.setAttribute("href", el.getAttribute("href"));
209
+ links.push(
210
+ new Promise((resolve) => {
211
+ ["load", "error"].forEach((evName) => c.addEventListener(evName, resolve));
212
+ document.head.append(c);
213
+ })
214
+ );
215
+ }
216
+ }
217
+ links.length && await Promise.all(links);
218
+ if (fallback === "animate") {
219
+ const currentAnimations = document.getAnimations();
220
+ document.documentElement.dataset.astroTransitionFallback = "old";
221
+ const newAnimations = document.getAnimations().filter((a) => !currentAnimations.includes(a) && !isInfinite(a));
222
+ const finished = Promise.all(newAnimations.map((a) => a.finished));
223
+ const fallbackSwap = () => {
224
+ swap();
225
+ document.documentElement.dataset.astroTransitionFallback = "new";
226
+ };
227
+ await finished;
228
+ fallbackSwap();
229
+ } else {
230
+ swap();
231
+ }
232
+ }
233
+ async function transition(direction, toLocation, options, popState) {
234
+ let finished;
235
+ const href = toLocation.href;
236
+ const response = await fetchHTML(href);
237
+ if (response === null) {
238
+ location.href = href;
239
+ return;
240
+ }
241
+ if (response.redirected) {
242
+ toLocation = new URL(response.redirected);
243
+ }
244
+ const newDocument = parser.parseFromString(response.html, response.mediaType);
245
+ newDocument.querySelectorAll("noscript").forEach((el) => el.remove());
246
+ if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
247
+ location.href = href;
248
+ return;
249
+ }
250
+ if (!popState) {
251
+ history.replaceState({ ...history.state, scrollX, scrollY }, "");
252
+ }
253
+ document.documentElement.dataset.astroTransition = direction;
254
+ if (supportsViewTransitions) {
255
+ finished = document.startViewTransition(
256
+ () => updateDOM(newDocument, toLocation, options, popState)
257
+ ).finished;
258
+ } else {
259
+ finished = updateDOM(newDocument, toLocation, options, popState, getFallback());
260
+ }
261
+ try {
262
+ await finished;
263
+ } finally {
264
+ await runScripts();
265
+ markScriptsExec();
266
+ onPageLoad();
267
+ announce();
268
+ }
269
+ }
270
+ function navigate(href, options) {
271
+ if (!transitionEnabledOnThisPage()) {
272
+ location.href = href;
273
+ return;
274
+ }
275
+ const toLocation = new URL(href, location.href);
276
+ if (location.origin === toLocation.origin && samePage(toLocation)) {
277
+ updateHistoryAndScrollPosition(toLocation, options?.history === "replace", true);
278
+ } else {
279
+ transition("forward", toLocation, options ?? {});
280
+ }
281
+ }
282
+ if (supportsViewTransitions || getFallback() !== "none") {
283
+ addEventListener("popstate", (ev) => {
284
+ if (!transitionEnabledOnThisPage() && ev.state) {
285
+ if (history.scrollRestoration) {
286
+ history.scrollRestoration = "manual";
287
+ }
288
+ location.reload();
289
+ return;
290
+ }
291
+ if (ev.state === null) {
292
+ if (history.scrollRestoration) {
293
+ history.scrollRestoration = "auto";
294
+ }
295
+ return;
296
+ }
297
+ if (history.scrollRestoration) {
298
+ history.scrollRestoration = "manual";
299
+ }
300
+ const state = history.state;
301
+ if (state.intraPage) {
302
+ scrollTo(state.scrollX, state.scrollY);
303
+ } else {
304
+ const nextIndex = state.index;
305
+ const direction = nextIndex > currentHistoryIndex ? "forward" : "back";
306
+ currentHistoryIndex = nextIndex;
307
+ transition(direction, new URL(location.href), {}, state);
308
+ }
309
+ });
310
+ addEventListener("load", onPageLoad);
311
+ const updateState = () => {
312
+ persistState({ ...history.state, scrollX, scrollY });
313
+ };
314
+ if ("onscrollend" in window)
315
+ addEventListener("scrollend", updateState);
316
+ else
317
+ addEventListener("scroll", throttle(updateState, 300));
318
+ markScriptsExec();
319
+ }
320
+ export {
321
+ navigate,
322
+ supportsViewTransitions,
323
+ transitionEnabledOnThisPage
324
+ };
@@ -1,6 +1,8 @@
1
1
  import * as vite from "vite";
2
2
  const virtualModuleId = "astro:transitions";
3
3
  const resolvedVirtualModuleId = "\0" + virtualModuleId;
4
+ const virtualClientModuleId = "astro:transitions/client";
5
+ const resolvedVirtualClientModuleId = "\0" + virtualClientModuleId;
4
6
  function astroTransitions() {
5
7
  return {
6
8
  name: "astro:transitions",
@@ -8,6 +10,9 @@ function astroTransitions() {
8
10
  if (id === virtualModuleId) {
9
11
  return resolvedVirtualModuleId;
10
12
  }
13
+ if (id === virtualClientModuleId) {
14
+ return resolvedVirtualClientModuleId;
15
+ }
11
16
  },
12
17
  load(id) {
13
18
  if (id === resolvedVirtualModuleId) {
@@ -16,6 +21,11 @@ function astroTransitions() {
16
21
  export { default as ViewTransitions } from "astro/components/ViewTransitions.astro";
17
22
  `;
18
23
  }
24
+ if (id === resolvedVirtualClientModuleId) {
25
+ return `
26
+ export * from "astro/transitions/router";
27
+ `;
28
+ }
19
29
  }
20
30
  };
21
31
  }
@@ -155,19 +155,20 @@ async function handleRoute({
155
155
  let response = await pipeline.renderRoute(renderContext, mod);
156
156
  if (response.status === 404 && has404Route(manifestData)) {
157
157
  const fourOhFourRoute = await matchRoute("/404", manifestData, pipeline);
158
- return handleRoute({
159
- ...options,
160
- matchedRoute: fourOhFourRoute,
161
- url: new URL(pathname, url),
162
- status: 404,
163
- body,
164
- origin,
165
- pipeline,
166
- manifestData,
167
- incomingRequest,
168
- incomingResponse,
169
- manifest
170
- });
158
+ if (fourOhFourRoute?.route !== options.route)
159
+ return handleRoute({
160
+ ...options,
161
+ matchedRoute: fourOhFourRoute,
162
+ url: new URL(pathname, url),
163
+ status: 404,
164
+ body,
165
+ origin,
166
+ pipeline,
167
+ manifestData,
168
+ incomingRequest,
169
+ incomingResponse,
170
+ manifest
171
+ });
171
172
  }
172
173
  if (route.type === "endpoint") {
173
174
  await writeWebResponse(incomingResponse, response);
@@ -259,7 +260,7 @@ function getStatus(matchedRoute) {
259
260
  return 500;
260
261
  }
261
262
  function has404Route(manifest) {
262
- return manifest.routes.find((route) => route.route === "/404");
263
+ return manifest.routes.some((route) => route.route === "/404");
263
264
  }
264
265
  export {
265
266
  handleRoute,
@@ -5,7 +5,7 @@ import {
5
5
  import matter from "gray-matter";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
- import { fileURLToPath } from "node:url";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { normalizePath } from "vite";
10
10
  import { AstroError, AstroErrorData, MarkdownError } from "../core/errors/index.js";
11
11
  import { isMarkdownFile } from "../core/util.js";
@@ -57,8 +57,9 @@ function markdown({ settings, logger }) {
57
57
  const { fileId, fileUrl } = getFileInfo(id, settings.config);
58
58
  const rawFile = await fs.promises.readFile(fileId, "utf-8");
59
59
  const raw = safeMatter(rawFile, id);
60
+ const fileURL = pathToFileURL(fileId);
60
61
  const renderResult = await processor.render(raw.content, {
61
- fileURL: new URL(`file://${fileId}`),
62
+ fileURL,
62
63
  frontmatter: raw.data
63
64
  }).catch((err) => {
64
65
  if (err instanceof InvalidAstroDataError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
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",
@@ -77,7 +77,8 @@
77
77
  "types": "./dist/core/middleware/namespace.d.ts",
78
78
  "default": "./dist/core/middleware/namespace.js"
79
79
  },
80
- "./transitions": "./dist/transitions/index.js"
80
+ "./transitions": "./dist/transitions/index.js",
81
+ "./transitions/router": "./dist/transitions/router.js"
81
82
  },
82
83
  "imports": {
83
84
  "#astro/*": "./dist/*.js"