From db81db6890904f01c85ec1b9efca0eeefca15326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=ED=9B=88?= Date: Tue, 15 Oct 2024 16:09:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20[9]=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/index.ts | 4 +- src/utils/add-tree.ts | 7 ++ src/utils/build-tree.ts | 109 ++++--------------------------- src/utils/create-route-object.ts | 46 +++++++++++++ src/utils/get-path-segments.ts | 6 +- src/utils/get-routes.ts | 30 ++------- src/utils/suspens-wrapper.ts | 13 ++++ 7 files changed, 92 insertions(+), 123 deletions(-) create mode 100644 src/utils/create-route-object.ts create mode 100644 src/utils/suspens-wrapper.ts diff --git a/src/types/index.ts b/src/types/index.ts index caf54a1..89a47df 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,11 +9,13 @@ export type RouteNode = { pageElement?: React.LazyExoticComponent; /** error component */ errorElement?: React.LazyExoticComponent; + /** loading component */ + loadingElement?: React.LazyExoticComponent; /** children 라우트 */ children?: Record; }; export type NodeData = { - type: 'page' | 'layout' | 'error'; + type: 'page' | 'layout' | 'error' | 'loading'; element: React.LazyExoticComponent; }; diff --git a/src/utils/add-tree.ts b/src/utils/add-tree.ts index a75340a..1ab994f 100644 --- a/src/utils/add-tree.ts +++ b/src/utils/add-tree.ts @@ -27,6 +27,11 @@ export function addToRouteTree( tree.errorElement = nodeData.element; return; } + /** 루트 로딩이면 트리의 최상위에 loadingElement를 설정 */ + if (isRootLayout && nodeData.type === 'loading') { + tree.loadingElement = nodeData.element; + return; + } for (let index = 0; index < segments.length; index++) { let segment = segments[index]; @@ -76,6 +81,8 @@ export function addToRouteTree( current.layoutElement = nodeData.element; } else if (nodeData.type === 'error') { current.errorElement = nodeData.element; + } else if (nodeData.type === 'loading') { + current.loadingElement = nodeData.element; } } } diff --git a/src/utils/build-tree.ts b/src/utils/build-tree.ts index bee7a7c..5dda2cb 100644 --- a/src/utils/build-tree.ts +++ b/src/utils/build-tree.ts @@ -1,62 +1,34 @@ -// src/utils/build-tree.ts import { RouteObject } from 'react-router-dom'; import { RouteNode } from '../types'; -import React from 'react'; +import { createRouteObject } from './create-route-object'; +import { wrapWithSuspense } from './suspens-wrapper'; /** - * 트리구조를 라우터 형태로 변경하는 함수 - * @param node - * @param parentPath - * @returns + * 트리 구조를 route object로 변경하는 함수 + * @param node - RouteNode + * @param parentPath - 상위 path + * @returns RouteObject[] */ export function buildRoutesFromTree( node: RouteNode, parentPath: string = '', ): RouteObject[] { - /** 루트 노드이고 레이아웃 요소가 있는 경우 */ + /** 최상위 루트에 layout이 존재하는 경우 */ if (parentPath === '' && node.layoutElement) { - const rootRoute: RouteObject = { - path: '/', - element: React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.layoutElement), - ), - errorElement: node.errorElement - ? React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.errorElement), - ) - : undefined, - - children: [], - }; + const rootRoute: RouteObject = createRouteObject(node, '/'); const childRoutes: RouteObject[] = []; - /** 루트 노드에 페이지 요소가 있는 경우 인덱스 라우트로 추가 */ if (node.pageElement) { - const pageElement = React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.pageElement), - ); - childRoutes.push({ index: true, - element: pageElement, + element: wrapWithSuspense(node.pageElement), errorElement: node.errorElement - ? React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.errorElement), - ) + ? wrapWithSuspense(node.errorElement) : undefined, }); } - /** 자식 노드를 재귀적으로 처리 */ if (node.children) { for (const childSegment in node.children) { const childNode = node.children[childSegment]; @@ -68,65 +40,13 @@ export function buildRoutesFromTree( } } - /** 자식 라우트를 루트 라우트의 children에 추가 */ rootRoute.children = childRoutes; - - /** 루트 라우트만 반환하여 모든 경로에 루트 레이아웃이 적용되도록 함 */ return [rootRoute]; } const routes: RouteObject[] = []; + const route = createRouteObject(node, parentPath || undefined); - const route: RouteObject = {}; - - /** 현재 노드의 경로 설정 */ - if (parentPath !== '') { - route.path = parentPath; - } - - /** 레이아웃 요소 설정 */ - if (node.layoutElement) { - route.element = React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.layoutElement), - ); - } - - /** 페이지 요소 설정 (레이아웃과 함께 있을 때는 인덱스 라우트로 추가) */ - if (node.pageElement) { - const pageElement = React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.pageElement), - ); - - const errorElement = node.errorElement - ? React.createElement( - React.Suspense, - { fallback: null }, - React.createElement(node.errorElement), - ) - : undefined; - - if (node.layoutElement) { - /** 레이아웃이 있는 경우, 자식으로 인덱스 라우트 추가 */ - if (!route.children) { - route.children = []; - } - route.children.push({ - index: true, - element: pageElement, - errorElement, - }); - } else { - /** 레이아웃이 없는 경우, 현재 라우트에 페이지 요소 설정 */ - route.element = pageElement; - route.errorElement = errorElement; - } - } - - /** 자식 노드 처리 */ if (node.children) { const childRoutes: RouteObject[] = []; @@ -137,18 +57,13 @@ export function buildRoutesFromTree( } if (node.layoutElement) { - /** 레이아웃이 있는 경우, 자식 라우트를 route.children에 추가 */ - if (!route.children) { - route.children = []; - } + route.children = route.children || []; route.children.push(...childRoutes); } else { - /** 레이아웃이 없는 경우, 현재 라우트의 자식으로 추가 */ routes.push(...childRoutes); } } routes.push(route); - return routes; } diff --git a/src/utils/create-route-object.ts b/src/utils/create-route-object.ts new file mode 100644 index 0000000..16e8505 --- /dev/null +++ b/src/utils/create-route-object.ts @@ -0,0 +1,46 @@ +import { RouteObject } from 'react-router-dom'; +import { RouteNode } from '../types'; +import { wrapWithSuspense } from './suspens-wrapper'; + +/** + * Creates a route object 함수 + * @param node - RouteNode + * @param path - route의 path + * @returns RouteObject + */ +export function createRouteObject(node: RouteNode, path?: string): RouteObject { + const route: RouteObject = {}; + + if (path) { + route.path = path; + } + + /** layout 처리 */ + if (node.layoutElement) { + route.element = wrapWithSuspense(node.layoutElement); + } + + /** error 처리 */ + if (node.errorElement) { + route.errorElement = wrapWithSuspense(node.errorElement); + } + + /** page 처리 */ + if (node.pageElement) { + const pageElement = wrapWithSuspense(node.pageElement); + + if (node.layoutElement) { + route.children = [ + { + index: true, + element: pageElement, + errorElement: route.errorElement, + }, + ]; + } else { + route.element = pageElement; + } + } + + return route; +} diff --git a/src/utils/get-path-segments.ts b/src/utils/get-path-segments.ts index aee69c2..d7cf391 100644 --- a/src/utils/get-path-segments.ts +++ b/src/utils/get-path-segments.ts @@ -24,7 +24,8 @@ export function getPathSegments(filePath: string): string[] { if ( segments[0].startsWith('page.') || segments[0].startsWith('layout.') || - segments[0].startsWith('error.') + segments[0].startsWith('error.') || + segments[0].startsWith('loading.') ) { segments[0] = '/'; } @@ -33,7 +34,8 @@ export function getPathSegments(filePath: string): string[] { if ( segments[segments.length - 1].startsWith('page.') || segments[segments.length - 1].startsWith('layout.') || - segments[segments.length - 1].startsWith('error.') + segments[segments.length - 1].startsWith('error.') || + segments[segments.length - 1].startsWith('loading.') ) { segments.pop(); } diff --git a/src/utils/get-routes.ts b/src/utils/get-routes.ts index dd3fcbb..2f2eeee 100644 --- a/src/utils/get-routes.ts +++ b/src/utils/get-routes.ts @@ -12,45 +12,28 @@ import { buildRoutesFromTree } from './build-tree'; export function getRoutes(): RouteObject[] { /** * pages 디렉토리의 page.tsx 파일을 모두 가져옵니다. - * @example - * ```js - * { - * '/src/pages/home/page.tsx': () => import('/src/pages/home/page.tsx'), - * '/src/pages/about/page.tsx': () => import('/src/pages/about/page.tsx'), - * } - * ``` */ const modules = import.meta.glob('/src/pages/**/page.{jsx,tsx}'); /** * pages 디렉토리의 layout.tsx 파일을 모두 가져옵니다. - * @example - * ```js - * { - * '/src/pages/home/layout.tsx': () => import('/src/pages/home/layout.tsx'), - * '/src/pages/about/layout.tsx': () => import('/src/pages/about/layout.tsx'), - * } - * ``` */ const layouts = import.meta.glob('/src/pages/**/layout.{jsx,tsx}'); /** * pages 디렉토리의 error.tsx 파일을 모두 가져옵니다. - * @example - * ```js - * { - * '/src/pages/home/error.tsx': () => import('/src/pages/home/error.tsx'), - * '/src/pages/about/error.tsx': () => import('/src/pages/about/error.tsx'), - * } - * ``` */ const errors = import.meta.glob('/src/pages/**/error.{jsx,tsx}'); + /** + * pages 디렉토리의 loading.tsx 파일을 모두 가져옵니다. + */ + const loadings = import.meta.glob('/src/pages/**/loading.{jsx,tsx}'); /** 트리 구조를 생성하기 위한 루트 노드 */ const routeTree: RouteNode = {}; - /** 모든 {page | layout | error} 파일을 순회하여 트리 구조에 추가합니다 */ + /** 모든 {page | layout | error | loading} 파일을 순회하여 트리 구조에 추가합니다 */ const addElementsToTree = ( glob: Record Promise>, - type: 'page' | 'layout' | 'error', + type: 'page' | 'layout' | 'error' | 'loading', ) => { Object.keys(glob).forEach((filePath) => { const pathSegments = getPathSegments(filePath); @@ -66,6 +49,7 @@ export function getRoutes(): RouteObject[] { addElementsToTree(modules, 'page'); addElementsToTree(layouts, 'layout'); addElementsToTree(errors, 'error'); + addElementsToTree(loadings, 'loading'); /** 트리 구조를 기반으로 RouteObject 배열 생성 */ const routeObjects = buildRoutesFromTree(routeTree); diff --git a/src/utils/suspens-wrapper.ts b/src/utils/suspens-wrapper.ts new file mode 100644 index 0000000..e71d6e9 --- /dev/null +++ b/src/utils/suspens-wrapper.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +/** Suspense로 감싸진 컴포넌트를 생성하는 헬퍼 함수 */ +export function wrapWithSuspense( + Component: React.ComponentType, + loadingComponent?: React.ReactNode, +): React.ReactElement { + return React.createElement( + React.Suspense, + { fallback: loadingComponent }, + React.createElement(Component), + ); +} From 19dd02bb60957db0c0c065f2c422d2d76f3daf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=ED=9B=88?= Date: Wed, 16 Oct 2024 07:27:26 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feature:=20[9]=20loading=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/build-tree.ts | 7 ++++++- src/utils/create-route-object.ts | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/utils/build-tree.ts b/src/utils/build-tree.ts index 5dda2cb..10c9972 100644 --- a/src/utils/build-tree.ts +++ b/src/utils/build-tree.ts @@ -2,6 +2,7 @@ import { RouteObject } from 'react-router-dom'; import { RouteNode } from '../types'; import { createRouteObject } from './create-route-object'; import { wrapWithSuspense } from './suspens-wrapper'; +import React from 'react'; /** * 트리 구조를 route object로 변경하는 함수 @@ -19,10 +20,14 @@ export function buildRoutesFromTree( const childRoutes: RouteObject[] = []; + const loadingComponent = node.loadingElement + ? React.createElement(node.loadingElement) + : undefined; + if (node.pageElement) { childRoutes.push({ index: true, - element: wrapWithSuspense(node.pageElement), + element: wrapWithSuspense(node.pageElement, loadingComponent), errorElement: node.errorElement ? wrapWithSuspense(node.errorElement) : undefined, diff --git a/src/utils/create-route-object.ts b/src/utils/create-route-object.ts index 16e8505..dc2d45e 100644 --- a/src/utils/create-route-object.ts +++ b/src/utils/create-route-object.ts @@ -1,6 +1,7 @@ import { RouteObject } from 'react-router-dom'; import { RouteNode } from '../types'; import { wrapWithSuspense } from './suspens-wrapper'; +import React from 'react'; /** * Creates a route object 함수 @@ -11,23 +12,27 @@ import { wrapWithSuspense } from './suspens-wrapper'; export function createRouteObject(node: RouteNode, path?: string): RouteObject { const route: RouteObject = {}; + const loadingComponent = node.loadingElement + ? React.createElement(node.loadingElement) + : undefined; + if (path) { route.path = path; } /** layout 처리 */ if (node.layoutElement) { - route.element = wrapWithSuspense(node.layoutElement); + route.element = wrapWithSuspense(node.layoutElement, loadingComponent); } /** error 처리 */ if (node.errorElement) { - route.errorElement = wrapWithSuspense(node.errorElement); + route.errorElement = wrapWithSuspense(node.errorElement, loadingComponent); } /** page 처리 */ if (node.pageElement) { - const pageElement = wrapWithSuspense(node.pageElement); + const pageElement = wrapWithSuspense(node.pageElement, loadingComponent); if (node.layoutElement) { route.children = [ From 1eb87a564857e66a95fedc75497ba31d37d752ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=ED=9B=88?= Date: Wed, 16 Oct 2024 07:27:46 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20[9]=200.1.4=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64724a9..8777187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-router-file-routing", - "version": "0.1.3", + "version": "0.1.4", "description": "A library to support folder-based routing of next.js to react-router-dom", "main": "dist/index.js", "files": [ From 85b834c3cdd133fe70631b1119e0924618dd3d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=ED=9B=88?= Date: Wed, 16 Oct 2024 07:32:29 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20[9]=20readme=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 ++++++++++++++++++- docs/README.md | 21 +++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e4ee99..d0e9f2d 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ export default function HomeError() { console.log(error); return ( -
+

