@uniflowed/vite 0.13.0 → 0.14.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.
- package/index.js +99 -18
- package/internal/flight.js +12 -2
- package/internal/refresh-runtime.js +14 -2
- package/internal/refresh.js +45 -5
- package/internal/routes.js +62 -16
- package/package.json +5 -5
package/index.js
CHANGED
|
@@ -175,6 +175,51 @@ const CLIENT_COMMONJS_DEPENDENCIES = Object.freeze([
|
|
|
175
175
|
*/
|
|
176
176
|
const STYLE_PREFIX = "uf-style:";
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A reserved route file: `$page.js`, `$layout.jsx`, `$not-found.mdx`, …
|
|
180
|
+
*
|
|
181
|
+
* Built from `RESERVED` rather than written out. It used to be the literal
|
|
182
|
+
* `(page|layout|middleware|not-found)`, which is a fourth spelling of a grammar
|
|
183
|
+
* that already has three, and it was already missing `route` — so adding a
|
|
184
|
+
* route handler to a running dev server did not rebuild the table and the
|
|
185
|
+
* handler stayed invisible until a restart. A list that has to match another
|
|
186
|
+
* list has to be that list.
|
|
187
|
+
*/
|
|
188
|
+
const RESERVED_FILE = new RegExp(
|
|
189
|
+
`/(${Object.values(RESERVED)
|
|
190
|
+
.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
191
|
+
.join("|")})(\\.[a-z]+)?\\.(js|jsx|mdx)$`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* What the dev server sends the browser when the URL on screen has to be
|
|
196
|
+
* rendered again rather than reloaded: a server component changed, or the
|
|
197
|
+
* route table did. `virtual:uf/client` listens for it and refreshes the router
|
|
198
|
+
* on screen, which keeps every client component's state; see
|
|
199
|
+
* `refreshForHotUpdate` in `@uniflowed/router`. Any other module can listen
|
|
200
|
+
* too, through `import.meta.hot.on("uf:refresh", …)`.
|
|
201
|
+
*/
|
|
202
|
+
const HOT_REFRESH = Object.freeze({ type: "custom", event: "uf:refresh", data: {} });
|
|
203
|
+
|
|
204
|
+
/** Whether two action tables name the same actions, module by module. */
|
|
205
|
+
function sameActions(previous, next) {
|
|
206
|
+
if (previous.size !== next.size) return false;
|
|
207
|
+
for (const [file, rows] of previous) {
|
|
208
|
+
const other = next.get(file);
|
|
209
|
+
if (other == null || other.length !== rows.length) return false;
|
|
210
|
+
for (let at = 0; at < rows.length; at += 1) {
|
|
211
|
+
if (rows[at].id !== other[at].id || rows[at].export !== other[at].export) return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Send `message` to the browser, through the client environment where there is one. */
|
|
218
|
+
function sendToClient(devServer, message) {
|
|
219
|
+
const hot = devServer.environments?.client?.hot ?? devServer.ws;
|
|
220
|
+
hot.send(message);
|
|
221
|
+
}
|
|
222
|
+
|
|
178
223
|
/** The URL a NUL-prefixed module is served at in development. */
|
|
179
224
|
export function devUrlFor(id) {
|
|
180
225
|
return `/@id/__x00__${id}`;
|
|
@@ -390,6 +435,8 @@ function flowPlugin({
|
|
|
390
435
|
{
|
|
391
436
|
shipsPage: (route) => kept.has(route),
|
|
392
437
|
relativeTo: root,
|
|
438
|
+
// Loaders a hot update can reach; see `routesModuleSource`.
|
|
439
|
+
hot: server != null && !isProduction,
|
|
393
440
|
},
|
|
394
441
|
);
|
|
395
442
|
};
|
|
@@ -615,6 +662,7 @@ function flowPlugin({
|
|
|
615
662
|
return flightClientSource(entryPath, {
|
|
616
663
|
instrumentation: instrumentationFile(appRoot, true),
|
|
617
664
|
strictMode: strictMode && !isProduction,
|
|
665
|
+
hot: server != null && !isProduction,
|
|
618
666
|
navigation,
|
|
619
667
|
staleTime,
|
|
620
668
|
routing,
|
|
@@ -624,6 +672,7 @@ function flowPlugin({
|
|
|
624
672
|
return clientModuleSource(entryPath, {
|
|
625
673
|
instrumentation: instrumentationFile(appRoot, true),
|
|
626
674
|
strictMode: strictMode && !isProduction,
|
|
675
|
+
hot: server != null && !isProduction,
|
|
627
676
|
navigation,
|
|
628
677
|
staleTime,
|
|
629
678
|
mount,
|
|
@@ -789,7 +838,10 @@ function flowPlugin({
|
|
|
789
838
|
const moduleSideEffects = styled ? true : undefined;
|
|
790
839
|
if (!refresh) return { code: output, map, moduleSideEffects };
|
|
791
840
|
const relative = path.relative(root, cleanId(id)).split(path.sep).join("/");
|
|
792
|
-
|
|
841
|
+
// A route module under the router root is refreshed as one: its loader
|
|
842
|
+
// and metadata go to the router rather than forcing a reload.
|
|
843
|
+
const route = cleanId(id).startsWith(appRoot) && RESERVED_FILE.test(cleanId(id));
|
|
844
|
+
return { ...addRefreshWrapper(output, map, relative, { route }), moduleSideEffects };
|
|
793
845
|
},
|
|
794
846
|
|
|
795
847
|
buildEnd() {
|
|
@@ -847,9 +899,13 @@ function flowPlugin({
|
|
|
847
899
|
},
|
|
848
900
|
|
|
849
901
|
// An edit to a server component changes what the rsc graph renders and no
|
|
850
|
-
// module the browser holds, so Vite has nothing to tell the browser. It
|
|
851
|
-
//
|
|
852
|
-
//
|
|
902
|
+
// module the browser holds, so Vite has nothing to tell the browser. It
|
|
903
|
+
// used to reload the document, which renders the edit and throws away every
|
|
904
|
+
// `useState` on the page. Now the browser is told `uf:refresh`, and the
|
|
905
|
+
// router on screen fetches the payload for its URL again — the same thing
|
|
906
|
+
// `router.refresh()` does — so the edit arrives as a React update and the
|
|
907
|
+
// client components under it keep their state. An edit to a client module,
|
|
908
|
+
// which the rsc graph only holds references to, is left to Fast Refresh.
|
|
853
909
|
hotUpdate({ modules }) {
|
|
854
910
|
if (flightState == null || server == null) return;
|
|
855
911
|
if (this.environment?.name !== RSC_ENVIRONMENT) return;
|
|
@@ -857,7 +913,7 @@ function flowPlugin({
|
|
|
857
913
|
const file = module.file ?? cleanId(module.id ?? "");
|
|
858
914
|
return file !== "" && !flightState.clientModules.has(file);
|
|
859
915
|
});
|
|
860
|
-
if (serverSide) server.environments.client.hot.send(
|
|
916
|
+
if (serverSide) server.environments.client.hot.send(HOT_REFRESH);
|
|
861
917
|
},
|
|
862
918
|
|
|
863
919
|
configureServer(devServer) {
|
|
@@ -920,18 +976,30 @@ function flowPlugin({
|
|
|
920
976
|
|
|
921
977
|
// A reserved file appearing or disappearing changes the route table,
|
|
922
978
|
// which lives in a virtual module the watcher knows nothing about.
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
979
|
+
// The browser's copy of the route table, read before it is invalidated:
|
|
980
|
+
// an application rendered from its modules holds one, and
|
|
981
|
+
// `virtual:uf/client` accepts a new one in place. React Server
|
|
982
|
+
// Components keep theirs on the server.
|
|
983
|
+
const browserRouteTable = () =>
|
|
984
|
+
flightState == null ? devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes)) : null;
|
|
985
|
+
// Tell the browser its route table changed, without a reload either way.
|
|
986
|
+
// React Server Components render the table on the server, so the browser
|
|
987
|
+
// only has to ask for its URL again; an application rendered from its
|
|
988
|
+
// modules is sent the new table as a hot update. A browser that has not
|
|
989
|
+
// loaded the table yet has nothing to keep, and is reloaded.
|
|
990
|
+
const sendRouteTable = (clientTable) => {
|
|
991
|
+
if (flightState != null) {
|
|
992
|
+
sendToClient(devServer, HOT_REFRESH);
|
|
993
|
+
} else if (clientTable != null && typeof devServer.reloadModule === "function") {
|
|
994
|
+
devServer.reloadModule(clientTable).catch(() => {
|
|
995
|
+
sendToClient(devServer, { type: "full-reload", path: "*" });
|
|
996
|
+
});
|
|
997
|
+
} else {
|
|
998
|
+
sendToClient(devServer, { type: "full-reload", path: "*" });
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
933
1001
|
const onRouteFile = (file) => {
|
|
934
|
-
if (!
|
|
1002
|
+
if (!RESERVED_FILE.test(file) || !file.startsWith(appRoot)) return;
|
|
935
1003
|
// The rsc graph's table too, where the pages, the route handlers and
|
|
936
1004
|
// the middleware of an application React Server Components render
|
|
937
1005
|
// live (ubugeeei-prod/uf#1487).
|
|
@@ -939,13 +1007,14 @@ function flowPlugin({
|
|
|
939
1007
|
devServer.moduleGraph,
|
|
940
1008
|
devServer.environments?.[RSC_ENVIRONMENT]?.moduleGraph,
|
|
941
1009
|
].filter((graph) => graph != null);
|
|
1010
|
+
const clientTable = browserRouteTable();
|
|
942
1011
|
for (const graph of graphs) {
|
|
943
1012
|
for (const id of [VIRTUAL.routes, VIRTUAL.server, VIRTUAL.client]) {
|
|
944
1013
|
const module = graph.getModuleById(resolved(id));
|
|
945
1014
|
if (module) graph.invalidateModule(module);
|
|
946
1015
|
}
|
|
947
1016
|
}
|
|
948
|
-
|
|
1017
|
+
sendRouteTable(clientTable);
|
|
949
1018
|
};
|
|
950
1019
|
devServer.watcher.on("add", onRouteFile);
|
|
951
1020
|
devServer.watcher.on("unlink", onRouteFile);
|
|
@@ -976,6 +1045,7 @@ function flowPlugin({
|
|
|
976
1045
|
devServer.moduleGraph,
|
|
977
1046
|
devServer.environments?.[RSC_ENVIRONMENT]?.moduleGraph,
|
|
978
1047
|
].filter((graph) => graph != null);
|
|
1048
|
+
const clientTable = browserRouteTable();
|
|
979
1049
|
for (const graph of graphs) {
|
|
980
1050
|
invalidateActionModules(graph, previousActions);
|
|
981
1051
|
invalidateActionModules(graph, nextActions);
|
|
@@ -984,7 +1054,18 @@ function flowPlugin({
|
|
|
984
1054
|
const actions = graph.getModuleById(resolved(VIRTUAL.actions));
|
|
985
1055
|
if (actions) graph.invalidateModule(actions);
|
|
986
1056
|
}
|
|
987
|
-
|
|
1057
|
+
// A new or removed action changes the references the browser's
|
|
1058
|
+
// modules were compiled with, and those are not modules a render can
|
|
1059
|
+
// replace, so that is still a reload. Anything else the manifest says
|
|
1060
|
+
// — which routes ship a page, which modules are client components —
|
|
1061
|
+
// is the route table's business, and arrives the way a new route
|
|
1062
|
+
// file does. `uf dev` rewrites the manifest whenever a route file
|
|
1063
|
+
// comes or goes, so a reload here was a reload on every new page.
|
|
1064
|
+
if (sameActions(previousActions, nextActions)) {
|
|
1065
|
+
sendRouteTable(clientTable);
|
|
1066
|
+
} else {
|
|
1067
|
+
sendToClient(devServer, { type: "full-reload", path: "*" });
|
|
1068
|
+
}
|
|
988
1069
|
};
|
|
989
1070
|
devServer.watcher.on("add", onManifest);
|
|
990
1071
|
devServer.watcher.on("change", onManifest);
|
package/internal/flight.js
CHANGED
|
@@ -718,10 +718,20 @@ export function flightClientSource(appEntry, options = {}) {
|
|
|
718
718
|
// `app.rendering.staleTime`, in seconds, and nothing for the default `0`.
|
|
719
719
|
const staleTime =
|
|
720
720
|
options.staleTime > 0 ? `, staleTime: ${JSON.stringify(options.staleTime)}` : "";
|
|
721
|
-
|
|
721
|
+
// In development, the dev server's `uf:refresh` — a server component or the
|
|
722
|
+
// route table changed — renders the URL on screen again instead of reloading
|
|
723
|
+
// it. See `hotUpdate` in `../index.js`.
|
|
724
|
+
const hot =
|
|
725
|
+
options.hot === true
|
|
726
|
+
? `if (import.meta.hot) {
|
|
727
|
+
import.meta.hot.on("uf:refresh", () => refreshForHotUpdate());
|
|
728
|
+
}
|
|
729
|
+
`
|
|
730
|
+
: "";
|
|
731
|
+
return `import { hydrateFlight${options.hot === true ? ", refreshForHotUpdate" : ""} } from "@uniflowed/router/rsc/client";
|
|
722
732
|
import App from ${JSON.stringify(appEntry)};
|
|
723
733
|
${clientInstrumentationSource(options.instrumentation)}hydrateFlight({ App${strictMode}${navigation}${staleTime}${routing} });
|
|
724
|
-
`;
|
|
734
|
+
${hot}`;
|
|
725
735
|
}
|
|
726
736
|
|
|
727
737
|
/**
|
|
@@ -599,7 +599,18 @@ const enqueueUpdate = debounce(async () => {
|
|
|
599
599
|
performReactRefresh();
|
|
600
600
|
}, 16);
|
|
601
601
|
|
|
602
|
-
|
|
602
|
+
// `dataExports` is uf's: a route module (`$page.js`, `$layout.js`, …) exports
|
|
603
|
+
// its `loader`, `metadata` and friends beside its component, and each of those
|
|
604
|
+
// is a new identity after every edit. Plain Fast Refresh calls that an
|
|
605
|
+
// incompatible export and reloads the page. The router re-reads them instead —
|
|
606
|
+
// the wrapper hands it the next exports — so here they only have to exist on
|
|
607
|
+
// both sides.
|
|
608
|
+
export function validateRefreshBoundaryAndEnqueueUpdate(
|
|
609
|
+
id,
|
|
610
|
+
prevExports,
|
|
611
|
+
nextExports,
|
|
612
|
+
dataExports = false,
|
|
613
|
+
) {
|
|
603
614
|
const ignoredExports = window.__getReactRefreshIgnoredExports?.({ id }) ?? [];
|
|
604
615
|
if (predicateOnExport(ignoredExports, prevExports, (key) => key in nextExports) !== true) {
|
|
605
616
|
return "Could not Fast Refresh (export removed)";
|
|
@@ -616,13 +627,14 @@ export function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExp
|
|
|
616
627
|
hasExports = true;
|
|
617
628
|
if (isLikelyComponentType(value)) return true;
|
|
618
629
|
if (isCompoundComponent(value)) return true;
|
|
630
|
+
if (dataExports) return true;
|
|
619
631
|
return prevExports[key] === nextExports[key];
|
|
620
632
|
},
|
|
621
633
|
);
|
|
622
634
|
if (hasExports && allExportsAreComponentsOrUnchanged === true) {
|
|
623
635
|
enqueueUpdate();
|
|
624
636
|
} else {
|
|
625
|
-
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at
|
|
637
|
+
return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports`;
|
|
626
638
|
}
|
|
627
639
|
}
|
|
628
640
|
|
package/internal/refresh.js
CHANGED
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
// * every module that registered a component gets a header that points
|
|
13
13
|
// `$RefreshReg$`/`$RefreshSig$` at this module and a footer that validates
|
|
14
14
|
// the boundary and enqueues the refresh on `import.meta.hot.accept`.
|
|
15
|
+
//
|
|
16
|
+
// Two additions of uf's own. Every evaluation of a component module is
|
|
17
|
+
// published under its path in `window.__UF_LATEST_MODULES__`, so a Server
|
|
18
|
+
// Components payload that names the module resolves to the newest one — the
|
|
19
|
+
// one React has mounted — rather than to the first; see `latestModule` in
|
|
20
|
+
// `@uniflowed/router/internal/flight-browser.js`.
|
|
21
|
+
//
|
|
22
|
+
// And a route module (`$page.js`, `$layout.js`, …) is
|
|
23
|
+
// wrapped with `{ route: true }`. Its `loader`, `metadata` and the rest are not
|
|
24
|
+
// components, so plain Fast Refresh would give up on the first edit and reload
|
|
25
|
+
// the page, state and all. Here the component half is refreshed in place and
|
|
26
|
+
// the next exports go to the router (`window.__UF_HOT_ROUTE__`, installed by
|
|
27
|
+
// `@uniflowed/router` in development), which runs the route again with the new
|
|
28
|
+
// loader over the components React kept.
|
|
15
29
|
|
|
16
30
|
import { readFileSync } from "node:fs";
|
|
17
31
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,8 +33,19 @@ import { fileURLToPath } from "node:url";
|
|
|
19
33
|
/** Public URL the refresh runtime is served from. */
|
|
20
34
|
export const RUNTIME_PUBLIC_PATH = "/@react-refresh";
|
|
21
35
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The resolved id Vite hands back for the runtime: the public path itself, as
|
|
38
|
+
* `@vitejs/plugin-react` does.
|
|
39
|
+
*
|
|
40
|
+
* It was `\0uf:react-refresh`, and that made Fast Refresh a no-op. The
|
|
41
|
+
* preamble in the document imports `/@react-refresh` by URL, while a component
|
|
42
|
+
* module's `import "/@react-refresh"` is rewritten by Vite to the URL of the id
|
|
43
|
+
* it resolved to — `/@id/__x00__uf:react-refresh`. Two URLs are two module
|
|
44
|
+
* instances to a browser, so the runtime the preamble hooked into React was
|
|
45
|
+
* not the runtime components registered with, and every edit was "hot
|
|
46
|
+
* updated" without React ever hearing of it. One id is one URL is one runtime.
|
|
47
|
+
*/
|
|
48
|
+
export const RUNTIME_RESOLVED_ID = RUNTIME_PUBLIC_PATH;
|
|
24
49
|
|
|
25
50
|
const RUNTIME_SOURCE_PATH = fileURLToPath(new URL("./refresh-runtime.js", import.meta.url));
|
|
26
51
|
|
|
@@ -53,7 +78,8 @@ const REACT_CLASS_COMPONENT = /extends\s+(?:React\.)?(?:Pure)?Component/;
|
|
|
53
78
|
* The source map is shifted by the number of lines prepended, which is what
|
|
54
79
|
* keeps a stack trace pointing at the author's line.
|
|
55
80
|
*/
|
|
56
|
-
export function addRefreshWrapper(code, map, id) {
|
|
81
|
+
export function addRefreshWrapper(code, map, id, options = {}) {
|
|
82
|
+
const route = options.route === true;
|
|
57
83
|
const hasRefresh = REFRESH_CONTENT.test(code);
|
|
58
84
|
const onlyReactComponent = !hasRefresh && REACT_CLASS_COMPONENT.test(code);
|
|
59
85
|
if (!hasRefresh && !onlyReactComponent) {
|
|
@@ -98,9 +124,23 @@ ${nextCode}
|
|
|
98
124
|
if (import.meta.hot && !inWebWorker) {
|
|
99
125
|
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
|
|
100
126
|
RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(id)}, currentExports);
|
|
127
|
+
(window.__UF_LATEST_MODULES__ ??= new Map()).set(new URL(import.meta.url).pathname, currentExports);
|
|
101
128
|
import.meta.hot.accept((nextExports) => {
|
|
102
|
-
if (!nextExports) return
|
|
103
|
-
|
|
129
|
+
if (!nextExports) return;${
|
|
130
|
+
// First, and whether or not the boundary holds: the router answers
|
|
131
|
+
// this module's loader with these exports from now on, so a route
|
|
132
|
+
// table rebuilt after an invalidation still renders this instance and
|
|
133
|
+
// not a second copy of the module that React has never seen.
|
|
134
|
+
route
|
|
135
|
+
? `
|
|
136
|
+
if (typeof window.__UF_HOT_ROUTE__ !== "function") {
|
|
137
|
+
import.meta.hot.invalidate("No router is mounted to run the route again");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
window.__UF_HOT_ROUTE__(${JSON.stringify(id)}, nextExports);`
|
|
141
|
+
: ""
|
|
142
|
+
}
|
|
143
|
+
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify(id)}, currentExports, nextExports${route ? ", true" : ""});
|
|
104
144
|
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
|
|
105
145
|
});
|
|
106
146
|
});
|
package/internal/routes.js
CHANGED
|
@@ -1312,6 +1312,16 @@ export const VIRTUAL = Object.freeze({
|
|
|
1312
1312
|
* relative to it. Diagnostics keep a path a person can act on — a shorter one
|
|
1313
1313
|
* — and a deploy stops describing the machine it was built on.
|
|
1314
1314
|
*
|
|
1315
|
+
* # `hot`, and which module a loader loads
|
|
1316
|
+
*
|
|
1317
|
+
* A dev server's browser table passes `hot: true`, and every loader then
|
|
1318
|
+
* carries `ufHotFile`: the module's path relative to the root, the same key the
|
|
1319
|
+
* Fast Refresh wrapper hands the router when that module is replaced. It is
|
|
1320
|
+
* how a hot update to `$page.js` reaches the route that loads it — the loader
|
|
1321
|
+
* itself is a closure over a specifier, and asking it again returns the module
|
|
1322
|
+
* the browser already evaluated. See "Hot updates" in
|
|
1323
|
+
* `@uniflowed/router/internal/runtime.js`.
|
|
1324
|
+
*
|
|
1315
1325
|
* @param {{
|
|
1316
1326
|
* routes: Route[],
|
|
1317
1327
|
* handlers?: Handler[],
|
|
@@ -1322,11 +1332,13 @@ export const VIRTUAL = Object.freeze({
|
|
|
1322
1332
|
* @param {{
|
|
1323
1333
|
* shipsPage?: (route: Route) => boolean,
|
|
1324
1334
|
* relativeTo?: string,
|
|
1335
|
+
* hot?: boolean,
|
|
1325
1336
|
* }} [options]
|
|
1326
1337
|
*/
|
|
1327
1338
|
export function routesModuleSource(table, options = {}) {
|
|
1328
1339
|
const shipsPage = options.shipsPage ?? (() => true);
|
|
1329
1340
|
const relativeTo = options.relativeTo ?? null;
|
|
1341
|
+
const hot = options.hot === true && relativeTo != null;
|
|
1330
1342
|
/**
|
|
1331
1343
|
* A `file` as this table should state it.
|
|
1332
1344
|
*
|
|
@@ -1342,6 +1354,19 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1342
1354
|
}
|
|
1343
1355
|
return path.relative(relativeTo, file).split(path.sep).join("/");
|
|
1344
1356
|
};
|
|
1357
|
+
/** A lazy `import()` of `file`, tagged with its hot-update key under `hot`. */
|
|
1358
|
+
const lazy = (file) =>
|
|
1359
|
+
hot
|
|
1360
|
+
? `hotLoader(${JSON.stringify(displayFile(file))}, () => import(${JSON.stringify(file)}))`
|
|
1361
|
+
: `() => import(${JSON.stringify(file)})`;
|
|
1362
|
+
// Declared once at the top of a development table; see `hot` above.
|
|
1363
|
+
// `hotFiles` is every key the table's loaders carry, so the router can tell
|
|
1364
|
+
// a module that is still routed from one whose file was deleted.
|
|
1365
|
+
const hotHeader = hot
|
|
1366
|
+
? [
|
|
1367
|
+
"export const hotFiles = new Set();\nconst hotLoader = (ufHotFile, load) => {\n hotFiles.add(ufHotFile);\n load.ufHotFile = ufHotFile;\n return load;\n};",
|
|
1368
|
+
]
|
|
1369
|
+
: [];
|
|
1345
1370
|
const layoutIds = new Map();
|
|
1346
1371
|
const layoutImports = [];
|
|
1347
1372
|
const layoutId = (file) => {
|
|
@@ -1349,7 +1374,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1349
1374
|
if (id === undefined) {
|
|
1350
1375
|
id = `layout${layoutIds.size}`;
|
|
1351
1376
|
layoutIds.set(file, id);
|
|
1352
|
-
layoutImports.push(`const ${id} =
|
|
1377
|
+
layoutImports.push(`const ${id} = ${lazy(file)};`);
|
|
1353
1378
|
}
|
|
1354
1379
|
return id;
|
|
1355
1380
|
};
|
|
@@ -1372,7 +1397,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1372
1397
|
if (id === undefined) {
|
|
1373
1398
|
id = `loading${loadingIds.size}`;
|
|
1374
1399
|
loadingIds.set(file, id);
|
|
1375
|
-
loadingImports.push(`const ${id} =
|
|
1400
|
+
loadingImports.push(`const ${id} = ${lazy(file)};`);
|
|
1376
1401
|
}
|
|
1377
1402
|
return id;
|
|
1378
1403
|
};
|
|
@@ -1389,7 +1414,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1389
1414
|
if (id === undefined) {
|
|
1390
1415
|
id = `template${templateIds.size}`;
|
|
1391
1416
|
templateIds.set(file, id);
|
|
1392
|
-
templateImports.push(`const ${id} =
|
|
1417
|
+
templateImports.push(`const ${id} = ${lazy(file)};`);
|
|
1393
1418
|
}
|
|
1394
1419
|
return id;
|
|
1395
1420
|
};
|
|
@@ -1406,9 +1431,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1406
1431
|
const slotDefinitions = [];
|
|
1407
1432
|
const slotFiles = new Set();
|
|
1408
1433
|
const slotErrorBoundary = (boundary) =>
|
|
1409
|
-
boundary == null
|
|
1410
|
-
? "null"
|
|
1411
|
-
: `{ above: ${boundary.above}, module: () => import(${JSON.stringify(boundary.module)}) }`;
|
|
1434
|
+
boundary == null ? "null" : `{ above: ${boundary.above}, module: ${lazy(boundary.module)} }`;
|
|
1412
1435
|
// One route a slot may render, as source. The same shape for a slot's own
|
|
1413
1436
|
// routes and for its interceptions, because an intercepting page is composed
|
|
1414
1437
|
// exactly the way every other page in the slot is.
|
|
@@ -1423,7 +1446,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1423
1446
|
params: ${JSON.stringify(route.params)},
|
|
1424
1447
|
mdx: ${route.mdx},
|
|
1425
1448
|
file: ${JSON.stringify(displayFile(route.page))},
|
|
1426
|
-
page:
|
|
1449
|
+
page: ${lazy(route.page)},
|
|
1427
1450
|
layouts: [${route.layouts.map(layoutId).join(", ")}],
|
|
1428
1451
|
loading: [${(route.loading ?? [])
|
|
1429
1452
|
.map((boundary) => `{ above: ${boundary.above}, module: ${loadingId(boundary.module)} }`)
|
|
@@ -1454,7 +1477,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
1454
1477
|
const fallback =
|
|
1455
1478
|
slot.defaultPage == null
|
|
1456
1479
|
? " defaultPage: null,"
|
|
1457
|
-
: ` defaultPage:
|
|
1480
|
+
: ` defaultPage: ${lazy(slot.defaultPage)},
|
|
1458
1481
|
defaultFile: ${JSON.stringify(displayFile(slot.defaultPage))},`;
|
|
1459
1482
|
// Only when there is one, so a slot that intercepts nothing is emitted byte
|
|
1460
1483
|
// for byte the way it was before interception existed.
|
|
@@ -1504,7 +1527,7 @@ ${routes.join(",\n")}
|
|
|
1504
1527
|
params: ${JSON.stringify(route.params)},
|
|
1505
1528
|
mdx: ${route.mdx},
|
|
1506
1529
|
file: ${JSON.stringify(displayFile(route.page))},
|
|
1507
|
-
page:
|
|
1530
|
+
page: ${lazy(route.page)},
|
|
1508
1531
|
layouts: [${layouts.join(", ")}],
|
|
1509
1532
|
loading: [${loading.join(", ")}],
|
|
1510
1533
|
templates: [${templates.join(", ")}],
|
|
@@ -1517,8 +1540,7 @@ ${routes.join(",\n")}
|
|
|
1517
1540
|
// emits a loader, and a name for `file` rather than a path nothing wrote.
|
|
1518
1541
|
// See the note in `scanRoutes` and ubugeeei-prod/uf#351.
|
|
1519
1542
|
const SYNTHESISED = JSON.stringify("@uniflowed/router");
|
|
1520
|
-
const boundaryModule = (file) =>
|
|
1521
|
-
file == null ? "null" : `() => import(${JSON.stringify(file)})`;
|
|
1543
|
+
const boundaryModule = (file) => (file == null ? "null" : `${lazy(file)}`);
|
|
1522
1544
|
const boundaryFile = (file) => (file == null ? SYNTHESISED : JSON.stringify(displayFile(file)));
|
|
1523
1545
|
|
|
1524
1546
|
// A list, because a not-found is a segment file: every directory may declare
|
|
@@ -1556,7 +1578,7 @@ ${routes.join(",\n")}
|
|
|
1556
1578
|
path: ${JSON.stringify(handler.path)},
|
|
1557
1579
|
params: ${JSON.stringify(handler.params)},
|
|
1558
1580
|
file: ${JSON.stringify(displayFile(handler.module))},
|
|
1559
|
-
load:
|
|
1581
|
+
load: ${lazy(handler.module)},
|
|
1560
1582
|
}`,
|
|
1561
1583
|
);
|
|
1562
1584
|
|
|
@@ -1569,7 +1591,7 @@ ${routes.join(",\n")}
|
|
|
1569
1591
|
(entry) => ` {
|
|
1570
1592
|
path: ${JSON.stringify(entry.path)},
|
|
1571
1593
|
file: ${JSON.stringify(displayFile(entry.module))},
|
|
1572
|
-
load:
|
|
1594
|
+
load: ${lazy(entry.module)},
|
|
1573
1595
|
}`,
|
|
1574
1596
|
);
|
|
1575
1597
|
|
|
@@ -1603,7 +1625,7 @@ ${routes.join(",\n")}
|
|
|
1603
1625
|
}
|
|
1604
1626
|
}
|
|
1605
1627
|
|
|
1606
|
-
return `${[...styleOnlyImports, ...layoutImports, ...loadingImports, ...templateImports, ...slotDefinitions].join("\n")}
|
|
1628
|
+
return `${[...hotHeader, ...styleOnlyImports, ...layoutImports, ...loadingImports, ...templateImports, ...slotDefinitions].join("\n")}
|
|
1607
1629
|
export const routes = [
|
|
1608
1630
|
${entries.join(",\n")}
|
|
1609
1631
|
];
|
|
@@ -1705,6 +1727,7 @@ function slotModuleFiles(slots) {
|
|
|
1705
1727
|
* strictMode?: boolean,
|
|
1706
1728
|
* navigation?: "client" | "document",
|
|
1707
1729
|
* mount?: "hydrate" | "render",
|
|
1730
|
+
* hot?: boolean,
|
|
1708
1731
|
* }} [options]
|
|
1709
1732
|
*/
|
|
1710
1733
|
export function clientModuleSource(appEntry, options = {}) {
|
|
@@ -1715,11 +1738,34 @@ export function clientModuleSource(appEntry, options = {}) {
|
|
|
1715
1738
|
// `app.rendering.staleTime`, in seconds, and nothing for the default `0`.
|
|
1716
1739
|
const staleTime =
|
|
1717
1740
|
options.staleTime > 0 ? `, staleTime: ${JSON.stringify(options.staleTime)}` : "";
|
|
1718
|
-
|
|
1741
|
+
// In development this entry is the application's hot-update boundary for
|
|
1742
|
+
// the route table: a route file added or removed is a new
|
|
1743
|
+
// `virtual:uf/routes`, accepted here and installed in place, and the dev
|
|
1744
|
+
// server's `uf:refresh` renders the URL on screen again. Neither reloads the
|
|
1745
|
+
// document, so no component on it loses its state. A route module's own
|
|
1746
|
+
// edits reach the router through its Fast Refresh wrapper instead; see
|
|
1747
|
+
// `internal/refresh.js`.
|
|
1748
|
+
const hot =
|
|
1749
|
+
options.hot === true
|
|
1750
|
+
? `if (import.meta.hot) {
|
|
1751
|
+
acceptHotRouteModules();
|
|
1752
|
+
import.meta.hot.on("uf:refresh", () => refreshForHotUpdate());
|
|
1753
|
+
import.meta.hot.accept(${JSON.stringify(VIRTUAL.routes)}, (next) => {
|
|
1754
|
+
if (next == null) import.meta.hot.invalidate("The route table could not be replaced");
|
|
1755
|
+
else replaceRoutesForHotUpdate(next);
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
`
|
|
1759
|
+
: "";
|
|
1760
|
+
const imports =
|
|
1761
|
+
options.hot === true
|
|
1762
|
+
? `${mount}, acceptHotRouteModules, refreshForHotUpdate, replaceRoutesForHotUpdate`
|
|
1763
|
+
: mount;
|
|
1764
|
+
return `import { ${imports} } from "@uniflowed/router/client";
|
|
1719
1765
|
import { routes, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
|
|
1720
1766
|
import App from ${JSON.stringify(appEntry)};
|
|
1721
1767
|
${clientInstrumentationSource(options.instrumentation)}${mount}({ App, routes, notFound, errors${strictMode}${navigation}${staleTime}${routing} });
|
|
1722
|
-
`;
|
|
1768
|
+
${hot}`;
|
|
1723
1769
|
}
|
|
1724
1770
|
|
|
1725
1771
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"@mdx-js/rollup": "^3.1.1",
|
|
37
37
|
"@ox-content/napi": "3.2.11",
|
|
38
38
|
"@shikijs/rehype": "^3.23.0",
|
|
39
|
-
"@uniflowed/host": "0.
|
|
40
|
-
"@uniflowed/router": "0.
|
|
41
|
-
"@uniflowed/server": "0.
|
|
42
|
-
"@uniflowed/validator": "0.
|
|
39
|
+
"@uniflowed/host": "0.14.0",
|
|
40
|
+
"@uniflowed/router": "0.14.0",
|
|
41
|
+
"@uniflowed/server": "0.14.0",
|
|
42
|
+
"@uniflowed/validator": "0.14.0",
|
|
43
43
|
"babel-plugin-relay": "^21.0.1",
|
|
44
44
|
"estree-util-value-to-estree": "^3.5.0",
|
|
45
45
|
"remark-gfm": "^4.0.1",
|