@kasperrt/smolter 0.3.0 → 0.3.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 +161 -0
- package/.generated/client/index.ts +2 -0
- package/.generated/vite/application/lazy-files.ts +2 -0
- package/.generated/vite/deployment.ts +46 -0
- package/.generated/vite/index.ts +11 -4
- package/.generated/vite/preact.ts +5 -0
- package/.generated/vite/react.ts +5 -0
- package/CHANGELOG.md +9 -0
- package/README.md +56 -6
- package/dist/client/deployment.d.ts +12 -0
- package/dist/client/deployment.js +102 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +2 -1
- package/dist/preact/client.js +2 -1
- package/dist/vite/application/lazy-files.js +1 -0
- package/dist/vite/deployment.d.ts +8 -0
- package/dist/vite/deployment.js +36 -0
- package/dist/vite/index.js +9 -4
- package/dist/vite/preact.d.ts +1 -0
- package/dist/vite/preact.js +2 -1
- package/dist/vite/react.d.ts +1 -0
- package/dist/vite/react.js +2 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { NavigationRouter } from "../core/index.ts";
|
|
2
|
+
|
|
3
|
+
/** Build identity and the fresh, non-cacheable manifest URL emitted by deploymentPlugin. */
|
|
4
|
+
export interface DeploymentOptions {
|
|
5
|
+
version: string;
|
|
6
|
+
manifestURL: string;
|
|
7
|
+
/** Maximum time to wait for the update check before continuing navigation. */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
/** Override document navigation, for example to ask about unsaved changes. */
|
|
10
|
+
reload?: (destination: URL) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
14
|
+
export function watchDeployment(
|
|
15
|
+
router: NavigationRouter,
|
|
16
|
+
options: DeploymentOptions | null,
|
|
17
|
+
): () => void {
|
|
18
|
+
if (!options || typeof window === "undefined") return () => {};
|
|
19
|
+
|
|
20
|
+
const { version, manifestURL, timeout = 2000 } = options;
|
|
21
|
+
const navigate = router.navigate;
|
|
22
|
+
const dispose = router.dispose;
|
|
23
|
+
const manifest = new URL(manifestURL, location.href);
|
|
24
|
+
const key = `smolter:deployment:${manifest.href}:${version}`;
|
|
25
|
+
let sequence = 0;
|
|
26
|
+
let stopped = false;
|
|
27
|
+
let replay = false;
|
|
28
|
+
let destination = new URL(location.href);
|
|
29
|
+
let pending: Promise<boolean> | undefined;
|
|
30
|
+
let controller: AbortController | undefined;
|
|
31
|
+
|
|
32
|
+
function changed() {
|
|
33
|
+
if (pending) return pending;
|
|
34
|
+
|
|
35
|
+
controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller?.abort(), timeout);
|
|
37
|
+
pending = fetch(manifest, { cache: "no-store", signal: controller.signal })
|
|
38
|
+
.then(async (response) => {
|
|
39
|
+
if (!response.ok) return false;
|
|
40
|
+
|
|
41
|
+
const data: unknown = await response.json();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
typeof data === "object" &&
|
|
45
|
+
data !== null &&
|
|
46
|
+
"version" in data &&
|
|
47
|
+
typeof data.version === "string" &&
|
|
48
|
+
data.version.length > 0 &&
|
|
49
|
+
data.version !== version
|
|
50
|
+
);
|
|
51
|
+
})
|
|
52
|
+
.catch(() => false)
|
|
53
|
+
.finally(() => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
pending = undefined;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return pending;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function reload(target: URL) {
|
|
62
|
+
// If storage is unavailable, do not risk reloading the same cached document forever.
|
|
63
|
+
const allowed = await Promise.resolve()
|
|
64
|
+
.then(() => {
|
|
65
|
+
if (sessionStorage.getItem(key)) return false;
|
|
66
|
+
|
|
67
|
+
sessionStorage.setItem(key, "1");
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
})
|
|
71
|
+
.catch(() => false);
|
|
72
|
+
|
|
73
|
+
if (allowed && !stopped) {
|
|
74
|
+
if (options?.reload) options.reload(target);
|
|
75
|
+
if (!options?.reload) {
|
|
76
|
+
if (target.href === location.href) location.reload();
|
|
77
|
+
if (target.href !== location.href) location.assign(target.href);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return allowed && !stopped;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const guarded: NavigationRouter["navigate"] = async (href, operation) => {
|
|
85
|
+
const target = new URL(href, router.getSnapshot().url);
|
|
86
|
+
const currentURL = new URL(router.getSnapshot().url);
|
|
87
|
+
const current = ++sequence;
|
|
88
|
+
destination = target;
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
target.origin === location.origin &&
|
|
92
|
+
(target.pathname !== currentURL.pathname ||
|
|
93
|
+
target.search !== currentURL.search) &&
|
|
94
|
+
(await changed())
|
|
95
|
+
) {
|
|
96
|
+
if (current !== sequence || stopped || (await reload(target))) return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (current === sequence && !stopped) await navigate(href, operation);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const pop = (event: PopStateEvent) => {
|
|
103
|
+
if (replay) return;
|
|
104
|
+
|
|
105
|
+
const target = new URL(location.href);
|
|
106
|
+
const previous = new URL(router.getSnapshot().url);
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
target.pathname === previous.pathname &&
|
|
110
|
+
target.search === previous.search
|
|
111
|
+
)
|
|
112
|
+
return;
|
|
113
|
+
|
|
114
|
+
event.stopImmediatePropagation();
|
|
115
|
+
const current = ++sequence;
|
|
116
|
+
destination = target;
|
|
117
|
+
void changed().then(async (updated) => {
|
|
118
|
+
if (stopped || current !== sequence) return;
|
|
119
|
+
if (updated && (await reload(target))) return;
|
|
120
|
+
|
|
121
|
+
replay = true;
|
|
122
|
+
window.dispatchEvent(
|
|
123
|
+
new PopStateEvent("popstate", { state: event.state }),
|
|
124
|
+
);
|
|
125
|
+
replay = false;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const preloadError = () => {
|
|
130
|
+
if (router.getSnapshot().state === "submitting") return;
|
|
131
|
+
|
|
132
|
+
const current = sequence;
|
|
133
|
+
const target = destination;
|
|
134
|
+
void changed().then((updated) => {
|
|
135
|
+
if (updated && current === sequence && !stopped) return reload(target);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function cleanup() {
|
|
140
|
+
stopped = true;
|
|
141
|
+
sequence++;
|
|
142
|
+
controller?.abort();
|
|
143
|
+
window.removeEventListener("popstate", pop, true);
|
|
144
|
+
window.removeEventListener("vite:preloadError", preloadError);
|
|
145
|
+
|
|
146
|
+
if (router.navigate === guarded) router.navigate = navigate;
|
|
147
|
+
if (router.dispose === guardedDispose) router.dispose = dispose;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function guardedDispose() {
|
|
151
|
+
cleanup();
|
|
152
|
+
dispose();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
router.navigate = guarded;
|
|
156
|
+
router.dispose = guardedDispose;
|
|
157
|
+
window.addEventListener("popstate", pop, true);
|
|
158
|
+
window.addEventListener("vite:preloadError", preloadError);
|
|
159
|
+
|
|
160
|
+
return cleanup;
|
|
161
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
/** Deployment manifest configuration. Serve the emitted file with Cache-Control: no-store. */
|
|
5
|
+
export interface DeploymentPluginOptions {
|
|
6
|
+
/** Public URL, optionally on an asset host. Defaults to Vite base + smolter-manifest.json. */
|
|
7
|
+
manifestURL?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Emit a build manifest and expose its identity through virtual:smolter/deployment. */
|
|
11
|
+
export function deploymentPlugin(
|
|
12
|
+
options: DeploymentPluginOptions = {},
|
|
13
|
+
): Plugin {
|
|
14
|
+
const version = randomUUID();
|
|
15
|
+
const virtualId = "\0virtual:smolter/deployment";
|
|
16
|
+
let enabled = false;
|
|
17
|
+
let manifestURL = "";
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: "smolter-deployment",
|
|
21
|
+
configResolved(config) {
|
|
22
|
+
enabled = config.command === "build" && !config.build.ssr;
|
|
23
|
+
manifestURL =
|
|
24
|
+
options.manifestURL ?? `${config.base}smolter-manifest.json`;
|
|
25
|
+
},
|
|
26
|
+
resolveId(id) {
|
|
27
|
+
if (id === "virtual:smolter/deployment") return virtualId;
|
|
28
|
+
},
|
|
29
|
+
load(id) {
|
|
30
|
+
if (id !== virtualId) return;
|
|
31
|
+
|
|
32
|
+
const deployment = enabled ? { version, manifestURL } : null;
|
|
33
|
+
|
|
34
|
+
return `export default ${JSON.stringify(deployment)};`;
|
|
35
|
+
},
|
|
36
|
+
generateBundle() {
|
|
37
|
+
if (!enabled) return;
|
|
38
|
+
|
|
39
|
+
this.emitFile({
|
|
40
|
+
type: "asset",
|
|
41
|
+
fileName: "smolter-manifest.json",
|
|
42
|
+
source: JSON.stringify({ version }),
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/.generated/vite/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
3
|
import { flatRoutes } from "../files/index.ts";
|
|
4
4
|
import { type Plugin, transformWithOxc } from "vite";
|
|
5
5
|
import { configureTransport } from "./transport.ts";
|
|
@@ -185,11 +185,18 @@ export function routerPlugin({
|
|
|
185
185
|
server.watcher.add(root);
|
|
186
186
|
|
|
187
187
|
const refresh = (file: string) => {
|
|
188
|
-
if (
|
|
188
|
+
if (clientRoutes || serverRoutes) return;
|
|
189
189
|
|
|
190
|
-
const
|
|
190
|
+
const path = relative(root, file).replaceAll("\\", "/");
|
|
191
191
|
|
|
192
|
-
if (
|
|
192
|
+
if (!/^(?:[^/]+\.[jt]sx?|[^/]+\/route\.[jt]sx?)$/.test(path)) return;
|
|
193
|
+
if (path.endsWith(".d.ts")) return;
|
|
194
|
+
|
|
195
|
+
for (const environment of Object.values(server.environments)) {
|
|
196
|
+
const module = environment.moduleGraph.getModuleById(virtualId);
|
|
197
|
+
|
|
198
|
+
if (module) environment.moduleGraph.invalidateModule(module);
|
|
199
|
+
}
|
|
193
200
|
|
|
194
201
|
server.ws.send({ type: "full-reload" });
|
|
195
202
|
};
|
package/.generated/vite/react.ts
CHANGED
|
@@ -16,3 +16,8 @@ export function routerPlugin(options: FileRouterOptions = {}): Plugin {
|
|
|
16
16
|
|
|
17
17
|
/** File routing is the default; this alias keeps configuration explicit. */
|
|
18
18
|
export const fileRouter = routerPlugin;
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
type DeploymentPluginOptions,
|
|
22
|
+
deploymentPlugin,
|
|
23
|
+
} from "./deployment.ts";
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
- Refresh browser and SSR file-route discovery on additions, renames and removals; preserve normal Vite HMR for colocated helpers and components.
|
|
6
|
+
|
|
7
|
+
- Add optional production deployment manifests through `deploymentPlugin` for both React and Preact Vite integrations.
|
|
8
|
+
- Add `watchDeployment` to both client entrypoints: check before navigation and Back/Forward, recover stale Vite chunks after confirmed build changes, and preserve the requested URL.
|
|
9
|
+
- Skip hash-only links, development builds and SSR; bound failed checks and prevent repeated reloads. Keep routing-only bundles unchanged.
|
|
10
|
+
- Document production cache headers, CDN/application-specific manifest paths, unsaved state and rollout limitations.
|
|
11
|
+
|
|
3
12
|
## 0.3.0
|
|
4
13
|
|
|
5
14
|
### Migration from 0.2.0
|
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.3.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.3.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.**
|
|
@@ -20,6 +20,8 @@ Start with [loaders](https://kasperrt.github.io/smolter/react/docs/loaders/), [a
|
|
|
20
20
|
|
|
21
21
|
## Contents
|
|
22
22
|
|
|
23
|
+
- [Deployment updates](#deployment-updates)
|
|
24
|
+
|
|
23
25
|
- [Install](#install)
|
|
24
26
|
- [Try the four standalone examples](#try-the-four-standalone-examples)
|
|
25
27
|
- [Choose file routing or manual routes](#choose-file-routing-or-manual-routes)
|
|
@@ -567,6 +569,22 @@ This optional entry does not contribute to the base React bundle measurement.
|
|
|
567
569
|
|
|
568
570
|
## Size and checks
|
|
569
571
|
|
|
572
|
+
| Need | Public import |
|
|
573
|
+
| --- | --- |
|
|
574
|
+
| Navigation only | `@kasperrt/smolter/react` or `/preact`, using `createRouter` |
|
|
575
|
+
| Loaders, actions, forms and fetchers | Same entry, using `createDataRouter` |
|
|
576
|
+
| Browser server-request transport | Renderer `/client` subpath |
|
|
577
|
+
| Preact application hydration | `@kasperrt/smolter/preact/client` |
|
|
578
|
+
| Preact document components | `@kasperrt/smolter/preact/document` |
|
|
579
|
+
| Lazy Preact route modules | `@kasperrt/smolter/preact/lazy-route` |
|
|
580
|
+
| Optional React resources | `@kasperrt/smolter/react/data` |
|
|
581
|
+
| Server rendering / Vite integration | Renderer `/server` / `/vite` subpaths; keep out of browser imports |
|
|
582
|
+
|
|
583
|
+
The measurements below describe retained exports, not the cost of an import
|
|
584
|
+
statement. Browser transport rows include the router and transport together;
|
|
585
|
+
application rows include the listed modules together. Server and Vite modules
|
|
586
|
+
have no browser cost when kept in their intended server/build entrypoints.
|
|
587
|
+
|
|
570
588
|
Use `createRouter` for navigation only, or `createDataRouter` for loaders,
|
|
571
589
|
actions, fetchers and revalidation. Both are named exports from
|
|
572
590
|
`@kasperrt/smolter/preact` and `/react`. No extra package or plugin is required.
|
|
@@ -581,14 +599,16 @@ a claim of identical features.
|
|
|
581
599
|
<!-- size-comparison:start -->
|
|
582
600
|
| Browser exports | Gzip bytes |
|
|
583
601
|
| --- | ---: |
|
|
602
|
+
| `React Router: declarative routing subset` | 12882 |
|
|
603
|
+
| `React Router: data routing subset` | 29780 |
|
|
584
604
|
| `React: routing-only named exports` | 1245 |
|
|
585
605
|
| `Preact: routing-only named exports` | 1405 |
|
|
586
606
|
| `@kasperrt/smolter/react` | 2873 |
|
|
587
607
|
| `@kasperrt/smolter/preact` | 4748 |
|
|
588
|
-
| `@kasperrt/smolter/react + @kasperrt/smolter/react/client` |
|
|
589
|
-
| `@kasperrt/smolter/preact + server transport` |
|
|
590
|
-
| `@kasperrt/smolter/preact + client + document (application without lazy routes)` |
|
|
591
|
-
| `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` |
|
|
608
|
+
| `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 3870 |
|
|
609
|
+
| `@kasperrt/smolter/preact + server transport` | 5697 |
|
|
610
|
+
| `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 8033 |
|
|
611
|
+
| `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 8317 |
|
|
592
612
|
| `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
|
|
593
613
|
| `React: common routing/data exports` | 2683 |
|
|
594
614
|
| `Preact: common routing/data exports` | 2966 |
|
|
@@ -604,6 +624,13 @@ The common-export rows measure the same named API set: `createDataRouter`,
|
|
|
604
624
|
`useLoaderData`, `useActionData`, `useParams`, `useLocation`,
|
|
605
625
|
`useRevalidator` and `useFetcher`. Preact’s link and submission APIs currently
|
|
606
626
|
have additional behavior; equal names do not imply identical implementations.
|
|
627
|
+
React Router's declarative subset retains `BrowserRouter`, `Routes`, `Route`,
|
|
628
|
+
`Link`, `useParams`, and `useLocation`. Its data subset retains
|
|
629
|
+
`createBrowserRouter`, `RouterProvider`, `Link`, `Form`, `useLoaderData`,
|
|
630
|
+
`useActionData`, `useParams`, `useLocation`, `useRevalidator`, and `useFetcher`.
|
|
631
|
+
React Router has no dedicated Preact entry; these are React API measurements,
|
|
632
|
+
not a Preact compatibility build. The selected APIs have different capabilities.
|
|
633
|
+
|
|
607
634
|
The routing-only rows retain `createRouter`, `defineRoute`, `RouterProvider`,
|
|
608
635
|
`Routes`, `Link`, `useParams` and `useLocation`. Full Preact also exposes richer
|
|
609
636
|
application APIs. These tiers overlap: do not add the rows together.
|
|
@@ -683,3 +710,26 @@ pnpm add /absolute/path/to/kasperrt-smolter-0.0.1.tgz
|
|
|
683
710
|
```
|
|
684
711
|
|
|
685
712
|
Workspace structure and rendering checks are documented in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
713
|
+
|
|
714
|
+
## Deployment updates
|
|
715
|
+
|
|
716
|
+
For production tabs that survive a deployment, add `deploymentPlugin()` from
|
|
717
|
+
`@kasperrt/smolter/react/vite` or `/preact/vite`, then connect the router:
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
import deployment from "virtual:smolter/deployment";
|
|
721
|
+
import { watchDeployment } from "@kasperrt/smolter/preact/client";
|
|
722
|
+
|
|
723
|
+
watchDeployment(router, deployment);
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Serve the emitted `smolter-manifest.json` with `Cache-Control: no-store` and HTML
|
|
727
|
+
with `no-cache`. Navigation checks its build version before loading another route;
|
|
728
|
+
a newer version reloads the destination, including its query and hash. Vite chunk
|
|
729
|
+
failures are checked again. Hash-only links skip the check, failed checks continue
|
|
730
|
+
normally, and a session marker prevents reload loops. Development/SSR builds
|
|
731
|
+
disable the helper. This optional client module adds nothing to the routing-only
|
|
732
|
+
entrypoints and never replays form submissions.
|
|
733
|
+
|
|
734
|
+
See [deployment updates](https://kasperrt.github.io/smolter/react/docs/deployments/)
|
|
735
|
+
for Express, CDN paths, TypeScript declarations and unsaved-state handling.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NavigationRouter } from "../core/index.ts";
|
|
2
|
+
/** Build identity and the fresh, non-cacheable manifest URL emitted by deploymentPlugin. */
|
|
3
|
+
export interface DeploymentOptions {
|
|
4
|
+
version: string;
|
|
5
|
+
manifestURL: string;
|
|
6
|
+
/** Maximum time to wait for the update check before continuing navigation. */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Override document navigation, for example to ask about unsaved changes. */
|
|
9
|
+
reload?: (destination: URL) => void;
|
|
10
|
+
}
|
|
11
|
+
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
12
|
+
export declare function watchDeployment(router: NavigationRouter, options: DeploymentOptions | null): () => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region .generated/client/deployment.ts
|
|
2
|
+
/** Check for deployment changes before navigation. Returns a cleanup function. */
|
|
3
|
+
function watchDeployment(router, options) {
|
|
4
|
+
if (!options || typeof window === "undefined") return () => {};
|
|
5
|
+
const { version, manifestURL, timeout = 2e3 } = options;
|
|
6
|
+
const navigate = router.navigate;
|
|
7
|
+
const dispose = router.dispose;
|
|
8
|
+
const manifest = new URL(manifestURL, location.href);
|
|
9
|
+
const key = `smolter:deployment:${manifest.href}:${version}`;
|
|
10
|
+
let sequence = 0;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
let replay = false;
|
|
13
|
+
let destination = new URL(location.href);
|
|
14
|
+
let pending;
|
|
15
|
+
let controller;
|
|
16
|
+
function changed() {
|
|
17
|
+
if (pending) return pending;
|
|
18
|
+
controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller?.abort(), timeout);
|
|
20
|
+
pending = fetch(manifest, {
|
|
21
|
+
cache: "no-store",
|
|
22
|
+
signal: controller.signal
|
|
23
|
+
}).then(async (response) => {
|
|
24
|
+
if (!response.ok) return false;
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return typeof data === "object" && data !== null && "version" in data && typeof data.version === "string" && data.version.length > 0 && data.version !== version;
|
|
27
|
+
}).catch(() => false).finally(() => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
pending = void 0;
|
|
30
|
+
});
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
|
33
|
+
async function reload(target) {
|
|
34
|
+
const allowed = await Promise.resolve().then(() => {
|
|
35
|
+
if (sessionStorage.getItem(key)) return false;
|
|
36
|
+
sessionStorage.setItem(key, "1");
|
|
37
|
+
return true;
|
|
38
|
+
}).catch(() => false);
|
|
39
|
+
if (allowed && !stopped) {
|
|
40
|
+
if (options?.reload) options.reload(target);
|
|
41
|
+
if (!options?.reload) {
|
|
42
|
+
if (target.href === location.href) location.reload();
|
|
43
|
+
if (target.href !== location.href) location.assign(target.href);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return allowed && !stopped;
|
|
47
|
+
}
|
|
48
|
+
const guarded = async (href, operation) => {
|
|
49
|
+
const target = new URL(href, router.getSnapshot().url);
|
|
50
|
+
const currentURL = new URL(router.getSnapshot().url);
|
|
51
|
+
const current = ++sequence;
|
|
52
|
+
destination = target;
|
|
53
|
+
if (target.origin === location.origin && (target.pathname !== currentURL.pathname || target.search !== currentURL.search) && await changed()) {
|
|
54
|
+
if (current !== sequence || stopped || await reload(target)) return;
|
|
55
|
+
}
|
|
56
|
+
if (current === sequence && !stopped) await navigate(href, operation);
|
|
57
|
+
};
|
|
58
|
+
const pop = (event) => {
|
|
59
|
+
if (replay) return;
|
|
60
|
+
const target = new URL(location.href);
|
|
61
|
+
const previous = new URL(router.getSnapshot().url);
|
|
62
|
+
if (target.pathname === previous.pathname && target.search === previous.search) return;
|
|
63
|
+
event.stopImmediatePropagation();
|
|
64
|
+
const current = ++sequence;
|
|
65
|
+
destination = target;
|
|
66
|
+
changed().then(async (updated) => {
|
|
67
|
+
if (stopped || current !== sequence) return;
|
|
68
|
+
if (updated && await reload(target)) return;
|
|
69
|
+
replay = true;
|
|
70
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state: event.state }));
|
|
71
|
+
replay = false;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const preloadError = () => {
|
|
75
|
+
if (router.getSnapshot().state === "submitting") return;
|
|
76
|
+
const current = sequence;
|
|
77
|
+
const target = destination;
|
|
78
|
+
changed().then((updated) => {
|
|
79
|
+
if (updated && current === sequence && !stopped) return reload(target);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
function cleanup() {
|
|
83
|
+
stopped = true;
|
|
84
|
+
sequence++;
|
|
85
|
+
controller?.abort();
|
|
86
|
+
window.removeEventListener("popstate", pop, true);
|
|
87
|
+
window.removeEventListener("vite:preloadError", preloadError);
|
|
88
|
+
if (router.navigate === guarded) router.navigate = navigate;
|
|
89
|
+
if (router.dispose === guardedDispose) router.dispose = dispose;
|
|
90
|
+
}
|
|
91
|
+
function guardedDispose() {
|
|
92
|
+
cleanup();
|
|
93
|
+
dispose();
|
|
94
|
+
}
|
|
95
|
+
router.navigate = guarded;
|
|
96
|
+
router.dispose = guardedDispose;
|
|
97
|
+
window.addEventListener("popstate", pop, true);
|
|
98
|
+
window.addEventListener("vite:preloadError", preloadError);
|
|
99
|
+
return cleanup;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { watchDeployment };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -22,3 +22,4 @@ export interface ServerHandlers {
|
|
|
22
22
|
}
|
|
23
23
|
/** Browser transport for server routes. File-router builds generate this call. */
|
|
24
24
|
export declare function serverRoute<M extends RouteModule = RouteModule>(path: string, module: ClientRouteModule, handlers?: ServerHandlers): Route<M>;
|
|
25
|
+
export { type DeploymentOptions, watchDeployment } from "./deployment.ts";
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deserialize } from "./deserialize.js";
|
|
2
|
+
import { watchDeployment } from "./deployment.js";
|
|
2
3
|
//#region .generated/client/index.ts
|
|
3
4
|
/** Browser transport for server routes. File-router builds generate this call. */
|
|
4
5
|
function serverRoute(path, module, handlers = {
|
|
@@ -43,4 +44,4 @@ function serverRoute(path, module, handlers = {
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
//#endregion
|
|
46
|
-
export { deserialize, serverRoute };
|
|
47
|
+
export { deserialize, serverRoute, watchDeployment };
|
package/dist/preact/client.js
CHANGED
|
@@ -6,6 +6,7 @@ import { connectNavigation } from "../core/navigation.js";
|
|
|
6
6
|
import { attachRootData } from "./root-data.js";
|
|
7
7
|
import { RouterApplication, prepareRoutes } from "./router.js";
|
|
8
8
|
import { deserialize } from "../client/deserialize.js";
|
|
9
|
+
import { watchDeployment } from "../client/deployment.js";
|
|
9
10
|
import { serverRoute } from "../client/index.js";
|
|
10
11
|
import { flushSync } from "preact/compat";
|
|
11
12
|
import { jsx } from "preact/jsx-runtime";
|
|
@@ -64,4 +65,4 @@ function connectHashHistory(router) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
//#endregion
|
|
67
|
-
export { configureApplication, deserialize, mountApplication, serverRoute };
|
|
68
|
+
export { configureApplication, deserialize, mountApplication, serverRoute, watchDeployment };
|
|
@@ -45,6 +45,7 @@ function lazyFiles(directory) {
|
|
|
45
45
|
if (id !== "\0virtual:smolter/routes" || options?.ssr) return;
|
|
46
46
|
const routes = await flatRoutes(root);
|
|
47
47
|
if (routes instanceof Error) return this.error(routes.message);
|
|
48
|
+
modules.clear();
|
|
48
49
|
const definitions = [];
|
|
49
50
|
for (const route of routes) {
|
|
50
51
|
this.addWatchFile(route.file);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/** Deployment manifest configuration. Serve the emitted file with Cache-Control: no-store. */
|
|
3
|
+
export interface DeploymentPluginOptions {
|
|
4
|
+
/** Public URL, optionally on an asset host. Defaults to Vite base + smolter-manifest.json. */
|
|
5
|
+
manifestURL?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Emit a build manifest and expose its identity through virtual:smolter/deployment. */
|
|
8
|
+
export declare function deploymentPlugin(options?: DeploymentPluginOptions): Plugin;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
//#region .generated/vite/deployment.ts
|
|
3
|
+
/** Emit a build manifest and expose its identity through virtual:smolter/deployment. */
|
|
4
|
+
function deploymentPlugin(options = {}) {
|
|
5
|
+
const version = randomUUID();
|
|
6
|
+
const virtualId = "\0virtual:smolter/deployment";
|
|
7
|
+
let enabled = false;
|
|
8
|
+
let manifestURL = "";
|
|
9
|
+
return {
|
|
10
|
+
name: "smolter-deployment",
|
|
11
|
+
configResolved(config) {
|
|
12
|
+
enabled = config.command === "build" && !config.build.ssr;
|
|
13
|
+
manifestURL = options.manifestURL ?? `${config.base}smolter-manifest.json`;
|
|
14
|
+
},
|
|
15
|
+
resolveId(id) {
|
|
16
|
+
if (id === "virtual:smolter/deployment") return virtualId;
|
|
17
|
+
},
|
|
18
|
+
load(id) {
|
|
19
|
+
if (id !== virtualId) return;
|
|
20
|
+
return `export default ${JSON.stringify(enabled ? {
|
|
21
|
+
version,
|
|
22
|
+
manifestURL
|
|
23
|
+
} : null)};`;
|
|
24
|
+
},
|
|
25
|
+
generateBundle() {
|
|
26
|
+
if (!enabled) return;
|
|
27
|
+
this.emitFile({
|
|
28
|
+
type: "asset",
|
|
29
|
+
fileName: "smolter-manifest.json",
|
|
30
|
+
source: JSON.stringify({ version })
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { deploymentPlugin };
|
package/dist/vite/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { flatRoutes } from "../files/index.js";
|
|
2
2
|
import { configureTransport } from "./transport.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { relative, resolve } from "node:path";
|
|
5
5
|
import { transformWithOxc } from "vite";
|
|
6
6
|
//#region .generated/vite/index.ts
|
|
7
7
|
/** Build-time flat route discovery and the optional server data transport. */
|
|
@@ -83,9 +83,14 @@ function routerPlugin({ directory = "src/routes", clientRoutes, serverRoutes, fr
|
|
|
83
83
|
configureServer(server) {
|
|
84
84
|
server.watcher.add(root);
|
|
85
85
|
const refresh = (file) => {
|
|
86
|
-
if (
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
86
|
+
if (clientRoutes || serverRoutes) return;
|
|
87
|
+
const path = relative(root, file).replaceAll("\\", "/");
|
|
88
|
+
if (!/^(?:[^/]+\.[jt]sx?|[^/]+\/route\.[jt]sx?)$/.test(path)) return;
|
|
89
|
+
if (path.endsWith(".d.ts")) return;
|
|
90
|
+
for (const environment of Object.values(server.environments)) {
|
|
91
|
+
const module = environment.moduleGraph.getModuleById(virtualId);
|
|
92
|
+
if (module) environment.moduleGraph.invalidateModule(module);
|
|
93
|
+
}
|
|
89
94
|
server.ws.send({ type: "full-reload" });
|
|
90
95
|
};
|
|
91
96
|
server.watcher.on("add", refresh);
|
package/dist/vite/preact.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export declare function routerPlugin(options?: FileRouterOptions): Plugin;
|
|
|
9
9
|
export declare const fileRouter: typeof routerPlugin;
|
|
10
10
|
export type { ApplicationOptions } from "./application/vite.ts";
|
|
11
11
|
export { applicationPlugins, preactConfig, } from "./application/vite.ts";
|
|
12
|
+
export { type DeploymentPluginOptions, deploymentPlugin, } from "./deployment.ts";
|
package/dist/vite/preact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { flatRoutes } from "../files/index.js";
|
|
2
2
|
import { routerPlugin as routerPlugin$1 } from "./index.js";
|
|
3
|
+
import { deploymentPlugin } from "./deployment.js";
|
|
3
4
|
import { applicationPlugins, preactConfig } from "./application/vite.js";
|
|
4
5
|
//#region .generated/vite/preact.ts
|
|
5
6
|
/** Configure flat routes and server transport for Preact. */
|
|
@@ -12,4 +13,4 @@ function routerPlugin(options = {}) {
|
|
|
12
13
|
/** File routing is the default; this alias keeps configuration explicit. */
|
|
13
14
|
var fileRouter = routerPlugin;
|
|
14
15
|
//#endregion
|
|
15
|
-
export { applicationPlugins, fileRouter, flatRoutes, preactConfig, routerPlugin };
|
|
16
|
+
export { applicationPlugins, deploymentPlugin, fileRouter, flatRoutes, preactConfig, routerPlugin };
|
package/dist/vite/react.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export type { FileRouterOptions } from "./index.ts";
|
|
|
7
7
|
export declare function routerPlugin(options?: FileRouterOptions): Plugin;
|
|
8
8
|
/** File routing is the default; this alias keeps configuration explicit. */
|
|
9
9
|
export declare const fileRouter: typeof routerPlugin;
|
|
10
|
+
export { type DeploymentPluginOptions, deploymentPlugin, } from "./deployment.ts";
|
package/dist/vite/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { flatRoutes } from "../files/index.js";
|
|
2
2
|
import { routerPlugin as routerPlugin$1 } from "./index.js";
|
|
3
|
+
import { deploymentPlugin } from "./deployment.js";
|
|
3
4
|
//#region .generated/vite/react.ts
|
|
4
5
|
/** Configure flat routes and server transport for React. */
|
|
5
6
|
function routerPlugin(options = {}) {
|
|
@@ -11,4 +12,4 @@ function routerPlugin(options = {}) {
|
|
|
11
12
|
/** File routing is the default; this alias keeps configuration explicit. */
|
|
12
13
|
var fileRouter = routerPlugin;
|
|
13
14
|
//#endregion
|
|
14
|
-
export { fileRouter, flatRoutes, routerPlugin };
|
|
15
|
+
export { deploymentPlugin, fileRouter, flatRoutes, routerPlugin };
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"wouter": "3.13.0",
|
|
57
57
|
"wouter-preact": "3.13.0"
|
|
58
58
|
},
|
|
59
|
-
"version": "0.3.
|
|
59
|
+
"version": "0.3.1",
|
|
60
60
|
"description": "A tiny typed data router for React and Preact.",
|
|
61
61
|
"sideEffects": false,
|
|
62
62
|
"license": "MIT",
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"optional": true
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
|
-
"author": "Kasper Rynning-
|
|
157
|
+
"author": "Kasper Rynning-Tønnesen <kasperrt@users.noreply.github.com> (https://github.com/kasperrt)",
|
|
158
158
|
"homepage": "https://kasperrt.github.io/smolter/",
|
|
159
159
|
"bugs": {
|
|
160
160
|
"url": "https://github.com/kasperrt/smolter/issues"
|