Home Page Error

test

@@ -160,6 +160,23 @@ export default function HomeError() { } ``` +#### **7. Loading Support** + +You can add a `loading.tsx` file inside the folder to handle **loading** to that path.
+For more information, see [Suspense fallback in React](https://react.dev/reference/react/Suspense#suspense) + +```tsx +// src/pages/loading.tsx +export default function HomeLoading() { + return ( +
+

Home Page Title

+
+
+ ); +} +``` + --- ### **📄 How to Contribute** diff --git a/docs/README.md b/docs/README.md index 0ee423a..99c8b04 100644 --- a/docs/README.md +++ b/docs/README.md @@ -138,7 +138,7 @@ export default function DashboardLayout() { --- -#### **4. 에러 지원** +#### **6. 에러 지원** 폴더 내에 `error.tsx` 파일을 추가하여 해당 경로에 **에러**에 대한 처리를 수행할 수 있습니다.
자세한 내용은 [React Router의 errorElement](https://reactrouter.com/en/main/route/error-element)를 참고해주세요 @@ -152,7 +152,7 @@ export default function HomeError() { console.log(error); return ( -
+

Home Page Error

test

@@ -160,6 +160,23 @@ export default function HomeError() { } ``` +#### **7. 로딩 지원** + +폴더 내에 `loading.tsx` 파일을 추가하여 해당 경로에 **로딩**에 대한 처리를 수행할 수 있습니다.
+자세한 내용은 [React의 Suspense fallback](https://react.dev/reference/react/Suspense#suspense)을 참고해주세요 + +```tsx +// src/pages/loading.tsx +export default function HomeLoading() { + return ( +
+

Home Page Title

+
+
+ ); +} +``` + ### **📄 기여 방법** 이 프로젝트에 기여하고 싶으시다면, 다음 절차를 따라주세요: