rasengan 1.1.2 → 1.2.0-beta.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/esm/cli/index.js +2 -2
  3. package/lib/esm/core/config/vite/defaults.js +1 -17
  4. package/lib/esm/core/dynamic/index.js +32 -0
  5. package/lib/esm/core/plugins/index.js +1 -1
  6. package/lib/esm/entries/client/render.js +45 -16
  7. package/lib/esm/entries/server/entry.server.js +1 -0
  8. package/lib/esm/entries/server/index.js +3 -3
  9. package/lib/esm/routing/components/fallback.js +8 -0
  10. package/lib/esm/routing/components/index.js +97 -6
  11. package/lib/esm/routing/components/template.js +8 -7
  12. package/lib/esm/routing/index.js +3 -3
  13. package/lib/esm/routing/providers/metadata.js +23 -13
  14. package/lib/esm/routing/utils/define-router.js +10 -6
  15. package/lib/esm/routing/utils/flat-routes.js +50 -78
  16. package/lib/esm/routing/utils/generate-routes.js +279 -178
  17. package/lib/esm/server/build/manifest.js +19 -7
  18. package/lib/esm/server/dev/handlers.js +16 -1
  19. package/lib/esm/server/dev/server.js +19 -1
  20. package/lib/esm/server/node/index.js +8 -2
  21. package/lib/tsconfig.esm.tsbuildinfo +1 -1
  22. package/lib/tsconfig.types.tsbuildinfo +1 -1
  23. package/lib/types/core/dynamic/index.d.ts +15 -0
  24. package/lib/types/core/types.d.ts +1 -1
  25. package/lib/types/entries/client/render.d.ts +3 -2
  26. package/lib/types/routing/components/fallback.d.ts +5 -0
  27. package/lib/types/routing/components/index.d.ts +11 -0
  28. package/lib/types/routing/components/template.d.ts +1 -1
  29. package/lib/types/routing/index.d.ts +3 -3
  30. package/lib/types/routing/interfaces.d.ts +5 -4
  31. package/lib/types/routing/providers/metadata.d.ts +1 -3
  32. package/lib/types/routing/types.d.ts +21 -3
  33. package/lib/types/routing/utils/define-routes-group.d.ts +1 -1
  34. package/lib/types/routing/utils/flat-routes.d.ts +21 -3
  35. package/lib/types/routing/utils/generate-routes.d.ts +6 -6
  36. package/lib/types/server/build/manifest.d.ts +4 -4
  37. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 1.2.0-beta.0 (2025-09-18)
4
+
5
+ ## 1.1.5 (2025-08-30)
6
+
7
+ ## 1.1.4 (2025-08-30)
8
+
9
+ ## 1.1.4 (2025-08-30)
10
+
11
+ ## 1.1.3 (2025-08-30)
12
+
3
13
  ## 1.1.2 (2025-08-16)
4
14
 
5
15
  ## 1.1.1 (2025-08-16)
@@ -8,9 +8,9 @@ program
8
8
  // Handle the dev command
9
9
  program
10
10
  .command('dev')
11
- .option('-p <port>')
11
+ .option('-p, --port <port>', 'Port number')
12
12
  .description('Start development server')
