astro 3.5.6 → 3.6.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.
@@ -1,8 +1,23 @@
1
- const updateScrollPosition = (positions) => history.state && history.replaceState({ ...history.state, ...positions }, "");
1
+ import {
2
+ TRANSITION_AFTER_SWAP,
3
+ TransitionBeforeSwapEvent,
4
+ doPreparation,
5
+ doSwap
6
+ } from "./events.js";
7
+ const updateScrollPosition = (positions) => {
8
+ if (history.state) {
9
+ history.scrollRestoration = "manual";
10
+ history.replaceState({ ...history.state, ...positions }, "");
11
+ }
12
+ };
2
13
  const inBrowser = import.meta.env.SSR === false;
3
14
  const supportsViewTransitions = inBrowser && !!document.startViewTransition;
4
15
  const transitionEnabledOnThisPage = () => inBrowser && !!document.querySelector('[name="astro-view-transitions-enabled"]');
5
- const samePage = (otherLocation) => location.pathname === otherLocation.pathname && location.search === otherLocation.search;
16
+ const samePage = (thisLocation, otherLocation) => thisLocation.origin === otherLocation.origin && thisLocation.pathname === otherLocation.pathname && thisLocation.search === otherLocation.search;
17
+ let originalLocation;
18
+ let viewTransition;
19
+ let skipTransition = false;
20
+ let viewTransitionFinished;
6
21
  const triggerEvent = (name) => document.dispatchEvent(new Event(name));
7
22
  const onPageLoad = () => triggerEvent("astro:page-load");
8
23
  const announce = () => {
@@ -23,6 +38,8 @@ const announce = () => {
23
38
  );
24
39
  };
25
40
  const PERSIST_ATTR = "data-astro-transition-persist";
41
+ const DIRECTION_ATTR = "data-astro-transition";
42
+ const OLD_NEW_ATTR = "data-astro-transition-fallback";
26
43
  const VITE_ID = "data-vite-dev-id";
27
44
  let parser;
28
45
  let currentHistoryIndex = 0;
@@ -31,7 +48,8 @@ if (inBrowser) {
31
48
  currentHistoryIndex = history.state.index;
32
49
  scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
33
50
  } else if (transitionEnabledOnThisPage()) {
34
- history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, "");
51
+ history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, "");
52
+ history.scrollRestoration = "manual";
35
53
  }
36
54
  }
37
55
  const throttle = (cb, delay) => {
@@ -98,41 +116,49 @@ function runScripts() {
98
116
  }
99
117
  return wait;
100
118
  }
