@epicabdou/linkr 0.0.2

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 ADDED
@@ -0,0 +1,156 @@
1
+ # linkr.js
2
+
3
+ File-based routing for **React Router v7** in Vite + React SPA apps. Generates a route tree from a `src/pages/` directory using `import.meta.glob` (the consumer passes the glob result; the library does not call it).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @epicabdou/linkr react react-dom react-router
9
+ ```
10
+
11
+ Peer deps: `react`, `react-dom`, `react-router` (v7+).
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import { createRoutes } from "@epicabdou/linkr";
17
+ import { createBrowserRouter, RouterProvider } from "react-router";
18
+
19
+ const pages = import.meta.glob("./pages/**/*.tsx");
20
+ const routes = createRoutes({ pagesGlob: pages, pagesDir: "pages" });
21
+ const router = createBrowserRouter(routes);
22
+
23
+ // Then render <RouterProvider router={router} />
24
+ ```
25
+
26
+ - **`pagesGlob`** (required): result of `import.meta.glob("...")` from your app.
27
+ - **`pagesDir`** (optional, default `"src/pages"`): base path used to normalize file paths. Must match the prefix you strip from glob keys (e.g. use `"pages"` when glob is `./pages/**/*.tsx` from `src/`).
28
+ - **`layoutFileName`** (optional, default `"_layout"`).
29
+ - **`notFoundFileName`** (optional, default `"404"`).
30
+ - **`routeExtensions`** (optional, default `[".tsx", ".ts", ".jsx", ".js"]`).
31
+ - **`defaultRedirectTo`** (optional, default `"/"`): default redirect path when a route's `protect` check fails and no `redirectTo` is set.
32
+
33
+ ## Conventions
34
+
35
+ | File / pattern | Route |
36
+ |-----------------------|---------------------------------|
37
+ | `index.tsx` | `/` (index route) |
38
+ | `about.tsx` | `/about` |
39
+ | `blog/index.tsx` | `/blog` (index under blog) |
40
+ | `blog/[id].tsx` | `/blog/:id` |
41
+ | `docs/[...slug].tsx` | `/docs/*` (splat) |
42
+ | `404.tsx` | `path: "*"` (last) |
43
+ | `_layout.tsx` (any folder) | Layout route; children render in `<Outlet />` |
44
+
45
+ - **Nested routes**: folder nesting = nested routes.
46
+ - **Layouts**: `_layout.tsx` in a folder becomes the parent route for that segment; all siblings (index, segment files, nested folders) are its children.
47
+ - **Sort order**: among siblings, static segments first, then dynamic (`:id`), then splat (`*`).
48
+ - **Lazy loading**: every route uses React Router's `lazy()` for code splitting.
49
+
50
+ ## Page module exports
51
+
52
+ - **`default`**: React component used as the route element (required).
53
+ - **`ErrorBoundary`**: optional; used as `errorElement`.
54
+ - **`handle`**: optional; attached to the route's `handle`.
55
+ - **`protect`**: optional; run a condition before rendering the route (or layout). When the check returns `false`, the user is redirected. See [Route protection](#route-protection).
56
+
57
+ ## Route protection
58
+
59
+ You can guard a route or an entire layout in two ways.
60
+
61
+ ### 1. `protect` export on a page or layout
62
+
63
+ Export `protect` from any page or `_layout` module. The check runs in the route loader; if it returns `false`, the user is redirected before the page renders.
64
+
65
+ **Shorthand (function):** redirect uses `defaultRedirectTo` from `createRoutes` options (or `"/"`).
66
+
67
+ ```tsx
68
+ // pages/dashboard.tsx
69
+ export const protect = () => !!getAuthToken();
70
+
71
+ export default function Dashboard() {
72
+ return <div>Dashboard</div>;
73
+ }
74
+ ```
75
+
76
+ **Full form (object):** specify redirect and optional fallback for async checks.
77
+
78
+ ```tsx
79
+ // pages/settings/_layout.tsx
80
+ export const protect = {
81
+ check: async () => (await fetchUser())?.role === "admin",
82
+ redirectTo: "/login",
83
+ };
84
+
85
+ export default function SettingsLayout() {
86
+ return (
87
+ <div>
88
+ <nav>...</nav>
89
+ <Outlet />
90
+ </div>
91
+ );
92
+ }
93
+ ```
94
+
95
+ Pass a default redirect when creating routes:
96
+
97
+ ```tsx
98
+ createRoutes({
99
+ pagesGlob: pages,
100
+ pagesDir: "pages",
101
+ defaultRedirectTo: "/login",
102
+ });
103
+ ```
104
+
105
+ ### 2. `<Protect>` component (wrap a route or layout content)
106
+
107
+ Use the `<Protect>` component when you want to guard content inside a layout (e.g. wrap `<Outlet />`) or need a stable condition with a fallback UI.
108
+
109
+ **Predefined config (recommended):** define `condition`, `redirectTo`, and `fallback` once in a config file, then reuse with `<Protect {...config}>`.
110
+
111
+ ```tsx
112
+ // src/config/protect.tsx (or src/lib/auth.tsx)
113
+ import type { ProtectConfig } from "@epicabdou/linkr";
114
+
115
+ export const authProtect: ProtectConfig = {
116
+ condition: () => !!localStorage.getItem("token"),
117
+ redirectTo: "/login",
118
+ fallback: <div>Checking access…</div>,
119
+ };
120
+ ```
121
+
122
+ ```tsx
123
+ // In any layout
124
+ import { Protect } from "@epicabdou/linkr";
125
+ import { Outlet } from "react-router";
126
+ import { authProtect } from "../config/protect";
127
+
128
+ export default function DashboardLayout() {
129
+ return (
130
+ <Protect {...authProtect}>
131
+ <Outlet />
132
+ </Protect>
133
+ );
134
+ }
135
+ ```
136
+
137
+ You can define multiple configs (e.g. `authProtect`, `adminProtect`) in the same file and import the one you need.
138
+
139
+ - **`condition`**: sync or async function; return `true` to allow, `false` to redirect.
140
+ - **`redirectTo`**: path to redirect to when the condition fails.
141
+ - **`fallback`**: optional React node shown while an async condition is pending.
142
+ - **`children`**: content to render when the condition is true.
143
+
144
+ ## Quick start with CLI
145
+
146
+ ```bash
147
+ npx create-linkrjs-app my-app
148
+ cd my-app && pnpm dev
149
+ ```
150
+
151
+ ## Build & test
152
+
153
+ ```bash
154
+ pnpm --filter linkr build
155
+ pnpm --filter linkr test
156
+ ```
@@ -0,0 +1,85 @@
1
+ import { RouteObject } from 'react-router';
2
+ export { RouteObject } from 'react-router';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { ReactNode } from 'react';
5
+
6
+ /**
7
+ * Result of import.meta.glob("...") - keys are file paths, values are import functions.
8
+ */
9
+ type PagesGlob = Record<string, () => Promise<unknown>>;
10
+ /**
11
+ * Protection check: sync or async function that returns true to allow access, false to redirect.
12
+ * Can be a plain function or an object with check, redirectTo, and optional fallback.
13
+ */
14
+ type RouteProtect = (() => boolean | Promise<boolean>) | {
15
+ /** Returns true to allow the route, false to redirect. */
16
+ check: () => boolean | Promise<boolean>;
17
+ /** Where to redirect when check returns false (default from createRoutes options or "/"). */
18
+ redirectTo?: string;
19
+ /** Optional React node to show while an async check is pending (e.g. a spinner). */
20
+ fallback?: React.ReactNode;
21
+ };
22
+ /**
23
+ * Options for createRoutes().
24
+ */
25
+ interface CreateRoutesOptions {
26
+ /** Result of import.meta.glob(...) from the consumer app (required). */
27
+ pagesGlob: PagesGlob;
28
+ /** Base directory for pages, used for path normalization (default: "src/pages"). */
29
+ pagesDir?: string;
30
+ /** Layout file name (default: "_layout"). */
31
+ layoutFileName?: string;
32
+ /** Not-found file name (default: "404"). */
33
+ notFoundFileName?: string;
34
+ /** Extensions that define route files (default: [".tsx", ".ts", ".jsx", ".js"]). */
35
+ routeExtensions?: string[];
36
+ /** Default redirect path when a route's protect check fails and redirectTo is not set (default: "/"). */
37
+ defaultRedirectTo?: string;
38
+ }
39
+
40
+ declare function createRoutes(options: CreateRoutesOptions): RouteObject[];
41
+
42
+ /**
43
+ * Normalize path to use forward slashes (Windows-safe).
44
+ */
45
+ declare function normalizePath(p: string): string;
46
+ type SegmentKind = "static" | "dynamic" | "splat";
47
+ interface ParsedSegment {
48
+ /** Original segment string, e.g. "blog", "[id]", "[...slug]" */
49
+ raw: string;
50
+ kind: SegmentKind;
51
+ /** For dynamic: "id"; for splat: "slug"; for static: same as raw */
52
+ paramName?: string;
53
+ }
54
+ /**
55
+ * Parse a single path segment into kind and param name.
56
+ */
57
+ declare function parseSegment(segment: string): ParsedSegment;
58
+ /**
59
+ * Compare two segments for sort order: static < dynamic < splat.
60
+ */
61
+ declare function compareSegments(a: ParsedSegment, b: ParsedSegment): number;
62
+
63
+ interface ProtectProps {
64
+ /** Sync or async function that returns true to allow access, false to redirect. */
65
+ condition: () => boolean | Promise<boolean>;
66
+ /** Where to redirect when condition returns false. */
67
+ redirectTo: string;
68
+ /** Optional content to show while an async condition is pending (e.g. a spinner). */
69
+ fallback?: ReactNode;
70
+ /** Content to render when condition is true. */
71
+ children: ReactNode;
72
+ }
73
+ /**
74
+ * Reusable protection config: condition, redirectTo, and optional fallback.
75
+ * Define once in a config file and spread into <Protect {...config}>.
76
+ */
77
+ type ProtectConfig = Pick<ProtectProps, "condition" | "redirectTo" | "fallback">;
78
+ /**
79
+ * Protects content by checking a condition before rendering. If the condition
80
+ * returns false (sync or async), redirects to redirectTo. Use this to guard
81
+ * a single route's content or an entire layout's <Outlet />.
82
+ */
83
+ declare function Protect({ condition, redirectTo, fallback, children }: ProtectProps): react_jsx_runtime.JSX.Element | null;
84
+
85
+ export { type CreateRoutesOptions, type PagesGlob, type ParsedSegment, Protect, type ProtectConfig, type ProtectProps, type RouteProtect, type SegmentKind, compareSegments, createRoutes, normalizePath, parseSegment };
package/dist/index.js ADDED
@@ -0,0 +1,394 @@
1
+ // src/createRoutes.ts
2
+ import { redirect } from "react-router";
3
+
4
+ // src/pathUtils.ts
5
+ function normalizePath(p) {
6
+ return p.replace(/\\/g, "/");
7
+ }
8
+ function getRouteExtension(filename, extensions) {
9
+ const lower = filename.toLowerCase();
10
+ for (const ext of extensions) {
11
+ if (lower.endsWith(ext)) return ext;
12
+ }
13
+ return null;
14
+ }
15
+ function parseSegment(segment) {
16
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
17
+ const param = segment.slice(4, -1);
18
+ return { raw: segment, kind: "splat", paramName: param || "splat" };
19
+ }
20
+ if (segment.startsWith("[") && segment.endsWith("]")) {
21
+ const param = segment.slice(1, -1);
22
+ return { raw: segment, kind: "dynamic", paramName: param || "param" };
23
+ }
24
+ return { raw: segment, kind: "static" };
25
+ }
26
+ function compareSegments(a, b) {
27
+ const order = { static: 0, dynamic: 1, splat: 2 };
28
+ const diff = order[a.kind] - order[b.kind];
29
+ if (diff !== 0) return diff;
30
+ return a.raw.localeCompare(b.raw);
31
+ }
32
+
33
+ // src/createRoutes.ts
34
+ var DEFAULT_PAGES_DIR = "src/pages";
35
+ var DEFAULT_LAYOUT_FILE = "_layout";
36
+ var DEFAULT_NOT_FOUND_FILE = "404";
37
+ var DEFAULT_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
38
+ function stripExtension(filename, extensions) {
39
+ const lower = filename.toLowerCase();
40
+ for (const ext of extensions) {
41
+ if (lower.endsWith(ext)) return filename.slice(0, -ext.length);
42
+ }
43
+ return filename;
44
+ }
45
+ function isValidSplatSegment(segment) {
46
+ return /^\[\.\.\.[^\]]*\]$/.test(segment);
47
+ }
48
+ function warnNoDefault(path) {
49
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
50
+ console.warn(`[linkr] Module has no default export, skipping route: ${path}`);
51
+ }
52
+ }
53
+ function warnInvalidSplat(path) {
54
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
55
+ console.warn(
56
+ `[linkr] Invalid splat usage (e.g. foo[...slug].tsx); use only [...slug].tsx in filename. Skipping: ${path}`
57
+ );
58
+ }
59
+ }
60
+ function parseGlobKey(key, opts) {
61
+ const normalized = normalizePath(key);
62
+ const withoutLeadingSlash = normalized.replace(/^\.\//, "");
63
+ const dir = normalizePath(opts.pagesDir).replace(/\/$/, "");
64
+ let relative = withoutLeadingSlash;
65
+ if (relative.startsWith(dir + "/")) {
66
+ relative = relative.slice(dir.length + 1);
67
+ } else if (relative.startsWith(dir)) {
68
+ relative = relative.slice(dir.length).replace(/^\//, "");
69
+ } else if (dir.includes("/")) {
70
+ const lastDirSegment = dir.split("/").pop();
71
+ if (relative.startsWith(lastDirSegment + "/")) {
72
+ relative = relative.slice(lastDirSegment.length + 1);
73
+ }
74
+ }
75
+ const ext = getRouteExtension(relative, opts.routeExtensions);
76
+ if (!ext) return null;
77
+ const withoutExt = relative.slice(0, -ext.length);
78
+ const parts = withoutExt.split("/").filter(Boolean);
79
+ if (parts.length === 0) return null;
80
+ const lastPart = parts[parts.length - 1];
81
+ const isLayout = stripExtension(lastPart, opts.routeExtensions) === opts.layoutFileName;
82
+ const is404 = stripExtension(lastPart, opts.routeExtensions) === opts.notFoundFileName;
83
+ const isIndex = lastPart.toLowerCase().replace(new RegExp("\\" + ext + "$", "i"), "") === "index";
84
+ const segmentParts = isIndex ? parts.slice(0, -1) : parts;
85
+ const segmentStrings = segmentParts.map((p, i) => {
86
+ const isLast = i === segmentParts.length - 1;
87
+ return isLast ? stripExtension(p, opts.routeExtensions) : p;
88
+ });
89
+ const segmentKinds = segmentStrings.map(parseSegment);
90
+ for (const s of segmentStrings) {
91
+ if (s.includes("[...") && !isValidSplatSegment(s)) {
92
+ warnInvalidSplat(key);
93
+ return null;
94
+ }
95
+ }
96
+ return {
97
+ path: key,
98
+ segments: segmentStrings,
99
+ isLayout,
100
+ is404,
101
+ isIndex,
102
+ segmentKinds
103
+ };
104
+ }
105
+ function buildTree(entries) {
106
+ const root = {
107
+ path: "",
108
+ layout: null,
109
+ indexRoute: null,
110
+ children: /* @__PURE__ */ new Map(),
111
+ childLayouts: /* @__PURE__ */ new Map()
112
+ };
113
+ for (const entry of entries) {
114
+ if (entry.is404) continue;
115
+ if (entry.isLayout) {
116
+ const layoutSegments = entry.segments.slice(0, -1);
117
+ let node2 = root;
118
+ for (let i = 0; i < layoutSegments.length; i++) {
119
+ const key = layoutSegments.slice(0, i + 1).join("/");
120
+ if (!node2.childLayouts.has(key)) {
121
+ node2.childLayouts.set(key, {
122
+ path: key,
123
+ layout: null,
124
+ indexRoute: null,
125
+ children: /* @__PURE__ */ new Map(),
126
+ childLayouts: /* @__PURE__ */ new Map()
127
+ });
128
+ }
129
+ node2 = node2.childLayouts.get(key);
130
+ }
131
+ node2.layout = entry;
132
+ continue;
133
+ }
134
+ if (entry.isIndex && entry.segments.length === 0) {
135
+ root.indexRoute = entry;
136
+ continue;
137
+ }
138
+ if (entry.isIndex) {
139
+ let node2 = root;
140
+ const segs2 = entry.segments;
141
+ for (let i = 0; i < segs2.length; i++) {
142
+ const key = segs2.slice(0, i + 1).join("/");
143
+ if (node2.childLayouts.has(key)) {
144
+ node2 = node2.childLayouts.get(key);
145
+ } else {
146
+ const next = {
147
+ path: key,
148
+ layout: null,
149
+ indexRoute: null,
150
+ children: /* @__PURE__ */ new Map(),
151
+ childLayouts: /* @__PURE__ */ new Map()
152
+ };
153
+ node2.childLayouts.set(key, next);
154
+ node2 = next;
155
+ }
156
+ }
157
+ node2.indexRoute = entry;
158
+ continue;
159
+ }
160
+ const segs = entry.segments;
161
+ let node = root;
162
+ for (let i = 0; i < segs.length - 1; i++) {
163
+ const key = segs.slice(0, i + 1).join("/");
164
+ if (node.childLayouts.has(key)) {
165
+ node = node.childLayouts.get(key);
166
+ } else {
167
+ const next = {
168
+ path: key,
169
+ layout: null,
170
+ indexRoute: null,
171
+ children: /* @__PURE__ */ new Map(),
172
+ childLayouts: /* @__PURE__ */ new Map()
173
+ };
174
+ node.childLayouts.set(key, next);
175
+ node = next;
176
+ }
177
+ }
178
+ const lastSeg = segs[segs.length - 1];
179
+ node.children.set(lastSeg, { entry });
180
+ }
181
+ return root;
182
+ }
183
+ function segmentToPath(segment) {
184
+ if (segment.kind === "static") return segment.raw;
185
+ if (segment.kind === "dynamic") return ":" + (segment.paramName ?? "param");
186
+ if (segment.kind === "splat") return "*";
187
+ return segment.raw;
188
+ }
189
+ function makeLazyRoute(loader, filePath, defaultRedirectTo) {
190
+ return async () => {
191
+ const mod = await loader();
192
+ if (!mod.default) {
193
+ warnNoDefault(filePath);
194
+ return { Component: () => null };
195
+ }
196
+ const result = { Component: mod.default };
197
+ if (mod.ErrorBoundary) result.ErrorBoundary = mod.ErrorBoundary;
198
+ if (mod.handle !== void 0) result.handle = mod.handle;
199
+ if (mod.protect) {
200
+ const check = typeof mod.protect === "function" ? mod.protect : mod.protect.check;
201
+ const redirectTo = (typeof mod.protect === "object" && mod.protect.redirectTo != null ? mod.protect.redirectTo : defaultRedirectTo) ?? "/";
202
+ result.loader = async () => {
203
+ const ok = await Promise.resolve(check());
204
+ if (!ok) throw redirect(redirectTo);
205
+ return null;
206
+ };
207
+ }
208
+ return result;
209
+ };
210
+ }
211
+ function nodeToRoutes(node, pathPrefix, pagesGlob, defaultRedirectTo) {
212
+ const childRouteObjects = [];
213
+ if (node.indexRoute) {
214
+ const loader = pagesGlob[node.indexRoute.path];
215
+ if (loader) {
216
+ childRouteObjects.push({
217
+ index: true,
218
+ lazy: makeLazyRoute(loader, node.indexRoute.path, defaultRedirectTo)
219
+ });
220
+ }
221
+ }
222
+ const childSegments = Array.from(node.children.entries());
223
+ childSegments.sort(([a], [b]) => compareSegments(parseSegment(a), parseSegment(b)));
224
+ for (const [segStr, { entry }] of childSegments) {
225
+ const seg = parseSegment(segStr);
226
+ const pathSeg = segmentToPath(seg);
227
+ const loader = pagesGlob[entry.path];
228
+ if (!loader) continue;
229
+ childRouteObjects.push({
230
+ path: pathSeg,
231
+ lazy: makeLazyRoute(loader, entry.path, defaultRedirectTo)
232
+ });
233
+ }
234
+ const sortedLayoutKeys = Array.from(node.childLayouts.keys()).sort((a, b) => {
235
+ const segsA = a.split("/").map(parseSegment);
236
+ const segsB = b.split("/").map(parseSegment);
237
+ const len = Math.min(segsA.length, segsB.length);
238
+ for (let i = 0; i < len; i++) {
239
+ const c = compareSegments(segsA[i], segsB[i]);
240
+ if (c !== 0) return c;
241
+ }
242
+ return segsA.length - segsB.length;
243
+ });
244
+ for (const key of sortedLayoutKeys) {
245
+ const childNode = node.childLayouts.get(key);
246
+ const firstSeg = key.split("/")[0];
247
+ const seg = parseSegment(firstSeg);
248
+ const pathSeg = segmentToPath(seg);
249
+ const nested = nodeToRoutes(childNode, pathPrefix ? pathPrefix + "/" + pathSeg : pathSeg, pagesGlob, defaultRedirectTo);
250
+ if (childNode.layout) {
251
+ if (childNode.layout.path in pagesGlob) {
252
+ childRouteObjects.push(...nested);
253
+ } else {
254
+ childRouteObjects.push({ path: pathSeg, children: nested });
255
+ }
256
+ } else if (nested.length > 0) {
257
+ childRouteObjects.push({ path: pathSeg, children: nested });
258
+ }
259
+ }
260
+ if (node.layout) {
261
+ if (node.layout.path in pagesGlob) {
262
+ const layoutLoader = pagesGlob[node.layout.path];
263
+ return [{
264
+ path: pathPrefix === "" ? "/" : pathPrefix,
265
+ lazy: makeLazyRoute(layoutLoader, node.layout.path, defaultRedirectTo),
266
+ children: childRouteObjects.length ? childRouteObjects : void 0
267
+ }];
268
+ }
269
+ }
270
+ return childRouteObjects;
271
+ }
272
+ function flattenRootChildren(root, pagesGlob, defaultRedirectTo) {
273
+ const result = [];
274
+ if (root.indexRoute) {
275
+ const loader = pagesGlob[root.indexRoute.path];
276
+ if (loader) {
277
+ result.push({
278
+ index: true,
279
+ lazy: makeLazyRoute(loader, root.indexRoute.path, defaultRedirectTo)
280
+ });
281
+ }
282
+ }
283
+ const childSegments = Array.from(root.children.entries());
284
+ childSegments.sort(([a], [b]) => compareSegments(parseSegment(a), parseSegment(b)));
285
+ for (const [segStr, { entry }] of childSegments) {
286
+ const seg = parseSegment(segStr);
287
+ const pathSeg = segmentToPath(seg);
288
+ const loader = pagesGlob[entry.path];
289
+ if (!loader) continue;
290
+ result.push({
291
+ path: pathSeg,
292
+ lazy: makeLazyRoute(loader, entry.path, defaultRedirectTo)
293
+ });
294
+ }
295
+ const layoutKeys = Array.from(root.childLayouts.keys()).sort((a, b) => {
296
+ const segsA = a.split("/").map(parseSegment);
297
+ const segsB = b.split("/").map(parseSegment);
298
+ const len = Math.min(segsA.length, segsB.length);
299
+ for (let i = 0; i < len; i++) {
300
+ const c = compareSegments(segsA[i], segsB[i]);
301
+ if (c !== 0) return c;
302
+ }
303
+ return segsA.length - segsB.length;
304
+ });
305
+ for (const key of layoutKeys) {
306
+ const childNode = root.childLayouts.get(key);
307
+ const firstSeg = key.split("/")[0];
308
+ const seg = parseSegment(firstSeg);
309
+ const pathSeg = segmentToPath(seg);
310
+ const nested = nodeToRoutes(childNode, pathSeg, pagesGlob, defaultRedirectTo);
311
+ if (childNode.layout) {
312
+ if (childNode.layout.path in pagesGlob) {
313
+ result.push(...nested);
314
+ } else {
315
+ result.push({ path: pathSeg, children: nested });
316
+ }
317
+ } else if (nested.length > 0) {
318
+ result.push({ path: pathSeg, children: nested });
319
+ }
320
+ }
321
+ return result;
322
+ }
323
+ function createRoutes(options) {
324
+ const pagesDir = normalizePath(options.pagesDir ?? DEFAULT_PAGES_DIR).replace(/\/$/, "");
325
+ const opts = {
326
+ pagesDir,
327
+ layoutFileName: options.layoutFileName ?? DEFAULT_LAYOUT_FILE,
328
+ notFoundFileName: options.notFoundFileName ?? DEFAULT_NOT_FOUND_FILE,
329
+ routeExtensions: options.routeExtensions ?? DEFAULT_EXTENSIONS
330
+ };
331
+ const entries = [];
332
+ for (const key of Object.keys(options.pagesGlob)) {
333
+ const entry = parseGlobKey(key, opts);
334
+ if (entry) entries.push(entry);
335
+ }
336
+ const notFoundEntry = entries.find((e) => e.is404);
337
+ const rest = entries.filter((e) => !e.is404);
338
+ const root = buildTree(rest);
339
+ let topLevelRoutes;
340
+ if (root.layout && root.layout.path in options.pagesGlob) {
341
+ const layoutLoader = options.pagesGlob[root.layout.path];
342
+ topLevelRoutes = [{
343
+ path: "/",
344
+ lazy: makeLazyRoute(layoutLoader, root.layout.path, options.defaultRedirectTo),
345
+ children: flattenRootChildren(root, options.pagesGlob, options.defaultRedirectTo)
346
+ }];
347
+ } else {
348
+ topLevelRoutes = flattenRootChildren(root, options.pagesGlob, options.defaultRedirectTo);
349
+ }
350
+ if (notFoundEntry && notFoundEntry.path in options.pagesGlob) {
351
+ const loader = options.pagesGlob[notFoundEntry.path];
352
+ topLevelRoutes.push({
353
+ path: "*",
354
+ lazy: makeLazyRoute(loader, notFoundEntry.path, options.defaultRedirectTo)
355
+ });
356
+ }
357
+ return topLevelRoutes;
358
+ }
359
+
360
+ // src/Protect.tsx
361
+ import { useEffect, useState } from "react";
362
+ import { useNavigate } from "react-router";
363
+ import { Fragment, jsx } from "react/jsx-runtime";
364
+ function Protect({ condition, redirectTo, fallback = null, children }) {
365
+ const navigate = useNavigate();
366
+ const [allowed, setAllowed] = useState(null);
367
+ useEffect(() => {
368
+ let cancelled = false;
369
+ const result = condition();
370
+ if (typeof result === "boolean") {
371
+ if (!cancelled) setAllowed(result);
372
+ if (!result) navigate(redirectTo, { replace: true });
373
+ return;
374
+ }
375
+ result.then((ok) => {
376
+ if (!cancelled) setAllowed(ok);
377
+ if (!ok) navigate(redirectTo, { replace: true });
378
+ });
379
+ return () => {
380
+ cancelled = true;
381
+ };
382
+ }, [condition, redirectTo, navigate]);
383
+ if (allowed === null) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
384
+ if (!allowed) return null;
385
+ return /* @__PURE__ */ jsx(Fragment, { children });
386
+ }
387
+ export {
388
+ Protect,
389
+ compareSegments,
390
+ createRoutes,
391
+ normalizePath,
392
+ parseSegment
393
+ };
394
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/createRoutes.ts","../src/pathUtils.ts","../src/Protect.tsx"],"sourcesContent":["import type { RouteObject } from \"react-router\";\r\nimport { redirect } from \"react-router\";\r\nimport type { CreateRoutesOptions, PagesGlob, RouteProtect } from \"./types\";\r\nimport {\r\n normalizePath,\r\n getRouteExtension,\r\n parseSegment,\r\n compareSegments,\r\n type ParsedSegment,\r\n} from \"./pathUtils\";\r\n\r\nconst DEFAULT_PAGES_DIR = \"src/pages\";\r\nconst DEFAULT_LAYOUT_FILE = \"_layout\";\r\nconst DEFAULT_NOT_FOUND_FILE = \"404\";\r\nconst DEFAULT_EXTENSIONS = [\".tsx\", \".ts\", \".jsx\", \".js\"];\r\n\r\ntype LazyModule = {\r\n default?: React.ComponentType;\r\n ErrorBoundary?: React.ComponentType;\r\n handle?: unknown;\r\n protect?: RouteProtect;\r\n};\r\n\r\n/**\r\n * Strip extension from filename.\r\n */\r\nfunction stripExtension(filename: string, extensions: string[]): string {\r\n const lower = filename.toLowerCase();\r\n for (const ext of extensions) {\r\n if (lower.endsWith(ext)) return filename.slice(0, -ext.length);\r\n }\r\n return filename;\r\n}\r\n\r\n/**\r\n * Check if segment is a valid splat (exactly \"[...name]\").\r\n */\r\nfunction isValidSplatSegment(segment: string): boolean {\r\n return /^\\[\\.\\.\\.[^\\]]*\\]$/.test(segment);\r\n}\r\n\r\n/**\r\n * Warn in dev when a module has no default export (called from lazy).\r\n */\r\nfunction warnNoDefault(path: string): void {\r\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\") {\r\n console.warn(`[linkr] Module has no default export, skipping route: ${path}`);\r\n }\r\n}\r\n\r\n/**\r\n * Warn when [...slug] is used with other segments in the same filename.\r\n */\r\nfunction warnInvalidSplat(path: string): void {\r\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\") {\r\n console.warn(\r\n `[linkr] Invalid splat usage (e.g. foo[...slug].tsx); use only [...slug].tsx in filename. Skipping: ${path}`\r\n );\r\n }\r\n}\r\n\r\ninterface FileEntry {\r\n path: string;\r\n segments: string[];\r\n isLayout: boolean;\r\n is404: boolean;\r\n isIndex: boolean;\r\n segmentKinds: ReturnType<typeof parseSegment>[];\r\n}\r\n\r\nfunction parseGlobKey(\r\n key: string,\r\n opts: Required<Pick<CreateRoutesOptions, \"pagesDir\" | \"layoutFileName\" | \"notFoundFileName\" | \"routeExtensions\">>\r\n): FileEntry | null {\r\n const normalized = normalizePath(key);\r\n const withoutLeadingSlash = normalized.replace(/^\\.\\//, \"\");\r\n const dir = normalizePath(opts.pagesDir).replace(/\\/$/, \"\");\r\n let relative = withoutLeadingSlash;\r\n if (relative.startsWith(dir + \"/\")) {\r\n relative = relative.slice(dir.length + 1);\r\n } else if (relative.startsWith(dir)) {\r\n relative = relative.slice(dir.length).replace(/^\\//, \"\");\r\n } else if (dir.includes(\"/\")) {\r\n const lastDirSegment = dir.split(\"/\").pop()!;\r\n if (relative.startsWith(lastDirSegment + \"/\")) {\r\n relative = relative.slice(lastDirSegment.length + 1);\r\n }\r\n }\r\n\r\n const ext = getRouteExtension(relative, opts.routeExtensions);\r\n if (!ext) return null;\r\n\r\n const withoutExt = relative.slice(0, -ext.length);\r\n const parts = withoutExt.split(\"/\").filter(Boolean);\r\n if (parts.length === 0) return null;\r\n\r\n const lastPart = parts[parts.length - 1];\r\n const isLayout = stripExtension(lastPart, opts.routeExtensions) === opts.layoutFileName;\r\n const is404 = stripExtension(lastPart, opts.routeExtensions) === opts.notFoundFileName;\r\n const isIndex = lastPart.toLowerCase().replace(new RegExp(\"\\\\\" + ext + \"$\", \"i\"), \"\") === \"index\";\r\n const segmentParts = isIndex ? parts.slice(0, -1) : parts;\r\n const segmentStrings = segmentParts.map((p, i) => {\r\n const isLast = i === segmentParts.length - 1;\r\n return isLast ? stripExtension(p, opts.routeExtensions) : p;\r\n });\r\n\r\n const segmentKinds = segmentStrings.map(parseSegment);\r\n for (const s of segmentStrings) {\r\n if (s.includes(\"[...\") && !isValidSplatSegment(s)) {\r\n warnInvalidSplat(key);\r\n return null;\r\n }\r\n }\r\n\r\n return {\r\n path: key,\r\n segments: segmentStrings,\r\n isLayout,\r\n is404,\r\n isIndex,\r\n segmentKinds,\r\n };\r\n}\r\n\r\ninterface RouteNode {\r\n path: string;\r\n layout: FileEntry | null;\r\n indexRoute: FileEntry | null;\r\n children: Map<string, { entry: FileEntry }>;\r\n childLayouts: Map<string, RouteNode>;\r\n}\r\n\r\nfunction buildTree(entries: FileEntry[]): RouteNode {\r\n const root: RouteNode = {\r\n path: \"\",\r\n layout: null,\r\n indexRoute: null,\r\n children: new Map(),\r\n childLayouts: new Map(),\r\n };\r\n\r\n for (const entry of entries) {\r\n if (entry.is404) continue;\r\n if (entry.isLayout) {\r\n const layoutSegments = entry.segments.slice(0, -1);\r\n let node = root;\r\n for (let i = 0; i < layoutSegments.length; i++) {\r\n const key = layoutSegments.slice(0, i + 1).join(\"/\");\r\n if (!node.childLayouts.has(key)) {\r\n node.childLayouts.set(key, {\r\n path: key,\r\n layout: null,\r\n indexRoute: null,\r\n children: new Map(),\r\n childLayouts: new Map(),\r\n });\r\n }\r\n node = node.childLayouts.get(key)!;\r\n }\r\n node.layout = entry;\r\n continue;\r\n }\r\n\r\n if (entry.isIndex && entry.segments.length === 0) {\r\n root.indexRoute = entry;\r\n continue;\r\n }\r\n\r\n if (entry.isIndex) {\r\n let node = root;\r\n const segs = entry.segments;\r\n for (let i = 0; i < segs.length; i++) {\r\n const key = segs.slice(0, i + 1).join(\"/\");\r\n if (node.childLayouts.has(key)) {\r\n node = node.childLayouts.get(key)!;\r\n } else {\r\n const next: RouteNode = {\r\n path: key,\r\n layout: null,\r\n indexRoute: null,\r\n children: new Map(),\r\n childLayouts: new Map(),\r\n };\r\n node.childLayouts.set(key, next);\r\n node = next;\r\n }\r\n }\r\n node.indexRoute = entry;\r\n continue;\r\n }\r\n\r\n const segs = entry.segments;\r\n let node = root;\r\n for (let i = 0; i < segs.length - 1; i++) {\r\n const key = segs.slice(0, i + 1).join(\"/\");\r\n if (node.childLayouts.has(key)) {\r\n node = node.childLayouts.get(key)!;\r\n } else {\r\n const next: RouteNode = {\r\n path: key,\r\n layout: null,\r\n indexRoute: null,\r\n children: new Map(),\r\n childLayouts: new Map(),\r\n };\r\n node.childLayouts.set(key, next);\r\n node = next;\r\n }\r\n }\r\n const lastSeg = segs[segs.length - 1];\r\n node.children.set(lastSeg, { entry });\r\n }\r\n\r\n return root;\r\n}\r\n\r\nfunction segmentToPath(segment: ParsedSegment): string {\r\n if (segment.kind === \"static\") return segment.raw;\r\n if (segment.kind === \"dynamic\") return \":\" + (segment.paramName ?? \"param\");\r\n if (segment.kind === \"splat\") return \"*\";\r\n return segment.raw;\r\n}\r\n\r\nfunction makeLazyRoute(\r\n loader: () => Promise<unknown>,\r\n filePath: string,\r\n defaultRedirectTo?: string\r\n): RouteObject[\"lazy\"] {\r\n return async () => {\r\n const mod = (await loader()) as LazyModule;\r\n if (!mod.default) {\r\n warnNoDefault(filePath);\r\n return { Component: () => null };\r\n }\r\n const result: Record<string, unknown> = { Component: mod.default };\r\n if (mod.ErrorBoundary) result.ErrorBoundary = mod.ErrorBoundary;\r\n if (mod.handle !== undefined) result.handle = mod.handle;\r\n\r\n if (mod.protect) {\r\n const check = typeof mod.protect === \"function\" ? mod.protect : mod.protect.check;\r\n const redirectTo =\r\n (typeof mod.protect === \"object\" && mod.protect.redirectTo != null\r\n ? mod.protect.redirectTo\r\n : defaultRedirectTo) ?? \"/\";\r\n result.loader = async () => {\r\n const ok = await Promise.resolve(check());\r\n if (!ok) throw redirect(redirectTo);\r\n return null;\r\n };\r\n }\r\n\r\n return result as {\r\n Component: React.ComponentType;\r\n ErrorBoundary?: React.ComponentType;\r\n handle?: unknown;\r\n loader?: RouteObject[\"loader\"];\r\n };\r\n };\r\n}\r\n\r\nfunction nodeToRoutes(\r\n node: RouteNode,\r\n pathPrefix: string,\r\n pagesGlob: PagesGlob,\r\n defaultRedirectTo?: string\r\n): RouteObject[] {\r\n const childRouteObjects: RouteObject[] = [];\r\n\r\n if (node.indexRoute) {\r\n const loader = pagesGlob[node.indexRoute.path];\r\n if (loader) {\r\n childRouteObjects.push({\r\n index: true,\r\n lazy: makeLazyRoute(loader, node.indexRoute.path, defaultRedirectTo),\r\n });\r\n }\r\n }\r\n\r\n const childSegments = Array.from(node.children.entries());\r\n childSegments.sort(([a], [b]) => compareSegments(parseSegment(a), parseSegment(b)));\r\n\r\n for (const [segStr, { entry }] of childSegments) {\r\n const seg = parseSegment(segStr);\r\n const pathSeg = segmentToPath(seg);\r\n const loader = pagesGlob[entry.path];\r\n if (!loader) continue;\r\n childRouteObjects.push({\r\n path: pathSeg,\r\n lazy: makeLazyRoute(loader, entry.path, defaultRedirectTo),\r\n });\r\n }\r\n\r\n const sortedLayoutKeys = Array.from(node.childLayouts.keys()).sort((a, b) => {\r\n const segsA = a.split(\"/\").map(parseSegment);\r\n const segsB = b.split(\"/\").map(parseSegment);\r\n const len = Math.min(segsA.length, segsB.length);\r\n for (let i = 0; i < len; i++) {\r\n const c = compareSegments(segsA[i], segsB[i]);\r\n if (c !== 0) return c;\r\n }\r\n return segsA.length - segsB.length;\r\n });\r\n\r\n for (const key of sortedLayoutKeys) {\r\n const childNode = node.childLayouts.get(key)!;\r\n const firstSeg = key.split(\"/\")[0];\r\n const seg = parseSegment(firstSeg);\r\n const pathSeg = segmentToPath(seg);\r\n const nested = nodeToRoutes(childNode, pathPrefix ? pathPrefix + \"/\" + pathSeg : pathSeg, pagesGlob, defaultRedirectTo);\r\n if (childNode.layout) {\r\n if (childNode.layout.path in pagesGlob) {\r\n childRouteObjects.push(...nested);\r\n } else {\r\n childRouteObjects.push({ path: pathSeg, children: nested });\r\n }\r\n } else if (nested.length > 0) {\r\n childRouteObjects.push({ path: pathSeg, children: nested });\r\n }\r\n }\r\n\r\n if (node.layout) {\r\n if (node.layout.path in pagesGlob) {\r\n const layoutLoader = pagesGlob[node.layout.path];\r\n return [{\r\n path: pathPrefix === \"\" ? \"/\" : pathPrefix,\r\n lazy: makeLazyRoute(layoutLoader, node.layout.path, defaultRedirectTo),\r\n children: childRouteObjects.length ? childRouteObjects : undefined,\r\n }];\r\n }\r\n }\r\n\r\n return childRouteObjects;\r\n}\r\n\r\nfunction flattenRootChildren(root: RouteNode, pagesGlob: PagesGlob, defaultRedirectTo?: string): RouteObject[] {\r\n const result: RouteObject[] = [];\r\n\r\n if (root.indexRoute) {\r\n const loader = pagesGlob[root.indexRoute.path];\r\n if (loader) {\r\n result.push({\r\n index: true,\r\n lazy: makeLazyRoute(loader, root.indexRoute.path, defaultRedirectTo),\r\n });\r\n }\r\n }\r\n\r\n const childSegments = Array.from(root.children.entries());\r\n childSegments.sort(([a], [b]) => compareSegments(parseSegment(a), parseSegment(b)));\r\n\r\n for (const [segStr, { entry }] of childSegments) {\r\n const seg = parseSegment(segStr);\r\n const pathSeg = segmentToPath(seg);\r\n const loader = pagesGlob[entry.path];\r\n if (!loader) continue;\r\n result.push({\r\n path: pathSeg,\r\n lazy: makeLazyRoute(loader, entry.path, defaultRedirectTo),\r\n });\r\n }\r\n\r\n const layoutKeys = Array.from(root.childLayouts.keys()).sort((a, b) => {\r\n const segsA = a.split(\"/\").map(parseSegment);\r\n const segsB = b.split(\"/\").map(parseSegment);\r\n const len = Math.min(segsA.length, segsB.length);\r\n for (let i = 0; i < len; i++) {\r\n const c = compareSegments(segsA[i], segsB[i]);\r\n if (c !== 0) return c;\r\n }\r\n return segsA.length - segsB.length;\r\n });\r\n\r\n for (const key of layoutKeys) {\r\n const childNode = root.childLayouts.get(key)!;\r\n const firstSeg = key.split(\"/\")[0];\r\n const seg = parseSegment(firstSeg);\r\n const pathSeg = segmentToPath(seg);\r\n const nested = nodeToRoutes(childNode, pathSeg, pagesGlob, defaultRedirectTo);\r\n if (childNode.layout) {\r\n if (childNode.layout.path in pagesGlob) {\r\n result.push(...nested);\r\n } else {\r\n result.push({ path: pathSeg, children: nested });\r\n }\r\n } else if (nested.length > 0) {\r\n result.push({ path: pathSeg, children: nested });\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\nexport function createRoutes(options: CreateRoutesOptions): RouteObject[] {\r\n const pagesDir = normalizePath(options.pagesDir ?? DEFAULT_PAGES_DIR).replace(/\\/$/, \"\");\r\n const opts = {\r\n pagesDir,\r\n layoutFileName: options.layoutFileName ?? DEFAULT_LAYOUT_FILE,\r\n notFoundFileName: options.notFoundFileName ?? DEFAULT_NOT_FOUND_FILE,\r\n routeExtensions: options.routeExtensions ?? DEFAULT_EXTENSIONS,\r\n };\r\n\r\n const entries: FileEntry[] = [];\r\n for (const key of Object.keys(options.pagesGlob)) {\r\n const entry = parseGlobKey(key, opts);\r\n if (entry) entries.push(entry);\r\n }\r\n\r\n const notFoundEntry = entries.find((e) => e.is404);\r\n const rest = entries.filter((e) => !e.is404);\r\n const root = buildTree(rest);\r\n\r\n let topLevelRoutes: RouteObject[];\r\n\r\n if (root.layout && root.layout.path in options.pagesGlob) {\r\n const layoutLoader = options.pagesGlob[root.layout.path];\r\n topLevelRoutes = [{\r\n path: \"/\",\r\n lazy: makeLazyRoute(layoutLoader, root.layout.path, options.defaultRedirectTo),\r\n children: flattenRootChildren(root, options.pagesGlob, options.defaultRedirectTo),\r\n }];\r\n } else {\r\n topLevelRoutes = flattenRootChildren(root, options.pagesGlob, options.defaultRedirectTo);\r\n }\r\n\r\n if (notFoundEntry && notFoundEntry.path in options.pagesGlob) {\r\n const loader = options.pagesGlob[notFoundEntry.path];\r\n topLevelRoutes.push({\r\n path: \"*\",\r\n lazy: makeLazyRoute(loader, notFoundEntry.path, options.defaultRedirectTo),\r\n });\r\n }\r\n\r\n return topLevelRoutes;\r\n}\r\n","/**\r\n * Normalize path to use forward slashes (Windows-safe).\r\n */\r\nexport function normalizePath(p: string): string {\r\n return p.replace(/\\\\/g, \"/\");\r\n}\r\n\r\n/**\r\n * Get route extension from filename (e.g. \".tsx\") or null if not a route file.\r\n */\r\nexport function getRouteExtension(\r\n filename: string,\r\n extensions: string[]\r\n): string | null {\r\n const lower = filename.toLowerCase();\r\n for (const ext of extensions) {\r\n if (lower.endsWith(ext)) return ext;\r\n }\r\n return null;\r\n}\r\n\r\nexport type SegmentKind = \"static\" | \"dynamic\" | \"splat\";\r\n\r\nexport interface ParsedSegment {\r\n /** Original segment string, e.g. \"blog\", \"[id]\", \"[...slug]\" */\r\n raw: string;\r\n kind: SegmentKind;\r\n /** For dynamic: \"id\"; for splat: \"slug\"; for static: same as raw */\r\n paramName?: string;\r\n}\r\n\r\n/**\r\n * Parse a single path segment into kind and param name.\r\n */\r\nexport function parseSegment(segment: string): ParsedSegment {\r\n if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\r\n const param = segment.slice(4, -1);\r\n return { raw: segment, kind: \"splat\", paramName: param || \"splat\" };\r\n }\r\n if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\r\n const param = segment.slice(1, -1);\r\n return { raw: segment, kind: \"dynamic\", paramName: param || \"param\" };\r\n }\r\n return { raw: segment, kind: \"static\" };\r\n}\r\n\r\n/**\r\n * Compare two segments for sort order: static < dynamic < splat.\r\n */\r\nexport function compareSegments(a: ParsedSegment, b: ParsedSegment): number {\r\n const order = { static: 0, dynamic: 1, splat: 2 };\r\n const diff = order[a.kind] - order[b.kind];\r\n if (diff !== 0) return diff;\r\n return a.raw.localeCompare(b.raw);\r\n}\r\n","import { useEffect, useState, type ReactNode } from \"react\";\r\nimport { useNavigate } from \"react-router\";\r\n\r\nexport interface ProtectProps {\r\n /** Sync or async function that returns true to allow access, false to redirect. */\r\n condition: () => boolean | Promise<boolean>;\r\n /** Where to redirect when condition returns false. */\r\n redirectTo: string;\r\n /** Optional content to show while an async condition is pending (e.g. a spinner). */\r\n fallback?: ReactNode;\r\n /** Content to render when condition is true. */\r\n children: ReactNode;\r\n}\r\n\r\n/**\r\n * Reusable protection config: condition, redirectTo, and optional fallback.\r\n * Define once in a config file and spread into <Protect {...config}>.\r\n */\r\nexport type ProtectConfig = Pick<ProtectProps, \"condition\" | \"redirectTo\" | \"fallback\">;\r\n\r\n/**\r\n * Protects content by checking a condition before rendering. If the condition\r\n * returns false (sync or async), redirects to redirectTo. Use this to guard\r\n * a single route's content or an entire layout's <Outlet />.\r\n */\r\nexport function Protect({ condition, redirectTo, fallback = null, children }: ProtectProps) {\r\n const navigate = useNavigate();\r\n const [allowed, setAllowed] = useState<boolean | null>(null);\r\n\r\n useEffect(() => {\r\n let cancelled = false;\r\n const result = condition();\r\n if (typeof result === \"boolean\") {\r\n if (!cancelled) setAllowed(result);\r\n if (!result) navigate(redirectTo, { replace: true });\r\n return;\r\n }\r\n result.then((ok) => {\r\n if (!cancelled) setAllowed(ok);\r\n if (!ok) navigate(redirectTo, { replace: true });\r\n });\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [condition, redirectTo, navigate]);\r\n\r\n if (allowed === null) return <>{fallback}</>;\r\n if (!allowed) return null;\r\n return <>{children}</>;\r\n}\r\n"],"mappings":"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","names":["node","segs"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@epicabdou/linkr",
3
+ "version": "0.0.2",
4
+ "description": "linkr.js — File-based routing for React Router (v7) with Vite",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "peerDependencies": {
19
+ "react": ">=18.0.0",
20
+ "react-dom": ">=18.0.0",
21
+ "react-router": ">=7.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.0.0",
25
+ "@types/react": "^18.2.0",
26
+ "@types/react-dom": "^18.2.0",
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0",
29
+ "react-router": "^7.0.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.3.0",
32
+ "vitest": "^2.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/epicabdou/linkrjs.git"
37
+ },
38
+ "license": "MIT",
39
+ "keywords": [
40
+ "react",
41
+ "router",
42
+ "vite",
43
+ "file-based",
44
+ "routing",
45
+ "react-router"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest"
54
+ }
55
+ }