13
- .action(async ({ p: port }) => {
13
+ .action(async ({ port }) => {
14
14
  const convertedPort = Number(port);
15
15
  // Checking port
16
16
  if (port &&
@@ -1,15 +1,8 @@
1
1
  // core/config/defaults.ts
2
2
  import { join } from 'node:path';
3
- // Define core external packages
4
- const CORE_EXTERNALS = [
5
- // '@rasenganjs/mdx',
6
- // '@rasenganjs/vercel',
7
- // '@rasenganjs/netlify',
8
- ];
9
3
  export const createDefaultViteConfig = (rootPath, __dirname, mode, config) => {
10
4
  // Combine core externals with user-defined externals
11
5
  const externals = [
12
- ...CORE_EXTERNALS,
13
6
  ...(Array.isArray(config.vite?.build?.external)
14
7
  ? config.vite.build.external
15
8
  : []),
@@ -28,21 +21,12 @@ export const createDefaultViteConfig = (rootPath, __dirname, mode, config) => {
28
21
  return 'vendor';
29
22
  if (id.includes('src/components'))
30
23
  return 'shared-components';
31
- if (config.ssr) {
32
- if (id.includes('src/app') && id.includes('.page.')) {
33
- const parts = id.split('src/app')[1]?.split('/');
34
- if (parts?.length) {
35
- const pageName = parts.pop()?.split('.')[0];
36
- return pageName ? `page-${pageName}` : undefined;
37
- }
38
- }
39
- }
40
24
  return undefined;
41
25
  },
42
26
  },
43
27
  },
44
28
  outDir: 'dist',
45
- chunkSizeWarningLimit: 1000,
29
+ chunkSizeWarningLimit: 2000,
46
30
  },
47
31
  environments: {
48
32
  client: {
@@ -19,3 +19,35 @@ export function dynamicLoad(load, fallback = _jsx(_Fragment, {}) // Default pend
19
19
  throw new Error(error);
20
20
  }
21
21
  }
22
+ /**
23
+ * It allows you to defer loading of page components,
24
+ * and only include them in the client bundle when they're needed
25
+ * @param filePath Path to the page component we want to load lazily
26
+ * @returns
27
+ */
28
+ export function lazyLoadPage(routePath, filePath) {
29
+ // use import.meta.glob with a fixed pattern that covers your pages
30
+ const modules = import.meta.glob('/src/app/**/*.{js,ts,jsx,tsx,md,mdx}');
31
+ // if started with ./ then removed it
32
+ const normalizePath = filePath.startsWith('./')
33
+ ? filePath.slice(2)
34
+ : filePath;
35
+ // Get the full path from the modules object
36
+ const fullPath = Object.keys(modules).find((key) => key.endsWith(normalizePath));
37
+ if (!fullPath) {
38
+ throw new Error(`[rasengan lazyLoad]: No module found for path "${filePath}". Did you include it in the glob?`);
39
+ }
40
+ const loader = modules[fullPath];
41
+ if (!loader) {
42
+ throw new Error(`[rasengan lazyLoad]: No module found for path "${filePath}". Did you include it in the glob?`);
43
+ }
44
+ const page = {
45
+ path: routePath,
46
+ fullPath,
47
+ segment: routePath, // TODO: Handle segments
48
+ module: loader,
49
+ source: filePath,
50
+ isLayout: false,
51
+ };
52
+ return page;
53
+ }
@@ -84,7 +84,7 @@ function flatRoutesPlugin() {
84
84
  '/src/app/_routes/**/layout.{js,ts,jsx,tsx}',
85
85
  '/src/app/_routes/**/*.page.{md,mdx,js,ts,jsx,tsx}',
86
86
  ],
87
- { eager: true }
87
+ // { eager: true }
88
88
  );
89
89
  });
90
90
 
@@ -2,27 +2,56 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { hydrateRoot, createRoot } from 'react-dom/client';
3
3
  import { StrictMode } from 'react';
4
4
  import { RootComponent } from '../../routing/components/template.js';
5
- const isSpaMode = window.__RASENGAN_SPA_MODE__;
6
- export default function renderApp(App, options) {
5
+ import { generateRoutes, preloadMatches, } from '../../routing/utils/generate-routes.js';
6
+ import { createBrowserRouter, RouterProvider } from 'react-router';
7
+ const isSpaMode = Boolean(window.__RASENGAN_SPA_MODE__);
8
+ export default async function renderApp(App, AppRouter, options) {
9
+ // Get root element
10
+ // We need to get the root element to render the app - (SPA mode)
11
+ // or to hydrate the app - (SSR mode)
7
12
  const root = document.getElementById('root');
8
13
  if (!root) {
9
- throw new Error('Root element not found');
14
+ throw new Error('#root element not found in the DOM');
10
15
  }
11
- // If SPA mode, render the app
16
+ // Resolve app router
17
+ const RasenganRouter = await AppRouter;
18
+ // Generate routes for the browser routing
19
+ const routes = generateRoutes(RasenganRouter);
20
+ // Preload lazy routes
21
+ // We have to only preload the routes that are matched for the current URL
22
+ // The remaining routing will be lazy loaded on the client after routes change
23
+ await preloadMatches(window.location, routes);
24
+ // Create router
25
+ let router = createBrowserRouter(routes, {
26
+ hydrationData: window.__staticRouterHydrationData,
27
+ });
28
+ // Generate client router
29
+ const ClientRouter = () => _jsx(RouterProvider, { router: router });
30
+ // Generate app tree
31
+ const appTree = options.reactStrictMode ? (_jsx(StrictMode, { children: _jsx(App, { Component: (props) => (_jsx(RootComponent, { ...props, Router: ClientRouter })) }) })) : (_jsx(App, { Component: (props) => _jsx(RootComponent, { ...props, Router: ClientRouter }) }));
32
+ // Render app
12
33
  if (isSpaMode) {
13
- if (options.reactStrictMode) {
14
- createRoot(root).render(_jsx(StrictMode, { children: _jsx(App, { Component: RootComponent }) }));
15
- }
16
- else {
17
- createRoot(root).render(_jsx(App, { Component: RootComponent }));
18
- }
19
- return;
20
- }
21
- // Handling hydration
22
- if (options.reactStrictMode) {
23
- hydrateRoot(root, _jsx(StrictMode, { children: _jsx(App, { Component: RootComponent }) }));
34
+ // No SSR markup, so start fresh
35
+ createRoot(root, {
36
+ onCaughtError: (error) => {
37
+ console.error(error);
38
+ },
39
+ onRecoverableError(error, errorInfo) {
40
+ console.error(error);
41
+ console.error(errorInfo);
42
+ },
43
+ }).render(appTree);
24
44
  }
25
45
  else {
26
- hydrateRoot(root, _jsx(App, { Component: RootComponent }));
46
+ // SSR markup present, hydrate instead of re-rendering
47
+ hydrateRoot(root, appTree, {
48
+ onCaughtError: (error) => {
49
+ console.error(error);
50
+ },
51
+ onRecoverableError(error, errorInfo) {
52
+ console.error(error);
53
+ console.error(errorInfo);
54
+ },
55
+ });
27
56
  }
28
57
  }
@@ -16,6 +16,7 @@ export const render = async (StaticRouterComponent, res, options) => {
16
16
  const rootPath = process.cwd();
17
17
  let App;
18
18
  let Template;
19
+ // If build options are provided, that means we are in production mode
19
20
  if (buildOptions) {
20
21
  App = (await loadModuleSSR(posix.join(buildOptions.buildDirectory, buildOptions.serverPathDirectory, 'main.js'))).default;
21
22
  Template = (await loadModuleSSR(join(buildOptions.buildDirectory, buildOptions.serverPathDirectory, 'template.js'))).default;
@@ -19,13 +19,13 @@ export const TemplateLayout = ({ StaticRouterComponent, metadata, assets, App, T
19
19
  }
20
20
  if (isSpaMode) {
21
21
  otherScripts = (_jsxs(React.Fragment, { children: [_jsx("script", { type: "module", dangerouslySetInnerHTML: {
22
- __html: `window.__RASENGAN_SPA_MODE__ = true;`,
22
+ __html: `window.__RASENGAN_SPA_MODE__=true;`,
23
23
  } }), !assets && (_jsx("script", { type: "module", src: "/src/index", async: true }))] }));
24
24
  }
25
25
  else {
26
26
  otherScripts = (_jsx(React.Fragment, { children: _jsx("script", { type: "module", dangerouslySetInnerHTML: {
27
- __html: `window.__RASENGAN_SPA_MODE__ = false;`,
27
+ __html: `window.__RASENGAN_SPA_MODE__=false;`,
28
28
  } }) }));
29
29
  }
30
- return (_jsx(Template, { Head: ({ children }) => (_jsxs(HeadComponent, { metadata: metadata, assets: assets, children: [viteScripts, otherScripts, children] })), Body: ({ children }) => (_jsx(BodyComponent, { asChild: App ? true : false, AppContent: App && _jsx(App, { Component: RootComponent, children: StaticRouterComponent }), children: children })), Script: ({ children }) => _jsx(ScriptComponent, { children: children }) }));
30
+ return (_jsx(Template, { Head: ({ children }) => (_jsxs(HeadComponent, { metadata: metadata, assets: assets, children: [viteScripts, otherScripts, children] })), Body: ({ children }) => (_jsx(BodyComponent, { asChild: App ? true : false, AppContent: App && (_jsx(App, { Component: (props) => _jsx(RootComponent, { ...props }), children: StaticRouterComponent })), children: children })), Script: ({ children }) => _jsx(ScriptComponent, { children: children }) }));
31
31
  };
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * This component is used as a fallback when the page is not yet hydrated
4
+ * @returns
5
+ */
6
+ export const HydrationFallback = () => {
7
+ return _jsx("div", { children: "Loading page..." });
8
+ };
@@ -1,12 +1,40 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { isRouteErrorResponse, Link, useParams, useRouteError, } from 'react-router';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isRouteErrorResponse, Link, useLocation, useParams, useRouteError, } from 'react-router';
3
+ import { useEffect, useRef } from 'react';
4
+ // Extract the environment variables
5
+ const extractEnv = () => {
6
+ try {
7
+ const env = import.meta.env;
8
+ // If not env, use process.env on the server only
9
+ if (!env) {
10
+ const serverEnv = process.env;
11
+ return {
12
+ DEV: serverEnv.NODE_ENV === 'development',
13
+ PROD: serverEnv.NODE_ENV === 'production',
14
+ TEST: serverEnv.NODE_ENV === 'test',
15
+ };
16
+ }
17
+ return {
18
+ DEV: env.DEV,
19
+ PROD: env.PROD,
20
+ TEST: env.TEST,
21
+ };
22
+ }
23
+ catch (error) {
24
+ console.error(error);
25
+ return {
26
+ DEV: false,
27
+ PROD: true,
28
+ TEST: false,
29
+ };
30
+ }
31
+ };
3
32
  /**
4
33
  * Error boundary component that will be displayed if an error occurs during a routing
5
34
  * @returns
6
35
  */
7
36
  export function ErrorBoundary() {
8
- const { DEV } = import.meta.env;
9
- console.log(import.meta.env);
37
+ const { DEV } = extractEnv();
10
38
  let error = useRouteError();
11
39
  if (!DEV)
12
40
  return (_jsx("section", { style: {
@@ -28,10 +56,31 @@ export function ErrorBoundary() {
28
56
  fontSize: '18px',
29
57
  }, children: "Application Error" }) }));
30
58
  if (isRouteErrorResponse(error)) {
31
- return (_jsxs(_Fragment, { children: [_jsx("p", { children: "Application Error" }), _jsxs("h1", { children: [error.status, " ", error.statusText] }), _jsx("p", { children: error.data })] }));
59
+ return (_jsxs("section", { style: {
60
+ position: 'fixed',
61
+ top: 0,
62
+ left: 0,
63
+ right: 0,
64
+ bottom: 0,
65
+ zIndex: 100,
66
+ display: 'flex',
67
+ flexDirection: 'row',
68
+ justifyContent: 'center',
69
+ alignItems: 'center',
70
+ height: '100vh',
71
+ width: '100vw',
72
+ gap: 10,
73
+ backgroundColor: '#fff',
74
+ }, children: [_jsx("p", { style: {
75
+ fontSize: '18px',
76
+ }, children: "Application Error" }), _jsxs("h1", { style: {
77
+ fontSize: '18px',
78
+ }, children: [error.status, " ", error.statusText] }), _jsx("p", { style: {
79
+ fontSize: '18px',
80
+ }, children: error.data })] }));
32
81
  }
33
82
  else if (error instanceof Error) {
34
- return (_jsxs("div", { style: {
83
+ return (_jsxs("section", { style: {
35
84
  position: 'fixed',
36
85
  top: 0,
37
86
  left: 0,
@@ -129,3 +178,45 @@ export const CustomLink = (props) => {
129
178
  }
130
179
  return (_jsx(Link, { to: to, ...rest, children: children }));
131
180
  };
181
+ // Store scroll positions globally (per location.key)
182
+ const scrollPositions = {};
183
+ /**
184
+ * Scroll restoration component
185
+ * @param {Props} props
186
+ * @returns
187
+ */
188
+ export function ScrollRestoration({ alwaysToTop = false, target }) {
189
+ const location = useLocation();
190
+ const pathnameRef = useRef(location.pathname);
191
+ useEffect(() => {
192
+ if (typeof window === 'undefined')
193
+ return;
194
+ const prevPathname = pathnameRef.current;
195
+ const el = target?.current; // easier to reference
196
+ if (alwaysToTop) {
197
+ if (el) {
198
+ el.scrollTo(0, 0);
199
+ }
200
+ else {
201
+ window.scrollTo(0, 0);
202
+ }
203
+ pathnameRef.current = location.pathname;
204
+ return;
205
+ }
206
+ // Save scroll position of the previous page
207
+ if (prevPathname) {
208
+ scrollPositions[prevPathname] = el ? el.scrollTop : window.scrollY;
209
+ }
210
+ // Restore scroll position of the new page (default to 0 if not stored)
211
+ const storedY = scrollPositions[location.pathname] ?? 0;
212
+ if (el) {
213
+ el.scrollTo(0, storedY);
214
+ }
215
+ else {
216
+ window.scrollTo(0, storedY);
217
+ }
218
+ // Update ref
219
+ pathnameRef.current = location.pathname;
220
+ }, [location.pathname, target?.current]); // depend on target.current
221
+ return null;
222
+ }
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { use, useMemo } from 'react';
3
- import { generateMetadata, getRouter } from '../utils/index.js';
2
+ import React, { useMemo } from 'react';
3
+ import { generateMetadata } from '../utils/index.js';
4
4
  import { Outlet } from 'react-router';
5
5
  /**
6
6
  * App component that represent the entry point of the application
7
7
  */
8
- export const RootComponent = ({ router: AppRouterPromise, children = undefined, }) => {
8
+ export const RootComponent = ({ Router, // Client Router
9
+ children = undefined, // Static Router for SSR when provided
10
+ }) => {
9
11
  // Return children if they exist
10
12
  if (children)
11
- return children;
12
- const AppRouter = use(AppRouterPromise);
13
- // Otherwise, get the router and return it
14
- let Router = getRouter(AppRouter);
13
+ return children; // For the SSR
14
+ if (!Router)
15
+ return null;
15
16
  return _jsx(Router, {});
16
17
  };
17
18
  /**
@@ -1,5 +1,5 @@
1
- import { CustomLink } from './components/index.js';
1
+ import { CustomLink, ScrollRestoration } from './components/index.js';
2
2
  export { defineRouter, defineRoutesGroup, flatRoutes } from './utils/index.js';
3
3
  export { RouterComponent } from './interfaces.js';
4
- export { Outlet, ScrollRestoration, useLocation, useNavigate, useNavigation, useParams, useSearchParams, useFetcher, useMatch, useRoutes, useResolvedPath, matchRoutes, generatePath, matchPath, createRoutesFromChildren, Navigate, NavLink, } from 'react-router';
5
- export { CustomLink as Link };
4
+ export { Outlet, useLocation, useNavigate, useNavigation, useParams, useSearchParams, useFetcher, useMatch, useRoutes, useResolvedPath, matchRoutes, generatePath, matchPath, createRoutesFromChildren, Navigate, NavLink, } from 'react-router';
5
+ export { CustomLink as Link, ScrollRestoration };
@@ -1,22 +1,30 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { useLocation } from 'react-router';
2
+ import { useLocation, useMatches } from 'react-router';
3
3
  import { useEffect } from 'react';
4
4
  import { generateMetadata } from '../utils/generate-metadata.js';
5
5
  import ReactDOMServer from 'react-dom/server';
6
- export default function MetadataProvider({ children, metadataMapping, }) {
7
- const { pathname } = useLocation();
6
+ export default function MetadataProvider({ children, }) {
7
+ const location = useLocation();
8
+ const routes = useMatches();
8
9
  useEffect(() => {
9
- const metadata = metadataMapping[pathname];
10
- if (!metadata)
11
- return;
12
- handleInjectMetadata(metadata);
13
- }, [pathname]);
14
- const handleInjectMetadata = (metadata) => {
15
- if (!metadata)
10
+ if (typeof window === 'undefined')
16
11
  return;
12
+ (async () => {
13
+ const loadersData = routes.map((route) => route.loaderData);
14
+ await handleApplyMetadata(loadersData);
15
+ })();
16
+ }, [location]);
17
+ const handleApplyMetadata = async (loadersData) => {
18
+ // We generate the metadata
19
+ const metadatas = generateMetadata(loadersData.map((item) => item.meta));
20
+ // We get the last metadata
21
+ // This is the metadata of the page
22
+ const leafMetadata = loadersData.at(-1)?.meta;
23
+ handleInjectMetadata(metadatas, leafMetadata);
24
+ };
25
+ const handleInjectMetadata = (metaTags, leafMetadata) => {
17
26
  // Check if we are on the browser
18
27
  if (typeof window !== 'undefined') {
19
- const metaTags = generateMetadata([metadata]);
20
28
  // Find all meta tags with data-rg attribute and remove them
21
29
  const metaTagsToRemove = document.querySelectorAll('meta[data-rg="true"]');
22
30
  metaTagsToRemove.forEach((metaTag) => {
@@ -28,12 +36,14 @@ export default function MetadataProvider({ children, metadataMapping, }) {
28
36
  const metaTagString = ReactDOMServer.renderToStaticMarkup(metaTag);
29
37
  document.head.insertAdjacentHTML('beforeend', metaTagString);
30
38
  });
39
+ if (!leafMetadata)
40
+ return;
31
41
  // Change the title of the page
32
- document.title = metadata.title;
42
+ document.title = leafMetadata.title;
33
43
  // Change the description of the page
34
44
  const metaDescription = document.createElement('meta');
35
45
  metaDescription.setAttribute('name', 'description');
36
- metaDescription.setAttribute('content', metadata.description);
46
+ metaDescription.setAttribute('content', leafMetadata.description);
37
47
  metaDescription.setAttribute('data-rg', 'true');
38
48
  document.head.appendChild(metaDescription);
39
49
  }
@@ -8,11 +8,6 @@ import { DefaultLayout } from '../components/template.js';
8
8
  export const defineRouter = (option) => {
9
9
  const { imports, layout, pages, loaderComponent, notFoundComponent, useParentLayout, } = option;
10
10
  return async (Router) => {
11
- // Handle errors
12
- // if (!option.pages)
13
- // throw new Error(
14
- // 'You must provide a list of pages in the router option object'
15
- // );
16
11
  // Create router
17
12
  const router = new Router();
18
13
  // List of pages component
@@ -21,6 +16,10 @@ export const defineRouter = (option) => {
21
16
  // Check if p is an array
22
17
  if (Array.isArray(p)) {
23
18
  for (let page of p) {
19
+ if ('source' in page) {
20
+ pageComponentList.push(page);
21
+ continue;
22
+ }
24
23
  if (isMDXPage(page)) {
25
24
  const Page = await convertMDXPageToPageComponent(page);
26
25
  pageComponentList.push(Page);
@@ -31,6 +30,10 @@ export const defineRouter = (option) => {
31
30
  }
32
31
  continue;
33
32
  }
33
+ if ('source' in p) {
34
+ pageComponentList.push(p);
35
+ continue;
36
+ }
34
37
  // When p is a MDXPageComponent
35
38
  if (isMDXPage(p)) {
36
39
  const Page = await convertMDXPageToPageComponent(p);
@@ -75,6 +78,7 @@ const loadMDXRenderer = async () => {
75
78
  return MDXRenderer;
76
79
  }
77
80
  catch (e) {
78
- throw new Error('Failed to load MDXRenderer component from @rasenganjs/mdx, make sure you have installed the package');
81
+ console.error(e);
82
+ throw e;
79
83
  }
80
84
  };