olova-router 1.0.3 → 1.0.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
- export { NotFoundPageConfig, OlovaRouter, SearchParams, SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams } from './router.js';
2
+ export { LayoutRoute, NotFoundPageConfig, OlovaRouter, Outlet, SearchParams, SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams } from './router.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
5
5
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { createContext, useContext, useState, useEffect } from 'react';
4
- import { jsx } from 'react/jsx-runtime';
3
+ import { createContext, useContext, useState, useEffect, useMemo } from 'react';
4
+ import { jsx, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/index.ts
7
7
  function parseSearchParams(search) {
@@ -27,6 +27,7 @@ function buildSearchString(params) {
27
27
  return str ? `?${str}` : "";
28
28
  }
29
29
  var RouterContext = createContext(null);
30
+ var OutletContext = createContext(null);
30
31
  function useRouter() {
31
32
  const context = useContext(RouterContext);
32
33
  if (!context) throw new Error("useRouter must be used within OlovaRouter");
@@ -41,6 +42,12 @@ function useSearchParams() {
41
42
  if (!context) throw new Error("useSearchParams must be used within OlovaRouter");
42
43
  return [context.searchParams, context.setSearchParams];
43
44
  }
45
+ function Outlet() {
46
+ const context = useContext(OutletContext);
47
+ if (!context?.component) return null;
48
+ const Component = context.component;
49
+ return /* @__PURE__ */ jsx(Component, {});
50
+ }
44
51
  function matchRoute(pattern, pathname) {
45
52
  const patternParts = pattern.split("/").filter(Boolean);
46
53
  const pathParts = pathname.split("/").filter(Boolean);
@@ -66,6 +73,10 @@ function matchRoute(pattern, pathname) {
66
73
  }
67
74
  return { match: true, params };
68
75
  }
76
+ function matchLayoutScope(layoutPath, pathname) {
77
+ if (layoutPath === "/") return true;
78
+ return pathname === layoutPath || pathname.startsWith(layoutPath + "/");
79
+ }
69
80
  function findNotFoundPage(path2, notFoundPages) {
70
81
  if (!notFoundPages || notFoundPages.length === 0) return null;
71
82
  const sorted = [...notFoundPages].sort(
@@ -81,7 +92,7 @@ function findNotFoundPage(path2, notFoundPages) {
81
92
  }
82
93
  return null;
83
94
  }
84
- function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
95
+ function OlovaRouter({ routes, layouts = [], notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
85
96
  const [currentPath, setCurrentPath] = useState(window.location.pathname);
86
97
  const [searchParams, setSearchParamsState] = useState(
87
98
  () => parseSearchParams(window.location.search)
@@ -121,38 +132,56 @@ function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ js
121
132
  }
122
133
  setSearchParamsState(parseSearchParams(searchString));
123
134
  };
124
- let Component = null;
135
+ const sortedRoutes = useMemo(() => {
136
+ return [...routes].sort((a, b) => {
137
+ const aHasCatchAll = a.path.includes("*");
138
+ const bHasCatchAll = b.path.includes("*");
139
+ const aHasDynamic = a.path.includes(":");
140
+ const bHasDynamic = b.path.includes(":");
141
+ if (aHasCatchAll && !bHasCatchAll) return 1;
142
+ if (!aHasCatchAll && bHasCatchAll) return -1;
143
+ if (aHasDynamic && !bHasDynamic) return 1;
144
+ if (!aHasDynamic && bHasDynamic) return -1;
145
+ return b.path.length - a.path.length;
146
+ });
147
+ }, [routes]);
148
+ const matchingLayouts = useMemo(() => {
149
+ return layouts.filter((layout) => matchLayoutScope(layout.path, currentPath)).sort((a, b) => a.path.length - b.path.length);
150
+ }, [layouts, currentPath]);
151
+ let MatchedComponent = null;
125
152
  let params = {};
126
- const sortedRoutes = [...routes].sort((a, b) => {
127
- const aHasCatchAll = a.path.includes("*");
128
- const bHasCatchAll = b.path.includes("*");
129
- const aHasDynamic = a.path.includes(":");
130
- const bHasDynamic = b.path.includes(":");
131
- if (aHasCatchAll && !bHasCatchAll) return 1;
132
- if (!aHasCatchAll && bHasCatchAll) return -1;
133
- if (aHasDynamic && !bHasDynamic) return 1;
134
- if (!aHasDynamic && bHasDynamic) return -1;
135
- return b.path.length - a.path.length;
136
- });
137
153
  for (const route of sortedRoutes) {
138
154
  if (route.path === "/" && currentPath === "/") {
139
- Component = route.component;
155
+ MatchedComponent = route.component;
140
156
  break;
141
157
  }
142
158
  const result = matchRoute(route.path, currentPath);
143
159
  if (result.match) {
144
- Component = route.component;
160
+ MatchedComponent = route.component;
145
161
  params = result.params;
146
162
  break;
147
163
  }
148
164
  }
149
- if (!Component) {
165
+ if (!MatchedComponent) {
150
166
  const NotFoundComponent = findNotFoundPage(currentPath, notFoundPages);
151
167
  if (NotFoundComponent) {
152
- Component = NotFoundComponent;
168
+ MatchedComponent = NotFoundComponent;
153
169
  }
154
170
  }
155
- return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: Component ? /* @__PURE__ */ jsx(Component, {}) : notFound });
171
+ const renderContent = () => {
172
+ const content = MatchedComponent ? /* @__PURE__ */ jsx(MatchedComponent, {}) : notFound;
173
+ if (matchingLayouts.length === 0) {
174
+ return content;
175
+ }
176
+ let result = content;
177
+ for (let i = matchingLayouts.length - 1; i >= 0; i--) {
178
+ const Layout = matchingLayouts[i].layout;
179
+ const wrapped = result;
180
+ result = /* @__PURE__ */ jsx(OutletContext.Provider, { value: { component: () => /* @__PURE__ */ jsx(Fragment, { children: wrapped }) }, children: /* @__PURE__ */ jsx(Layout, {}) });
181
+ }
182
+ return result;
183
+ };
184
+ return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: renderContent() });
156
185
  }
157
186
  function createLink() {
158
187
  return function Link({ href, children, className }) {
@@ -166,7 +195,7 @@ function createLink() {
166
195
 
167
196
  // src/index.ts
168
197
  function parseDynamicSegment(segment) {
169
- if (segment === "$" || segment.match(/^\[\.\.\..+\]$/)) {
198
+ if (segment === "$" || segment.match(/^\[\.\.\.(.+)\]$/)) {
170
199
  const paramName = segment === "$" ? "slug" : segment.match(/^\[\.\.\.(.+)\]$/)?.[1] || "slug";
171
200
  return { isDynamic: true, paramName, isCatchAll: true };
172
201
  }
@@ -224,18 +253,25 @@ function detectExportType(filePath) {
224
253
  return { hasDefault: true, namedExport: null };
225
254
  }
226
255
  }
227
- function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, isRoot = false) {
256
+ function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, layouts, isRoot = false) {
228
257
  if (!fs.existsSync(dir)) return;
229
258
  const entries = fs.readdirSync(dir, { withFileTypes: true });
230
259
  for (const entry of entries) {
231
260
  const fullPath = path.join(dir, entry.name);
232
261
  if (entry.isDirectory()) {
233
262
  if (entry.name === "node_modules" || entry.name === "assets" || entry.name.startsWith("_")) continue;
234
- scanDirectory(fullPath, rootDir, extensions, routes, notFoundPages, false);
263
+ scanDirectory(fullPath, rootDir, extensions, routes, notFoundPages, layouts, false);
235
264
  } else if (entry.isFile()) {
236
265
  const ext = path.extname(entry.name);
237
266
  const baseName = path.basename(entry.name, ext);
238
- if (baseName === "404" && extensions.includes(ext)) {
267
+ if (baseName === "_layout" && extensions.includes(ext)) {
268
+ const relativePath = path.relative(rootDir, dir);
269
+ const { routePath } = pathToRoute(relativePath, path.sep);
270
+ layouts.push({
271
+ path: isRoot ? "/" : routePath,
272
+ filePath: fullPath
273
+ });
274
+ } else if (baseName === "404" && extensions.includes(ext)) {
239
275
  const relativeParts = path.relative(rootDir, dir).split(path.sep).filter(Boolean);
240
276
  const filteredParts = relativeParts.filter((p) => !isRouteGroup(p));
241
277
  const pathPrefix = isRoot ? "" : "/" + filteredParts.join("/");
@@ -253,16 +289,18 @@ function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, isRoot =
253
289
  function scanRoutes(rootDir, extensions) {
254
290
  const routes = [];
255
291
  const notFoundPages = [];
292
+ const layouts = [];
256
293
  const absoluteRoot = path.isAbsolute(rootDir) ? rootDir : path.resolve(rootDir);
257
294
  if (!fs.existsSync(absoluteRoot)) {
258
295
  throw new Error(`Olova Router: Root directory does not exist: ${absoluteRoot}`);
259
296
  }
260
- scanDirectory(absoluteRoot, absoluteRoot, extensions, routes, notFoundPages, true);
297
+ scanDirectory(absoluteRoot, absoluteRoot, extensions, routes, notFoundPages, layouts, true);
261
298
  routes.sort((a, b) => a.isDynamic !== b.isDynamic ? a.isDynamic ? 1 : -1 : a.path.localeCompare(b.path));
262
299
  notFoundPages.sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
263
- return { routes, notFoundPages };
300
+ layouts.sort((a, b) => a.path.length - b.path.length);
301
+ return { routes, notFoundPages, layouts };
264
302
  }
265
- function generateRouteTree(routes, notFoundPages, srcDir) {
303
+ function generateRouteTree(routes, notFoundPages, layouts, srcDir) {
266
304
  const routeImports = routes.map((route, index) => {
267
305
  const relativePath = "./" + path.relative(srcDir, route.component).replace(/\\/g, "/").replace(/\.tsx?$/, "");
268
306
  if (route.hasDefault) {
@@ -283,18 +321,31 @@ function generateRouteTree(routes, notFoundPages, srcDir) {
283
321
  return `import NotFound${index} from '${relativePath}';`;
284
322
  }
285
323
  }).join("\n");
324
+ const layoutImports = layouts.map((layout, index) => {
325
+ const relativePath = "./" + path.relative(srcDir, layout.filePath).replace(/\\/g, "/").replace(/\.tsx?$/, "");
326
+ if (layout.hasDefault) {
327
+ return `import Layout${index} from '${relativePath}';`;
328
+ } else if (layout.namedExport) {
329
+ return `import { ${layout.namedExport} as Layout${index} } from '${relativePath}';`;
330
+ } else {
331
+ return `import Layout${index} from '${relativePath}';`;
332
+ }
333
+ }).join("\n");
286
334
  const routeObjects = routes.map((route, index) => {
287
335
  return ` { path: '${route.path}', component: Route${index} }`;
288
336
  }).join(",\n");
289
337
  const notFoundObjects = notFoundPages.map((nf, index) => {
290
338
  return ` { pathPrefix: '${nf.pathPrefix}', component: NotFound${index} }`;
291
339
  }).join(",\n");
340
+ const layoutObjects = layouts.map((layout, index) => {
341
+ return ` { path: '${layout.path}', layout: Layout${index}, children: [] }`;
342
+ }).join(",\n");
292
343
  const routePaths = routes.length > 0 ? routes.map((r) => `'${r.path}'`).join(" | ") : "never";
293
- const allImports = [routeImports, notFoundImports].filter(Boolean).join("\n");
344
+ const allImports = [routeImports, notFoundImports, layoutImports].filter(Boolean).join("\n");
294
345
  return `// Auto-generated by olova-router - DO NOT EDIT
295
346
  // This file is auto-updated when you add/remove route folders
296
347
 
297
- import { createLink, OlovaRouter, useRouter, useParams, useSearchParams } from 'olova-router/router';
348
+ import { createLink, OlovaRouter, useRouter, useParams, useSearchParams, Outlet } from 'olova-router/router';
298
349
  ${allImports}
299
350
 
300
351
  export const routes = [
@@ -305,11 +356,15 @@ export const notFoundPages = [
305
356
  ${notFoundObjects || ""}
306
357
  ];
307
358
 
359
+ export const layouts = [
360
+ ${layoutObjects || ""}
361
+ ];
362
+
308
363
  export type RoutePaths = ${routePaths};
309
364
 
310
365
  export const Link = createLink<RoutePaths>();
311
- export { OlovaRouter, useRouter, useParams, useSearchParams };
312
- export type { NotFoundPageConfig, SearchParams, SetSearchParamsOptions } from 'olova-router/router';
366
+ export { OlovaRouter, useRouter, useParams, useSearchParams, Outlet };
367
+ export type { NotFoundPageConfig, SearchParams, SetSearchParamsOptions, LayoutRoute } from 'olova-router/router';
313
368
  `;
314
369
  }
315
370
  function olovaRouter(options = {}) {
@@ -319,7 +374,7 @@ function olovaRouter(options = {}) {
319
374
  let absoluteRootDir;
320
375
  let watcher = null;
321
376
  function generateRouteTreeFile() {
322
- const { routes, notFoundPages } = scanRoutes(absoluteRootDir, extensions);
377
+ const { routes, notFoundPages, layouts } = scanRoutes(absoluteRootDir, extensions);
323
378
  const routeConfigs = routes.map((r) => {
324
379
  const exportInfo = detectExportType(r.filePath);
325
380
  return {
@@ -339,7 +394,16 @@ function olovaRouter(options = {}) {
339
394
  namedExport: exportInfo.namedExport
340
395
  };
341
396
  });
342
- const content = generateRouteTree(routeConfigs, notFoundConfigs, absoluteRootDir);
397
+ const layoutConfigs = layouts.map((l) => {
398
+ const exportInfo = detectExportType(l.filePath);
399
+ return {
400
+ path: l.path,
401
+ filePath: l.filePath.replace(/\\/g, "/"),
402
+ hasDefault: exportInfo.hasDefault,
403
+ namedExport: exportInfo.namedExport
404
+ };
405
+ });
406
+ const content = generateRouteTree(routeConfigs, notFoundConfigs, layoutConfigs, absoluteRootDir);
343
407
  const treePath = path.resolve(absoluteRootDir, "route.tree.ts");
344
408
  const existing = fs.existsSync(treePath) ? fs.readFileSync(treePath, "utf-8") : "";
345
409
  if (existing !== content) {
@@ -349,14 +413,17 @@ function olovaRouter(options = {}) {
349
413
  }
350
414
  function startWatcher() {
351
415
  if (watcher) return;
352
- watcher = fs.watch(absoluteRootDir, { recursive: true }, (_eventType, filename) => {
416
+ watcher = fs.watch(absoluteRootDir, { recursive: true }, (eventType, filename) => {
353
417
  if (!filename) return;
354
418
  if (filename.includes("route.tree.ts")) return;
355
419
  const isIndexFile = filename.endsWith("index.tsx") || filename.endsWith("index.ts");
356
420
  const isAppFile = filename === "App.tsx" || filename === "App.ts";
357
421
  const is404File = filename.endsWith("404.tsx") || filename.endsWith("404.ts");
422
+ const isLayoutFile = filename.endsWith("_layout.tsx") || filename.endsWith("_layout.ts");
358
423
  const isDirectory = !filename.includes(".");
359
- if (isIndexFile || isAppFile || is404File || isDirectory) {
424
+ const isDynamicSegment = filename.includes("$") || filename.includes("[");
425
+ const isRenameEvent = eventType === "rename";
426
+ if (isIndexFile || isAppFile || is404File || isLayoutFile || isDirectory || isDynamicSegment || isRenameEvent) {
360
427
  setTimeout(() => generateRouteTreeFile(), 100);
361
428
  }
362
429
  });
@@ -384,4 +451,4 @@ function olovaRouter(options = {}) {
384
451
  }
385
452
  var src_default = olovaRouter;
386
453
 
387
- export { OlovaRouter, createLink, src_default as default, olovaRouter, useParams, useRouter, useSearchParams };
454
+ export { OlovaRouter, Outlet, createLink, src_default as default, olovaRouter, useParams, useRouter, useSearchParams };
package/dist/router.d.ts CHANGED
@@ -5,6 +5,12 @@ interface Route {
5
5
  path: string;
6
6
  component: ComponentType;
7
7
  }
8
+ /** Layout route for nested layouts */
9
+ interface LayoutRoute {
10
+ path: string;
11
+ layout: ComponentType;
12
+ children: Route[];
13
+ }
8
14
  /** 404 page configuration */
9
15
  interface NotFoundPageConfig {
10
16
  pathPrefix: string;
@@ -33,18 +39,32 @@ declare function useSearchParams(): [
33
39
  SearchParams,
34
40
  (params: Record<string, string | string[] | null>, options?: SetSearchParamsOptions) => void
35
41
  ];
42
+ /**
43
+ * Outlet component - renders the matched child route
44
+ * Use this in _layout.tsx to render nested routes
45
+ */
46
+ declare function Outlet(): react_jsx_runtime.JSX.Element | null;
36
47
  interface OlovaRouterProps {
37
48
  routes: Route[];
49
+ layouts?: LayoutRoute[];
38
50
  notFoundPages?: NotFoundPageConfig[];
39
51
  notFound?: ReactNode;
40
52
  }
41
53
  /** Main router component - wrap your app with this */
42
- declare function OlovaRouter({ routes, notFoundPages, notFound }: OlovaRouterProps): react_jsx_runtime.JSX.Element;
43
- /** Creates a type-safe Link component for the given route paths */
54
+ declare function OlovaRouter({ routes, layouts, notFoundPages, notFound }: OlovaRouterProps): react_jsx_runtime.JSX.Element;
55
+ /**
56
+ * Type utilities for resolving dynamic route paths
57
+ * Transforms '/users/:id' into accepting '/users/anything'
58
+ */
59
+ type ResolveSegment<S extends string> = S extends `:${string}` ? string : S extends '*' ? string : S;
60
+ type ResolvePathSegments<Path extends string> = Path extends `${infer Segment}/${infer Rest}` ? `${ResolveSegment<Segment>}/${ResolvePathSegments<Rest>}` : ResolveSegment<Path>;
61
+ type ResolveRoutePath<Path extends string> = Path extends `${infer Base}/*` ? `${ResolvePathSegments<Base>}/${string}` : ResolvePathSegments<Path>;
62
+ type ResolveRoutes<T extends string> = T extends string ? ResolveRoutePath<T> : never;
63
+ /** Creates a type-safe Link component that accepts resolved dynamic paths */
44
64
  declare function createLink<T extends string>(): ({ href, children, className }: {
45
- href: T;
65
+ href: ResolveRoutes<T>;
46
66
  children: ReactNode;
47
67
  className?: string;
48
68
  }) => react_jsx_runtime.JSX.Element;
49
69
 
50
- export { type NotFoundPageConfig, OlovaRouter, type SearchParams, type SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams };
70
+ export { type LayoutRoute, type NotFoundPageConfig, OlovaRouter, Outlet, type ResolveRoutePath, type SearchParams, type SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams };
package/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createContext, useContext, useState, useEffect } from 'react';
2
- import { jsx } from 'react/jsx-runtime';
1
+ import { createContext, useContext, useState, useEffect, useMemo } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/router.tsx
5
5
  function parseSearchParams(search) {
@@ -25,6 +25,7 @@ function buildSearchString(params) {
25
25
  return str ? `?${str}` : "";
26
26
  }
27
27
  var RouterContext = createContext(null);
28
+ var OutletContext = createContext(null);
28
29
  function useRouter() {
29
30
  const context = useContext(RouterContext);
30
31
  if (!context) throw new Error("useRouter must be used within OlovaRouter");
@@ -39,6 +40,12 @@ function useSearchParams() {
39
40
  if (!context) throw new Error("useSearchParams must be used within OlovaRouter");
40
41
  return [context.searchParams, context.setSearchParams];
41
42
  }
43
+ function Outlet() {
44
+ const context = useContext(OutletContext);
45
+ if (!context?.component) return null;
46
+ const Component = context.component;
47
+ return /* @__PURE__ */ jsx(Component, {});
48
+ }
42
49
  function matchRoute(pattern, pathname) {
43
50
  const patternParts = pattern.split("/").filter(Boolean);
44
51
  const pathParts = pathname.split("/").filter(Boolean);
@@ -64,6 +71,10 @@ function matchRoute(pattern, pathname) {
64
71
  }
65
72
  return { match: true, params };
66
73
  }
74
+ function matchLayoutScope(layoutPath, pathname) {
75
+ if (layoutPath === "/") return true;
76
+ return pathname === layoutPath || pathname.startsWith(layoutPath + "/");
77
+ }
67
78
  function findNotFoundPage(path, notFoundPages) {
68
79
  if (!notFoundPages || notFoundPages.length === 0) return null;
69
80
  const sorted = [...notFoundPages].sort(
@@ -79,7 +90,7 @@ function findNotFoundPage(path, notFoundPages) {
79
90
  }
80
91
  return null;
81
92
  }
82
- function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
93
+ function OlovaRouter({ routes, layouts = [], notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
83
94
  const [currentPath, setCurrentPath] = useState(window.location.pathname);
84
95
  const [searchParams, setSearchParamsState] = useState(
85
96
  () => parseSearchParams(window.location.search)
@@ -119,38 +130,56 @@ function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ js
119
130
  }
120
131
  setSearchParamsState(parseSearchParams(searchString));
121
132
  };
122
- let Component = null;
133
+ const sortedRoutes = useMemo(() => {
134
+ return [...routes].sort((a, b) => {
135
+ const aHasCatchAll = a.path.includes("*");
136
+ const bHasCatchAll = b.path.includes("*");
137
+ const aHasDynamic = a.path.includes(":");
138
+ const bHasDynamic = b.path.includes(":");
139
+ if (aHasCatchAll && !bHasCatchAll) return 1;
140
+ if (!aHasCatchAll && bHasCatchAll) return -1;
141
+ if (aHasDynamic && !bHasDynamic) return 1;
142
+ if (!aHasDynamic && bHasDynamic) return -1;
143
+ return b.path.length - a.path.length;
144
+ });
145
+ }, [routes]);
146
+ const matchingLayouts = useMemo(() => {
147
+ return layouts.filter((layout) => matchLayoutScope(layout.path, currentPath)).sort((a, b) => a.path.length - b.path.length);
148
+ }, [layouts, currentPath]);
149
+ let MatchedComponent = null;
123
150
  let params = {};
124
- const sortedRoutes = [...routes].sort((a, b) => {
125
- const aHasCatchAll = a.path.includes("*");
126
- const bHasCatchAll = b.path.includes("*");
127
- const aHasDynamic = a.path.includes(":");
128
- const bHasDynamic = b.path.includes(":");
129
- if (aHasCatchAll && !bHasCatchAll) return 1;
130
- if (!aHasCatchAll && bHasCatchAll) return -1;
131
- if (aHasDynamic && !bHasDynamic) return 1;
132
- if (!aHasDynamic && bHasDynamic) return -1;
133
- return b.path.length - a.path.length;
134
- });
135
151
  for (const route of sortedRoutes) {
136
152
  if (route.path === "/" && currentPath === "/") {
137
- Component = route.component;
153
+ MatchedComponent = route.component;
138
154
  break;
139
155
  }
140
156
  const result = matchRoute(route.path, currentPath);
141
157
  if (result.match) {
142
- Component = route.component;
158
+ MatchedComponent = route.component;
143
159
  params = result.params;
144
160
  break;
145
161
  }
146
162
  }
147
- if (!Component) {
163
+ if (!MatchedComponent) {
148
164
  const NotFoundComponent = findNotFoundPage(currentPath, notFoundPages);
149
165
  if (NotFoundComponent) {
150
- Component = NotFoundComponent;
166
+ MatchedComponent = NotFoundComponent;
151
167
  }
152
168
  }
153
- return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: Component ? /* @__PURE__ */ jsx(Component, {}) : notFound });
169
+ const renderContent = () => {
170
+ const content = MatchedComponent ? /* @__PURE__ */ jsx(MatchedComponent, {}) : notFound;
171
+ if (matchingLayouts.length === 0) {
172
+ return content;
173
+ }
174
+ let result = content;
175
+ for (let i = matchingLayouts.length - 1; i >= 0; i--) {
176
+ const Layout = matchingLayouts[i].layout;
177
+ const wrapped = result;
178
+ result = /* @__PURE__ */ jsx(OutletContext.Provider, { value: { component: () => /* @__PURE__ */ jsx(Fragment, { children: wrapped }) }, children: /* @__PURE__ */ jsx(Layout, {}) });
179
+ }
180
+ return result;
181
+ };
182
+ return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: renderContent() });
154
183
  }
155
184
  function createLink() {
156
185
  return function Link({ href, children, className }) {
@@ -162,4 +191,4 @@ function createLink() {
162
191
  };
163
192
  }
164
193
 
165
- export { OlovaRouter, createLink, useParams, useRouter, useSearchParams };
194
+ export { OlovaRouter, Outlet, createLink, useParams, useRouter, useSearchParams };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "olova-router",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Next.js-style folder-based routing for React + Vite applications",
5
5
  "author": "",
6
6
  "license": "MIT",