101
- function isInfinite(animation) {
102
- const effect = animation.effect;
103
- if (!effect || !(effect instanceof KeyframeEffect) || !effect.target)
104
- return false;
105
- const style = window.getComputedStyle(effect.target, effect.pseudoElement);
106
- return style.animationIterationCount === "infinite";
107
- }
108
- const moveToLocation = (toLocation, replace, intraPage) => {
109
- const fresh = !samePage(toLocation);
119
+ const moveToLocation = (to, from, options, historyState) => {
120
+ const intraPage = samePage(from, to);
110
121
  let scrolledToTop = false;
111
- if (toLocation.href !== location.href) {
112
- if (replace) {
113
- history.replaceState({ ...history.state }, "", toLocation.href);
122
+ if (to.href !== location.href && !historyState) {
123
+ if (options.history === "replace") {
124
+ const current = history.state;
125
+ history.replaceState(
126
+ {
127
+ ...options.state,
128
+ index: current.index,
129
+ scrollX: current.scrollX,
130
+ scrollY: current.scrollY
131
+ },
132
+ "",
133
+ to.href
134
+ );
114
135
  } else {
115
- history.replaceState({ ...history.state, intraPage }, "");
116
136
  history.pushState(
117
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
137
+ { ...options.state, index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
118
138
  "",
119
- toLocation.href
139
+ to.href
120
140
  );
121
141
  }
122
- if (fresh) {
123
- scrollTo({ left: 0, top: 0, behavior: "instant" });
124
- scrolledToTop = true;
125
- }
142
+ history.scrollRestoration = "manual";
143
+ }
144
+ originalLocation = to;
145
+ if (!intraPage) {
146
+ scrollTo({ left: 0, top: 0, behavior: "instant" });
147
+ scrolledToTop = true;
126
148
  }
127
- if (toLocation.hash) {
128
- location.href = toLocation.href;
149
+ if (historyState) {
150
+ scrollTo(historyState.scrollX, historyState.scrollY);
129
151
  } else {
130
- if (!scrolledToTop) {
131
- scrollTo({ left: 0, top: 0, behavior: "instant" });
152
+ if (to.hash) {
153
+ location.href = to.href;
154
+ } else {
155
+ if (!scrolledToTop) {
156
+ scrollTo({ left: 0, top: 0, behavior: "instant" });
157
+ }
132
158
  }
133
159
  }
134
160
  };
135
- function stylePreloadLinks(newDocument) {
161
+ function preloadStyleLinks(newDocument) {
136
162
  const links = [];
137
163
  for (const el of newDocument.querySelectorAll("head link[rel=stylesheet]")) {
138
164
  if (!document.querySelector(
@@ -154,16 +180,16 @@ function stylePreloadLinks(newDocument) {
154
180
  }
155
181
  return links;
156
182
  }
157
- async function updateDOM(newDocument, toLocation, options, popState, fallback) {
158
- const persistedHeadElement = (el) => {
183
+ async function updateDOM(preparationEvent, options, historyState, fallback) {
184
+ const persistedHeadElement = (el, newDoc) => {
159
185
  const id = el.getAttribute(PERSIST_ATTR);
160
- const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
186
+ const newEl = id && newDoc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
161
187
  if (newEl) {
162
188
  return newEl;
163
189
  }
164
190
  if (el.matches("link[rel=stylesheet]")) {
165
191
  const href = el.getAttribute("href");
166
- return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
192
+ return newDoc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
167
193
  }
168
194
  return null;
169
195
  };
@@ -189,16 +215,16 @@ async function updateDOM(newDocument, toLocation, options, popState, fallback) {
189
215
  }
190
216
  }
191
217
  };
192
- const swap = () => {
218
+ const defaultSwap = (beforeSwapEvent) => {
193
219
  const html = document.documentElement;
194
- const astro = [...html.attributes].filter(
220
+ const astroAttributes = [...html.attributes].filter(
195
221
  ({ name }) => (html.removeAttribute(name), name.startsWith("data-astro-"))
196
222
  );
197
- [...newDocument.documentElement.attributes, ...astro].forEach(
223
+ [...beforeSwapEvent.newDocument.documentElement.attributes, ...astroAttributes].forEach(
198
224
  ({ name, value }) => html.setAttribute(name, value)
199
225
  );
200
226
  for (const s1 of document.scripts) {
201
- for (const s2 of newDocument.scripts) {
227
+ for (const s2 of beforeSwapEvent.newDocument.scripts) {
202
228
  if (
203
229
  // Inline
204
230
  !s1.src && s1.textContent === s2.textContent || // External
@@ -210,17 +236,17 @@ async function updateDOM(newDocument, toLocation, options, popState, fallback) {
210
236
  }
211
237
  }
212
238
  for (const el of Array.from(document.head.children)) {
213
- const newEl = persistedHeadElement(el);
239
+ const newEl = persistedHeadElement(el, beforeSwapEvent.newDocument);
214
240
  if (newEl) {
215
241
  newEl.remove();
216
242
  } else {
217
243
  el.remove();
218
244
  }
219
245
  }
220
- document.head.append(...newDocument.head.children);
246
+ document.head.append(...beforeSwapEvent.newDocument.head.children);
221
247
  const oldBody = document.body;
222
248
  const savedFocus = saveFocus();
223
- document.body.replaceWith(newDocument.body);
249
+ document.body.replaceWith(beforeSwapEvent.newDocument.body);
224
250
  for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
225
251
  const id = el.getAttribute(PERSIST_ATTR);
226
252
  const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
@@ -229,73 +255,131 @@ async function updateDOM(newDocument, toLocation, options, popState, fallback) {
229
255
  }
230
256
  }
231
257
  restoreFocus(savedFocus);
232
- if (popState) {
233
- scrollTo(popState.scrollX, popState.scrollY);
234
- } else {
235
- moveToLocation(toLocation, options.history === "replace", false);
236
- }
237
- triggerEvent("astro:after-swap");
238
258
  };
239
- const links = stylePreloadLinks(newDocument);
240
- links.length && await Promise.all(links);
241
- if (fallback === "animate") {
259
+ async function animate(phase) {
260
+ function isInfinite(animation) {
261
+ const effect = animation.effect;
262
+ if (!effect || !(effect instanceof KeyframeEffect) || !effect.target)
263
+ return false;
264
+ const style = window.getComputedStyle(effect.target, effect.pseudoElement);
265
+ return style.animationIterationCount === "infinite";
266
+ }
242
267
  const currentAnimations = document.getAnimations();
243
- document.documentElement.dataset.astroTransitionFallback = "old";
244
- const newAnimations = document.getAnimations().filter((a) => !currentAnimations.includes(a) && !isInfinite(a));
245
- const finished = Promise.all(newAnimations.map((a) => a.finished));
246
- await finished;
247
- swap();
248
- document.documentElement.dataset.astroTransitionFallback = "new";
268
+ document.documentElement.setAttribute(OLD_NEW_ATTR, phase);
269
+ const nextAnimations = document.getAnimations();
270
+ const newAnimations = nextAnimations.filter(
271
+ (a) => !currentAnimations.includes(a) && !isInfinite(a)
272
+ );
273
+ return Promise.all(newAnimations.map((a) => a.finished));
274
+ }
275
+ if (!skipTransition) {
276
+ document.documentElement.setAttribute(DIRECTION_ATTR, preparationEvent.direction);
277
+ if (fallback === "animate") {
278
+ await animate("old");
279
+ }
249
280
  } else {
250
- swap();
281
+ throw new DOMException("Transition was skipped");
251
282
  }
252
- }
253
- async function transition(direction, toLocation, options, popState) {
254
- let finished;
255
- const href = toLocation.href;
256
- const init = {};
257
- if (options.formData) {
258
- init.method = "POST";
259
- init.body = options.formData;
283
+ const swapEvent = await doSwap(preparationEvent, viewTransition, defaultSwap);
284
+ moveToLocation(swapEvent.to, swapEvent.from, options, historyState);
285
+ triggerEvent(TRANSITION_AFTER_SWAP);
286
+ if (fallback === "animate" && !skipTransition) {
287
+ animate("new").then(() => viewTransitionFinished());
260
288
  }
261
- const response = await fetchHTML(href, init);
262
- if (response === null) {
263
- location.href = href;
289
+ }
290
+ async function transition(direction, from, to, options, historyState) {
291
+ const navigationType = historyState ? "traverse" : options.history === "replace" ? "replace" : "push";
292
+ if (samePage(from, to) && !options.formData) {
293
+ if (navigationType !== "traverse") {
294
+ updateScrollPosition({ scrollX, scrollY });
295
+ }
296
+ moveToLocation(to, from, options, historyState);
264
297
  return;
265
298
  }
266
- if (response.redirected) {
267
- toLocation = new URL(response.redirected);
268
- }
269
- parser ??= new DOMParser();
270
- const newDocument = parser.parseFromString(response.html, response.mediaType);
271
- newDocument.querySelectorAll("noscript").forEach((el) => el.remove());
272
- if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]') && !options.formData) {
273
- location.href = href;
299
+ const prepEvent = await doPreparation(
300
+ from,
301
+ to,
302
+ direction,
303
+ navigationType,
304
+ options.sourceElement,
305
+ options.info,
306
+ options.formData,
307
+ defaultLoader
308
+ );
309
+ if (prepEvent.defaultPrevented) {
310
+ location.href = to.href;
274
311
  return;
275
312
  }
276
- if (import.meta.env.DEV)
277
- await prepareForClientOnlyComponents(newDocument, toLocation);
278
- if (!popState) {
279
- history.replaceState({ ...history.state, scrollX, scrollY }, "");
313
+ function pageMustReload(preparationEvent) {
314
+ return preparationEvent.to.hash === "" || !samePage(preparationEvent.from, preparationEvent.to) || preparationEvent.sourceElement instanceof HTMLFormElement;
315
+ }
316
+ async function defaultLoader(preparationEvent) {
317
+ if (pageMustReload(preparationEvent)) {
318
+ const href = preparationEvent.to.href;
319
+ const init = {};
320
+ if (preparationEvent.formData) {
321
+ init.method = "POST";
322
+ init.body = preparationEvent.formData;
323
+ }
324
+ const response = await fetchHTML(href, init);
325
+ if (response === null) {
326
+ preparationEvent.preventDefault();
327
+ return;
328
+ }
329
+ if (response.redirected) {
330
+ preparationEvent.to = new URL(response.redirected);
331
+ }
332
+ parser ??= new DOMParser();
333
+ preparationEvent.newDocument = parser.parseFromString(response.html, response.mediaType);
334
+ preparationEvent.newDocument.querySelectorAll("noscript").forEach((el) => el.remove());
335
+ if (!preparationEvent.newDocument.querySelector('[name="astro-view-transitions-enabled"]') && !preparationEvent.formData) {
336
+ preparationEvent.preventDefault();
337
+ return;
338
+ }
339
+ const links = preloadStyleLinks(preparationEvent.newDocument);
340
+ links.length && await Promise.all(links);
341
+ if (import.meta.env.DEV)
342
+ await prepareForClientOnlyComponents(preparationEvent.newDocument, preparationEvent.to);
343
+ } else {
344
+ preparationEvent.newDocument = document;
345
+ return;
346
+ }
280
347
  }
281
- document.documentElement.dataset.astroTransition = direction;
348
+ skipTransition = false;
282
349
  if (supportsViewTransitions) {
283
- finished = document.startViewTransition(
284
- () => updateDOM(newDocument, toLocation, options, popState)
285
- ).finished;
350
+ viewTransition = document.startViewTransition(
351
+ async () => await updateDOM(prepEvent, options, historyState)
352
+ );
286
353
  } else {
287
- finished = updateDOM(newDocument, toLocation, options, popState, getFallback());
354
+ const updateDone = (async () => {
355
+ await new Promise((r) => setTimeout(r));
356
+ await updateDOM(prepEvent, options, historyState, getFallback());
357
+ })();
358
+ viewTransition = {
359
+ updateCallbackDone: updateDone,
360
+ // this is about correct
361
+ ready: updateDone,
362
+ // good enough
363
+ finished: new Promise((r) => viewTransitionFinished = r),
364
+ // see end of updateDOM
365
+ skipTransition: () => {
366
+ skipTransition = true;
367
+ }
368
+ };
288
369
  }
289
- try {
290
- await finished;
291
- } finally {
370
+ viewTransition.ready.then(async () => {
292
371
  await runScripts();
293
372
  onPageLoad();
294
373
  announce();
295
- }
374
+ });
375
+ viewTransition.finished.then(() => {
376
+ document.documentElement.removeAttribute(DIRECTION_ATTR);
377
+ document.documentElement.removeAttribute(OLD_NEW_ATTR);
378
+ });
379
+ await viewTransition.ready;
296
380
  }
297
381
  let navigateOnServerWarned = false;
298
- function navigate(href, options) {
382
+ async function navigate(href, options) {
299
383
  if (inBrowser === false) {
300
384
  if (!navigateOnServerWarned) {
301
385
  const warning = new Error(
@@ -311,45 +395,28 @@ function navigate(href, options) {
311
395
  location.href = href;
312
396
  return;
313
397
  }
314
- const toLocation = new URL(href, location.href);
315
- if (location.origin === toLocation.origin && samePage(toLocation) && !options?.formData) {
316
- moveToLocation(toLocation, options?.history === "replace", true);
317
- } else {
318
- transition("forward", toLocation, options ?? {});
319
- }
398
+ await transition("forward", originalLocation, new URL(href, location.href), options ?? {});
320
399
  }
321
400
  function onPopState(ev) {
322
401
  if (!transitionEnabledOnThisPage() && ev.state) {
323
- if (history.scrollRestoration) {
324
- history.scrollRestoration = "manual";
325
- }
326
402
  location.reload();
327
403
  return;
328
404
  }
329
405
  if (ev.state === null) {
330
- if (history.scrollRestoration) {
331
- history.scrollRestoration = "auto";
332
- }
333
406
  return;
334
407
  }
335
- if (history.scrollRestoration) {
336
- history.scrollRestoration = "manual";
337
- }
338
408
  const state = history.state;
339
- if (state.intraPage) {
340
- scrollTo(state.scrollX, state.scrollY);
341
- } else {
342
- const nextIndex = state.index;
343
- const direction = nextIndex > currentHistoryIndex ? "forward" : "back";
344
- currentHistoryIndex = nextIndex;
345
- transition(direction, new URL(location.href), {}, state);
346
- }
409
+ const nextIndex = state.index;
410
+ const direction = nextIndex > currentHistoryIndex ? "forward" : "back";
411
+ currentHistoryIndex = nextIndex;
412
+ transition(direction, originalLocation, new URL(location.href), {}, state);
347
413
  }
348
414
  const onScroll = () => {
349
415
  updateScrollPosition({ scrollX, scrollY });
350
416
  };
351
417
  if (inBrowser) {
352
418
  if (supportsViewTransitions || getFallback() !== "none") {
419
+ originalLocation = new URL(location.href);
353
420
  addEventListener("popstate", onPopState);
354
421
  addEventListener("load", onPageLoad);
355
422
  if ("onscrollend" in window)
@@ -404,5 +471,6 @@ async function prepareForClientOnlyComponents(newDocument, toLocation) {
404
471
  export {
405
472
  navigate,
406
473
  supportsViewTransitions,
407
- transitionEnabledOnThisPage
474
+ transitionEnabledOnThisPage,
475
+ updateScrollPosition
408
476
  };
@@ -0,0 +1,10 @@
1
+ export type Fallback = 'none' | 'animate' | 'swap';
2
+ export type Direction = 'forward' | 'back';
3
+ export type NavigationTypeString = 'push' | 'replace' | 'traverse';
4
+ export type Options = {
5
+ history?: 'auto' | 'push' | 'replace';
6
+ info?: any;
7
+ state?: any;
8
+ formData?: FormData;
9
+ sourceElement?: Element;
10
+ };
File without changes
@@ -23,7 +23,14 @@ function astroTransitions({ settings }) {
23
23
  }
24
24
  if (id === resolvedVirtualClientModuleId) {
25
25
  return `
26
- export * from "astro/transitions/router";
26
+ export { navigate, supportsViewTransitions, transitionEnabledOnThisPage } from "astro/transitions/router";
27
+ export * from "astro/transitions/types";
28
+ export {
29
+ TRANSITION_BEFORE_PREPARATION, isTransitionBeforePreparationEvent, TransitionBeforePreparationEvent,
30
+ TRANSITION_AFTER_PREPARATION,
31
+ TRANSITION_BEFORE_SWAP, isTransitionBeforeSwapEvent, TransitionBeforeSwapEvent,
32
+ TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD
33
+ } from "astro/transitions/events";
27
34
  `;
28
35
  }
29
36
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "3.5.6",
3
+ "version": "3.6.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",
@@ -78,7 +78,9 @@
78
78
  "default": "./dist/core/middleware/namespace.js"
79
79
  },
80
80
  "./transitions": "./dist/transitions/index.js",
81
+ "./transitions/events": "./dist/transitions/events.js",
81
82
  "./transitions/router": "./dist/transitions/router.js",
83
+ "./transitions/types": "./dist/transitions/types.js",
82
84
  "./prefetch": "./dist/prefetch/index.js",
83
85
  "./i18n": "./dist/i18n/index.js"
84
86
  },
@@ -165,8 +167,8 @@
165
167
  "yargs-parser": "^21.1.1",
166
168
  "zod": "^3.22.4",
167
169
  "@astrojs/markdown-remark": "3.5.0",
168
- "@astrojs/internal-helpers": "0.2.1",
169
- "@astrojs/telemetry": "3.0.4"
170
+ "@astrojs/telemetry": "3.0.4",
171
+ "@astrojs/internal-helpers": "0.2.1"
170
172
  },
171
173
  "optionalDependencies": {
172
174
  "sharp": "^0.32.5"