swup 4.2.0 → 4.3.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.
Files changed (65) hide show
  1. package/dist/Swup.cjs +1 -1
  2. package/dist/Swup.cjs.map +1 -1
  3. package/dist/Swup.modern.js +1 -1
  4. package/dist/Swup.modern.js.map +1 -1
  5. package/dist/Swup.module.js +1 -1
  6. package/dist/Swup.module.js.map +1 -1
  7. package/dist/Swup.umd.js +1 -1
  8. package/dist/Swup.umd.js.map +1 -1
  9. package/dist/types/Swup.d.ts +119 -119
  10. package/dist/types/__test__/index.test.d.ts +1 -1
  11. package/dist/types/config/version.d.ts +5 -5
  12. package/dist/types/helpers/Location.d.ts +24 -24
  13. package/dist/types/helpers/__test__/matchPath.test.d.ts +1 -1
  14. package/dist/types/helpers/classify.d.ts +2 -2
  15. package/dist/types/helpers/createHistoryRecord.d.ts +9 -2
  16. package/dist/types/helpers/delegateEvent.d.ts +7 -7
  17. package/dist/types/helpers/getCurrentUrl.d.ts +4 -4
  18. package/dist/types/helpers/matchPath.d.ts +4 -4
  19. package/dist/types/helpers/updateHistoryRecord.d.ts +2 -2
  20. package/dist/types/helpers.d.ts +7 -7
  21. package/dist/types/index.d.ts +14 -14
  22. package/dist/types/modules/Cache.d.ts +34 -34
  23. package/dist/types/modules/Classes.d.ts +13 -13
  24. package/dist/types/modules/Hooks.d.ts +268 -250
  25. package/dist/types/modules/Visit.d.ts +85 -77
  26. package/dist/types/modules/__test__/cache.test.d.ts +1 -1
  27. package/dist/types/modules/__test__/{delegateEvent.d.ts → delegateEvent.test.d.ts} +1 -1
  28. package/dist/types/modules/__test__/hooks.test.d.ts +1 -1
  29. package/dist/types/modules/__test__/plugins.test.d.ts +1 -1
  30. package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -1
  31. package/dist/types/modules/animatePageIn.d.ts +6 -6
  32. package/dist/types/modules/animatePageOut.d.ts +6 -6
  33. package/dist/types/modules/awaitAnimations.d.ts +19 -19
  34. package/dist/types/modules/fetchPage.d.ts +27 -29
  35. package/dist/types/modules/getAnchorElement.d.ts +9 -9
  36. package/dist/types/modules/navigate.d.ts +41 -35
  37. package/dist/types/modules/plugins.d.ts +26 -26
  38. package/dist/types/modules/renderPage.d.ts +7 -7
  39. package/dist/types/modules/replaceContent.d.ts +13 -13
  40. package/dist/types/modules/resolveUrl.d.ts +14 -14
  41. package/dist/types/modules/scrollToContent.d.ts +6 -6
  42. package/dist/types/utils/index.d.ts +20 -20
  43. package/dist/types/utils.d.ts +1 -1
  44. package/package.json +9 -3
  45. package/src/Swup.ts +7 -6
  46. package/src/config/version.ts +1 -2
  47. package/src/helpers/createHistoryRecord.ts +9 -1
  48. package/src/helpers/matchPath.ts +1 -1
  49. package/src/helpers/updateHistoryRecord.ts +4 -2
  50. package/src/modules/Cache.ts +2 -2
  51. package/src/modules/Classes.ts +1 -1
  52. package/src/modules/Hooks.ts +91 -39
  53. package/src/modules/Visit.ts +13 -0
  54. package/src/modules/__test__/cache.test.ts +3 -3
  55. package/src/modules/__test__/hooks.test.ts +12 -13
  56. package/src/modules/animatePageIn.ts +1 -1
  57. package/src/modules/animatePageOut.ts +2 -2
  58. package/src/modules/awaitAnimations.ts +1 -1
  59. package/src/modules/fetchPage.ts +7 -5
  60. package/src/modules/navigate.ts +23 -4
  61. package/src/modules/plugins.ts +3 -3
  62. package/src/modules/renderPage.ts +1 -5
  63. package/src/modules/replaceContent.ts +13 -0
  64. package/src/utils/index.ts +5 -4
  65. /package/src/modules/__test__/{delegateEvent.ts → delegateEvent.test.ts} +0 -0
