@troyio/vite-file-router 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -12
- package/devtools/app.js +961 -143
- package/devtools/index.html +147 -33
- package/devtools/style.css +1136 -231
- package/dist/devtools-app.d.ts +59 -0
- package/dist/devtools.d.ts +50 -0
- package/dist/generator.d.ts +8 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +413 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -139,15 +139,15 @@ function generateImport(file, projectRoot) {
|
|
|
139
139
|
Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
|
|
140
140
|
})`;
|
|
141
141
|
}
|
|
142
|
-
function generateRouteLazy(node, projectRoot, includeLayout, includeLoader) {
|
|
142
|
+
function generateRouteLazy(node, projectRoot, includeLayout, includeLoader, instrumentId) {
|
|
143
143
|
const properties = [];
|
|
144
144
|
if (includeLayout && node.layout) properties.push(`Component: withOutlet((await import(${JSON.stringify(getImportPath(node.layout, projectRoot))})).default)`);
|
|
145
145
|
if (includeLoader && node.loader) return `async () => {
|
|
146
146
|
const loaderModule = await import(${JSON.stringify(getImportPath(node.loader, projectRoot))});
|
|
147
147
|
return {
|
|
148
|
-
loader: loaderModule.loader,
|
|
148
|
+
loader: ${instrumentId ? `instrumentRouteData("loader", ${JSON.stringify(instrumentId)}, loaderModule.loader)` : "loaderModule.loader"},
|
|
149
149
|
...(typeof loaderModule.shouldRevalidate === "function"
|
|
150
|
-
? { shouldRevalidate: loaderModule.shouldRevalidate }
|
|
150
|
+
? { shouldRevalidate: ${instrumentId ? `instrumentRevalidate(${JSON.stringify(instrumentId)}, loaderModule.shouldRevalidate)` : "loaderModule.shouldRevalidate"} }
|
|
151
151
|
: {}),
|
|
152
152
|
${properties.join(",\n ")}
|
|
153
153
|
};
|
|
@@ -160,8 +160,12 @@ function generateLazyComponent(file, projectRoot) {
|
|
|
160
160
|
function generateLazyLayout(file, projectRoot) {
|
|
161
161
|
return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}).then(({ default: Layout }) => ({ default: withOutlet(Layout) })))`;
|
|
162
162
|
}
|
|
163
|
-
function generateDataHandler(file, projectRoot) {
|
|
164
|
-
|
|
163
|
+
function generateDataHandler(file, projectRoot, instrumentId) {
|
|
164
|
+
const handler = `async (args) => (await import(${JSON.stringify(getImportPath(file, projectRoot))})).action(args)`;
|
|
165
|
+
return instrumentId ? `instrumentRouteData("action", ${JSON.stringify(instrumentId)}, ${handler})` : handler;
|
|
166
|
+
}
|
|
167
|
+
function getInstrumentId(node, context) {
|
|
168
|
+
return context.devClient ? getRouteId(node.directory, context.projectRoot) : void 0;
|
|
165
169
|
}
|
|
166
170
|
function generatePreload(files, projectRoot, children = []) {
|
|
167
171
|
const imports = files.filter((file) => Boolean(file)).map((file) => `import(${JSON.stringify(getImportPath(file, projectRoot))})`);
|
|
@@ -170,6 +174,129 @@ function generatePreload(files, projectRoot, children = []) {
|
|
|
170
174
|
if (children.length) preload.children = genArrayFromRaw(children);
|
|
171
175
|
return genObjectFromRaw(preload);
|
|
172
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Dev-only client for the DevTools Routes panel. It reports the tab's current
|
|
179
|
+
* URL and every loader, action, and `shouldRevalidate` call over Vite's HMR
|
|
180
|
+
* channel. Handler results pass through unchanged; only a short text preview
|
|
181
|
+
* of returned data is sent. Production builds never include this code.
|
|
182
|
+
*/
|
|
183
|
+
var DEVTOOLS_CLIENT = `const devtoolsHot =
|
|
184
|
+
typeof window === "undefined" ? undefined : import.meta.hot;
|
|
185
|
+
const devtoolsTabId = Math.random().toString(36).slice(2, 10);
|
|
186
|
+
|
|
187
|
+
function sendToDevtools(event, payload) {
|
|
188
|
+
try {
|
|
189
|
+
devtoolsHot?.send(event, { ...payload, tabId: devtoolsTabId });
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function previewRouteData(value) {
|
|
194
|
+
try {
|
|
195
|
+
const text = JSON.stringify(value, (_key, item) =>
|
|
196
|
+
typeof item === "bigint" ? String(item) : item,
|
|
197
|
+
);
|
|
198
|
+
if (text === undefined) return String(value);
|
|
199
|
+
return text.length > 600 ? text.slice(0, 600) + "…" : text;
|
|
200
|
+
} catch {
|
|
201
|
+
return String(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function describeRouteResult(value, thrown) {
|
|
206
|
+
if (value instanceof Response) {
|
|
207
|
+
const location = value.headers.get("Location");
|
|
208
|
+
if (location && value.status >= 300 && value.status < 400) {
|
|
209
|
+
return { outcome: "redirect", status: value.status, location };
|
|
210
|
+
}
|
|
211
|
+
return { outcome: thrown ? "error" : "response", status: value.status };
|
|
212
|
+
}
|
|
213
|
+
if (thrown) {
|
|
214
|
+
return {
|
|
215
|
+
outcome: "error",
|
|
216
|
+
message: value instanceof Error ? value.message : previewRouteData(value),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { outcome: "data", preview: previewRouteData(value) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function instrumentRouteData(type, routeId, handler) {
|
|
223
|
+
if (!devtoolsHot || typeof handler !== "function") return handler;
|
|
224
|
+
return async (args) => {
|
|
225
|
+
const startedAt = Date.now();
|
|
226
|
+
const started = performance.now();
|
|
227
|
+
const request = args?.request;
|
|
228
|
+
const report = (result, thrown) =>
|
|
229
|
+
sendToDevtools("vite-file-router:data", {
|
|
230
|
+
type,
|
|
231
|
+
routeId,
|
|
232
|
+
url: request?.url,
|
|
233
|
+
method: request?.method,
|
|
234
|
+
startedAt,
|
|
235
|
+
duration: performance.now() - started,
|
|
236
|
+
...describeRouteResult(result, thrown),
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
const result = await handler(args);
|
|
240
|
+
report(result, false);
|
|
241
|
+
return result;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
report(error, true);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function instrumentRevalidate(routeId, shouldRevalidate) {
|
|
250
|
+
if (!devtoolsHot) return shouldRevalidate;
|
|
251
|
+
return (args) => {
|
|
252
|
+
const decision = shouldRevalidate(args);
|
|
253
|
+
sendToDevtools("vite-file-router:data", {
|
|
254
|
+
type: "revalidate",
|
|
255
|
+
routeId,
|
|
256
|
+
url: args?.nextUrl?.href,
|
|
257
|
+
startedAt: Date.now(),
|
|
258
|
+
decision,
|
|
259
|
+
defaultShouldRevalidate: args?.defaultShouldRevalidate,
|
|
260
|
+
});
|
|
261
|
+
return decision;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (devtoolsHot) {
|
|
266
|
+
let reportedUrl;
|
|
267
|
+
const currentUrl = () =>
|
|
268
|
+
window.location.hash.startsWith("#/")
|
|
269
|
+
? window.location.hash.slice(1)
|
|
270
|
+
: window.location.pathname + window.location.search;
|
|
271
|
+
const reportLocation = () => {
|
|
272
|
+
const url = currentUrl();
|
|
273
|
+
if (url === reportedUrl) return;
|
|
274
|
+
reportedUrl = url;
|
|
275
|
+
sendToDevtools("vite-file-router:location", { url, title: document.title });
|
|
276
|
+
};
|
|
277
|
+
// React Router's browser router navigates with pushState and replaceState,
|
|
278
|
+
// which fire no event, so they're wrapped once per page.
|
|
279
|
+
if (!window.__viteFileRouterHistoryPatched) {
|
|
280
|
+
window.__viteFileRouterHistoryPatched = true;
|
|
281
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
282
|
+
const original = history[method];
|
|
283
|
+
history[method] = function (...args) {
|
|
284
|
+
const result = original.apply(this, args);
|
|
285
|
+
window.dispatchEvent(new Event("vite-file-router:navigate"));
|
|
286
|
+
return result;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const event of ["vite-file-router:navigate", "popstate", "hashchange"]) {
|
|
291
|
+
window.addEventListener(event, reportLocation);
|
|
292
|
+
}
|
|
293
|
+
// Report again after the dev server restarts and the socket reconnects.
|
|
294
|
+
devtoolsHot.on("vite:ws:connect", () => {
|
|
295
|
+
reportedUrl = undefined;
|
|
296
|
+
reportLocation();
|
|
297
|
+
});
|
|
298
|
+
reportLocation();
|
|
299
|
+
}`;
|
|
173
300
|
function getRouteId(directory, projectRoot) {
|
|
174
301
|
return `file-route:${path.relative(projectRoot, directory).split(path.sep).join("/") || "."}`;
|
|
175
302
|
}
|
|
@@ -183,7 +310,7 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
183
310
|
context.hasErrorBoundaries = true;
|
|
184
311
|
route.ErrorBoundary = `createErrorBoundary(${generateLazyComponent(node.error, context.projectRoot)})`;
|
|
185
312
|
}
|
|
186
|
-
if (node.action) route.action = generateDataHandler(node.action, context.projectRoot);
|
|
313
|
+
if (node.action) route.action = generateDataHandler(node.action, context.projectRoot, getInstrumentId(node, context));
|
|
187
314
|
let loadingComponent;
|
|
188
315
|
if (node.loading) {
|
|
189
316
|
loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
|
|
@@ -196,18 +323,18 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
196
323
|
} else if (node.layout) {
|
|
197
324
|
if (hasBoundary) route.Component = generateLazyLayout(node.layout, context.projectRoot);
|
|
198
325
|
}
|
|
199
|
-
if (node.layout && !hasBoundary || node.loader) route.lazy = generateRouteLazy(node, context.projectRoot, Boolean(node.layout && !hasBoundary), Boolean(node.loader));
|
|
326
|
+
if (node.layout && !hasBoundary || node.loader) route.lazy = generateRouteLazy(node, context.projectRoot, Boolean(node.layout && !hasBoundary), Boolean(node.loader), node.loader ? getInstrumentId(node, context) : void 0);
|
|
200
327
|
const children = [];
|
|
201
328
|
const preloadChildren = [];
|
|
202
329
|
if (node.page) {
|
|
203
330
|
const page = hasBoundary ? {
|
|
204
331
|
index: "true",
|
|
205
332
|
Component: generateLazyComponent(node.page, context.projectRoot),
|
|
206
|
-
...node.action ? { action: generateDataHandler(node.action, context.projectRoot) } : {}
|
|
333
|
+
...node.action ? { action: generateDataHandler(node.action, context.projectRoot, getInstrumentId(node, context)) } : {}
|
|
207
334
|
} : {
|
|
208
335
|
index: "true",
|
|
209
336
|
lazy: generateImport(node.page, context.projectRoot),
|
|
210
|
-
...node.action ? { action: generateDataHandler(node.action, context.projectRoot) } : {}
|
|
337
|
+
...node.action ? { action: generateDataHandler(node.action, context.projectRoot, getInstrumentId(node, context)) } : {}
|
|
211
338
|
};
|
|
212
339
|
children.push(genObjectFromRaw(page));
|
|
213
340
|
preloadChildren.push(generatePreload([node.page], context.projectRoot));
|
|
@@ -234,7 +361,7 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
234
361
|
preload: generatePreload([node.layout, node.loader], context.projectRoot, preloadChildren)
|
|
235
362
|
};
|
|
236
363
|
}
|
|
237
|
-
function generate(node, root = false, projectRoot = process.cwd()) {
|
|
364
|
+
function generate(node, root = false, projectRoot = process.cwd(), options = {}) {
|
|
238
365
|
validateRouteGroupNotFound(node, [], root);
|
|
239
366
|
const conflicts = findPageRouteConflicts(node, /* @__PURE__ */ new Map(), [], root);
|
|
240
367
|
if (conflicts.length) throw new RouteConflictError(conflicts);
|
|
@@ -244,7 +371,8 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
244
371
|
nextLoadingId: 0,
|
|
245
372
|
hasBoundaries: false,
|
|
246
373
|
hasLayouts: false,
|
|
247
|
-
hasErrorBoundaries: false
|
|
374
|
+
hasErrorBoundaries: false,
|
|
375
|
+
devClient: Boolean(options.devClient)
|
|
248
376
|
};
|
|
249
377
|
const { route: routes, preload: preloads } = generateNode(node, root, context, false);
|
|
250
378
|
const imports = [...context.loadingImports];
|
|
@@ -282,6 +410,7 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
282
410
|
);
|
|
283
411
|
};
|
|
284
412
|
}`);
|
|
413
|
+
if (context.devClient) helpers.push(DEVTOOLS_CLIENT);
|
|
285
414
|
return {
|
|
286
415
|
imports,
|
|
287
416
|
helpers,
|
|
@@ -342,6 +471,216 @@ function createRouteDeclarations(tree, projectRoot, declarationFile) {
|
|
|
342
471
|
""
|
|
343
472
|
].join("\n");
|
|
344
473
|
}
|
|
474
|
+
// src/devtools.ts
|
|
475
|
+
var ROUTE_INSPECTOR_PATH = "/__vite-file-router/";
|
|
476
|
+
var HEARTBEAT_INTERVAL = 25e3;
|
|
477
|
+
/**
|
|
478
|
+
* Mirrors the generated React Router tree: each manifest node becomes a
|
|
479
|
+
* route with an index child for its page and a `*` child for its not-found
|
|
480
|
+
* page, so matching agrees with the runtime routes.
|
|
481
|
+
*/
|
|
482
|
+
function toMatchableRoute(node) {
|
|
483
|
+
const children = [];
|
|
484
|
+
if (node.files.page) children.push({
|
|
485
|
+
index: true,
|
|
486
|
+
node,
|
|
487
|
+
kind: "page"
|
|
488
|
+
});
|
|
489
|
+
for (const child of node.children) children.push(toMatchableRoute(child));
|
|
490
|
+
if (node.files.notFound) children.push({
|
|
491
|
+
path: "*",
|
|
492
|
+
node,
|
|
493
|
+
kind: "notFound"
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
...node.segment === null ? {} : { path: node.segment },
|
|
497
|
+
node,
|
|
498
|
+
...children.length ? { children } : {}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function matchRouteManifest(manifest, url) {
|
|
502
|
+
const { matchRoutes } = await import("react-router");
|
|
503
|
+
const pathname = new URL(url, "http://vite-file-router").pathname;
|
|
504
|
+
const matches = matchRoutes([toMatchableRoute(manifest)], pathname);
|
|
505
|
+
if (!matches) return {
|
|
506
|
+
matched: false,
|
|
507
|
+
pathname
|
|
508
|
+
};
|
|
509
|
+
const leaf = matches[matches.length - 1].route;
|
|
510
|
+
const chain = matches.filter(({ route }) => !route.kind).map(({ route }) => route.node);
|
|
511
|
+
const nearest = (kind) => [...chain].reverse().find((node) => node.files[kind])?.files[kind];
|
|
512
|
+
const collect = (kind) => chain.flatMap((node) => node.files[kind] ? [node.files[kind]] : []);
|
|
513
|
+
return {
|
|
514
|
+
matched: true,
|
|
515
|
+
pathname,
|
|
516
|
+
params: matches[matches.length - 1].params,
|
|
517
|
+
chain: chain.map(({ fullPath, segment, routeGroup, files }) => ({
|
|
518
|
+
fullPath,
|
|
519
|
+
segment,
|
|
520
|
+
routeGroup,
|
|
521
|
+
files
|
|
522
|
+
})),
|
|
523
|
+
...leaf.kind === "page" ? { page: leaf.node?.files.page } : {},
|
|
524
|
+
...leaf.kind === "notFound" ? { notFound: leaf.node?.files.notFound } : {},
|
|
525
|
+
layouts: collect("layout"),
|
|
526
|
+
loaders: collect("loader"),
|
|
527
|
+
...nearest("loading") ? { loading: nearest("loading") } : {},
|
|
528
|
+
...nearest("error") ? { error: nearest("error") } : {}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function writeEvent(response, event, data) {
|
|
532
|
+
response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
533
|
+
}
|
|
534
|
+
/** Pushes route state to open DevTools panels over Server-Sent Events. */
|
|
535
|
+
function createRouteEventStream(getState, getSnapshot = () => []) {
|
|
536
|
+
const clients = /* @__PURE__ */ new Set();
|
|
537
|
+
let pending;
|
|
538
|
+
function send(response) {
|
|
539
|
+
writeEvent(response, "routes", getState());
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
clients,
|
|
543
|
+
connect(request, response) {
|
|
544
|
+
response.statusCode = 200;
|
|
545
|
+
response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
546
|
+
response.setHeader("Cache-Control", "no-store");
|
|
547
|
+
response.setHeader("Connection", "keep-alive");
|
|
548
|
+
response.flushHeaders?.();
|
|
549
|
+
response.write("retry: 1000\n\n");
|
|
550
|
+
send(response);
|
|
551
|
+
for (const [event, data] of getSnapshot()) writeEvent(response, event, data);
|
|
552
|
+
clients.add(response);
|
|
553
|
+
const heartbeat = setInterval(() => response.write(": heartbeat\n\n"), HEARTBEAT_INTERVAL);
|
|
554
|
+
heartbeat.unref?.();
|
|
555
|
+
request.on("close", () => {
|
|
556
|
+
clearInterval(heartbeat);
|
|
557
|
+
clients.delete(response);
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
/** Coalesces bursts of file events (such as a folder rename) into one push. */
|
|
561
|
+
notify() {
|
|
562
|
+
if (!clients.size) return;
|
|
563
|
+
clearTimeout(pending);
|
|
564
|
+
pending = setTimeout(() => {
|
|
565
|
+
pending = void 0;
|
|
566
|
+
for (const client of clients) send(client);
|
|
567
|
+
}, 50);
|
|
568
|
+
},
|
|
569
|
+
/** Sends a named event to every connected panel right away. */
|
|
570
|
+
broadcast(event, data) {
|
|
571
|
+
for (const client of clients) writeEvent(client, event, data);
|
|
572
|
+
},
|
|
573
|
+
close() {
|
|
574
|
+
clearTimeout(pending);
|
|
575
|
+
for (const client of clients) client.end();
|
|
576
|
+
clients.clear();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// src/devtools-app.ts
|
|
581
|
+
/** HMR events the dev-only client in `virtual:routes` sends to the server. */
|
|
582
|
+
var APP_LOCATION_EVENT = "vite-file-router:location";
|
|
583
|
+
var APP_DATA_EVENT = "vite-file-router:data";
|
|
584
|
+
var MAX_ACTIVITY = 200;
|
|
585
|
+
var MAX_TEXT = 600;
|
|
586
|
+
function text(value, limit = MAX_TEXT) {
|
|
587
|
+
if (typeof value !== "string") return void 0;
|
|
588
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
589
|
+
}
|
|
590
|
+
function finite(value) {
|
|
591
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Validates an event from the browser. Anything a page sends over the HMR
|
|
595
|
+
* channel is untrusted, so only known fields of the expected types are kept.
|
|
596
|
+
*/
|
|
597
|
+
function sanitizeDataEvent(input, id) {
|
|
598
|
+
if (!input || typeof input !== "object") return void 0;
|
|
599
|
+
const event = input;
|
|
600
|
+
const type = event.type;
|
|
601
|
+
if (type !== "loader" && type !== "action" && type !== "revalidate") return;
|
|
602
|
+
const routeId = text(event.routeId, 400);
|
|
603
|
+
const tabId = text(event.tabId, 100);
|
|
604
|
+
if (!routeId || !tabId) return void 0;
|
|
605
|
+
const outcome = event.outcome;
|
|
606
|
+
return {
|
|
607
|
+
id,
|
|
608
|
+
tabId,
|
|
609
|
+
type,
|
|
610
|
+
routeId,
|
|
611
|
+
url: text(event.url, 2e3),
|
|
612
|
+
method: text(event.method, 20),
|
|
613
|
+
startedAt: finite(event.startedAt) ?? Date.now(),
|
|
614
|
+
duration: finite(event.duration),
|
|
615
|
+
...outcome === "data" || outcome === "response" || outcome === "redirect" || outcome === "error" ? { outcome } : {},
|
|
616
|
+
status: finite(event.status),
|
|
617
|
+
location: text(event.location, 2e3),
|
|
618
|
+
message: text(event.message),
|
|
619
|
+
preview: text(event.preview),
|
|
620
|
+
...typeof event.decision === "boolean" ? { decision: event.decision } : {},
|
|
621
|
+
...typeof event.defaultShouldRevalidate === "boolean" ? { defaultShouldRevalidate: event.defaultShouldRevalidate } : {}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Tracks the app tabs connected to the dev server and their recent loader
|
|
626
|
+
* and action calls, and tells the panel when either changes.
|
|
627
|
+
*/
|
|
628
|
+
function createAppTracker(options) {
|
|
629
|
+
const tabs = /* @__PURE__ */ new Map();
|
|
630
|
+
const latestReport = /* @__PURE__ */ new Map();
|
|
631
|
+
const activity = [];
|
|
632
|
+
let nextEventId = 1;
|
|
633
|
+
let nextReport = 1;
|
|
634
|
+
const list = () => [...tabs.values()].sort((a, b) => a.connectedAt - b.connectedAt);
|
|
635
|
+
const changed = () => options.onTabsChange(list());
|
|
636
|
+
return {
|
|
637
|
+
tabs: list,
|
|
638
|
+
activity: () => [...activity],
|
|
639
|
+
async updateLocation(client, input) {
|
|
640
|
+
if (!input || typeof input !== "object") return;
|
|
641
|
+
const payload = input;
|
|
642
|
+
const id = text(payload.tabId, 100);
|
|
643
|
+
const url = text(payload.url, 2e3);
|
|
644
|
+
if (!id || !url?.startsWith("/")) return;
|
|
645
|
+
const report = nextReport++;
|
|
646
|
+
latestReport.set(client, report);
|
|
647
|
+
const match = await matchRouteManifest(options.getManifest(), url);
|
|
648
|
+
if (latestReport.get(client) !== report) return;
|
|
649
|
+
const now = Date.now();
|
|
650
|
+
tabs.set(client, {
|
|
651
|
+
id,
|
|
652
|
+
url,
|
|
653
|
+
title: text(payload.title, 200) ?? "",
|
|
654
|
+
connectedAt: tabs.get(client)?.connectedAt ?? now,
|
|
655
|
+
updatedAt: now,
|
|
656
|
+
match
|
|
657
|
+
});
|
|
658
|
+
changed();
|
|
659
|
+
},
|
|
660
|
+
disconnect(client) {
|
|
661
|
+
latestReport.delete(client);
|
|
662
|
+
if (tabs.delete(client)) changed();
|
|
663
|
+
},
|
|
664
|
+
/** Re-matches every tab, for example after route files change. */
|
|
665
|
+
async rematch() {
|
|
666
|
+
if (!tabs.size) return;
|
|
667
|
+
const manifest = options.getManifest();
|
|
668
|
+
for (const [client, tab] of tabs) tabs.set(client, {
|
|
669
|
+
...tab,
|
|
670
|
+
match: await matchRouteManifest(manifest, tab.url)
|
|
671
|
+
});
|
|
672
|
+
changed();
|
|
673
|
+
},
|
|
674
|
+
record(input) {
|
|
675
|
+
const event = sanitizeDataEvent(input, nextEventId);
|
|
676
|
+
if (!event) return;
|
|
677
|
+
nextEventId++;
|
|
678
|
+
activity.push(event);
|
|
679
|
+
if (activity.length > MAX_ACTIVITY) activity.shift();
|
|
680
|
+
options.onActivity(event);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
345
684
|
// src/index.ts
|
|
346
685
|
var ROUTES_MODULE_ID = "virtual:routes";
|
|
347
686
|
var ROUTES_RESOLVED_ID = `\0${ROUTES_MODULE_ID}`;
|
|
@@ -349,9 +688,13 @@ var MANIFEST_MODULE_ID = "virtual:route-manifest";
|
|
|
349
688
|
var MANIFEST_RESOLVED_ID = `\0${MANIFEST_MODULE_ID}`;
|
|
350
689
|
var ROUTE_HELPERS_MODULE_ID = "virtual:route-helpers";
|
|
351
690
|
var ROUTE_HELPERS_RESOLVED_ID = `\0${ROUTE_HELPERS_MODULE_ID}`;
|
|
352
|
-
var ROUTE_INSPECTOR_PATH = "/__vite-file-router/";
|
|
353
|
-
var ROUTE_INSPECTOR_DATA_PATH = "/__vite-file-router-data";
|
|
354
691
|
var DEFAULT_DTS = "src/route-types.d.ts";
|
|
692
|
+
var TYPESCRIPT_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
693
|
+
function resolveDeclarationFile(dts, projectRoot) {
|
|
694
|
+
if (dts === false) return void 0;
|
|
695
|
+
if (dts !== "auto") return path.resolve(projectRoot, dts);
|
|
696
|
+
return TYPESCRIPT_CONFIG_FILES.some((file) => fs.existsSync(path.join(projectRoot, file))) ? path.resolve(projectRoot, DEFAULT_DTS) : void 0;
|
|
697
|
+
}
|
|
355
698
|
function resolveOptions(input) {
|
|
356
699
|
const options = typeof input === "string" ? { directory: input } : input;
|
|
357
700
|
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("routes() expects a directory string or options object.");
|
|
@@ -367,8 +710,8 @@ function resolveOptions(input) {
|
|
|
367
710
|
}
|
|
368
711
|
const names = Object.values(conventions);
|
|
369
712
|
if (new Set(names).size !== names.length) throw new TypeError("Route file convention names must be unique.");
|
|
370
|
-
const dts = options.dts ??
|
|
371
|
-
if (dts !== false && (typeof dts !== "string" || dts.trim() === "" || !dts.endsWith(".d.ts"))) throw new TypeError("routes() dts must be false or a path ending in \".d.ts\".");
|
|
713
|
+
const dts = options.dts ?? "auto";
|
|
714
|
+
if (dts !== false && dts !== "auto" && (typeof dts !== "string" || dts.trim() === "" || !dts.endsWith(".d.ts"))) throw new TypeError("routes() dts must be \"auto\", false, or a path ending in \".d.ts\".");
|
|
372
715
|
return {
|
|
373
716
|
directory,
|
|
374
717
|
conventions,
|
|
@@ -381,6 +724,24 @@ function routes(input = "src/pages") {
|
|
|
381
724
|
let root = "";
|
|
382
725
|
let dev = false;
|
|
383
726
|
let declarationFile;
|
|
727
|
+
function getInspectorState() {
|
|
728
|
+
const tree = scan(root, options.conventions);
|
|
729
|
+
return {
|
|
730
|
+
projectRoot,
|
|
731
|
+
directory: path.relative(projectRoot, root).split(path.sep).join("/"),
|
|
732
|
+
manifest: createRouteManifest(tree, projectRoot),
|
|
733
|
+
conflicts: findPageRouteConflicts(tree).map(({ routePath, files }) => ({
|
|
734
|
+
routePath,
|
|
735
|
+
files: files.map((file) => path.relative(projectRoot, file).split(path.sep).join("/"))
|
|
736
|
+
}))
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const appTracker = createAppTracker({
|
|
740
|
+
getManifest: () => createRouteManifest(scan(root, options.conventions), projectRoot),
|
|
741
|
+
onTabsChange: (tabs) => routeEvents.broadcast("apps", tabs),
|
|
742
|
+
onActivity: (event) => routeEvents.broadcast("activity", event)
|
|
743
|
+
});
|
|
744
|
+
const routeEvents = createRouteEventStream(getInspectorState, () => [["apps", appTracker.tabs()], ["activity-snapshot", appTracker.activity()]]);
|
|
384
745
|
function writeDeclarations() {
|
|
385
746
|
if (!declarationFile) return;
|
|
386
747
|
const content = createRouteDeclarations(scan(root, options.conventions), projectRoot, declarationFile);
|
|
@@ -426,7 +787,7 @@ function routes(input = "src/pages") {
|
|
|
426
787
|
`;
|
|
427
788
|
return format(code, { parser: "typescript" });
|
|
428
789
|
}
|
|
429
|
-
const generated = generate(tree, true, projectRoot);
|
|
790
|
+
const generated = generate(tree, true, projectRoot, { devClient: dev });
|
|
430
791
|
const sections = [
|
|
431
792
|
sortImports(generated.imports).join("\n"),
|
|
432
793
|
generated.helpers.join("\n"),
|
|
@@ -560,7 +921,7 @@ function routes(input = "src/pages") {
|
|
|
560
921
|
projectRoot = config.root;
|
|
561
922
|
root = path.resolve(projectRoot, options.directory);
|
|
562
923
|
dev = config.command === "serve";
|
|
563
|
-
declarationFile = options.dts
|
|
924
|
+
declarationFile = resolveDeclarationFile(options.dts, projectRoot);
|
|
564
925
|
},
|
|
565
926
|
buildStart() {
|
|
566
927
|
writeDeclarations();
|
|
@@ -585,8 +946,19 @@ function routes(input = "src/pages") {
|
|
|
585
946
|
type: "iframe",
|
|
586
947
|
url: ROUTE_INSPECTOR_PATH
|
|
587
948
|
});
|
|
949
|
+
const sendJson = (res, body) => {
|
|
950
|
+
res.statusCode = 200;
|
|
951
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
952
|
+
res.setHeader("Cache-Control", "no-store");
|
|
953
|
+
res.end(JSON.stringify(body));
|
|
954
|
+
};
|
|
588
955
|
ctx.viteServer.middlewares.use((req, res, next) => {
|
|
589
|
-
|
|
956
|
+
const url = new URL(req.url ?? "/", "http://vite-file-router");
|
|
957
|
+
if (![
|
|
958
|
+
"/__vite-file-router-data",
|
|
959
|
+
"/__vite-file-router-events",
|
|
960
|
+
"/__vite-file-router-match"
|
|
961
|
+
].includes(url.pathname)) {
|
|
590
962
|
next();
|
|
591
963
|
return;
|
|
592
964
|
}
|
|
@@ -596,11 +968,21 @@ function routes(input = "src/pages") {
|
|
|
596
968
|
res.end("Method Not Allowed");
|
|
597
969
|
return;
|
|
598
970
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
971
|
+
if (url.pathname === "/__vite-file-router-data") {
|
|
972
|
+
sendJson(res, getInspectorState());
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (url.pathname === "/__vite-file-router-events") {
|
|
976
|
+
routeEvents.connect(req, res);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const target = url.searchParams.get("url");
|
|
980
|
+
if (!target) {
|
|
981
|
+
res.statusCode = 400;
|
|
982
|
+
res.end("Missing \"url\" query parameter.");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
matchRouteManifest(createRouteManifest(scan(root, options.conventions), projectRoot), target).then((result) => sendJson(res, result), next);
|
|
604
986
|
});
|
|
605
987
|
} },
|
|
606
988
|
configureServer: (server) => {
|
|
@@ -625,8 +1007,16 @@ function routes(input = "src/pages") {
|
|
|
625
1007
|
if (module) server.moduleGraph.invalidateModule(module);
|
|
626
1008
|
}
|
|
627
1009
|
writeDeclarations();
|
|
1010
|
+
routeEvents.notify();
|
|
1011
|
+
appTracker.rematch();
|
|
628
1012
|
server.ws.send({ type: "full-reload" });
|
|
629
1013
|
});
|
|
1014
|
+
server.httpServer?.on("close", routeEvents.close);
|
|
1015
|
+
server.ws.on(APP_LOCATION_EVENT, (data, client) => appTracker.updateLocation(client, data).catch(() => {}));
|
|
1016
|
+
server.ws.on(APP_DATA_EVENT, (data) => appTracker.record(data));
|
|
1017
|
+
server.ws.on("vite:client:disconnect", (_data, client) => {
|
|
1018
|
+
appTracker.disconnect(client);
|
|
1019
|
+
});
|
|
630
1020
|
}
|
|
631
1021
|
};
|
|
632
1022
|
}
|