diff --git a/apps/example/.gitignore b/apps/example/.gitignore
index a547bf3..7b20100 100644
--- a/apps/example/.gitignore
+++ b/apps/example/.gitignore
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Hana specific
+.hana
diff --git a/apps/example/pages/_404.tsx b/apps/example/pages/_404.tsx
new file mode 100644
index 0000000..347ece3
--- /dev/null
+++ b/apps/example/pages/_404.tsx
@@ -0,0 +1,10 @@
+const ErrorPage = () => {
+ return (
+
+
404 - Page Not Found
+
This is my custom 404 page
+
+ );
+}
+
+export default ErrorPage;
diff --git a/apps/example/pages/_app.tsx b/apps/example/pages/_app.tsx
index a9c11d8..2886d2a 100644
--- a/apps/example/pages/_app.tsx
+++ b/apps/example/pages/_app.tsx
@@ -3,7 +3,10 @@ import ReactDOM from 'react-dom/client';
import { createRouter } from '@hanabira/router';
import { PersistedState, createStore } from '@hanabira/store';
+import _404 from '../.hana/_404-page.json';
import routes from '../.hana/routes.json';
+import errorPages from '../.hana/error-pages.json';
+import loadingPages from '../.hana/loading-pages.json';
import './index.css';
@@ -25,6 +28,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
{createRouter({
root: import.meta.url,
+ loadingPages,
+ errorPages,
+ _404,
routes,
})}
diff --git a/apps/example/pages/dashboard/_loading.tsx b/apps/example/pages/dashboard/_loading.tsx
new file mode 100644
index 0000000..cc97337
--- /dev/null
+++ b/apps/example/pages/dashboard/_loading.tsx
@@ -0,0 +1,9 @@
+const Loading = () => {
+ return (
+
+ Loading dashboard component...
+
+ );
+};
+
+export default Loading;
diff --git a/apps/example/pages/dashboard/dashboard/[slug].tsx b/apps/example/pages/dashboard/dashboard/[slug].tsx
new file mode 100644
index 0000000..87ff770
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/[slug].tsx
@@ -0,0 +1,5 @@
+const Dashboard = () => {
+ return Dashboard
;
+};
+
+export default Dashboard;
diff --git a/apps/example/pages/dashboard/dashboard/_error.tsx b/apps/example/pages/dashboard/dashboard/_error.tsx
new file mode 100644
index 0000000..0d1f0ae
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/_error.tsx
@@ -0,0 +1,7 @@
+const Error = () => {
+ return (
+ Error screen for dashboard dashboard routes
+ );
+};
+
+export default Error;
diff --git a/apps/example/pages/dashboard/dashboard/_loading.tsx b/apps/example/pages/dashboard/dashboard/_loading.tsx
new file mode 100644
index 0000000..6efac0c
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/_loading.tsx
@@ -0,0 +1,9 @@
+const Loading = () => {
+ return (
+
+ Loading dashboard dashboard component...
+
+ );
+};
+
+export default Loading;
diff --git a/apps/example/pages/dashboard/dashboard/index.tsx b/apps/example/pages/dashboard/dashboard/index.tsx
new file mode 100644
index 0000000..d44de3c
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/index.tsx
@@ -0,0 +1,6 @@
+const Dashboard = () => {
+ console.log(__dirname);
+ return Dashboard
;
+}
+
+export default Dashboard;
diff --git a/apps/example/pages/dashboard/dashboard/people.tsx b/apps/example/pages/dashboard/dashboard/people.tsx
new file mode 100644
index 0000000..a7b39bf
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/people.tsx
@@ -0,0 +1,3 @@
+export default function People() {
+ return People
;
+}
diff --git a/apps/example/pages/dashboard/dashboard/van.tsx b/apps/example/pages/dashboard/dashboard/van.tsx
new file mode 100644
index 0000000..f396151
--- /dev/null
+++ b/apps/example/pages/dashboard/dashboard/van.tsx
@@ -0,0 +1,3 @@
+export default function Van() {
+ return Van
;
+}
diff --git a/apps/example/pages/dashboard/index.tsx b/apps/example/pages/dashboard/index.tsx
index 6434275..d44de3c 100644
--- a/apps/example/pages/dashboard/index.tsx
+++ b/apps/example/pages/dashboard/index.tsx
@@ -1,4 +1,5 @@
const Dashboard = () => {
+ console.log(__dirname);
return Dashboard
;
}
diff --git a/apps/example/pages/index.css b/apps/example/pages/index.css
index 6119ad9..7acf68a 100644
--- a/apps/example/pages/index.css
+++ b/apps/example/pages/index.css
@@ -13,56 +13,9 @@
-moz-osx-font-smoothing: grayscale;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
}
diff --git a/apps/example/pages/index.tsx b/apps/example/pages/index.tsx
new file mode 100644
index 0000000..a2607a6
--- /dev/null
+++ b/apps/example/pages/index.tsx
@@ -0,0 +1,4 @@
+export default function Index() {
+ __dirname;
+ return Index
;
+}
diff --git a/packages/router/package.json b/packages/router/package.json
index 83adbf4..4a1d3de 100644
--- a/packages/router/package.json
+++ b/packages/router/package.json
@@ -29,6 +29,7 @@
},
"dependencies": {
"@types/node": "^18.11.17",
+ "chalk": "^5.3.0",
"react-router-dom": "^6.19.0",
"vite": "^5.0.0"
}
diff --git a/packages/router/src/@types/index.ts b/packages/router/src/@types/index.ts
index 27c1558..e77f866 100644
--- a/packages/router/src/@types/index.ts
+++ b/packages/router/src/@types/index.ts
@@ -5,5 +5,8 @@ export interface HanaOptions {
export interface RouterOptions {
root: string;
+ _404: string[];
+ errorPages: string[];
+ loadingPages: string[];
routes: any[];
}
diff --git a/packages/router/src/router.tsx b/packages/router/src/router.tsx
index 3ae4b31..3244b10 100644
--- a/packages/router/src/router.tsx
+++ b/packages/router/src/router.tsx
@@ -4,19 +4,103 @@ import { RouterOptions } from './@types';
export function createRouter({
routes: appRoutes,
- root
+ loadingPages,
+ errorPages,
+ _404,
+ root,
}: RouterOptions) {
const routes: any = [];
for (const r of appRoutes) {
- const Component = lazy(() => import(`${root.replace('_app.tsx', r.file)}`));
+ let closestErrorPage: any = null;
+ let closestLoadingPage: any = null;
+
+ let closestErrorMatchLength = 0;
+ let closestLoadingMatchLength = 0;
+
+ errorPages.forEach((errorPage) => {
+ const routeFile = r.file.toLowerCase();
+ const errorPageFile = errorPage
+ .replace(/\_error.(jsx|tsx|js|ts)/, '')
+ .toLowerCase();
+
+ if (routeFile.startsWith(errorPageFile)) {
+ const matchLength = errorPageFile.length;
+
+ if (matchLength > closestErrorMatchLength) {
+ closestErrorPage = errorPage;
+ closestErrorMatchLength = matchLength;
+ }
+ }
+ });
+
+ loadingPages.forEach((loadingPage) => {
+ const routeFile = r.file.toLowerCase();
+ const loadingPageFile = loadingPage
+ .replace(/\_loading.(jsx|tsx|js|ts)/, '')
+ .toLowerCase();
+
+ if (routeFile.startsWith(loadingPageFile)) {
+ const matchLength = loadingPageFile.length;
+
+ if (matchLength > closestLoadingMatchLength) {
+ closestLoadingPage = loadingPage;
+ closestLoadingMatchLength = matchLength;
+ }
+ }
+ });
+
+ let ErrorComponent: any;
+ let LoadingComponent: any;
+
+ const Component = lazy(
+ () => import(/* @vite-ignore */ `${root.replace('_app.tsx', r.file)}`)
+ );
+
+ if (closestErrorPage) {
+ ErrorComponent = lazy(
+ () =>
+ import(
+ /* @vite-ignore */ `${root.replace('_app.tsx', closestErrorPage)}`
+ )
+ );
+ }
+
+ if (closestLoadingPage) {
+ LoadingComponent = lazy(
+ () =>
+ import(
+ /* @vite-ignore */ `${root.replace('_app.tsx', closestLoadingPage)}`
+ )
+ );
+ }
routes.push({
path: r.path,
- element: createElement(Suspense, {
- fallback: 'Loading...',
- children: createElement(Component),
- }),
+ errorElement: closestErrorPage ? createElement(ErrorComponent) : null,
+ element: closestLoadingPage
+ ? createElement(Suspense, {
+ fallback: createElement(LoadingComponent),
+ children: createElement(Component),
+ })
+ : createElement(Component),
+ });
+ }
+
+ if (_404) {
+ routes.push({
+ path: '*',
+ element: createElement(
+ lazy(
+ () =>
+ import(/* @vite-ignore */ `${root.replace('_app.tsx', _404[0])}`)
+ )
+ ),
+ });
+ } else {
+ routes.push({
+ path: '*',
+ element: createElement('div', null, '404'),
});
}
diff --git a/packages/router/src/vite-plugin.ts b/packages/router/src/vite-plugin.ts
index dd06654..8ee022d 100644
--- a/packages/router/src/vite-plugin.ts
+++ b/packages/router/src/vite-plugin.ts
@@ -10,18 +10,33 @@ export default function hana(options: HanaOptions): Plugin {
file.endsWith('.jsx') ||
file.endsWith('.ts') ||
file.endsWith('.tsx')) &&
- !file.startsWith('_') &&
!file.endsWith('.d.ts') &&
!file.endsWith('.test.js') &&
!file.endsWith('.test.jsx') &&
!file.endsWith('.test.ts') &&
!file.endsWith('.test.tsx');
+ const isNotHanaFile = (file: string) =>
+ isJavascriptFile(file) && !file.includes('/_');
+
+ const isErrorPage = (file: string) =>
+ isJavascriptFile(file) && file.includes('/_error.');
+
+ const isLoadingFile = (file: string) =>
+ isJavascriptFile(file) && file.includes('/_loading.');
+
const buildRoutes = () => {
console.log('Building your routes...');
- const compileRoutes: any = (dir = 'pages') => {
- const javascriptFiles = [];
+ const compileRoutes: (dir?: string) => {
+ javascriptFiles: any[];
+ loadingFiles: any[];
+ errorFiles: any[];
+ _404Page: string;
+ } = (dir = 'pages') => {
+ const javascriptFiles: any = [];
+ const errorFiles: any = [];
+ const loadingFiles: any = [];
const files = fs.readdirSync(path.resolve(options.root, dir), {
withFileTypes: true,
@@ -29,17 +44,34 @@ export default function hana(options: HanaOptions): Plugin {
for (const file of files) {
if (file.isDirectory()) {
- javascriptFiles.push(...compileRoutes(`${dir}/${file.name}`));
+ const data = compileRoutes(`${dir}/${file.name}`);
+
+ javascriptFiles.push(...data.javascriptFiles);
+ loadingFiles.push(...data.loadingFiles);
+ errorFiles.push(...data.errorFiles);
} else {
javascriptFiles.push(`${dir}/${file.name}`.replace('pages', ''));
+ loadingFiles.push(`${dir}/${file.name}`.replace('pages', ''));
+ errorFiles.push(`${dir}/${file.name}`.replace('pages', ''));
}
}
- return javascriptFiles.filter(isJavascriptFile);
+ return {
+ javascriptFiles: javascriptFiles.filter(isNotHanaFile),
+ loadingFiles: loadingFiles.filter(isLoadingFile),
+ errorFiles: errorFiles.filter(isErrorPage),
+ _404Page: javascriptFiles.find((file: string) =>
+ file.includes('/_404.')
+ ),
+ };
};
const routes: any = [];
- const appRoutes = compileRoutes();
+ const appFiles = compileRoutes();
+ const errorPages = appFiles.errorFiles;
+ const appRoutes = appFiles.javascriptFiles;
+ const loadingPages = appFiles.loadingFiles;
+ const _404Page = appFiles._404Page;
appRoutes.forEach((route: string) => {
const routePath = route
@@ -65,13 +97,28 @@ export default function hana(options: HanaOptions): Plugin {
});
});
- console.log('Routes built successfully!', routes);
+ console.log('Routes built successfully!');
fs.mkdirSync(path.resolve(options.root, '.hana'), { recursive: true });
fs.writeFileSync(
path.resolve(options.root, '.hana/routes.json'),
JSON.stringify(routes)
);
+
+ fs.writeFileSync(
+ path.resolve(options.root, '.hana/error-pages.json'),
+ JSON.stringify(errorPages)
+ );
+
+ fs.writeFileSync(
+ path.resolve(options.root, '.hana/loading-pages.json'),
+ JSON.stringify(loadingPages)
+ );
+
+ fs.writeFileSync(
+ path.resolve(options.root, '.hana/_404-page.json'),
+ JSON.stringify([_404Page])
+ );
};
return {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0fd20cd..e3570e2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -225,6 +225,9 @@ importers:
'@types/node':
specifier: ^18.11.17
version: 18.18.9
+ chalk:
+ specifier: ^5.3.0
+ version: 5.3.0
react-router-dom:
specifier: ^6.19.0
version: 6.19.0(react-dom@18.2.0)(react@17.0.2)
@@ -2599,6 +2602,11 @@ packages:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ /chalk@5.3.0:
+ resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ dev: false
+
/chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}