package/src/Swup.ts CHANGED
@@ -21,6 +21,7 @@ import { renderPage } from './modules/renderPage.js';
21
21
  import { use, unuse, findPlugin, Plugin } from './modules/plugins.js';
22
22
  import { isSameResolvedUrl, resolveUrl } from './modules/resolveUrl.js';
23
23
  import { nextTick } from './utils.js';
24
+ import { HistoryState } from './helpers/createHistoryRecord.js';
24
25
 
25
26
  /** Options for customizing swup's behavior. */
26
27
  export type Options = {
@@ -47,7 +48,7 @@ export type Options = {
47
48
  /** Rewrite URLs before loading them. */
48
49
  resolveUrl: (url: string) => string;
49
50
  /** Callback for telling swup to ignore certain popstate events. */
50
- skipPopStateHandling: (event: any) => boolean;
51
+ skipPopStateHandling: (event: PopStateEvent) => boolean;
51
52
  };
52
53
 
53
54
  const defaults: Options = {
@@ -56,7 +57,7 @@ const defaults: Options = {
56
57
  animationScope: 'html',
57
58
  cache: true,
58
59
  containers: ['#swup'],
59
- ignoreVisit: (url, { el, event } = {}) => !!el?.closest('[data-no-swup]'),
60
+ ignoreVisit: (url, { el } = {}) => !!el?.closest('[data-no-swup]'),
60
61
  linkSelector: 'a[href]',
61
62
  linkToSelf: 'scroll',
62
63
  plugins: [],
@@ -65,7 +66,7 @@ const defaults: Options = {
65
66
  'X-Requested-With': 'swup',
66
67
  'Accept': 'text/html, application/xhtml+xml'
67
68
  },
68
- skipPopStateHandling: (event) => event.state?.source !== 'swup'
69
+ skipPopStateHandling: (event) => (event.state as HistoryState)?.source !== 'swup'
69
70
  };
70
71
 
71
72
  /** Swup page transition library. */
@@ -101,7 +102,7 @@ export default class Swup {
101
102
  findPlugin = findPlugin;
102
103
 
103
104
  /** Log a message. Has no effect unless debug plugin is installed */
104
- log: (message: string, context?: any) => void = () => {};
105
+ log: (message: string, context?: unknown) => void = () => {};
105
106
 
106
107
  /** Navigate to a new URL */
107
108
  navigate = navigate;
@@ -296,7 +297,7 @@ export default class Swup {
296
297
  }
297
298
 
298
299
  protected handlePopState(event: PopStateEvent) {
299
- const href = event.state?.url ?? location.href;
300
+ const href: string = (event.state as HistoryState)?.url ?? location.href;
300
301
 
301
302
  // Exit early if this event should be ignored
302
303
  if (this.options.skipPopStateHandling(event)) {
@@ -324,7 +325,7 @@ export default class Swup {
324
325
  this.visit.history.popstate = true;
325
326
 
326
327
  // Determine direction of history visit
327
- const index = Number(event.state?.index);
328
+ const index = Number((event.state as HistoryState)?.index);
328
329
  if (index) {
329
330
  const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
330
331
  this.visit.history.direction = direction;
@@ -6,8 +6,7 @@
6
6
  // export { version as default } from '../../package.json';
7
7
 
8
8
  // This will work in microbundle + webpack 5, but won't treeshake in webpack 4
9
- // Ignore next line in TypeScript as package.json is outside of rootDir
10
- // @ts-ignore
9
+ // @ts-ignore: package.json is outside of rootDir
11
10
  import pckg from '../../package.json';
12
11
 
13
12
  export default pckg.version;
@@ -1,12 +1,20 @@
1
1
  import { getCurrentUrl } from './getCurrentUrl.js';
2
2
 
3
+ export interface HistoryState {
4
+ url: string;
5
+ source: 'swup';
6
+ random: number;
7
+ index?: number;
8
+ [key: string]: unknown;
9
+ }
10
+
3
11
  /** Create a new history record with a custom swup identifier. */
4
12
  export const createHistoryRecord = (
5
13
  url: string,
6
14
  customData: Record<string, unknown> = {}
7
15
  ): void => {
8
16
  url = url || getCurrentUrl({ hash: true });
9
- const data = {
17
+ const data: HistoryState = {
10
18
  url,
11
19
  random: Math.random(),
12
20
  source: 'swup',
@@ -18,6 +18,6 @@ export const matchPath = <P extends object = object>(
18
18
  try {
19
19
  return match<P>(path, options);
20
20
  } catch (error) {
21
- throw new Error(`[swup] Error parsing path "${path}":\n${error}`);
21
+ throw new Error(`[swup] Error parsing path "${String(path)}":\n${String(error)}`);
22
22
  }
23
23
  };
@@ -1,3 +1,4 @@
1
+ import { HistoryState } from './createHistoryRecord.js';
1
2
  import { getCurrentUrl } from './getCurrentUrl.js';
2
3
 
3
4
  /** Update the current history record with a custom swup identifier. */
@@ -6,8 +7,9 @@ export const updateHistoryRecord = (
6
7
  customData: Record<string, unknown> = {}
7
8
  ): void => {
8
9
  url = url || getCurrentUrl({ hash: true });
9
- const data = {
10
- ...history.state,
10
+ const state = (history.state as HistoryState) || {};
11
+ const data: HistoryState = {
12
+ ...state,
11
13
  url,
12
14
  random: Math.random(),
13
15
  source: 'swup',
@@ -53,7 +53,7 @@ export class Cache {
53
53
  }
54
54
 
55
55
  /** Update a cache record, overwriting or adding custom data. */
56
- update(url: string, payload: Record<string, any>) {
56
+ update(url: string, payload: object) {
57
57
  url = this.resolve(url);
58
58
  const page = { ...this.get(url), ...payload, url } as CacheData;
59
59
  this.pages.set(url, page);
@@ -67,7 +67,7 @@ export class Cache {
67
67
  /** Empty the cache. */
68
68
  clear(): void {
69
69
  this.pages.clear();
70
- this.swup.hooks.callSync('cache:clear');
70
+ this.swup.hooks.callSync('cache:clear', undefined);
71
71
  }
72
72
 
73
73
  /** Remove all cache entries that return true for a given predicate function. */
@@ -23,7 +23,7 @@ export class Classes {
23
23
 
24
24
  protected get targets(): HTMLElement[] {
25
25
  if (!this.selector.trim()) return [];
26
- return queryAll(this.selector) as HTMLElement[];
26
+ return queryAll(this.selector);
27
27
  }
28
28
 
29
29
  add(...classes: string[]): void {
@@ -34,19 +34,35 @@ export interface HookDefinitions {
34
34
  'visit:end': undefined;
35
35
  }
36
36
 
37
+ export interface HookReturnValues {
38
+ 'content:scroll': Promise<boolean>;
39
+ 'fetch:request': Promise<Response>;
40
+ 'page:load': Promise<PageData>;
41
+ 'scroll:top': boolean;
42
+ 'scroll:anchor': boolean;
43
+ }
44
+
37
45
  export type HookArguments<T extends HookName> = HookDefinitions[T];
38
46
 
39
47
  export type HookName = keyof HookDefinitions;
40
48
 
41
- /** A hook handler. */
49
+ /** A generic hook handler. */
42
50
  export type Handler<T extends HookName> = (
51
+ /** Context about the current visit. */
52
+ visit: Visit,
53
+ /** Local arguments passed into the handler. */
54
+ args: HookArguments<T>
55
+ ) => Promise<unknown> | unknown;
56
+
57
+ /** A default hook handler with an expected return type. */
58
+ export type DefaultHandler<T extends HookName> = (
43
59
  /** Context about the current visit. */
44
60
  visit: Visit,
45
61
  /** Local arguments passed into the handler. */
46
62
  args: HookArguments<T>,
47
63
  /** Default handler to be executed. Available if replacing an internal hook handler. */
48
- defaultHandler?: Handler<T>
49
- ) => Promise<any> | any;
64
+ defaultHandler?: DefaultHandler<T>
65
+ ) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise<unknown> | unknown;
50
66
 
51
67
  export type Handlers = {
52
68
  [K in HookName]: Handler<K>[];
@@ -67,11 +83,14 @@ export type HookOptions = {
67
83
  replace?: boolean;
68
84
  };
69
85
 
70
- export type HookRegistration<T extends HookName> = {
86
+ export type HookRegistration<
87
+ T extends HookName,
88
+ H extends Handler<T> | DefaultHandler<T> = Handler<T>
89
+ > = {
71
90
  id: number;
72
91
  hook: T;
73
- handler: Handler<T>;
74
- defaultHandler?: Handler<T>;
92
+ handler: H;
93
+ defaultHandler?: DefaultHandler<T>;
75
94
  } & HookOptions;
76
95
 
77
96
  type HookLedger<T extends HookName> = Map<Handler<T>, HookRegistration<T>>;
@@ -183,12 +202,18 @@ export class Hooks {
183
202
  * - `replace`: Replace the default handler with this handler
184
203
  * @returns A function to unregister the handler
185
204
  */
186
- on<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
187
- on<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
188
- on<T extends HookName>(
205
+
206
+ // Overload: replacing default handler
207
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: DefaultHandler<T>, options: O & { replace: true }): HookUnregister; // prettier-ignore
208
+ // Overload: passed in handler options
209
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: Handler<T>, options: O): HookUnregister; // prettier-ignore
210
+ // Overload: no handler options
211
+ on<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister; // prettier-ignore
212
+ // Implementation
213
+ on<T extends HookName, O extends HookOptions>(
189
214
  hook: T,
190
- handler: Handler<T>,
191
- options: HookOptions = {}
215
+ handler: O['replace'] extends true ? DefaultHandler<T> : Handler<T>,
216
+ options: Partial<O> = {}
192
217
  ): HookUnregister {
193
218
  const ledger = this.get(hook);
194
219
  if (!ledger) {
@@ -212,8 +237,11 @@ export class Hooks {
212
237
  * @returns A function to unregister the handler
213
238
  * @see on
214
239
  */
215
- before<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
240
+ // Overload: passed in handler options
216
241
  before<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
242
+ // Overload: no handler options
243
+ before<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
244
+ // Implementation
217
245
  before<T extends HookName>(
218
246
  hook: T,
219
247
  handler: Handler<T>,
@@ -231,11 +259,14 @@ export class Hooks {
231
259
  * @returns A function to unregister the handler
232
260
  * @see on
233
261
  */
234
- replace<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
235
- replace<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
262
+ // Overload: passed in handler options
263
+ replace<T extends HookName>(hook: T, handler: DefaultHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore
264
+ // Overload: no handler options
265
+ replace<T extends HookName>(hook: T, handler: DefaultHandler<T>): HookUnregister; // prettier-ignore
266
+ // Implementation
236
267
  replace<T extends HookName>(
237
268
  hook: T,
238
- handler: Handler<T>,
269
+ handler: DefaultHandler<T>,
239
270
  options: HookOptions = {}
240
271
  ): HookUnregister {
241
272
  return this.on(hook, handler, { ...options, replace: true });
@@ -249,8 +280,11 @@ export class Hooks {
249
280
  * @param options Any other event options (see `hooks.on()` for details)
250
281
  * @see on
251
282
  */
252
- once<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
283
+ // Overload: passed in handler options
253
284
  once<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
285
+ // Overload: no handler options
286
+ once<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
287
+ // Implementation
254
288
  once<T extends HookName>(
255
289
  hook: T,
256
290
  handler: Handler<T>,
@@ -265,9 +299,12 @@ export class Hooks {
265
299
  * @param handler The handler function that was registered.
266
300
  * If omitted, all handlers for the hook will be removed.
267
301
  */
302
+ // Overload: unregister a specific handler
303
+ off<T extends HookName>(hook: T, handler: Handler<T> | DefaultHandler<T>): void;
304
+ // Overload: unregister all handlers
268
305
  off<T extends HookName>(hook: T): void;
269
- off<T extends HookName>(hook: T, handler: Handler<T>): void;
270
- off<T extends HookName>(hook: T, handler?: Handler<T>): void {
306
+ // Implementation
307
+ off<T extends HookName>(hook: T, handler?: Handler<T> | DefaultHandler<T>): void {
271
308
  const ledger = this.get(hook);
272
309
  if (ledger && handler) {
273
310
  const deleted = ledger.delete(handler);
@@ -289,9 +326,9 @@ export class Hooks {
289
326
  */
290
327
  async call<T extends HookName>(
291
328
  hook: T,
292
- args?: HookArguments<T>,
293
- defaultHandler?: Handler<T>
294
- ): Promise<any> {
329
+ args: HookArguments<T>,
330
+ defaultHandler?: DefaultHandler<T>
331
+ ): Promise<Awaited<ReturnType<DefaultHandler<T>>>> {
295
332
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
296
333
  await this.run(before, args);
297
334
  const [result] = await this.run(handler, args);
@@ -310,9 +347,9 @@ export class Hooks {
310
347
  */
311
348
  callSync<T extends HookName>(
312
349
  hook: T,
313
- args?: HookArguments<T>,
314
- defaultHandler?: Handler<T>
315
- ): any {
350
+ args: HookArguments<T>,
351
+ defaultHandler?: DefaultHandler<T>
352
+ ): ReturnType<DefaultHandler<T>> {
316
353
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
317
354
  this.runSync(before, args);
318
355
  const [result] = this.runSync(handler, args);
@@ -326,10 +363,16 @@ export class Hooks {
326
363
  * @param registrations The registrations (handler + options) to execute
327
364
  * @param args Arguments to pass to the handler
328
365
  */
329
- protected async run<T extends HookName>(
330
- registrations: HookRegistration<T>[],
331
- args?: HookArguments<T>
332
- ): Promise<any> {
366
+
367
+ // Overload: running DefaultHandler: expect DefaultHandler return type
368
+ protected async run<T extends HookName>(registrations: HookRegistration<T, DefaultHandler<T>>[], args: HookArguments<T>): Promise<Awaited<ReturnType<DefaultHandler<T>>>[]>; // prettier-ignore
369
+ // Overload: running user handler: expect no specific type
370
+ protected async run<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore
371
+ // Implementation
372
+ protected async run<T extends HookName, R extends HookRegistration<T>[]>(
373
+ registrations: R,
374
+ args: HookArguments<T>
375
+ ): Promise<Awaited<ReturnType<DefaultHandler<T>>> | unknown[]> {
333
376
  const results = [];
334
377
  for (const { hook, handler, defaultHandler, once } of registrations) {
335
378
  const result = await runAsPromise(handler, [this.swup.visit, args, defaultHandler]);
@@ -346,13 +389,19 @@ export class Hooks {
346
389
  * @param registrations The registrations (handler + options) to execute
347
390
  * @param args Arguments to pass to the handler
348
391
  */
349
- protected runSync<T extends HookName>(
350
- registrations: HookRegistration<T>[],
351
- args?: HookArguments<T>
352
- ): any[] {
392
+
393
+ // Overload: running DefaultHandler: expect DefaultHandler return type
394
+ protected runSync<T extends HookName>(registrations: HookRegistration<T, DefaultHandler<T>>[], args: HookArguments<T> ): ReturnType<DefaultHandler<T>>[]; // prettier-ignore
395
+ // Overload: running user handler: expect no specific type
396
+ protected runSync<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): unknown[]; // prettier-ignore
397
+ // Implementation
398
+ protected runSync<T extends HookName, R extends HookRegistration<T>[]>(
399
+ registrations: R,
400
+ args: HookArguments<T>
401
+ ): (ReturnType<DefaultHandler<T>> | unknown)[] {
353
402
  const results = [];
354
403
  for (const { hook, handler, defaultHandler, once } of registrations) {
355
- const result = handler(this.swup.visit, args as HookArguments<T>, defaultHandler);
404
+ const result = (handler as DefaultHandler<T>)(this.swup.visit, args, defaultHandler);
356
405
  results.push(result);
357
406
  if (isPromise(result)) {
358
407
  console.warn(
@@ -374,30 +423,33 @@ export class Hooks {
374
423
  * @returns An object with the handlers sorted into `before` and `after` arrays,
375
424
  * as well as a flag indicating if the original handler was replaced
376
425
  */
377
- protected getHandlers<T extends HookName>(hook: T, defaultHandler?: Handler<T>) {
426
+ protected getHandlers<T extends HookName>(hook: T, defaultHandler?: DefaultHandler<T>) {
378
427
  const ledger = this.get(hook);
379
428
  if (!ledger) {
380
429
  return { found: false, before: [], handler: [], after: [], replaced: false };
381
430
  }
382
431
 
383
- const sort = this.sortRegistrations;
384
432
  const registrations = Array.from(ledger.values());
385
433
 
434
+ // Let TypeScript know that replaced handlers are default handlers by filtering to true
435
+ const def = (T: HookRegistration<T>): T is HookRegistration<T, DefaultHandler<T>> => true;
436
+ const sort = this.sortRegistrations;
437
+
386
438
  // Filter into before, after, and replace handlers
387
439
  const before = registrations.filter(({ before, replace }) => before && !replace).sort(sort);
388
- const replace = registrations.filter(({ replace }) => replace).sort(sort);
440
+ const replace = registrations.filter(({ replace }) => replace).filter(def).sort(sort); // prettier-ignore
389
441
  const after = registrations.filter(({ before, replace }) => !before && !replace).sort(sort);
390
442
  const replaced = replace.length > 0;
391
443
 
392
444
  // Define main handler registration
393
- // This is an array to allow passing it into hooks.run() directly
394
- let handler: HookRegistration<T>[] = [];
445
+ // Created as HookRegistration[] array to allow passing it into hooks.run() directly
446
+ let handler: HookRegistration<T, DefaultHandler<T>>[] = [];
395
447
  if (defaultHandler) {
396
448
  handler = [{ id: 0, hook, handler: defaultHandler }];
397
449
  if (replaced) {
398
450
  const index = replace.length - 1;
399
451
  const replacingHandler = replace[index].handler;
400
- const createDefaultHandler = (index: number): Handler<T> | undefined => {
452
+ const createDefaultHandler = (index: number): DefaultHandler<T> | undefined => {
401
453
  const next = replace[index - 1];
402
454
  if (next) {
403
455
  return (visit, args) =>
@@ -13,6 +13,8 @@ export interface Visit {
13
13
  animation: VisitAnimation;
14
14
  /** What triggered this visit */
15
15
  trigger: VisitTrigger;
16
+ /** Cache behavior for this visit */
17
+ cache: VisitCache;
16
18
  /** Browser history behavior on this visit */
17
19
  history: VisitHistory;
18
20
  /** Scroll behavior on this visit */
@@ -60,6 +62,13 @@ export interface VisitTrigger {
60
62
  event?: Event;
61
63
  }
62
64
 
65
+ export interface VisitCache {
66
+ /** Whether this visit will try to load the requested page from cache. */
67
+ read: boolean;
68
+ /** Whether this visit will save the loaded page in cache. */
69
+ write: boolean;
70
+ }
71
+
63
72
  export interface VisitHistory {
64
73
  /** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
65
74
  action: HistoryAction;
@@ -112,6 +121,10 @@ export function createVisit(
112
121
  el,
113
122
  event
114
123
  },
124
+ cache: {
125
+ read: this.options.cache,
126
+ write: this.options.cache
127
+ },
115
128
  history: {
116
129
  action,
117
130
  popstate: false,
@@ -90,19 +90,19 @@ describe('Cache', () => {
90
90
 
91
91
  swup.hooks.on('cache:set', (_, { page }) => {
92
92
  const ttl: CacheTtlData = { ttl: 1000, created: now };
93
- cache.update(page.url, ttl as AugmentedCacheData);
93
+ cache.update(page.url, ttl);
94
94
  });
95
95
 
96
96
  cache.set('/page', { url: '/page', html: '' });
97
97
 
98
- const page = cache.get('/page') as AugmentedCacheData;
98
+ const page = cache.get('/page');
99
99
 
100
100
  expect(page).toEqual({ url: '/page', html: '', ttl: 1000, created: now });
101
101
  });
102
102
 
103
103
  it('should allow manual pruning', () => {
104
104
  swup.hooks.on('cache:set', (_, { page }) => {
105
- cache.update(page.url, { index: cache.size } as AugmentedCacheData);
105
+ cache.update(page.url, { index: cache.size });
106
106
  });
107
107
 
108
108
  cache.set(page1.url, page1);
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import Swup from '../../Swup.js';
3
- import { Handler, Hooks } from '../Hooks.js';
3
+ import { DefaultHandler, Handler, Hooks } from '../Hooks.js';
4
4
  import { Visit } from '../Visit.js';
5
5
 
6
6
  describe('Hook registry', () => {
@@ -37,14 +37,14 @@ describe('Hook registry', () => {
37
37
  swup.hooks.on('enable', handler1);
38
38
  swup.hooks.on('enable', handler2);
39
39
 
40
- await swup.hooks.call('enable');
40
+ await swup.hooks.call('enable', undefined);
41
41
 
42
42
  expect(handler1).toBeCalledTimes(1);
43
43
  expect(handler2).toBeCalledTimes(1);
44
44
 
45
45
  swup.hooks.off('enable', handler2);
46
46
 
47
- await swup.hooks.call('enable');
47
+ await swup.hooks.call('enable', undefined);
48
48
 
49
49
  expect(handler1).toBeCalledTimes(2);
50
50
  expect(handler2).toBeCalledTimes(1);
@@ -60,14 +60,14 @@ describe('Hook registry', () => {
60
60
 
61
61
  expect(unregister1).toBeTypeOf('function');
62
62
 
63
- await swup.hooks.call('enable');
63
+ await swup.hooks.call('enable', undefined);
64
64
 
65
65
  expect(handler1).toBeCalledTimes(1);
66
66
  expect(handler2).toBeCalledTimes(1);
67
67
 
68
68
  unregister2();
69
69
 
70
- await swup.hooks.call('enable');
70
+ await swup.hooks.call('enable', undefined);
71
71
 
72
72
  expect(handler1).toBeCalledTimes(2);
73
73
  expect(handler2).toBeCalledTimes(1);
@@ -79,7 +79,7 @@ describe('Hook registry', () => {
79
79
 
80
80
  swup.hooks.on('enable', handler);
81
81
 
82
- await swup.hooks.call('enable');
82
+ await swup.hooks.call('enable', undefined);
83
83
 
84
84
  expect(handler).toBeCalledTimes(1);
85
85
  });
@@ -303,18 +303,17 @@ describe('Types', () => {
303
303
  const swup = new Swup();
304
304
 
305
305
  // @ts-expect-no-error
306
- swup.hooks.on(
307
- 'history:popstate',
308
- (visit: Visit, { event }: { event: PopStateEvent }) => {}
309
- );
306
+ swup.hooks.on('history:popstate', (visit: Visit, { event }: { event: PopStateEvent }) => {});
310
307
  // @ts-expect-no-error
311
308
  await swup.hooks.call('history:popstate', { event: new PopStateEvent('') });
312
309
 
313
- // @ts-expect-error
310
+ // @ts-expect-error: first arg must be Visit object
314
311
  swup.hooks.on('history:popstate', ({ event: MouseEvent }) => {});
315
- // @ts-expect-error
312
+ // @ts-expect-error: event arg must be PopStateEvent
316
313
  swup.hooks.on('history:popstate', (visit: Visit, { event }: { event: MouseEvent }) => {});
317
- // @ts-expect-error
314
+ // @ts-expect-error: event arg must be PopStateEvent
318
315
  await swup.hooks.call('history:popstate', { event: new MouseEvent('') });
316
+ // @ts-expect-error: handler arg must be optional: handler?
317
+ swup.hooks.replace('enable', (visit: Visit, args: undefined, handler: DefaultHandler<'enable'>) => {});
319
318
  });
320
319
  });
@@ -27,5 +27,5 @@ export const animatePageIn = async function (this: Swup) {
27
27
 
28
28
  await animation;
29
29
 
30
- await this.hooks.call('animation:in:end');
30
+ await this.hooks.call('animation:in:end', undefined);
31
31
  };
@@ -7,7 +7,7 @@ import { classify } from '../helpers.js';
7
7
  */
8
8
  export const animatePageOut = async function (this: Swup) {
9
9
  if (!this.visit.animation.animate) {
10
- await this.hooks.call('animation:skip');
10
+ await this.hooks.call('animation:skip', undefined);
11
11
  return;
12
12
  }
13
13
 
@@ -26,5 +26,5 @@ export const animatePageOut = async function (this: Swup) {
26
26
  await this.awaitAnimations({ selector: visit.animation.selector });
27
27
  });
28
28
 
29
- await this.hooks.call('animation:out:end');
29
+ await this.hooks.call('animation:out:end', undefined);
30
30
  };
@@ -150,7 +150,7 @@ export function getTransitionInfo(element: Element, expectedType?: AnimationType
150
150
  };
151
151
  }
152
152
 
153
- function isTransitionOrAnimationEvent(event: any): event is TransitionEvent | AnimationEvent {
153
+ function isTransitionOrAnimationEvent(event: Event): event is TransitionEvent | AnimationEvent {
154
154
  return [`${TRANSITION}end`, `${ANIMATION}end`].includes(event.type);
155
155
  }
156
156
 
@@ -10,13 +10,11 @@ export interface PageData {
10
10
  }
11
11
 
12
12
  /** Define how a page is fetched. */
13
- export interface FetchOptions extends RequestInit {
13
+ export interface FetchOptions extends Omit<RequestInit, 'cache'> {
14
14
  /** The request method. */
15
15
  method?: 'GET' | 'POST';
16
16
  /** The body of the request: raw string, form data object or URL params. */
17
17
  body?: string | FormData | URLSearchParams;
18
- /** The headers of the request: key/value object. */
19
- headers?: Record<string, string>;
20
18
  }
21
19
 
22
20
  export class FetchError extends Error {
@@ -66,8 +64,12 @@ export async function fetchPage(
66
64
  const { url: finalUrl } = Location.fromUrl(responseUrl);
67
65
  const page = { url: finalUrl, html };
68
66
 
69
- // Only save cache entry for non-redirects
70
- if (url === finalUrl) {
67
+ // Write to cache for safe methods and non-redirects
68
+ if (
69
+ this.visit.cache.write &&
70
+ (!options.method || options.method === 'GET') &&
71
+ url === finalUrl
72
+ ) {
71
73
  this.cache.set(page.url, page);
72
74
  }
73
75
 
@@ -1,11 +1,12 @@
1
1
  import Swup from '../Swup.js';
2
2
  import { createHistoryRecord, updateHistoryRecord, getCurrentUrl, Location } from '../helpers.js';
3
- import { FetchOptions } from './fetchPage.js';
3
+ import { FetchOptions, PageData } from './fetchPage.js';
4
4
  import { VisitInitOptions } from './Visit.js';
5
5
 
6
6
  export type HistoryAction = 'push' | 'replace';
7
7
  export type HistoryDirection = 'forwards' | 'backwards';
8
8
  export type NavigationToSelfAction = 'scroll' | 'navigate';
9
+ export type CacheControl = Partial<{ read: boolean; write: boolean }>;
9
10
 
10
11
  /** Define how to navigate to a page. */
11
12
  type NavigationOptions = {
@@ -15,6 +16,8 @@ type NavigationOptions = {
15
16
  animation?: string;
16
17
  /** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
17
18
  history?: HistoryAction;
19
+ /** Whether this visit should read from or write to the cache. */
20
+ cache?: CacheControl;
18
21
  };
19
22
 
20
23
  /**
@@ -83,14 +86,30 @@ export async function performNavigation(
83
86
  this.visit.animation.name = animation;
84
87
  }
85
88
 
89
+ // Sanitize cache option
90
+ if (typeof options.cache === 'object') {
91
+ this.visit.cache.read = options.cache.read ?? this.visit.cache.read;
92
+ this.visit.cache.write = options.cache.write ?? this.visit.cache.write;
93
+ } else if (options.cache !== undefined) {
94
+ this.visit.cache = { read: !!options.cache, write: !!options.cache };
95
+ }
96
+ // Delete this so that window.fetch doesn't mis-interpret it
97
+ delete options.cache;
98
+
86
99
  try {
87
- await this.hooks.call('visit:start');
100
+ await this.hooks.call('visit:start', undefined);
88
101
 
89
102
  // Begin loading page
90
103
  const pagePromise = this.hooks.call('page:load', { options }, async (visit, args) => {
91
- const cachedPage = this.cache.get(visit.to.url!);
92
- args.page = cachedPage || (await this.fetchPage(visit.to.url!, args.options));
104
+ // Read from cache
105
+ let cachedPage: PageData | undefined;
106
+ if (this.visit.cache.read) {
107
+ cachedPage = this.cache.get(visit.to.url);
108
+ }
109
+
110
+ args.page = cachedPage || (await this.fetchPage(visit.to.url, args.options));
93
111
  args.cache = !!cachedPage;
112
+
94
113
  return args.page;
95
114
  });
96
115