@real-router/preload-plugin 0.1.0 → 0.1.2
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/package.json +4 -5
- package/src/constants.ts +0 -12
- package/src/factory.ts +0 -30
- package/src/index.ts +0 -17
- package/src/network.ts +0 -23
- package/src/plugin.ts +0 -227
- package/src/types.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/preload-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Preload plugin — trigger data preloading on navigation intent (hover, touch)",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
22
|
-
"src"
|
|
21
|
+
"dist"
|
|
23
22
|
],
|
|
24
23
|
"repository": {
|
|
25
24
|
"type": "git",
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
},
|
|
46
45
|
"sideEffects": false,
|
|
47
46
|
"dependencies": {
|
|
48
|
-
"@real-router/core": "^0.
|
|
47
|
+
"@real-router/core": "^0.45.1"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"jsdom": "28.1.0"
|
|
@@ -55,7 +54,7 @@
|
|
|
55
54
|
"build": "tsdown --config-loader unrun",
|
|
56
55
|
"type-check": "tsc --noEmit",
|
|
57
56
|
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
58
|
-
"lint:package": "publint",
|
|
57
|
+
"lint:package": "bash ../../scripts/publint-filter.sh",
|
|
59
58
|
"lint:types": "attw --pack .",
|
|
60
59
|
"build:dist-only": "tsdown --config-loader unrun"
|
|
61
60
|
}
|
package/src/constants.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { PreloadPluginOptions } from "./types";
|
|
2
|
-
|
|
3
|
-
export const defaultOptions: Required<PreloadPluginOptions> = {
|
|
4
|
-
delay: 65,
|
|
5
|
-
networkAware: true,
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const GHOST_EVENT_THRESHOLD = 2500;
|
|
9
|
-
|
|
10
|
-
export const TOUCH_SCROLL_THRESHOLD = 10;
|
|
11
|
-
|
|
12
|
-
export const TOUCH_PRELOAD_DELAY = 100;
|
package/src/factory.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { getPluginApi } from "@real-router/core/api";
|
|
2
|
-
|
|
3
|
-
import { defaultOptions } from "./constants";
|
|
4
|
-
import { PreloadPlugin } from "./plugin";
|
|
5
|
-
|
|
6
|
-
import type { PreloadPluginOptions } from "./types";
|
|
7
|
-
import type { PluginFactory, Router } from "@real-router/core";
|
|
8
|
-
|
|
9
|
-
export function preloadPluginFactory(
|
|
10
|
-
opts?: Partial<PreloadPluginOptions>,
|
|
11
|
-
): PluginFactory {
|
|
12
|
-
const options: Required<PreloadPluginOptions> = {
|
|
13
|
-
...defaultOptions,
|
|
14
|
-
...opts,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
return function preloadPlugin(routerBase) {
|
|
18
|
-
if (typeof document === "undefined") {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const plugin = new PreloadPlugin(
|
|
23
|
-
routerBase as Router,
|
|
24
|
-
getPluginApi(routerBase),
|
|
25
|
-
options,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return plugin.getPlugin();
|
|
29
|
-
};
|
|
30
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/method-signature-style -- method syntax required for declaration merging overload (property syntax causes TS2717) */
|
|
2
|
-
import type { PreloadPluginOptions } from "./types";
|
|
3
|
-
import type { Params } from "@real-router/core";
|
|
4
|
-
|
|
5
|
-
export { preloadPluginFactory } from "./factory";
|
|
6
|
-
|
|
7
|
-
export type { PreloadPluginOptions } from "./types";
|
|
8
|
-
|
|
9
|
-
declare module "@real-router/core" {
|
|
10
|
-
interface Route {
|
|
11
|
-
preload?: (params: Params) => Promise<unknown>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface Router {
|
|
15
|
-
getPreloadSettings(): PreloadPluginOptions;
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/network.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
type NetworkConnection =
|
|
2
|
-
| { saveData?: boolean; effectiveType?: string }
|
|
3
|
-
| undefined;
|
|
4
|
-
|
|
5
|
-
interface NavigatorWithConnection extends Navigator {
|
|
6
|
-
connection?: NetworkConnection;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function isSlowConnection(): boolean {
|
|
10
|
-
const connection = (navigator as NavigatorWithConnection).connection;
|
|
11
|
-
|
|
12
|
-
if (!connection) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
if (connection.saveData) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
if (connection.effectiveType?.includes("2g")) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return false;
|
|
23
|
-
}
|
package/src/plugin.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
GHOST_EVENT_THRESHOLD,
|
|
3
|
-
TOUCH_PRELOAD_DELAY,
|
|
4
|
-
TOUCH_SCROLL_THRESHOLD,
|
|
5
|
-
} from "./constants";
|
|
6
|
-
import { isSlowConnection } from "./network";
|
|
7
|
-
|
|
8
|
-
import type { PreloadPluginOptions } from "./types";
|
|
9
|
-
import type { Params, Plugin, Router, State } from "@real-router/core";
|
|
10
|
-
import type { PluginApi } from "@real-router/core/api";
|
|
11
|
-
|
|
12
|
-
declare module "@real-router/core" {
|
|
13
|
-
interface Router {
|
|
14
|
-
matchUrl?: (url: string) => State | undefined;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class PreloadPlugin {
|
|
19
|
-
readonly #router: Router;
|
|
20
|
-
readonly #api: PluginApi;
|
|
21
|
-
readonly #options: Required<PreloadPluginOptions>;
|
|
22
|
-
readonly #removeExtensions: () => void;
|
|
23
|
-
|
|
24
|
-
#currentAnchor: HTMLAnchorElement | null = null;
|
|
25
|
-
#hoverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
-
#touchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
-
#touchStartY = 0;
|
|
28
|
-
#lastTouchStartEvent: {
|
|
29
|
-
target: EventTarget | null;
|
|
30
|
-
timeStamp: number;
|
|
31
|
-
} | null = null;
|
|
32
|
-
|
|
33
|
-
constructor(
|
|
34
|
-
router: Router,
|
|
35
|
-
api: PluginApi,
|
|
36
|
-
options: Required<PreloadPluginOptions>,
|
|
37
|
-
) {
|
|
38
|
-
this.#router = router;
|
|
39
|
-
this.#api = api;
|
|
40
|
-
this.#options = options;
|
|
41
|
-
|
|
42
|
-
this.#removeExtensions = api.extendRouter({
|
|
43
|
-
getPreloadSettings: () => ({ ...options }),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
getPlugin(): Plugin {
|
|
48
|
-
return {
|
|
49
|
-
onStart: () => {
|
|
50
|
-
document.addEventListener("mouseover", this.#handleMouseOver, {
|
|
51
|
-
capture: true,
|
|
52
|
-
passive: true,
|
|
53
|
-
});
|
|
54
|
-
document.addEventListener("touchstart", this.#handleTouchStart, {
|
|
55
|
-
capture: true,
|
|
56
|
-
passive: true,
|
|
57
|
-
});
|
|
58
|
-
document.addEventListener("touchmove", this.#handleTouchMove, {
|
|
59
|
-
capture: true,
|
|
60
|
-
passive: true,
|
|
61
|
-
});
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
onStop: () => {
|
|
65
|
-
this.#cleanup();
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
teardown: () => {
|
|
69
|
-
this.#cleanup();
|
|
70
|
-
this.#removeExtensions();
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
readonly #handleMouseOver = (event: MouseEvent): void => {
|
|
76
|
-
if (this.#isGhostMouseEvent(event)) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const target = event.target as Element | null;
|
|
81
|
-
|
|
82
|
-
if (!target || !("closest" in target)) {
|
|
83
|
-
this.#cancelHover();
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const anchor = target.closest<HTMLAnchorElement>("a[href]");
|
|
89
|
-
|
|
90
|
-
if (anchor === this.#currentAnchor) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.#cancelHover();
|
|
95
|
-
this.#currentAnchor = anchor;
|
|
96
|
-
|
|
97
|
-
const preload = this.#resolveAnchorPreload(anchor);
|
|
98
|
-
|
|
99
|
-
if (!preload) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.#hoverTimer = setTimeout(() => {
|
|
104
|
-
this.#hoverTimer = null;
|
|
105
|
-
preload.fn(preload.params).catch(() => {});
|
|
106
|
-
}, this.#options.delay);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
readonly #handleTouchStart = (event: TouchEvent): void => {
|
|
110
|
-
this.#lastTouchStartEvent = {
|
|
111
|
-
target: event.target,
|
|
112
|
-
timeStamp: event.timeStamp,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
this.#cancelTouch();
|
|
116
|
-
|
|
117
|
-
const target = event.target as Element | null;
|
|
118
|
-
const anchor =
|
|
119
|
-
target && "closest" in target
|
|
120
|
-
? target.closest<HTMLAnchorElement>("a[href]")
|
|
121
|
-
: null;
|
|
122
|
-
const preload = this.#resolveAnchorPreload(anchor);
|
|
123
|
-
|
|
124
|
-
if (!preload) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
this.#touchStartY = event.touches[0].clientY;
|
|
129
|
-
|
|
130
|
-
this.#touchTimer = setTimeout(() => {
|
|
131
|
-
this.#touchTimer = null;
|
|
132
|
-
preload.fn(preload.params).catch(() => {});
|
|
133
|
-
}, TOUCH_PRELOAD_DELAY);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
readonly #handleTouchMove = (event: TouchEvent): void => {
|
|
137
|
-
if (this.#touchTimer === null) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const deltaY = Math.abs(event.touches[0].clientY - this.#touchStartY);
|
|
142
|
-
|
|
143
|
-
if (deltaY > TOUCH_SCROLL_THRESHOLD) {
|
|
144
|
-
this.#cancelTouch();
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
#resolveAnchorPreload(
|
|
149
|
-
anchor: HTMLAnchorElement | null | undefined,
|
|
150
|
-
): { fn: (params: Params) => Promise<unknown>; params: Params } | undefined {
|
|
151
|
-
if (!anchor) {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if ("noPreload" in anchor.dataset) {
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (this.#options.networkAware && isSlowConnection()) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return this.#resolvePreload(anchor);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
#resolvePreload(
|
|
167
|
-
anchor: HTMLAnchorElement,
|
|
168
|
-
): { fn: (params: Params) => Promise<unknown>; params: Params } | undefined {
|
|
169
|
-
const state = this.#router.matchUrl?.(anchor.href);
|
|
170
|
-
|
|
171
|
-
if (!state) {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const config = this.#api.getRouteConfig(state.name);
|
|
176
|
-
|
|
177
|
-
if (typeof config?.preload !== "function") {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
fn: config.preload as (params: Params) => Promise<unknown>,
|
|
183
|
-
params: state.params,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
#isGhostMouseEvent(event: MouseEvent): boolean {
|
|
188
|
-
return (
|
|
189
|
-
this.#lastTouchStartEvent !== null &&
|
|
190
|
-
event.target === this.#lastTouchStartEvent.target &&
|
|
191
|
-
event.timeStamp - this.#lastTouchStartEvent.timeStamp <
|
|
192
|
-
GHOST_EVENT_THRESHOLD
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
#cancelHover(): void {
|
|
197
|
-
if (this.#hoverTimer !== null) {
|
|
198
|
-
clearTimeout(this.#hoverTimer);
|
|
199
|
-
this.#hoverTimer = null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
this.#currentAnchor = null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
#cancelTouch(): void {
|
|
206
|
-
if (this.#touchTimer !== null) {
|
|
207
|
-
clearTimeout(this.#touchTimer);
|
|
208
|
-
this.#touchTimer = null;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
#cleanup(): void {
|
|
213
|
-
document.removeEventListener("mouseover", this.#handleMouseOver, {
|
|
214
|
-
capture: true,
|
|
215
|
-
});
|
|
216
|
-
document.removeEventListener("touchstart", this.#handleTouchStart, {
|
|
217
|
-
capture: true,
|
|
218
|
-
});
|
|
219
|
-
document.removeEventListener("touchmove", this.#handleTouchMove, {
|
|
220
|
-
capture: true,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
this.#cancelHover();
|
|
224
|
-
this.#cancelTouch();
|
|
225
|
-
this.#lastTouchStartEvent = null;
|
|
226
|
-
}
|
|
227
|
-
}
|