@troyio/vite-file-router 0.5.0 → 0.6.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/README.md +44 -4
- package/devtools/app.js +215 -18
- package/devtools/index.html +116 -79
- package/devtools/style.css +279 -59
- package/dist/devtools.d.ts +2 -0
- package/dist/index.js +56 -5
- package/dist/scanner.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,14 +48,15 @@ plugins: [
|
|
|
48
48
|
loader: "load",
|
|
49
49
|
action: "mutate",
|
|
50
50
|
middleware: "guard",
|
|
51
|
+
hydrateFallback: "booting",
|
|
51
52
|
},
|
|
52
53
|
}),
|
|
53
54
|
];
|
|
54
55
|
```
|
|
55
56
|
|
|
56
57
|
Omitted convention values keep their defaults: `page`, `layout`, `loading`,
|
|
57
|
-
`not-found`, `error`, `loader`, `action`,
|
|
58
|
-
must have a unique filename stem.
|
|
58
|
+
`not-found`, `error`, `loader`, `action`, `middleware`, and
|
|
59
|
+
`hydrate-fallback`. Each convention must have a unique filename stem.
|
|
59
60
|
|
|
60
61
|
The router scans the pages directory recursively. Each directory becomes a
|
|
61
62
|
nested route, and the following files are recognized:
|
|
@@ -70,6 +71,7 @@ nested route, and the following files are recognized:
|
|
|
70
71
|
| `loader.js` / `loader.ts` | A React Router loader for the directory's route and its descendants. |
|
|
71
72
|
| `action.js` / `action.ts` | A React Router action selected when that directory route is the submission target. |
|
|
72
73
|
| `middleware.js` / `middleware.ts` | React Router middleware that runs before every loader and action at or below the directory. |
|
|
74
|
+
| `hydrate-fallback.jsx` / `hydrate-fallback.tsx` | What renders while the first page load's loaders run. |
|
|
73
75
|
|
|
74
76
|
Component files must default-export a React component. Loader and action
|
|
75
77
|
modules must named-export their handler as `loader` or `action`; if both
|
|
@@ -144,6 +146,32 @@ export default function Loading() {
|
|
|
144
146
|
|
|
145
147
|
A nested loading file creates a more specific boundary for that directory.
|
|
146
148
|
|
|
149
|
+
Loading files cover lazy page code, not loader time. On later navigations,
|
|
150
|
+
React Router keeps the current page on screen while loaders run, so show
|
|
151
|
+
pending UI with `useNavigation()` in a layout.
|
|
152
|
+
|
|
153
|
+
### Hydrate fallbacks
|
|
154
|
+
|
|
155
|
+
On the first page load, nothing renders until the matched routes' loaders and
|
|
156
|
+
lazy modules are ready. Add a `hydrate-fallback.jsx` or `hydrate-fallback.tsx`
|
|
157
|
+
to show something in the meantime:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
export default function HydrateFallback() {
|
|
161
|
+
return <p>Loading the app...</p>;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The file becomes the directory route's React Router `HydrateFallback`.
|
|
166
|
+
React Router renders the nearest one at or above the first route that is
|
|
167
|
+
still loading, below the layouts of the routes above it. A layout in the same
|
|
168
|
+
directory as the fallback doesn't wrap it. It only shows on the first load;
|
|
169
|
+
later navigations keep the current page on screen instead.
|
|
170
|
+
|
|
171
|
+
Hydrate fallbacks are imported up front rather than lazily, because they
|
|
172
|
+
render before any route module has loaded. Keep them small: they're part of
|
|
173
|
+
your app's initial bundle.
|
|
174
|
+
|
|
147
175
|
### Not-found pages
|
|
148
176
|
|
|
149
177
|
Add a `not-found.jsx` or `not-found.tsx` file to handle unmatched URLs. The plugin generates
|
|
@@ -386,11 +414,15 @@ export default defineConfig({
|
|
|
386
414
|
```
|
|
387
415
|
|
|
388
416
|
Open the **Routes** dock to use the panel. It's development-only and needs
|
|
389
|
-
Vite DevTools enabled.
|
|
417
|
+
Vite DevTools enabled. The panel has three pages, listed in a sidebar (or a
|
|
418
|
+
top bar when the dock is narrow): **Routes**, **Timeline**, and **Generated
|
|
419
|
+
code**. The page you're on is kept in the URL hash, so reloading the panel
|
|
420
|
+
stays on it. In the panel you can:
|
|
390
421
|
|
|
391
422
|
- **Browse the route tree.** Each route shows its URL segment, full path, and
|
|
392
423
|
badges for its files: page, layout, loading, error, not-found (`404`),
|
|
393
|
-
loader,
|
|
424
|
+
loader, action, middleware (`mw`), and hydrate fallback (`hydrate`). You
|
|
425
|
+
can filter by path, param, or file, collapse
|
|
394
426
|
branches, and move between routes with the arrow keys.
|
|
395
427
|
- **See route details.** Select a route to see its params, files, and
|
|
396
428
|
warnings. For a route with a loader or action, the **Data calls** section
|
|
@@ -445,6 +477,14 @@ Vite DevTools enabled. In the panel you can:
|
|
|
445
477
|
you can see when a loader was skipped. You can filter by call type, text,
|
|
446
478
|
or route. The dev server keeps the last 200 calls, so a panel opened later
|
|
447
479
|
still sees recent history.
|
|
480
|
+
- **Read the generated code.** The **Generated code** page shows
|
|
481
|
+
`virtual:routes`, `virtual:route-manifest`, `virtual:route-helpers`, and
|
|
482
|
+
the route types declaration file exactly as the plugin generates them, with
|
|
483
|
+
line numbers, size, simple highlighting, and a copy button. For
|
|
484
|
+
`virtual:routes`, switch between the dev server version, which includes the
|
|
485
|
+
DevTools client, and the build version. The page refreshes when route files
|
|
486
|
+
change, and a module that can't be generated (for example during a route
|
|
487
|
+
conflict) shows its error.
|
|
448
488
|
- **Inspect a call.** Expand a timeline entry to see its route params, the
|
|
449
489
|
request, and for actions the submitted form fields or body. Returned data
|
|
450
490
|
appears as a collapsible JSON tree with a copy button. Responses show their
|
package/devtools/app.js
CHANGED
|
@@ -3,6 +3,7 @@ const EVENTS_URL = "/__vite-file-router-events";
|
|
|
3
3
|
const MATCH_URL = "/__vite-file-router-match";
|
|
4
4
|
const OPEN_URL = "/__open-in-editor";
|
|
5
5
|
const NAVIGATE_URL = "/__vite-file-router-navigate";
|
|
6
|
+
const CODE_URL = "/__vite-file-router-code";
|
|
6
7
|
const RECENT_PARAMS_KEY = "vite-file-router:recent-params";
|
|
7
8
|
const TESTER_STORAGE_KEY = "vite-file-router:tester-url";
|
|
8
9
|
|
|
@@ -24,11 +25,11 @@ const detailsElement = $("#details");
|
|
|
24
25
|
const currentElement = $("#current");
|
|
25
26
|
const currentSubtitle = $("#current-subtitle");
|
|
26
27
|
const tabPicker = $("#tab-picker");
|
|
27
|
-
const
|
|
28
|
-
const timelineTab = $("#tab-timeline");
|
|
29
|
-
const inspectView = $("#view-inspect");
|
|
30
|
-
const timelineView = $("#view-timeline");
|
|
28
|
+
const pageNav = $("#page-nav");
|
|
31
29
|
const timelineCount = $("#timeline-count");
|
|
30
|
+
const warningCount = $("#warning-count");
|
|
31
|
+
const codeModulesElement = $("#code-modules");
|
|
32
|
+
const codeViewElement = $("#code-view");
|
|
32
33
|
const timelineElement = $("#timeline");
|
|
33
34
|
const timelineFilter = $("#timeline-filter");
|
|
34
35
|
const timelineTypes = $("#timeline-types");
|
|
@@ -47,6 +48,7 @@ const FILE_KINDS = [
|
|
|
47
48
|
["loader", "Loader", "loader"],
|
|
48
49
|
["action", "Action", "action"],
|
|
49
50
|
["middleware", "Middleware", "mw"],
|
|
51
|
+
["hydrateFallback", "Hydrate fallback", "hydrate"],
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
const state = {
|
|
@@ -69,6 +71,8 @@ const state = {
|
|
|
69
71
|
// Exact route ID the timeline is narrowed to, from a route's details.
|
|
70
72
|
timelineRoute: undefined,
|
|
71
73
|
openCalls: new Set(),
|
|
74
|
+
// Generated modules for the Generated code page, fetched when it opens.
|
|
75
|
+
code: { modules: undefined, selected: "virtual:routes", variant: "dev", stale: false, failure: undefined },
|
|
72
76
|
// Signature of the slow-loader warnings last rendered.
|
|
73
77
|
slowSignature: "",
|
|
74
78
|
};
|
|
@@ -406,7 +410,7 @@ function slowLoaderWarnings() {
|
|
|
406
410
|
severity: "info",
|
|
407
411
|
key: found.key,
|
|
408
412
|
fullPath: found.node.fullPath,
|
|
409
|
-
message: `This loader took up to ${formatDuration(duration)}. Loading files don't show while loaders run,
|
|
413
|
+
message: `This loader took up to ${formatDuration(duration)}. Loading files don't show while loaders run. Add a hydrate-fallback file for the first page load, and show pending UI with useNavigation() in a layout for later navigations.`,
|
|
410
414
|
files: found.node.files.loader ? [found.node.files.loader] : [],
|
|
411
415
|
},
|
|
412
416
|
];
|
|
@@ -467,6 +471,10 @@ function revealRoute(key) {
|
|
|
467
471
|
|
|
468
472
|
function renderWarnings() {
|
|
469
473
|
const warnings = allWarnings();
|
|
474
|
+
const severeCount = warnings.filter((warning) => warning.severity === "warning").length;
|
|
475
|
+
warningCount.hidden = severeCount === 0;
|
|
476
|
+
warningCount.textContent = String(severeCount);
|
|
477
|
+
warningCount.title = `${severeCount} route warning${severeCount === 1 ? "" : "s"}`;
|
|
470
478
|
warningsElement.hidden = warnings.length === 0;
|
|
471
479
|
if (!warnings.length) return;
|
|
472
480
|
const severe = warnings.filter((warning) => warning.severity === "warning").length;
|
|
@@ -694,6 +702,7 @@ function renderMatch() {
|
|
|
694
702
|
result.middleware?.length ? fact("Middleware, outermost first", result.middleware.map(fileLink)) : undefined,
|
|
695
703
|
result.loaders.length ? fact("Loaders", result.loaders.map(fileLink)) : undefined,
|
|
696
704
|
result.loading ? fact("Loading fallback", fileLink(result.loading)) : undefined,
|
|
705
|
+
result.hydrateFallback ? fact("Hydrate fallback (first load)", fileLink(result.hydrateFallback)) : undefined,
|
|
697
706
|
result.error ? fact("Error boundary", fileLink(result.error)) : undefined,
|
|
698
707
|
),
|
|
699
708
|
);
|
|
@@ -990,6 +999,201 @@ function rowOpenButton(node, key) {
|
|
|
990
999
|
);
|
|
991
1000
|
}
|
|
992
1001
|
|
|
1002
|
+
// ---------------------------------------------------------------------------
|
|
1003
|
+
// Pages
|
|
1004
|
+
|
|
1005
|
+
const PAGES = ["routes", "timeline", "code"];
|
|
1006
|
+
|
|
1007
|
+
function currentPage() {
|
|
1008
|
+
const name = window.location.hash.slice(1);
|
|
1009
|
+
return PAGES.includes(name) ? name : "routes";
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Shows a page. `inspect` is the Routes page's older name. */
|
|
1013
|
+
function showView(name) {
|
|
1014
|
+
const page = name === "inspect" ? "routes" : name;
|
|
1015
|
+
if (window.location.hash !== `#${page}`) {
|
|
1016
|
+
history.replaceState(null, "", `#${page}`);
|
|
1017
|
+
}
|
|
1018
|
+
applyPage();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function applyPage() {
|
|
1022
|
+
const page = currentPage();
|
|
1023
|
+
for (const name of PAGES) $(`#page-${name}`).hidden = name !== page;
|
|
1024
|
+
for (const link of pageNav.querySelectorAll("a")) {
|
|
1025
|
+
if (link.dataset.page === page) link.setAttribute("aria-current", "page");
|
|
1026
|
+
else link.removeAttribute("aria-current");
|
|
1027
|
+
}
|
|
1028
|
+
if (page === "timeline") state.unseen = 0;
|
|
1029
|
+
renderTimeline();
|
|
1030
|
+
if (page === "code" && (!state.code.modules || state.code.stale)) loadCode();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
// Generated code
|
|
1035
|
+
|
|
1036
|
+
const CODE_MODULES = [
|
|
1037
|
+
["virtual:routes", "Route objects passed to React Router, with lazy imports and preloading."],
|
|
1038
|
+
["virtual:route-manifest", "The route tree as data: paths, params, and files."],
|
|
1039
|
+
["virtual:route-helpers", "routePath(), useRouteParams(), and useRouteData()."],
|
|
1040
|
+
["route-types", "Types for route patterns, params, and loader data."],
|
|
1041
|
+
];
|
|
1042
|
+
|
|
1043
|
+
const KEYWORDS = new Set(
|
|
1044
|
+
"import export from const let var function return async await if else for of in new typeof type interface declare module extends default throw try catch null undefined true false class keyof infer as".split(
|
|
1045
|
+
" ",
|
|
1046
|
+
),
|
|
1047
|
+
);
|
|
1048
|
+
const TOKEN_PATTERN =
|
|
1049
|
+
/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\b([A-Za-z_$][\w$]*)\b|\b(\d+(?:\.\d+)?)\b/g;
|
|
1050
|
+
|
|
1051
|
+
/** A small highlighter for the generated JavaScript and TypeScript. */
|
|
1052
|
+
function highlight(code) {
|
|
1053
|
+
const pieces = [];
|
|
1054
|
+
let last = 0;
|
|
1055
|
+
for (const match of code.matchAll(TOKEN_PATTERN)) {
|
|
1056
|
+
const [text, comment, string, word, number] = match;
|
|
1057
|
+
const kind = comment
|
|
1058
|
+
? "comment"
|
|
1059
|
+
: string
|
|
1060
|
+
? "string"
|
|
1061
|
+
: number
|
|
1062
|
+
? "number"
|
|
1063
|
+
: word && KEYWORDS.has(word)
|
|
1064
|
+
? "keyword"
|
|
1065
|
+
: undefined;
|
|
1066
|
+
if (!kind) continue;
|
|
1067
|
+
if (match.index > last) pieces.push([undefined, code.slice(last, match.index)]);
|
|
1068
|
+
pieces.push([kind, text]);
|
|
1069
|
+
last = match.index + text.length;
|
|
1070
|
+
}
|
|
1071
|
+
if (last < code.length) pieces.push([undefined, code.slice(last)]);
|
|
1072
|
+
|
|
1073
|
+
// Split the pieces into lines, so each line can carry its number.
|
|
1074
|
+
const lines = [[]];
|
|
1075
|
+
for (const [kind, text] of pieces) {
|
|
1076
|
+
text.split("\n").forEach((part, index) => {
|
|
1077
|
+
if (index > 0) lines.push([]);
|
|
1078
|
+
if (part) lines.at(-1).push(kind ? el("span", { class: `tok-${kind}` }, part) : document.createTextNode(part));
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (lines.length > 1 && lines.at(-1).length === 0) lines.pop();
|
|
1082
|
+
return lines.map((parts) => el("span", { class: "code-line" }, parts.length ? parts : ""));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function formatBytes(bytes) {
|
|
1086
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function selectedModule() {
|
|
1090
|
+
const { modules, selected, variant } = state.code;
|
|
1091
|
+
return modules?.find(
|
|
1092
|
+
(module) => module.id === selected && (module.variant === undefined || module.variant === variant),
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function renderCode() {
|
|
1097
|
+
const { modules, selected, variant, failure } = state.code;
|
|
1098
|
+
codeModulesElement.replaceChildren(
|
|
1099
|
+
...CODE_MODULES.map(([id, description]) =>
|
|
1100
|
+
el(
|
|
1101
|
+
"button",
|
|
1102
|
+
{
|
|
1103
|
+
class: "code-module",
|
|
1104
|
+
type: "button",
|
|
1105
|
+
role: "tab",
|
|
1106
|
+
"aria-selected": String(id === selected),
|
|
1107
|
+
onclick: () => {
|
|
1108
|
+
state.code.selected = id;
|
|
1109
|
+
renderCode();
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
el("span", { class: "code-module-name" }, id === "route-types" ? "Route types (.d.ts)" : id),
|
|
1113
|
+
el("span", { class: "code-module-description" }, description),
|
|
1114
|
+
),
|
|
1115
|
+
),
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
if (failure) {
|
|
1119
|
+
codeViewElement.replaceChildren(el("p", { class: "empty" }, failure));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (!modules) {
|
|
1123
|
+
codeViewElement.replaceChildren(el("p", { class: "empty" }, "Loading generated code…"));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const module = selectedModule();
|
|
1127
|
+
const code = module?.code ?? "";
|
|
1128
|
+
const notes = [];
|
|
1129
|
+
if (module?.id === "virtual:routes") {
|
|
1130
|
+
notes.push(
|
|
1131
|
+
variant === "dev"
|
|
1132
|
+
? "As the dev server serves it, with the DevTools client and call reporting."
|
|
1133
|
+
: "As vite build generates it, without any DevTools code.",
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (module?.id === "route-types") {
|
|
1137
|
+
notes.push(
|
|
1138
|
+
module.written
|
|
1139
|
+
? `Written to ${module.file}.`
|
|
1140
|
+
: `Not written: the dts option is false, or the project has no tsconfig.json or jsconfig.json. With dts set, it goes to ${module.file}.`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
codeViewElement.replaceChildren(
|
|
1145
|
+
el(
|
|
1146
|
+
"div",
|
|
1147
|
+
{ class: "code-toolbar" },
|
|
1148
|
+
module?.id === "virtual:routes"
|
|
1149
|
+
? el(
|
|
1150
|
+
"div",
|
|
1151
|
+
{ class: "segmented code-variant", role: "group", "aria-label": "Variant" },
|
|
1152
|
+
["dev", "build"].map((name) =>
|
|
1153
|
+
el(
|
|
1154
|
+
"button",
|
|
1155
|
+
{
|
|
1156
|
+
type: "button",
|
|
1157
|
+
"aria-pressed": String(name === variant),
|
|
1158
|
+
onclick: () => {
|
|
1159
|
+
state.code.variant = name;
|
|
1160
|
+
renderCode();
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
name === "dev" ? "Dev server" : "Build",
|
|
1164
|
+
),
|
|
1165
|
+
),
|
|
1166
|
+
)
|
|
1167
|
+
: undefined,
|
|
1168
|
+
el(
|
|
1169
|
+
"span",
|
|
1170
|
+
{ class: "code-size muted" },
|
|
1171
|
+
module?.code === undefined
|
|
1172
|
+
? ""
|
|
1173
|
+
: `${code.split("\n").length - (code.endsWith("\n") ? 1 : 0)} lines · ${formatBytes(new TextEncoder().encode(code).length)}`,
|
|
1174
|
+
),
|
|
1175
|
+
module?.code !== undefined ? copyButton(code, "Copy") : undefined,
|
|
1176
|
+
),
|
|
1177
|
+
notes.length ? el("p", { class: "code-note muted" }, notes.join(" ")) : undefined,
|
|
1178
|
+
module?.error
|
|
1179
|
+
? el("p", { class: "code-error" }, module.error)
|
|
1180
|
+
: el("pre", { class: "code", tabindex: "0" }, el("code", {}, highlight(code))),
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function loadCode() {
|
|
1185
|
+
state.code.stale = false;
|
|
1186
|
+
try {
|
|
1187
|
+
const response = await fetch(CODE_URL, { cache: "no-store" });
|
|
1188
|
+
if (!response.ok) throw new Error(`Request failed (${response.status})`);
|
|
1189
|
+
state.code.modules = (await response.json()).modules;
|
|
1190
|
+
state.code.failure = undefined;
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
state.code.failure = `Couldn't load the generated code: ${error.message}`;
|
|
1193
|
+
}
|
|
1194
|
+
renderCode();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
993
1197
|
// ---------------------------------------------------------------------------
|
|
994
1198
|
// Loader and action timeline
|
|
995
1199
|
|
|
@@ -1415,7 +1619,7 @@ function addActivity(calls) {
|
|
|
1415
1619
|
if (state.activity.length > MAX_TIMELINE) {
|
|
1416
1620
|
state.activity.splice(0, state.activity.length - MAX_TIMELINE);
|
|
1417
1621
|
}
|
|
1418
|
-
if (
|
|
1622
|
+
if (currentPage() !== "timeline") state.unseen += calls.length;
|
|
1419
1623
|
renderTimeline();
|
|
1420
1624
|
refreshSlowWarnings();
|
|
1421
1625
|
// Keep the selected route's call stats current.
|
|
@@ -1424,15 +1628,6 @@ function addActivity(calls) {
|
|
|
1424
1628
|
if (routeId && calls.some((call) => call.routeId === routeId)) renderDetails();
|
|
1425
1629
|
}
|
|
1426
1630
|
|
|
1427
|
-
function showView(name) {
|
|
1428
|
-
const timeline = name === "timeline";
|
|
1429
|
-
inspectView.hidden = timeline;
|
|
1430
|
-
timelineView.hidden = !timeline;
|
|
1431
|
-
inspectTab.setAttribute("aria-selected", String(!timeline));
|
|
1432
|
-
timelineTab.setAttribute("aria-selected", String(timeline));
|
|
1433
|
-
if (timeline) state.unseen = 0;
|
|
1434
|
-
renderTimeline();
|
|
1435
|
-
}
|
|
1436
1631
|
|
|
1437
1632
|
function applyData(data) {
|
|
1438
1633
|
const snapshot = JSON.stringify(data);
|
|
@@ -1453,6 +1648,9 @@ function applyData(data) {
|
|
|
1453
1648
|
renderCurrent();
|
|
1454
1649
|
renderTimeline();
|
|
1455
1650
|
if (testerInput.value.trim()) runMatch();
|
|
1651
|
+
// Generated code follows the route files; refetch it now if it's showing.
|
|
1652
|
+
state.code.stale = true;
|
|
1653
|
+
if (currentPage() === "code" && state.code.modules) loadCode();
|
|
1456
1654
|
}
|
|
1457
1655
|
|
|
1458
1656
|
async function refresh() {
|
|
@@ -1506,8 +1704,7 @@ function connect() {
|
|
|
1506
1704
|
}
|
|
1507
1705
|
|
|
1508
1706
|
filterElement.addEventListener("input", renderTree);
|
|
1509
|
-
|
|
1510
|
-
timelineTab.addEventListener("click", () => showView("timeline"));
|
|
1707
|
+
window.addEventListener("hashchange", applyPage);
|
|
1511
1708
|
timelineFilter.addEventListener("input", renderTimeline);
|
|
1512
1709
|
timelineTypes.addEventListener("click", (event) => {
|
|
1513
1710
|
const type = event.target.closest("button")?.dataset.type;
|
|
@@ -1541,6 +1738,6 @@ testerForm.addEventListener("submit", (event) => {
|
|
|
1541
1738
|
testerInput.value = readStorage(TESTER_STORAGE_KEY);
|
|
1542
1739
|
renderTree();
|
|
1543
1740
|
renderCurrent();
|
|
1544
|
-
|
|
1741
|
+
applyPage();
|
|
1545
1742
|
refresh();
|
|
1546
1743
|
connect();
|
package/devtools/index.html
CHANGED
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
<symbol id="icon-go" viewBox="0 0 16 16">
|
|
25
25
|
<path d="M3 8h9M8.5 4.5L12 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
26
26
|
</symbol>
|
|
27
|
+
<symbol id="icon-timeline" viewBox="0 0 16 16">
|
|
28
|
+
<path d="M3 4h10M3 8h6M3 12h8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
29
|
+
</symbol>
|
|
30
|
+
<symbol id="icon-code" viewBox="0 0 16 16">
|
|
31
|
+
<path d="M6 4.5L2.5 8 6 11.5M10 4.5L13.5 8 10 11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
32
|
+
</symbol>
|
|
33
|
+
<symbol id="icon-copy" viewBox="0 0 16 16">
|
|
34
|
+
<path d="M5.5 5.5h7v7h-7zM3.5 10.5v-7h7" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" />
|
|
35
|
+
</symbol>
|
|
27
36
|
<symbol id="icon-route" viewBox="0 0 16 16">
|
|
28
37
|
<circle cx="4" cy="4" r="1.8" fill="none" stroke="currentColor" stroke-width="1.4" />
|
|
29
38
|
<circle cx="12" cy="12" r="1.8" fill="none" stroke="currentColor" stroke-width="1.4" />
|
|
@@ -58,96 +67,124 @@
|
|
|
58
67
|
</header>
|
|
59
68
|
|
|
60
69
|
<p class="alert" id="error" role="alert" hidden></p>
|
|
61
|
-
<section class="conflicts" id="conflicts" aria-label="Route conflicts" hidden></section>
|
|
62
|
-
<section class="warnings" id="warnings" aria-label="Route warnings" hidden></section>
|
|
63
70
|
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</
|
|
76
|
-
<
|
|
77
|
-
|
|
71
|
+
<div class="app">
|
|
72
|
+
<nav class="page-nav" id="page-nav" aria-label="Panel pages">
|
|
73
|
+
<a href="#routes" data-page="routes" aria-current="page">
|
|
74
|
+
<svg aria-hidden="true"><use href="#icon-route" /></svg>
|
|
75
|
+
<span>Routes</span>
|
|
76
|
+
<span class="nav-count" id="warning-count" hidden></span>
|
|
77
|
+
</a>
|
|
78
|
+
<a href="#timeline" data-page="timeline">
|
|
79
|
+
<svg aria-hidden="true"><use href="#icon-timeline" /></svg>
|
|
80
|
+
<span>Timeline</span>
|
|
81
|
+
<span class="nav-count" id="timeline-count" hidden></span>
|
|
82
|
+
</a>
|
|
83
|
+
<a href="#code" data-page="code">
|
|
84
|
+
<svg aria-hidden="true"><use href="#icon-code" /></svg>
|
|
85
|
+
<span>Generated code</span>
|
|
86
|
+
</a>
|
|
87
|
+
</nav>
|
|
78
88
|
|
|
79
|
-
<
|
|
80
|
-
<section class="
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
<div class="page" id="page-routes">
|
|
90
|
+
<section class="conflicts" id="conflicts" aria-label="Route conflicts" hidden></section>
|
|
91
|
+
<section class="warnings" id="warnings" aria-label="Route warnings" hidden></section>
|
|
92
|
+
|
|
93
|
+
<main class="layout">
|
|
94
|
+
<section class="panel tree-panel" aria-label="Route tree">
|
|
95
|
+
<div class="panel-toolbar">
|
|
96
|
+
<label class="field">
|
|
97
|
+
<svg class="field-icon" aria-hidden="true"><use href="#icon-search" /></svg>
|
|
98
|
+
<span class="visually-hidden">Filter routes</span>
|
|
99
|
+
<input id="filter" type="search" placeholder="Filter by path, param, or file" autocomplete="off" spellcheck="false" />
|
|
100
|
+
</label>
|
|
101
|
+
<button class="icon-button" id="toggle" type="button" aria-label="Collapse all" title="Collapse all">
|
|
102
|
+
<svg><use href="#icon-collapse" /></svg>
|
|
103
|
+
</button>
|
|
85
104
|
</div>
|
|
86
|
-
<div class="
|
|
87
|
-
</
|
|
88
|
-
<div id="current" aria-live="polite"></div>
|
|
89
|
-
</section>
|
|
105
|
+
<div class="tree" id="tree" role="tree" aria-label="Routes"></div>
|
|
106
|
+
</section>
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
<aside class="side">
|
|
109
|
+
<section class="panel current-panel" aria-labelledby="current-title">
|
|
110
|
+
<div class="panel-header panel-header-row">
|
|
111
|
+
<div>
|
|
112
|
+
<h2 id="current-title">Current page</h2>
|
|
113
|
+
<p id="current-subtitle">Follows the route your app is showing.</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="tab-picker" id="tab-picker" role="group" aria-label="App tabs" hidden></div>
|
|
116
|
+
</div>
|
|
117
|
+
<div id="current" aria-live="polite"></div>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<section class="panel" aria-labelledby="tester-title">
|
|
121
|
+
<div class="panel-header">
|
|
122
|
+
<h2 id="tester-title">Test a URL</h2>
|
|
123
|
+
<p>Shows which route files handle a URL.</p>
|
|
124
|
+
</div>
|
|
125
|
+
<form class="tester" id="tester">
|
|
126
|
+
<label class="field field-mono">
|
|
127
|
+
<span class="visually-hidden">URL to match</span>
|
|
128
|
+
<input id="tester-input" type="text" placeholder="/users/42?tab=posts" autocomplete="off" spellcheck="false" />
|
|
129
|
+
</label>
|
|
130
|
+
<button class="button" type="submit">Match</button>
|
|
131
|
+
</form>
|
|
132
|
+
<div id="tester-result" aria-live="polite"></div>
|
|
133
|
+
</section>
|
|
97
134
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
<section class="panel" aria-labelledby="details-title">
|
|
136
|
+
<div class="panel-header">
|
|
137
|
+
<h2 id="details-title">Route details</h2>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="details"></div>
|
|
140
|
+
</section>
|
|
141
|
+
</aside>
|
|
142
|
+
</main>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="page" id="page-timeline" hidden>
|
|
146
|
+
<section class="panel" aria-labelledby="timeline-title">
|
|
147
|
+
<div class="panel-header panel-header-row">
|
|
148
|
+
<div>
|
|
149
|
+
<h2 id="timeline-title">Loader and action timeline</h2>
|
|
150
|
+
<p>Calls made by your app, newest first.</p>
|
|
151
|
+
</div>
|
|
152
|
+
<button class="icon-button text-button" id="timeline-clear" type="button">Clear</button>
|
|
103
153
|
</div>
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
<
|
|
107
|
-
<
|
|
154
|
+
<div class="timeline-toolbar">
|
|
155
|
+
<div class="segmented" id="timeline-types" role="group" aria-label="Filter by call type">
|
|
156
|
+
<button type="button" data-type="all" aria-pressed="true">All</button>
|
|
157
|
+
<button type="button" data-type="loader" aria-pressed="false">Loaders</button>
|
|
158
|
+
<button type="button" data-type="action" aria-pressed="false">Actions</button>
|
|
159
|
+
<button type="button" data-type="middleware" aria-pressed="false">Middleware</button>
|
|
160
|
+
<button type="button" data-type="revalidate" aria-pressed="false">Revalidation</button>
|
|
161
|
+
</div>
|
|
162
|
+
<span class="route-pill" id="timeline-route" hidden></span>
|
|
163
|
+
<label class="field">
|
|
164
|
+
<svg class="field-icon" aria-hidden="true"><use href="#icon-search" /></svg>
|
|
165
|
+
<span class="visually-hidden">Filter calls</span>
|
|
166
|
+
<input id="timeline-filter" type="search" placeholder="Filter by route, URL, or outcome" autocomplete="off" spellcheck="false" />
|
|
108
167
|
</label>
|
|
109
|
-
<button class="button" type="submit">Match</button>
|
|
110
|
-
</form>
|
|
111
|
-
<div id="tester-result" aria-live="polite"></div>
|
|
112
|
-
</section>
|
|
113
|
-
|
|
114
|
-
<section class="panel" aria-labelledby="details-title">
|
|
115
|
-
<div class="panel-header">
|
|
116
|
-
<h2 id="details-title">Route details</h2>
|
|
117
168
|
</div>
|
|
118
|
-
<
|
|
169
|
+
<ol class="timeline" id="timeline" aria-label="Calls"></ol>
|
|
119
170
|
</section>
|
|
120
|
-
|
|
171
|
+
</div>
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</div>
|
|
129
|
-
<button class="icon-button text-button" id="timeline-clear" type="button">Clear</button>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="timeline-toolbar">
|
|
132
|
-
<div class="segmented" id="timeline-types" role="group" aria-label="Filter by call type">
|
|
133
|
-
<button type="button" data-type="all" aria-pressed="true">All</button>
|
|
134
|
-
<button type="button" data-type="loader" aria-pressed="false">Loaders</button>
|
|
135
|
-
<button type="button" data-type="action" aria-pressed="false">Actions</button>
|
|
136
|
-
<button type="button" data-type="middleware" aria-pressed="false">Middleware</button>
|
|
137
|
-
<button type="button" data-type="revalidate" aria-pressed="false">Revalidation</button>
|
|
138
|
-
</div>
|
|
139
|
-
<span class="route-pill" id="timeline-route" hidden></span>
|
|
140
|
-
<label class="field">
|
|
141
|
-
<svg class="field-icon" aria-hidden="true"><use href="#icon-search" /></svg>
|
|
142
|
-
<span class="visually-hidden">Filter calls</span>
|
|
143
|
-
<input id="timeline-filter" type="search" placeholder="Filter by route, URL, or outcome" autocomplete="off" spellcheck="false" />
|
|
144
|
-
</label>
|
|
173
|
+
<div class="page" id="page-code" hidden>
|
|
174
|
+
<section class="panel code-panel" aria-labelledby="code-title">
|
|
175
|
+
<div class="panel-header panel-header-row">
|
|
176
|
+
<div>
|
|
177
|
+
<h2 id="code-title">Generated code</h2>
|
|
178
|
+
<p>The modules the plugin generates from your route files.</p>
|
|
145
179
|
</div>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
</div>
|
|
181
|
+
<div class="code-layout">
|
|
182
|
+
<div class="code-modules" id="code-modules" role="tablist" aria-label="Generated modules"></div>
|
|
183
|
+
<div class="code-view" id="code-view" role="tabpanel" aria-live="polite"></div>
|
|
184
|
+
</div>
|
|
185
|
+
</section>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
151
188
|
<script type="module" src="/__vite-file-router/app.js"></script>
|
|
152
189
|
</body>
|
|
153
190
|
</html>
|
package/devtools/style.css
CHANGED
|
@@ -350,7 +350,7 @@ h1 {
|
|
|
350
350
|
|
|
351
351
|
.alert,
|
|
352
352
|
.conflicts {
|
|
353
|
-
margin:
|
|
353
|
+
margin: 0;
|
|
354
354
|
padding: 12px 14px;
|
|
355
355
|
border: 1px solid var(--border);
|
|
356
356
|
border-radius: 0;
|
|
@@ -389,7 +389,7 @@ h1 {
|
|
|
389
389
|
grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
|
|
390
390
|
gap: 16px;
|
|
391
391
|
align-items: start;
|
|
392
|
-
padding:
|
|
392
|
+
padding: 0;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
@media (max-width: 860px) {
|
|
@@ -918,62 +918,6 @@ mark {
|
|
|
918
918
|
}
|
|
919
919
|
|
|
920
920
|
|
|
921
|
-
/* Tabs */
|
|
922
|
-
|
|
923
|
-
.tabs {
|
|
924
|
-
display: flex;
|
|
925
|
-
gap: 4px;
|
|
926
|
-
padding: 4px;
|
|
927
|
-
border: 1px solid var(--border);
|
|
928
|
-
border-radius: 0;
|
|
929
|
-
background: var(--surface);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
.tab {
|
|
933
|
-
display: inline-flex;
|
|
934
|
-
flex: 1;
|
|
935
|
-
align-items: center;
|
|
936
|
-
justify-content: center;
|
|
937
|
-
gap: 6px;
|
|
938
|
-
height: 30px;
|
|
939
|
-
border: 0;
|
|
940
|
-
border-radius: 0;
|
|
941
|
-
background: none;
|
|
942
|
-
color: var(--text-2);
|
|
943
|
-
font-weight: 600;
|
|
944
|
-
cursor: pointer;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
.tab:hover {
|
|
948
|
-
color: var(--text);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
.tab[aria-selected="true"] {
|
|
952
|
-
background: var(--accent-soft);
|
|
953
|
-
color: var(--accent-text);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
.tab-count {
|
|
957
|
-
min-width: 18px;
|
|
958
|
-
height: 18px;
|
|
959
|
-
padding: 0 5px;
|
|
960
|
-
border-radius: 0;
|
|
961
|
-
background: var(--accent);
|
|
962
|
-
color: var(--accent-contrast);
|
|
963
|
-
font-size: 10.5px;
|
|
964
|
-
line-height: 18px;
|
|
965
|
-
font-variant-numeric: tabular-nums;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
.view {
|
|
969
|
-
display: grid;
|
|
970
|
-
gap: 16px;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
.view[hidden] {
|
|
974
|
-
display: none;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
921
|
.text-button {
|
|
978
922
|
width: auto;
|
|
979
923
|
padding: 0 10px;
|
|
@@ -1137,7 +1081,7 @@ mark {
|
|
|
1137
1081
|
/* Missing-file warnings */
|
|
1138
1082
|
|
|
1139
1083
|
.warnings {
|
|
1140
|
-
margin:
|
|
1084
|
+
margin: 0;
|
|
1141
1085
|
border: 1px solid var(--border);
|
|
1142
1086
|
background: var(--surface);
|
|
1143
1087
|
}
|
|
@@ -1488,3 +1432,279 @@ mark {
|
|
|
1488
1432
|
.result-open {
|
|
1489
1433
|
margin-left: auto;
|
|
1490
1434
|
}
|
|
1435
|
+
|
|
1436
|
+
/* Pages and navigation */
|
|
1437
|
+
|
|
1438
|
+
.alert {
|
|
1439
|
+
margin: 16px 20px 0;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.app {
|
|
1443
|
+
display: grid;
|
|
1444
|
+
grid-template-columns: 184px minmax(0, 1fr);
|
|
1445
|
+
align-items: start;
|
|
1446
|
+
min-height: calc(100vh - 57px);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
.page-nav {
|
|
1450
|
+
position: sticky;
|
|
1451
|
+
top: 57px;
|
|
1452
|
+
display: grid;
|
|
1453
|
+
align-content: start;
|
|
1454
|
+
gap: 2px;
|
|
1455
|
+
padding: 16px 10px;
|
|
1456
|
+
border-right: 1px solid var(--border);
|
|
1457
|
+
min-height: calc(100vh - 57px);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.page-nav a {
|
|
1461
|
+
display: flex;
|
|
1462
|
+
align-items: center;
|
|
1463
|
+
gap: 9px;
|
|
1464
|
+
height: 34px;
|
|
1465
|
+
padding: 0 10px;
|
|
1466
|
+
color: var(--text-2);
|
|
1467
|
+
font-weight: 550;
|
|
1468
|
+
text-decoration: none;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.page-nav a:hover {
|
|
1472
|
+
background: var(--surface-2);
|
|
1473
|
+
color: var(--text);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
.page-nav a[aria-current="page"] {
|
|
1477
|
+
background: var(--accent-soft);
|
|
1478
|
+
color: var(--text);
|
|
1479
|
+
font-weight: 650;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.page-nav svg {
|
|
1483
|
+
color: var(--text-3);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.page-nav a[aria-current="page"] svg {
|
|
1487
|
+
color: var(--text);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
.nav-count {
|
|
1491
|
+
min-width: 18px;
|
|
1492
|
+
height: 18px;
|
|
1493
|
+
margin-left: auto;
|
|
1494
|
+
padding: 0 5px;
|
|
1495
|
+
background: var(--accent);
|
|
1496
|
+
color: var(--accent-contrast);
|
|
1497
|
+
font-size: 10.5px;
|
|
1498
|
+
line-height: 18px;
|
|
1499
|
+
text-align: center;
|
|
1500
|
+
font-variant-numeric: tabular-nums;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
#warning-count {
|
|
1504
|
+
background: var(--surface-2);
|
|
1505
|
+
color: var(--warning-text);
|
|
1506
|
+
font-weight: 650;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.page {
|
|
1510
|
+
display: grid;
|
|
1511
|
+
gap: 16px;
|
|
1512
|
+
min-width: 0;
|
|
1513
|
+
padding: 16px 20px 24px;
|
|
1514
|
+
align-content: start;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
@media (max-width: 860px) {
|
|
1518
|
+
.app {
|
|
1519
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.page-nav {
|
|
1523
|
+
position: static;
|
|
1524
|
+
display: flex;
|
|
1525
|
+
gap: 2px;
|
|
1526
|
+
min-height: 0;
|
|
1527
|
+
padding: 6px 12px;
|
|
1528
|
+
overflow-x: auto;
|
|
1529
|
+
border-right: 0;
|
|
1530
|
+
border-bottom: 1px solid var(--border);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.page-nav a {
|
|
1534
|
+
flex: none;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.page {
|
|
1538
|
+
padding: 12px;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/* Generated code */
|
|
1543
|
+
|
|
1544
|
+
.code-layout {
|
|
1545
|
+
display: grid;
|
|
1546
|
+
grid-template-columns: 230px minmax(0, 1fr);
|
|
1547
|
+
border-top: 1px solid var(--border);
|
|
1548
|
+
margin-top: 14px;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.code-modules {
|
|
1552
|
+
display: grid;
|
|
1553
|
+
align-content: start;
|
|
1554
|
+
border-right: 1px solid var(--border);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.code-module {
|
|
1558
|
+
display: grid;
|
|
1559
|
+
gap: 2px;
|
|
1560
|
+
padding: 10px 14px;
|
|
1561
|
+
border: 0;
|
|
1562
|
+
border-bottom: 1px solid var(--border);
|
|
1563
|
+
background: none;
|
|
1564
|
+
text-align: left;
|
|
1565
|
+
cursor: pointer;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.code-module:hover {
|
|
1569
|
+
background: var(--surface-2);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
.code-module[aria-selected="true"] {
|
|
1573
|
+
background: var(--accent-soft);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
.code-module-name {
|
|
1577
|
+
font-family: var(--mono);
|
|
1578
|
+
font-size: 12px;
|
|
1579
|
+
font-weight: 600;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.code-module-description {
|
|
1583
|
+
color: var(--text-3);
|
|
1584
|
+
font-size: 11.5px;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.code-view {
|
|
1588
|
+
display: grid;
|
|
1589
|
+
align-content: start;
|
|
1590
|
+
gap: 8px;
|
|
1591
|
+
min-width: 0;
|
|
1592
|
+
padding: 12px 14px 14px;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.code-toolbar {
|
|
1596
|
+
display: flex;
|
|
1597
|
+
align-items: center;
|
|
1598
|
+
gap: 12px;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
.code-variant {
|
|
1602
|
+
flex: none;
|
|
1603
|
+
width: 220px;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
.code-size {
|
|
1607
|
+
margin-left: auto;
|
|
1608
|
+
font-size: 12px;
|
|
1609
|
+
font-variant-numeric: tabular-nums;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
.code-note {
|
|
1613
|
+
font-size: 12px;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
.code-error {
|
|
1617
|
+
padding: 10px 12px;
|
|
1618
|
+
background: var(--surface-2);
|
|
1619
|
+
color: var(--danger-text);
|
|
1620
|
+
white-space: pre-wrap;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.code {
|
|
1624
|
+
max-height: calc(100vh - 260px);
|
|
1625
|
+
min-height: 200px;
|
|
1626
|
+
margin: 0;
|
|
1627
|
+
overflow: auto;
|
|
1628
|
+
border: 1px solid var(--border);
|
|
1629
|
+
background: var(--surface-2);
|
|
1630
|
+
font-family: var(--mono);
|
|
1631
|
+
font-size: 12px;
|
|
1632
|
+
line-height: 1.6;
|
|
1633
|
+
counter-reset: line;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
.code code {
|
|
1637
|
+
display: block;
|
|
1638
|
+
min-width: max-content;
|
|
1639
|
+
padding: 8px 0;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
.code-line {
|
|
1643
|
+
display: block;
|
|
1644
|
+
padding-right: 16px;
|
|
1645
|
+
white-space: pre;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.code-line::before {
|
|
1649
|
+
counter-increment: line;
|
|
1650
|
+
content: counter(line);
|
|
1651
|
+
display: inline-block;
|
|
1652
|
+
width: 3.5em;
|
|
1653
|
+
margin-right: 14px;
|
|
1654
|
+
padding-right: 10px;
|
|
1655
|
+
border-right: 1px solid var(--border);
|
|
1656
|
+
color: var(--text-3);
|
|
1657
|
+
text-align: right;
|
|
1658
|
+
user-select: none;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
.tok-comment {
|
|
1662
|
+
color: var(--text-3);
|
|
1663
|
+
font-style: italic;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
.tok-string {
|
|
1667
|
+
color: var(--success);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
.tok-keyword {
|
|
1671
|
+
color: var(--text);
|
|
1672
|
+
font-weight: 650;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.tok-number {
|
|
1676
|
+
color: var(--warning-text);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
@media (max-width: 860px) {
|
|
1680
|
+
.code-layout {
|
|
1681
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.code-modules {
|
|
1685
|
+
display: flex;
|
|
1686
|
+
overflow-x: auto;
|
|
1687
|
+
border-right: 0;
|
|
1688
|
+
border-bottom: 1px solid var(--border);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.code-module {
|
|
1692
|
+
flex: none;
|
|
1693
|
+
border-bottom: 0;
|
|
1694
|
+
border-right: 1px solid var(--border);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.code-module-description {
|
|
1698
|
+
display: none;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
@media (max-width: 1080px) and (min-width: 861px) {
|
|
1703
|
+
.layout {
|
|
1704
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.side {
|
|
1708
|
+
position: static;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
package/dist/devtools.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const ROUTE_INSPECTOR_DATA_PATH = "/__vite-file-router-data";
|
|
|
6
6
|
export declare const ROUTE_INSPECTOR_EVENTS_PATH = "/__vite-file-router-events";
|
|
7
7
|
export declare const ROUTE_INSPECTOR_MATCH_PATH = "/__vite-file-router-match";
|
|
8
8
|
export declare const ROUTE_INSPECTOR_NAVIGATE_PATH = "/__vite-file-router-navigate";
|
|
9
|
+
export declare const ROUTE_INSPECTOR_CODE_PATH = "/__vite-file-router-code";
|
|
9
10
|
export type RouteInspectorState = {
|
|
10
11
|
/** Absolute Vite project root, so the panel can open files in an editor. */
|
|
11
12
|
projectRoot: string;
|
|
@@ -58,6 +59,7 @@ export type RouteMatchResult = {
|
|
|
58
59
|
loaders: string[];
|
|
59
60
|
middleware: string[];
|
|
60
61
|
loading?: string;
|
|
62
|
+
hydrateFallback?: string;
|
|
61
63
|
error?: string;
|
|
62
64
|
};
|
|
63
65
|
export declare function matchRouteManifest(manifest: RouteManifestNode, url: string): Promise<RouteMatchResult>;
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,8 @@ var DEFAULT_CONVENTIONS = {
|
|
|
12
12
|
error: "error",
|
|
13
13
|
loader: "loader",
|
|
14
14
|
action: "action",
|
|
15
|
-
middleware: "middleware"
|
|
15
|
+
middleware: "middleware",
|
|
16
|
+
hydrateFallback: "hydrate-fallback"
|
|
16
17
|
};
|
|
17
18
|
function getRouteSegment(name) {
|
|
18
19
|
if (name.startsWith("[...") && name.endsWith("]")) return "*";
|
|
@@ -526,6 +527,11 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
526
527
|
const instrumentId = getInstrumentId(node, context);
|
|
527
528
|
route.middleware = `[${instrumentId ? `instrumentMiddleware(${JSON.stringify(instrumentId)}, ${load})` : load}]`;
|
|
528
529
|
}
|
|
530
|
+
if (node.hydrateFallback) {
|
|
531
|
+
const fallback = genSafeVariableName(`HydrateFallback${context.nextLoadingId++}`);
|
|
532
|
+
context.loadingImports.push(`import ${fallback} from ${JSON.stringify(getImportPath(node.hydrateFallback, context.projectRoot))};`);
|
|
533
|
+
route.HydrateFallback = fallback;
|
|
534
|
+
}
|
|
529
535
|
let loadingComponent;
|
|
530
536
|
if (node.loading) {
|
|
531
537
|
loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
|
|
@@ -845,6 +851,7 @@ async function matchRouteManifest(manifest, url) {
|
|
|
845
851
|
loaders: collect("loader"),
|
|
846
852
|
middleware: collect("middleware"),
|
|
847
853
|
...nearest("loading") ? { loading: nearest("loading") } : {},
|
|
854
|
+
...nearest("hydrateFallback") ? { hydrateFallback: nearest("hydrateFallback") } : {},
|
|
848
855
|
...nearest("error") ? { error: nearest("error") } : {}
|
|
849
856
|
};
|
|
850
857
|
}
|
|
@@ -1175,6 +1182,44 @@ function routes(input = "src/pages") {
|
|
|
1175
1182
|
onActivity: (event) => routeEvents.broadcast("activity", event)
|
|
1176
1183
|
});
|
|
1177
1184
|
const routeEvents = createRouteEventStream(getInspectorState, () => [["apps", appTracker.tabs()], ["activity-snapshot", appTracker.activity()]]);
|
|
1185
|
+
/**
|
|
1186
|
+
* Every module the plugin generates, for the DevTools Generated code page.
|
|
1187
|
+
* A module that fails to generate (for example on a route conflict)
|
|
1188
|
+
* reports its error instead of failing the others.
|
|
1189
|
+
*/
|
|
1190
|
+
async function getGeneratedCode() {
|
|
1191
|
+
const capture = async (module, create) => {
|
|
1192
|
+
try {
|
|
1193
|
+
return {
|
|
1194
|
+
...module,
|
|
1195
|
+
code: await create()
|
|
1196
|
+
};
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
return {
|
|
1199
|
+
...module,
|
|
1200
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
const typesFile = declarationFile ?? path.resolve(projectRoot, DEFAULT_DTS);
|
|
1205
|
+
return Promise.all([
|
|
1206
|
+
capture({
|
|
1207
|
+
id: ROUTES_MODULE_ID,
|
|
1208
|
+
variant: "dev"
|
|
1209
|
+
}, () => execute(true)),
|
|
1210
|
+
capture({
|
|
1211
|
+
id: ROUTES_MODULE_ID,
|
|
1212
|
+
variant: "build"
|
|
1213
|
+
}, () => execute(false)),
|
|
1214
|
+
capture({ id: MANIFEST_MODULE_ID }, executeManifest),
|
|
1215
|
+
capture({ id: ROUTE_HELPERS_MODULE_ID }, executeRouteHelpers),
|
|
1216
|
+
capture({
|
|
1217
|
+
id: "route-types",
|
|
1218
|
+
file: path.relative(projectRoot, typesFile).split(path.sep).join("/"),
|
|
1219
|
+
written: Boolean(declarationFile)
|
|
1220
|
+
}, () => createRouteDeclarations(scan(root, options.conventions), projectRoot, typesFile))
|
|
1221
|
+
]);
|
|
1222
|
+
}
|
|
1178
1223
|
function writeDeclarations() {
|
|
1179
1224
|
if (!declarationFile) return;
|
|
1180
1225
|
const content = createRouteDeclarations(scan(root, options.conventions), projectRoot, declarationFile);
|
|
@@ -1191,12 +1236,13 @@ function routes(input = "src/pages") {
|
|
|
1191
1236
|
return leftSource.localeCompare(rightSource);
|
|
1192
1237
|
});
|
|
1193
1238
|
}
|
|
1194
|
-
|
|
1239
|
+
/** `devClient` adds the DevTools client, which only the dev server serves. */
|
|
1240
|
+
async function execute(devClient = dev) {
|
|
1195
1241
|
const tree = scan(root, options.conventions);
|
|
1196
1242
|
const conflicts = findPageRouteConflicts(tree);
|
|
1197
1243
|
if (conflicts.length) {
|
|
1198
1244
|
const error = new RouteConflictError(conflicts);
|
|
1199
|
-
if (!
|
|
1245
|
+
if (!devClient) throw error;
|
|
1200
1246
|
const code = `
|
|
1201
1247
|
import { createElement } from "react";
|
|
1202
1248
|
|
|
@@ -1220,7 +1266,7 @@ function routes(input = "src/pages") {
|
|
|
1220
1266
|
`;
|
|
1221
1267
|
return format(code, { parser: "typescript" });
|
|
1222
1268
|
}
|
|
1223
|
-
const generated = generate(tree, true, projectRoot, { devClient
|
|
1269
|
+
const generated = generate(tree, true, projectRoot, { devClient });
|
|
1224
1270
|
const sections = [
|
|
1225
1271
|
sortImports(generated.imports).join("\n"),
|
|
1226
1272
|
generated.helpers.join("\n"),
|
|
@@ -1435,7 +1481,8 @@ function routes(input = "src/pages") {
|
|
|
1435
1481
|
"/__vite-file-router-data",
|
|
1436
1482
|
"/__vite-file-router-events",
|
|
1437
1483
|
"/__vite-file-router-match",
|
|
1438
|
-
"/__vite-file-router-navigate"
|
|
1484
|
+
"/__vite-file-router-navigate",
|
|
1485
|
+
"/__vite-file-router-code"
|
|
1439
1486
|
].includes(url.pathname)) {
|
|
1440
1487
|
next();
|
|
1441
1488
|
return;
|
|
@@ -1447,6 +1494,10 @@ function routes(input = "src/pages") {
|
|
|
1447
1494
|
res.end("Method Not Allowed");
|
|
1448
1495
|
return;
|
|
1449
1496
|
}
|
|
1497
|
+
if (url.pathname === "/__vite-file-router-code") {
|
|
1498
|
+
getGeneratedCode().then((modules) => sendJson(res, { modules }), next);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1450
1501
|
if (url.pathname === "/__vite-file-router-navigate") {
|
|
1451
1502
|
navigateApp(req, res);
|
|
1452
1503
|
return;
|
package/dist/scanner.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ export type Node = {
|
|
|
10
10
|
loader?: string;
|
|
11
11
|
action?: string;
|
|
12
12
|
middleware?: string;
|
|
13
|
+
hydrateFallback?: string;
|
|
13
14
|
children: Node[];
|
|
14
15
|
};
|
|
15
|
-
export type RouteFileKind = "page" | "layout" | "loading" | "notFound" | "error" | "loader" | "action" | "middleware";
|
|
16
|
+
export type RouteFileKind = "page" | "layout" | "loading" | "notFound" | "error" | "loader" | "action" | "middleware" | "hydrateFallback";
|
|
16
17
|
export type RouteConventions = Partial<Record<RouteFileKind, string>>;
|
|
17
18
|
export type RouteManifestNode = {
|
|
18
19
|
segment: string | null;
|