feat(dashboard,admin-vite-plugin): Add support for outlet routes, loader, and handle (#11305)
**What**
- Add support for defining outlet routes using `@`, e.g. `/src/admin/routes/brands/@create/page.tsx`
- Add support for exporting a `loader` from a route file.
- Add support for exporting a `handle` from a route file.
Example usage of a loader and handle:
```tsx
// src/admin/routes/articles/[id]/page.tsx
import { Button, Container, Heading } from "@medusajs/ui";
import {
Link,
LoaderFunctionArgs,
Outlet,
UIMatch,
useLoaderData,
} from "react-router-dom";
export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params;
return {
id,
};
}
export const handle = {
breadcrumb: (match: UIMatch<{ id: string }>) => {
const { id } = match.params;
return `#${id}`;
},
};
const ProfilePage = () => {
const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>>;
return (
<div>
<Container className="flex justify-between items-center">
<Heading>Article {id}</Heading>
<Button size="small" variant="secondary" asChild>
<Link to="edit">Edit</Link>
</Button>
</Container>
{/* This will be used for the next example of an Outlet route */}
<Outlet />
</div>
);
};
export default ProfilePage;
```
In the above example we are passing data to the route from a loader, and defining a breadcrumb using the handle.
Example of a outlet route:
```tsx
// src/admin/routes/articles/[id]/@edit/page.tsx
import { Button, Container, Heading } from "@medusajs/ui";
const ProfileEditPage = () => {
return (
<div>
{/* Form goes here */}
</div>
);
};
export default ProfileEditPage;
```
This outlet route will be rendered in the <Outlet /> in the above example when the URL is /articles/1/edit
Resolves CMRC-913, CMRC-914, CMRC-915
This commit is contained in:
1 parent
c08e6ad5cf
commit
a88f6576bd
6 files changed
+468
-41
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-vite-plugin": patch
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(dashboard,admin-vite-plugin): Add support for parallel routes
|
||||||
@@ -51,14 +51,111 @@ const mockFileContents = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const expectedRoutesWithoutLoaders = `
|
const expectedRoutesWithoutLoaders = `
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
Component: RouteComponent0,
|
||||||
|
path: "/one"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Component: RouteComponent1,
|
||||||
|
path: "/two"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
const mockFileContentsWithParallel = [
|
||||||
|
// Parent route
|
||||||
|
`
|
||||||
|
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Brands</div>
|
||||||
|
}
|
||||||
|
export const config = defineRouteConfig({
|
||||||
|
label: "Brands",
|
||||||
|
})
|
||||||
|
export default Page
|
||||||
|
`,
|
||||||
|
// Parallel route
|
||||||
|
`
|
||||||
|
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Create Brand</div>
|
||||||
|
}
|
||||||
|
export const config = defineRouteConfig({
|
||||||
|
label: "Create Brand",
|
||||||
|
})
|
||||||
|
export default Page
|
||||||
|
`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedRoutesWithParallel = `
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
Component: RouteComponent0,
|
||||||
|
path: "/brands",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
Component: RouteComponent1,
|
||||||
|
path: "/brands/create"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
const mockFileContentsWithHandleLoader = [
|
||||||
|
`
|
||||||
|
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Page 1</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
someConfig: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loader = async () => {
|
||||||
|
return { data: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = defineRouteConfig({
|
||||||
|
label: "Page 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Page
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Page 2</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader() {
|
||||||
|
return { data: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = defineRouteConfig({
|
||||||
|
label: "Page 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Page
|
||||||
|
`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedRoutesWithHandleLoader = `
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
Component: RouteComponent0,
|
Component: RouteComponent0,
|
||||||
path: "/one",
|
path: "/one",
|
||||||
|
handle: handle0,
|
||||||
|
loader: loader0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Component: RouteComponent1,
|
Component: RouteComponent1,
|
||||||
path: "/two",
|
path: "/two",
|
||||||
|
loader: loader1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
`
|
`
|
||||||
@@ -103,4 +200,70 @@ describe("generateRoutes", () => {
|
|||||||
utils.normalizeString(expectedRoutesWithoutLoaders)
|
utils.normalizeString(expectedRoutesWithoutLoaders)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it("should handle parallel routes", async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
"Users/user/medusa/src/admin/routes/brands/page.tsx",
|
||||||
|
"Users/user/medusa/src/admin/routes/brands/@create/page.tsx",
|
||||||
|
]
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||||
|
Promise.resolve(
|
||||||
|
mockFileContentsWithParallel[mockFiles.indexOf(file as string)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||||
|
|
||||||
|
const result = await generateRoutes(
|
||||||
|
new Set(["Users/user/medusa/src/admin"])
|
||||||
|
)
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedRoutesWithParallel)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it("should handle parallel routes with windows paths", async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
"C:\\medusa\\src\\admin\\routes\\brands\\page.tsx",
|
||||||
|
"C:\\medusa\\src\\admin\\routes\\brands\\@create\\page.tsx",
|
||||||
|
]
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||||
|
Promise.resolve(
|
||||||
|
mockFileContentsWithParallel[mockFiles.indexOf(file as string)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||||
|
|
||||||
|
const result = await generateRoutes(new Set(["C:\\medusa\\src\\admin"]))
|
||||||
|
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedRoutesWithParallel)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it("should handle routes with handle and loader exports", async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
"Users/user/medusa/src/admin/routes/one/page.tsx",
|
||||||
|
"Users/user/medusa/src/admin/routes/two/page.tsx",
|
||||||
|
]
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||||
|
Promise.resolve(
|
||||||
|
mockFileContentsWithHandleLoader[mockFiles.indexOf(file as string)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||||
|
|
||||||
|
const result = await generateRoutes(
|
||||||
|
new Set(["Users/user/medusa/src/admin"])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedRoutesWithHandleLoader)
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { outdent } from "outdent"
|
import { outdent } from "outdent"
|
||||||
import { File, parse, ParseResult } from "../babel"
|
import { File, parse, ParseResult, traverse } from "../babel"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
import {
|
import {
|
||||||
crawl,
|
crawl,
|
||||||
@@ -13,6 +13,9 @@ import { getRoute } from "./helpers"
|
|||||||
type Route = {
|
type Route = {
|
||||||
Component: string
|
Component: string
|
||||||
path: string
|
path: string
|
||||||
|
handle?: string
|
||||||
|
loader?: string
|
||||||
|
children?: Route[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteResult = {
|
type RouteResult = {
|
||||||
@@ -42,9 +45,29 @@ function generateCode(results: RouteResult[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatRoute(route: Route): string {
|
function formatRoute(route: Route): string {
|
||||||
return `{
|
let base = `{
|
||||||
Component: ${route.Component},
|
Component: ${route.Component},
|
||||||
path: "${route.path}",
|
path: "${route.path}"`
|
||||||
|
|
||||||
|
if (route.handle) {
|
||||||
|
base += `,
|
||||||
|
handle: ${route.handle}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.loader) {
|
||||||
|
base += `,
|
||||||
|
loader: ${route.loader}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children?.length) {
|
||||||
|
return `${base},
|
||||||
|
children: [
|
||||||
|
${route.children.map((child) => formatRoute(child)).join(",\n ")}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,29 +86,42 @@ async function getRouteResults(files: string[]): Promise<RouteResult[]> {
|
|||||||
const results = (await Promise.all(files.map(parseFile))).filter(
|
const results = (await Promise.all(files.map(parseFile))).filter(
|
||||||
(result): result is RouteResult => result !== null
|
(result): result is RouteResult => result !== null
|
||||||
)
|
)
|
||||||
return results
|
|
||||||
|
const routeMap = new Map<string, RouteResult>()
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const routePath = result.route.path
|
||||||
|
const isParallel = routePath.includes("/@")
|
||||||
|
|
||||||
|
if (isParallel) {
|
||||||
|
const parentPath = routePath.split("/@")[0]
|
||||||
|
const parent = routeMap.get(parentPath)
|
||||||
|
if (parent) {
|
||||||
|
parent.route.children = parent.route.children || []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We do not want to include the @ in the final path, so we remove it.
|
||||||
|
*/
|
||||||
|
const finalRoute = {
|
||||||
|
...result.route,
|
||||||
|
path: result.route.path.replace("@", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.route.children.push(finalRoute)
|
||||||
|
parent.imports.push(...result.imports)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
routeMap.set(routePath, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(routeMap.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseFile(
|
async function parseFile(
|
||||||
file: string,
|
file: string,
|
||||||
index: number
|
index: number
|
||||||
): Promise<RouteResult | null> {
|
): Promise<RouteResult | null> {
|
||||||
if (!(await isValidRouteFile(file))) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const routePath = getRoute(file)
|
|
||||||
|
|
||||||
const imports = generateImports(file, index)
|
|
||||||
const route = generateRoute(routePath, index)
|
|
||||||
|
|
||||||
return {
|
|
||||||
imports,
|
|
||||||
route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isValidRouteFile(file: string): Promise<boolean> {
|
|
||||||
const code = await fs.readFile(file, "utf-8")
|
const code = await fs.readFile(file, "utf-8")
|
||||||
|
|
||||||
let ast: ParseResult<File> | null = null
|
let ast: ParseResult<File> | null = null
|
||||||
@@ -97,9 +133,29 @@ async function isValidRouteFile(file: string): Promise<boolean> {
|
|||||||
file,
|
file,
|
||||||
error: e,
|
error: e,
|
||||||
})
|
})
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await isValidRouteFile(ast, file))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasHandle, hasLoader } = await hasNamedExports(ast, file)
|
||||||
|
const routePath = getRoute(file)
|
||||||
|
|
||||||
|
const imports = generateImports(file, index, hasHandle, hasLoader)
|
||||||
|
const route = generateRoute(routePath, index, hasHandle, hasLoader)
|
||||||
|
|
||||||
|
return {
|
||||||
|
imports,
|
||||||
|
route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isValidRouteFile(
|
||||||
|
ast: ParseResult<File>,
|
||||||
|
file: string
|
||||||
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await hasDefaultExport(ast)
|
return await hasDefaultExport(ast)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -110,23 +166,96 @@ async function isValidRouteFile(file: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateImports(file: string, index: number): string[] {
|
function generateImports(
|
||||||
|
file: string,
|
||||||
|
index: number,
|
||||||
|
hasHandle: boolean,
|
||||||
|
hasLoader: boolean
|
||||||
|
): string[] {
|
||||||
const imports: string[] = []
|
const imports: string[] = []
|
||||||
const route = generateRouteComponentName(index)
|
const route = generateRouteComponentName(index)
|
||||||
const importPath = normalizePath(file)
|
const importPath = normalizePath(file)
|
||||||
|
|
||||||
|
if (!hasHandle && !hasLoader) {
|
||||||
imports.push(`import ${route} from "${importPath}"`)
|
imports.push(`import ${route} from "${importPath}"`)
|
||||||
|
} else {
|
||||||
|
const namedImports = [
|
||||||
|
hasHandle && `handle as ${generateHandleName(index)}`,
|
||||||
|
hasLoader && `loader as ${generateLoaderName(index)}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ")
|
||||||
|
imports.push(`import ${route}, { ${namedImports} } from "${importPath}"`)
|
||||||
|
}
|
||||||
|
|
||||||
return imports
|
return imports
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRoute(route: string, index: number): Route {
|
function generateRoute(
|
||||||
|
route: string,
|
||||||
|
index: number,
|
||||||
|
hasHandle: boolean,
|
||||||
|
hasLoader: boolean
|
||||||
|
): Route {
|
||||||
return {
|
return {
|
||||||
Component: generateRouteComponentName(index),
|
Component: generateRouteComponentName(index),
|
||||||
path: route,
|
path: route,
|
||||||
|
handle: hasHandle ? generateHandleName(index) : undefined,
|
||||||
|
loader: hasLoader ? generateLoaderName(index) : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRouteComponentName(index: number): string {
|
function generateRouteComponentName(index: number): string {
|
||||||
return `RouteComponent${index}`
|
return `RouteComponent${index}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateHandleName(index: number): string {
|
||||||
|
return `handle${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateLoaderName(index: number): string {
|
||||||
|
return `loader${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasNamedExports(
|
||||||
|
ast: ParseResult<File>,
|
||||||
|
file: string
|
||||||
|
): Promise<{ hasHandle: boolean; hasLoader: boolean }> {
|
||||||
|
let hasHandle = false
|
||||||
|
let hasLoader = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
traverse(ast, {
|
||||||
|
ExportNamedDeclaration(path) {
|
||||||
|
const declaration = path.node.declaration
|
||||||
|
|
||||||
|
// Handle: export const handle = {...}
|
||||||
|
if (declaration?.type === "VariableDeclaration") {
|
||||||
|
declaration.declarations.forEach((decl) => {
|
||||||
|
if (decl.id.type === "Identifier" && decl.id.name === "handle") {
|
||||||
|
hasHandle = true
|
||||||
|
}
|
||||||
|
if (decl.id.type === "Identifier" && decl.id.name === "loader") {
|
||||||
|
hasLoader = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle: export function loader() or export async function loader()
|
||||||
|
if (
|
||||||
|
declaration?.type === "FunctionDeclaration" &&
|
||||||
|
declaration.id?.name === "loader"
|
||||||
|
) {
|
||||||
|
hasLoader = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("An error occurred while checking for named exports.", {
|
||||||
|
file,
|
||||||
|
error: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasHandle, hasLoader }
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentType } from "react"
|
import { ComponentType } from "react"
|
||||||
import { RouteObject } from "react-router-dom"
|
import { LoaderFunction, RouteObject } from "react-router-dom"
|
||||||
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
||||||
import { RouteExtension, RouteModule } from "../types"
|
import { RouteExtension, RouteModule } from "../types"
|
||||||
|
|
||||||
@@ -21,16 +21,112 @@ export const getRouteExtensions = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createRouteMap = (
|
/**
|
||||||
routes: RouteExtension[],
|
* Creates a route object for a branch node in the route tree
|
||||||
ignore?: string
|
* @param segment - The path segment for this branch
|
||||||
): RouteObject[] => {
|
*/
|
||||||
const root: RouteObject[] = []
|
const createBranchRoute = (segment: string): RouteObject => ({
|
||||||
|
path: segment,
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a route object for a leaf node with its component
|
||||||
|
* @param Component - The React component to render at this route
|
||||||
|
*/
|
||||||
|
const createLeafRoute = (
|
||||||
|
Component: ComponentType,
|
||||||
|
loader?: LoaderFunction,
|
||||||
|
handle?: object
|
||||||
|
): RouteObject => ({
|
||||||
|
path: "",
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
async lazy() {
|
||||||
|
const result: {
|
||||||
|
Component: ComponentType
|
||||||
|
loader?: LoaderFunction
|
||||||
|
handle?: object
|
||||||
|
} = { Component }
|
||||||
|
|
||||||
|
if (loader) {
|
||||||
|
result.loader = loader
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle) {
|
||||||
|
result.handle = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a parallel route configuration
|
||||||
|
* @param path - The route path
|
||||||
|
* @param Component - The React component to render
|
||||||
|
*/
|
||||||
|
const createParallelRoute = (
|
||||||
|
path: string,
|
||||||
|
Component: ComponentType,
|
||||||
|
loader?: LoaderFunction,
|
||||||
|
handle?: object
|
||||||
|
) => ({
|
||||||
|
path,
|
||||||
|
async lazy() {
|
||||||
|
const result: {
|
||||||
|
Component: ComponentType
|
||||||
|
loader?: LoaderFunction
|
||||||
|
handle?: object
|
||||||
|
} = { Component }
|
||||||
|
|
||||||
|
if (loader) {
|
||||||
|
result.loader = loader
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle) {
|
||||||
|
result.handle = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes parallel routes by cleaning their paths relative to the current path
|
||||||
|
* @param parallelRoutes - Array of parallel route extensions
|
||||||
|
* @param currentFullPath - The full path of the current route
|
||||||
|
*/
|
||||||
|
const processParallelRoutes = (
|
||||||
|
parallelRoutes: RouteExtension[] | undefined,
|
||||||
|
currentFullPath: string
|
||||||
|
): RouteObject[] | undefined => {
|
||||||
|
return parallelRoutes
|
||||||
|
?.map(({ path, Component, loader, handle }) => {
|
||||||
|
const childPath = path?.replace(currentFullPath, "").replace(/^\/+/, "")
|
||||||
|
if (!childPath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return createParallelRoute(childPath, Component, loader, handle)
|
||||||
|
})
|
||||||
|
.filter(Boolean) as RouteObject[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively builds the route tree by adding routes at the correct level
|
||||||
|
* @param pathSegments - Array of remaining path segments to process
|
||||||
|
* @param Component - The React component for the route
|
||||||
|
* @param currentLevel - Current level in the route tree
|
||||||
|
* @param parallelRoutes - Optional parallel routes to add
|
||||||
|
* @param fullPath - The full path up to the current level
|
||||||
|
*/
|
||||||
const addRoute = (
|
const addRoute = (
|
||||||
pathSegments: string[],
|
pathSegments: string[],
|
||||||
Component: ComponentType,
|
Component: ComponentType,
|
||||||
currentLevel: RouteObject[]
|
currentLevel: RouteObject[],
|
||||||
|
loader?: LoaderFunction,
|
||||||
|
handle?: object,
|
||||||
|
parallelRoutes?: RouteExtension[],
|
||||||
|
fullPath?: string
|
||||||
) => {
|
) => {
|
||||||
if (!pathSegments.length) {
|
if (!pathSegments.length) {
|
||||||
return
|
return
|
||||||
@@ -40,31 +136,61 @@ export const createRouteMap = (
|
|||||||
let route = currentLevel.find((r) => r.path === currentSegment)
|
let route = currentLevel.find((r) => r.path === currentSegment)
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
route = { path: currentSegment, children: [] }
|
route = createBranchRoute(currentSegment)
|
||||||
currentLevel.push(route)
|
currentLevel.push(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentFullPath = fullPath
|
||||||
|
? `${fullPath}/${currentSegment}`
|
||||||
|
: currentSegment
|
||||||
|
|
||||||
if (remainingSegments.length === 0) {
|
if (remainingSegments.length === 0) {
|
||||||
route.children ||= []
|
route.children ||= []
|
||||||
route.children.push({
|
const leaf = createLeafRoute(Component, loader)
|
||||||
path: "",
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
/**
|
||||||
async lazy() {
|
* The handle needs to be set on the wrapper route object,
|
||||||
return { Component }
|
* in order for it to be resolved correctly thoughout
|
||||||
},
|
* the branch.
|
||||||
})
|
*/
|
||||||
|
if (handle) {
|
||||||
|
route.handle = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf.children = processParallelRoutes(parallelRoutes, currentFullPath)
|
||||||
|
route.children.push(leaf)
|
||||||
} else {
|
} else {
|
||||||
route.children ||= []
|
route.children ||= []
|
||||||
addRoute(remainingSegments, Component, route.children)
|
addRoute(
|
||||||
|
remainingSegments,
|
||||||
|
Component,
|
||||||
|
route.children,
|
||||||
|
loader,
|
||||||
|
handle,
|
||||||
|
parallelRoutes,
|
||||||
|
currentFullPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.forEach(({ path, Component }) => {
|
/**
|
||||||
|
* Creates a complete route map from route extensions
|
||||||
|
* @param routes - Array of route extensions to process
|
||||||
|
* @param ignore - Optional path prefix to ignore when processing routes
|
||||||
|
* @returns An array of route objects forming a route tree
|
||||||
|
*/
|
||||||
|
export const createRouteMap = (
|
||||||
|
routes: RouteExtension[],
|
||||||
|
ignore?: string
|
||||||
|
): RouteObject[] => {
|
||||||
|
const root: RouteObject[] = []
|
||||||
|
|
||||||
|
routes.forEach(({ path, Component, loader, handle, children }) => {
|
||||||
const cleanedPath = ignore
|
const cleanedPath = ignore
|
||||||
? path.replace(ignore, "").replace(/^\/+/, "")
|
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||||
: path.replace(/^\/+/, "")
|
: path.replace(/^\/+/, "")
|
||||||
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||||
addRoute(pathSegments, Component, root)
|
addRoute(pathSegments, Component, root, loader, handle, children)
|
||||||
})
|
})
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { ZodFirstPartySchemaTypes } from "zod"
|
|||||||
export type RouteExtension = {
|
export type RouteExtension = {
|
||||||
Component: ComponentType
|
Component: ComponentType
|
||||||
loader?: LoaderFunction
|
loader?: LoaderFunction
|
||||||
|
handle?: object
|
||||||
|
children?: RouteExtension[]
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ const routes = getRouteExtensions(routeModule, "core")
|
|||||||
* Core Route extensions.
|
* Core Route extensions.
|
||||||
*/
|
*/
|
||||||
export const RouteExtensions = createRouteMap(routes)
|
export const RouteExtensions = createRouteMap(routes)
|
||||||
|
console.log(RouteExtensions)
|
||||||
Reference in new issue
Block a user