@kasperrt/smolter 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.generated/client/deployment.ts +31 -5
- package/.generated/preact/deferred-hydration.ts +3 -5
- package/CHANGELOG.md +6 -0
- package/README.md +8 -6
- package/dist/client/deployment.d.ts +1 -1
- package/dist/client/deployment.js +22 -4
- package/dist/preact/deferred-hydration.d.ts +2 -4
- package/jsr.json +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NavigationRouter } from "../core/index.ts";
|
|
1
|
+
import type { NavigationRouter, Router } from "../core/index.ts";
|
|
2
2
|
import { deploymentCheck } from "./deployment-check.ts";
|
|
3
3
|
|
|
4
4
|
/** Build identity and the fresh, non-cacheable manifest URL emitted by deploymentPlugin. */
|
|
@@ -11,7 +11,7 @@ export interface DeploymentOptions {
|
|
|
11
11
|
reload?: (destination: URL) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
14
|
+
/** Check for deployment changes before navigation. Install before creating fetchers. Returns a cleanup function. */
|
|
15
15
|
export function watchDeployment(
|
|
16
16
|
router: NavigationRouter,
|
|
17
17
|
options: DeploymentOptions | null,
|
|
@@ -26,7 +26,31 @@ export function watchDeployment(
|
|
|
26
26
|
let sequence = 0;
|
|
27
27
|
let stopped = false;
|
|
28
28
|
let replay = false;
|
|
29
|
+
let mutations = 0;
|
|
30
|
+
const dataRouter = router as NavigationRouter &
|
|
31
|
+
Partial<Pick<Router, "createFetcher">>;
|
|
32
|
+
const createFetcher = dataRouter.createFetcher;
|
|
33
|
+
const guardedFetcher =
|
|
34
|
+
createFetcher &&
|
|
35
|
+
(() => {
|
|
36
|
+
const fetcher = createFetcher();
|
|
37
|
+
const submit = fetcher.submit;
|
|
38
|
+
|
|
39
|
+
fetcher.submit = (...args) => {
|
|
40
|
+
sequence++;
|
|
41
|
+
mutations++;
|
|
42
|
+
|
|
43
|
+
return submit(...args).finally(() => {
|
|
44
|
+
mutations--;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return fetcher;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (guardedFetcher) dataRouter.createFetcher = guardedFetcher;
|
|
29
52
|
const valid = (current: number) => current === sequence && !stopped;
|
|
53
|
+
const canReload = (current: number) => valid(current) && mutations === 0;
|
|
30
54
|
const unsubscribe = router.subscribe(() => {
|
|
31
55
|
if (read().state === "submitting") sequence++;
|
|
32
56
|
});
|
|
@@ -42,7 +66,7 @@ export function watchDeployment(
|
|
|
42
66
|
target.search !== currentURL.search) &&
|
|
43
67
|
(await changed())
|
|
44
68
|
) {
|
|
45
|
-
if (!valid(current) || (await reload(target, () =>
|
|
69
|
+
if (!valid(current) || (await reload(target, () => canReload(current))))
|
|
46
70
|
return;
|
|
47
71
|
}
|
|
48
72
|
|
|
@@ -65,7 +89,7 @@ export function watchDeployment(
|
|
|
65
89
|
event.stopImmediatePropagation();
|
|
66
90
|
void changed().then(async (updated) => {
|
|
67
91
|
if (!valid(current)) return;
|
|
68
|
-
if (updated && (await reload(target, () =>
|
|
92
|
+
if (updated && (await reload(target, () => canReload(current)))) return;
|
|
69
93
|
if (!valid(current)) return;
|
|
70
94
|
|
|
71
95
|
replay = true;
|
|
@@ -88,7 +112,7 @@ export function watchDeployment(
|
|
|
88
112
|
return reload(
|
|
89
113
|
target,
|
|
90
114
|
() =>
|
|
91
|
-
|
|
115
|
+
canReload(current) &&
|
|
92
116
|
read().state !== "submitting" &&
|
|
93
117
|
read().url === target.href,
|
|
94
118
|
);
|
|
@@ -103,6 +127,8 @@ export function watchDeployment(
|
|
|
103
127
|
window.removeEventListener("popstate", pop, true);
|
|
104
128
|
window.removeEventListener("vite:preloadError", preloadError);
|
|
105
129
|
|
|
130
|
+
if (dataRouter.createFetcher === guardedFetcher)
|
|
131
|
+
dataRouter.createFetcher = createFetcher;
|
|
106
132
|
if (router.navigate === guarded) router.navigate = navigate;
|
|
107
133
|
if (router.dispose === guardedDispose) router.dispose = dispose;
|
|
108
134
|
}
|
|
@@ -6,16 +6,14 @@ interface PendingValue {
|
|
|
6
6
|
reject: (reason: Error) => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
__smolterDeferred?: Settlement[];
|
|
12
|
-
}
|
|
9
|
+
export interface DeferredWindow extends Window {
|
|
10
|
+
__smolterDeferred?: Settlement[];
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
16
14
|
export function readHydration(json: string) {
|
|
17
15
|
const pending = new Map<number, PendingValue>();
|
|
18
|
-
const queue = (window.__smolterDeferred ??= []);
|
|
16
|
+
const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
|
|
19
17
|
|
|
20
18
|
function revive(_key: string, value: unknown): unknown {
|
|
21
19
|
if (!value || typeof value !== "object") return value;
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- Invalidate pending deployment recovery when an independent fetcher submits. Prevent recovery reloads while any tracked fetcher submission is pending, without blocking ordinary client navigation. Install the watcher before creating fetchers; mountApplication already does this.
|
|
6
|
+
- Add regressions for two fetcher mutations completing before an older manifest check, overlapping pending submissions, unused recovery markers, later recovery, and watcher cleanup. These deterministic tests do not establish the complete streaming/offline browser matrix.
|
|
7
|
+
- Clarify that root stylesheet URL imports rely on rendered stylesheet links and are outside route CSS discovery. Empty route styles arrays are expected in that setup; explicit host preload hints should be justified by measurements.
|
|
8
|
+
|
|
3
9
|
## 0.4.0
|
|
4
10
|
|
|
5
11
|
- Add opt-in `earlyHydration: true` to Preact `renderApplication`: hydrate the shell before optional nested loader promises settle, using Preact Suspense and independently streamed values/errors. Default hydration timing is unchanged.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# smolter _
|
|
2
2
|
|
|
3
3
|
<!-- bundlejs:start -->
|
|
4
|
-
[](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
|
|
5
|
+
[](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.1%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
|
|
6
6
|
<!-- bundlejs:end -->
|
|
7
7
|
|
|
8
8
|
A typed data router for React and Preact. **Zero runtime dependencies.**
|
|
@@ -605,10 +605,10 @@ a claim of identical features.
|
|
|
605
605
|
| `Preact: routing-only named exports` | 1405 |
|
|
606
606
|
| `@kasperrt/smolter/react` | 2948 |
|
|
607
607
|
| `@kasperrt/smolter/preact` | 4842 |
|
|
608
|
-
| `@kasperrt/smolter/react + @kasperrt/smolter/react/client` |
|
|
609
|
-
| `@kasperrt/smolter/preact + server transport` |
|
|
610
|
-
| `@kasperrt/smolter/preact + client + document (application without lazy routes)` |
|
|
611
|
-
| `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` |
|
|
608
|
+
| `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4186 |
|
|
609
|
+
| `@kasperrt/smolter/preact + server transport` | 6033 |
|
|
610
|
+
| `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9034 |
|
|
611
|
+
| `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9314 |
|
|
612
612
|
| `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
|
|
613
613
|
| `React: common routing/data exports` | 2761 |
|
|
614
614
|
| `Preact: common routing/data exports` | 3046 |
|
|
@@ -778,3 +778,5 @@ The generated route asset manifest follows static imports for each route and lea
|
|
|
778
778
|
### Matched-route asset hints
|
|
779
779
|
|
|
780
780
|
The file-router build emits `.vite/route-assets.json` with each route's static JavaScript dependency closure and its CSS in `styles`. `routePreloads` from `@kasperrt/smolter/utils/assets` produces modulepreload hints for JavaScript and `preload; as=style; crossorigin` hints for CSS (matching Vite’s anonymous CORS mode), respecting your CDN/base URL. Set these hints on the document response or send Early Hints from your host. Stylesheet preloads fetch files; they do not apply styles. Keep the application stylesheet links and Vite's CSS handling in place. Optional dynamic imports are excluded, and unrelated routes stay lazy.
|
|
781
|
+
|
|
782
|
+
Root stylesheet URL imports such as `import stylesUrl from "./index.css?url"` are not route CSS imports. Keep rendering `<link rel="stylesheet" href={stylesUrl}>` in the root document; an empty route `styles` array is expected when all CSS is loaded this way. Smolter does not infer stylesheet use from arbitrary asset URLs. If measurements show late root CSS discovery, your host can send an explicit preload or Early Hint for that same resolved URL, with the same CORS mode as the stylesheet link. Preloading does not replace the stylesheet link. Do not reorganize CSS merely to populate the route manifest.
|
|
@@ -8,7 +8,7 @@ export interface DeploymentOptions {
|
|
|
8
8
|
/** Override document navigation, for example to ask about unsaved changes. */
|
|
9
9
|
reload?: (destination: URL) => void;
|
|
10
10
|
}
|
|
11
|
-
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
11
|
+
/** Check for deployment changes before navigation. Install before creating fetchers. Returns a cleanup function. */
|
|
12
12
|
export declare function watchDeployment(router: NavigationRouter, options: DeploymentOptions | null): () => void;
|
|
13
13
|
/** Recover an initial lazy-import failure before a router has mounted. Returns whether a reload was requested. */
|
|
14
14
|
export declare function recoverDeployment(options: DeploymentOptions | null, destination?: URL): Promise<boolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deploymentCheck } from "./deployment-check.js";
|
|
2
2
|
//#region .generated/client/deployment.ts
|
|
3
|
-
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
3
|
+
/** Check for deployment changes before navigation. Install before creating fetchers. Returns a cleanup function. */
|
|
4
4
|
function watchDeployment(router, options) {
|
|
5
5
|
if (!options || typeof window === "undefined") return () => {};
|
|
6
6
|
const check = deploymentCheck(options);
|
|
@@ -11,7 +11,24 @@ function watchDeployment(router, options) {
|
|
|
11
11
|
let sequence = 0;
|
|
12
12
|
let stopped = false;
|
|
13
13
|
let replay = false;
|
|
14
|
+
let mutations = 0;
|
|
15
|
+
const dataRouter = router;
|
|
16
|
+
const createFetcher = dataRouter.createFetcher;
|
|
17
|
+
const guardedFetcher = createFetcher && (() => {
|
|
18
|
+
const fetcher = createFetcher();
|
|
19
|
+
const submit = fetcher.submit;
|
|
20
|
+
fetcher.submit = (...args) => {
|
|
21
|
+
sequence++;
|
|
22
|
+
mutations++;
|
|
23
|
+
return submit(...args).finally(() => {
|
|
24
|
+
mutations--;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
return fetcher;
|
|
28
|
+
});
|
|
29
|
+
if (guardedFetcher) dataRouter.createFetcher = guardedFetcher;
|
|
14
30
|
const valid = (current) => current === sequence && !stopped;
|
|
31
|
+
const canReload = (current) => valid(current) && mutations === 0;
|
|
15
32
|
const unsubscribe = router.subscribe(() => {
|
|
16
33
|
if (read().state === "submitting") sequence++;
|
|
17
34
|
});
|
|
@@ -20,7 +37,7 @@ function watchDeployment(router, options) {
|
|
|
20
37
|
const currentURL = new URL(router.getSnapshot().url);
|
|
21
38
|
const current = ++sequence;
|
|
22
39
|
if (target.origin === location.origin && (target.pathname !== currentURL.pathname || target.search !== currentURL.search) && await changed()) {
|
|
23
|
-
if (!valid(current) || await reload(target, () =>
|
|
40
|
+
if (!valid(current) || await reload(target, () => canReload(current))) return;
|
|
24
41
|
}
|
|
25
42
|
if (current === sequence && !stopped) await navigate(href, operation);
|
|
26
43
|
};
|
|
@@ -33,7 +50,7 @@ function watchDeployment(router, options) {
|
|
|
33
50
|
event.stopImmediatePropagation();
|
|
34
51
|
changed().then(async (updated) => {
|
|
35
52
|
if (!valid(current)) return;
|
|
36
|
-
if (updated && await reload(target, () =>
|
|
53
|
+
if (updated && await reload(target, () => canReload(current))) return;
|
|
37
54
|
if (!valid(current)) return;
|
|
38
55
|
replay = true;
|
|
39
56
|
window.dispatchEvent(new PopStateEvent("popstate", { state: event.state }));
|
|
@@ -45,7 +62,7 @@ function watchDeployment(router, options) {
|
|
|
45
62
|
const current = sequence;
|
|
46
63
|
const target = new URL(read().url);
|
|
47
64
|
changed().then((updated) => {
|
|
48
|
-
if (updated) return reload(target, () =>
|
|
65
|
+
if (updated) return reload(target, () => canReload(current) && read().state !== "submitting" && read().url === target.href);
|
|
49
66
|
});
|
|
50
67
|
};
|
|
51
68
|
function cleanup() {
|
|
@@ -55,6 +72,7 @@ function watchDeployment(router, options) {
|
|
|
55
72
|
unsubscribe();
|
|
56
73
|
window.removeEventListener("popstate", pop, true);
|
|
57
74
|
window.removeEventListener("vite:preloadError", preloadError);
|
|
75
|
+
if (dataRouter.createFetcher === guardedFetcher) dataRouter.createFetcher = createFetcher;
|
|
58
76
|
if (router.navigate === guarded) router.navigate = navigate;
|
|
59
77
|
if (router.dispose === guardedDispose) router.dispose = dispose;
|
|
60
78
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
type Settlement = [number, unknown, string | null];
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
__smolterDeferred?: Settlement[];
|
|
5
|
-
}
|
|
2
|
+
export interface DeferredWindow extends Window {
|
|
3
|
+
__smolterDeferred?: Settlement[];
|
|
6
4
|
}
|
|
7
5
|
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
8
6
|
export declare function readHydration(json: string): {
|
package/jsr.json
CHANGED