@troyio/vite-file-router 0.2.3 → 0.2.4

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.
Files changed (3) hide show
  1. package/README.md +82 -29
  2. package/dist/index.js +27 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,57 +28,110 @@ project root to use another directory:
28
28
  plugins: [routes("src/routes")];
29
29
  ```
30
30
 
31
- Each `page.jsx` or `page.tsx` becomes a route page, and each `layout.jsx` or
32
- `layout.tsx` becomes a lazy layout for its directory. Directories become nested
33
- routes. Add a `loading.jsx` or `loading.tsx` to provide a Suspense fallback for
34
- that directory's page and nested routes; a nested loading file provides a more
35
- specific fallback. Each file must default-export a React component. If both
36
- extensions exist for the same component, `.tsx` takes precedence. For example:
31
+ The router scans the pages directory recursively. Each directory becomes a
32
+ nested route, and the following files are recognized:
33
+
34
+ | File | Purpose |
35
+ | --- | --- |
36
+ | `page.jsx` / `page.tsx` | The page component for the directory's URL. |
37
+ | `layout.jsx` / `layout.tsx` | A layout around the directory's page and child routes. |
38
+ | `loading.jsx` / `loading.tsx` | A Suspense fallback for the directory's page and child routes. |
39
+
40
+ Each file must default-export a React component. If both extensions exist for
41
+ the same file type in one directory, `.tsx` takes precedence.
42
+
43
+ For example, this structure creates a home page, user pages, and two auth
44
+ pages:
37
45
 
38
46
  ```text
39
47
  src/pages/
40
- layout.jsx
41
- loading.jsx
42
48
  page.jsx
49
+ layout.tsx
50
+ loading.tsx
43
51
  users/
44
- loading.tsx
45
52
  page.tsx
46
53
  [id]/
47
54
  page.tsx
