@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/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
- return `async (args) => (await import(${JSON.stringify(getImportPath(file, projectRoot))})).action(args)`;
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 ?? DEFAULT_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 === false ? void 0 : path.resolve(projectRoot, 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
- if (new URL(req.url ?? "/", "http://vite-file-router").pathname !== ROUTE_INSPECTOR_DATA_PATH) {
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
- const manifest = createRouteManifest(scan(root, options.conventions), projectRoot);
600
- res.statusCode = 200;
601
- res.setHeader("Content-Type", "application/json; charset=utf-8");
602
- res.setHeader("Cache-Control", "no-store");
603
- res.end(JSON.stringify(manifest));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@troyio/vite-file-router",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "File-system routing plugin for Vite",
5
5
  "type": "module",
6
6
  "private": false,