55
+ (auth)/
56
+ layout.tsx
57
+ sign-in/
58
+ page.tsx
59
+ sign-up/
60
+ page.tsx
61
+ [...slug]/
62
+ page.tsx
48
63
  ```
49
64
 
50
- Dynamic segments such as `[id]` become `:id`; catch-all segments such as
51
- `[...slug]` become `*`. Parenthesized directories are route groups and do not
52
- add a URL segment. Route groups can contain `page`, `layout`, and `loading`
53
- files just like other directories. For example, `(auth)/sign-in/page.tsx` maps
54
- to `/sign-in`. If two pages resolve to the same URL, the dev server displays a
55
- diagnostic page listing the conflicting files and explaining how to fix them.
56
- Production builds fail with the same diagnostic until the conflicts are fixed.
65
+ This maps to `/`, `/users`, `/users/:id`, `/sign-in`, `/sign-up`, and `/*`.
66
+ Square-bracket segments are dynamic parameters: `[id]` becomes `:id`.
67
+ Catch-all segments such as `[...slug]` become `*`. Parenthesized directories
68
+ are route groups; they organize files and can have their own page, layout, and
69
+ loading files, but their names do not add a URL segment.
57
70
 
58
- Import the generated route configuration from the `virtual:routes` module in
59
- your application:
71
+ ### Layouts
60
72
 
61
- ```ts
62
- import { routes } from "virtual:routes";
73
+ The plugin adapts React Router's matched child route to a `children` prop. A
74
+ layout can render that prop directly and does not need to import or render
75
+ `Outlet`:
76
+
77
+ ```tsx
78
+ import type { PropsWithChildren } from "react";
79
+
80
+ export default function Layout({ children }: PropsWithChildren) {
81
+ return <div className="app-shell">{children}</div>;
82
+ }
63
83
  ```
64
84
 
65
- The virtual module exports a `routes` array with lazy-loaded page and layout
66
- components.
85
+ ### Loading fallbacks
86
+
87
+ Add a `loading.jsx` or `loading.tsx` beside a page or layout to show a Suspense
88
+ fallback while that route subtree loads:
89
+
90
+ ```tsx
91
+ export default function Loading() {
92
+ return <p>Loading...</p>;
93
+ }
94
+ ```
95
+
96
+ A nested loading file creates a more specific boundary for that directory.
97
+
98
+ ### Dynamic routes and route groups
99
+
100
+ Two pages must not resolve to the same URL. This includes pages with the same
101
+ path in separate route groups, such as `(auth)/sign-in/page.tsx` and
102
+ `(guest)/sign-in/page.tsx`. During development, the router displays a diagnostic
103
+ page listing the conflicting files; production builds fail with the same
104
+ diagnostic until the conflict is fixed.
105
+
106
+ ## Use the generated routes
107
+
108
+ Import `routes` from the virtual module and pass it to React Router:
109
+
110
+ ```tsx
111
+ import { createBrowserRouter, RouterProvider } from "react-router";
112
+ import { routes } from "virtual:routes";
113
+
114
+ const router = createBrowserRouter(routes);
115
+
116
+ export default function App() {
117
+ return <RouterProvider router={router} />;
118
+ }
119
+ ```
67
120
 
68
- ### TypeScript
121
+ ### TypeScript setup
69
122
 
70
- Make sure your application TypeScript project includes Vite's client types and
71
- the router package's type declarations. For example, add this file as
72
- `src/vite-env.d.ts`:
123
+ Add Vite's client types and the package's virtual-module declaration to your
124
+ application TypeScript project. For example, create `src/vite-env.d.ts`:
73
125
 
74
126
  ```ts
75
127
  /// <reference types="vite/client" />
76
128
  /// <reference types="@troyio/vite-file-router" />
77
129
  ```
78
130
 
79
- If your `tsconfig.json` sets `compilerOptions.types`, include both
80
- `"vite/client"` and `"@troyio/vite-file-router"` in that list. Then restart
81
- the TypeScript server in your editor.
131
+ If `tsconfig.json` sets `compilerOptions.types`, include both `"vite/client"`
132
+ and `"@troyio/vite-file-router"` in that list. This lets TypeScript resolve
133
+ `virtual:routes`. Restart your editor's TypeScript server if the declaration
134
+ isn't picked up immediately.
82
135
 
83
136
  ## Build
84
137
 
package/dist/index.js CHANGED
@@ -38,12 +38,21 @@ function generateImport(file, projectRoot) {
38
38
  Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
39
39
  })`;
40
40
  }
41
+ function generateLayoutImport(file, projectRoot) {
42
+ return `async () => ({
43
+ Component: withOutlet((await import(${JSON.stringify(getImportPath(file, projectRoot))})).default)
44
+ })`;
45
+ }
41
46
  function generateLazyComponent(file, projectRoot) {
42
47
  return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}))`;
43
48
  }
49
+ function generateLazyLayout(file, projectRoot) {
50
+ return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}).then(({ default: Layout }) => ({ default: withOutlet(Layout) })))`;
51
+ }
44
52
  function generateNode(node, root, context, inheritedBoundary) {
45
53
  const route = {};
46
54
  const hasBoundary = inheritedBoundary || Boolean(node.loading);
55
+ if (node.layout) context.hasLayouts = true;
47
56
  if (!root && !node.routeGroup) route.path = JSON.stringify(getRoutePath(node.directory));
48
57
  let loadingComponent;
49
58
  if (node.loading) {
@@ -52,11 +61,11 @@ function generateNode(node, root, context, inheritedBoundary) {
52
61
  context.hasBoundaries = true;
53
62
  }
54
63
  if (loadingComponent) {
55
- const child = node.layout ? generateLazyComponent(node.layout, context.projectRoot) : "Outlet";
64
+ const child = node.layout ? generateLazyLayout(node.layout, context.projectRoot) : "Outlet";
56
65
  route.Component = `createBoundary(${loadingComponent}, ${child})`;
57
66
  } else if (node.layout) {
58
- if (hasBoundary) route.Component = generateLazyComponent(node.layout, context.projectRoot);
59
- else route.lazy = generateImport(node.layout, context.projectRoot);
67
+ if (hasBoundary) route.Component = generateLazyLayout(node.layout, context.projectRoot);
68
+ else route.lazy = generateLayoutImport(node.layout, context.projectRoot);
60
69
  }
61
70
  const children = [];
62
71
  if (node.page) {
@@ -80,14 +89,20 @@ function generate(node, root = false, projectRoot = process.cwd()) {
80
89
  projectRoot,
81
90
  loadingImports: [],
82
91
  nextLoadingId: 0,
83
- hasBoundaries: false
92
+ hasBoundaries: false,
93
+ hasLayouts: false
84
94
  };
85
95
  const routes = generateNode(node, root, context, false);
86
96
  const imports = [...context.loadingImports];
87
97
  const helpers = [];
88
- if (context.hasBoundaries) {
89
- imports.unshift(`import { createElement, lazy, Suspense } from "react";`, `import { Outlet } from "react-router";`);
90
- helpers.push(`function createBoundary(Fallback, Component) {
98
+ if (context.hasBoundaries || context.hasLayouts) {
99
+ const reactImports = ["createElement"];
100
+ if (context.hasBoundaries || context.hasLayouts) reactImports.push("lazy");
101
+ if (context.hasBoundaries) reactImports.push("Suspense");
102
+ imports.unshift(`import { ${reactImports.join(", ")} } from "react";`);
103
+ if (context.hasBoundaries || context.hasLayouts) imports.push(`import { Outlet } from "react-router";`);
104
+ }
105
+ if (context.hasBoundaries) helpers.push(`function createBoundary(Fallback, Component) {
91
106
  return function RouteBoundary() {
92
107
  return createElement(
93
108
  Suspense,
@@ -96,7 +111,11 @@ function generate(node, root = false, projectRoot = process.cwd()) {
96
111
  );
97
112
  };
98
113
  }`);
99
- }
114
+ if (context.hasLayouts) helpers.push(`function withOutlet(Layout) {
115
+ return function LayoutRoute() {
116
+ return createElement(Layout, null, createElement(Outlet));
117
+ };
118
+ }`);
100
119
  return {
101
120
  imports,
102
121
  helpers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@troyio/vite-file-router",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "File-system routing plugin for Vite",
5
5
  "type": "module",
6
6
  "private": false,