diff --git a/.gitignore b/.gitignore index 35136c7..5d4d6ff 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ dist-ssr .eslintcache +# React Router generated files +.react-router/ +build/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/.lighthouserc.js b/.lighthouserc.js index 92cb789..b64680f 100644 --- a/.lighthouserc.js +++ b/.lighthouserc.js @@ -1,7 +1,7 @@ const ci = { collect: { - startServerCommand: 'npm run preview:prod', - url: 'http://localhost:5173/', + startServerCommand: 'npm run start', + url: 'http://localhost:3000/', }, assert: { // assert options here diff --git a/README.md b/README.md index 4b07958..2c88435 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,22 @@ It is intended to be helping, not dictating. If you don't know exactly what to u → Good, you're already set up! -Create a production build with `npm run build`, but be aware that a server runtime is needed as this project uses server-side rendering. +## Production + +To test the production build locally: + +1. Build for production: + + ``` + npm run build + ``` + +2. Start the production server: + ``` + npm start + ``` + +The production build will be created in the `build/` directory. Note that a server runtime is needed as this project uses server-side rendering. ## How does this work @@ -51,7 +66,7 @@ Where documenting inline is not possible or impractical, we have added explanati - **CSS and SCSS:** Based on Carbon, this project makes extensive use of SCSS. To improve the loading experience and avoid layout shifts and flash of unstyled content, we decided to load the SCSS from the `index.html` file. If you have a case where you load a lot of CSS under very dynamic conditions, you might decide to use an `import` statement inside one of your React components. However, be aware that this should stay as an exception, as the styles will be delayed after the component has been loaded. -- **Server-side rendering:** Server-side rendering is a critical component for most applications to improve their performance. This means here that **the project is separated in two parts - client-side and server-side**. +- **Server-side rendering:** This project uses React Router v7 in Framework mode, which provides built-in server-side rendering (SSR) with automatic code splitting and optimized hydration. The framework handles both client and server rendering seamlessly. - **Quality and productivity helpers:** This project contains quite a few helpers to help with consistency, productivity, and speed. For example, it has templates for unit and end-to-end testing. It also contains linters so your team doesn't have to lose time on code formatting. With time, we plan to add more helpers to help you monitor your accessibility and front-end performance. @@ -62,11 +77,11 @@ This project comes with a pre-configured testing setup using React Testing Libra #### Setup -- The test configuration is located in `vite.config.js`, which sets up Vitest with globals, a JSDOM environment, and points to the setup file `src/test/setup.js`. +- The test configuration is located in `vite.config.js`, which sets up Vitest with globals, a JSDOM environment, and points to the setup file `src/test/setup.js`. The React Router plugin is conditionally disabled during tests to prevent conflicts. -- This file handles mocking network requests and manages server startup and teardown. It also handles mocking browser features missing in JSDOM. +- The setup file handles mocking network requests and manages MSW server startup and teardown. It also handles mocking browser features missing in JSDOM. -- The `src/test/server.js` wraps the server with msw's setupServer function and injects networking utils from `src/test/networking.js` that track each outgoing network request and help debugging unit tests. +- The `src/test/server.js` configures MSW (Mock Service Worker) handlers for API endpoints, allowing you to mock external API calls in your tests. #### Writing Tests diff --git a/app/entry.client.jsx b/app/entry.client.jsx new file mode 100644 index 0000000..cbf5bbc --- /dev/null +++ b/app/entry.client.jsx @@ -0,0 +1,14 @@ +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); + +// Made with Bob diff --git a/app/entry.client.jsx.backup b/app/entry.client.jsx.backup new file mode 100644 index 0000000..2885de0 --- /dev/null +++ b/app/entry.client.jsx.backup @@ -0,0 +1,29 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router'; + +/** + * Client entry point for React Router Framework mode. + * + * This file handles client-side hydration of the server-rendered HTML. + * It's called by the browser to make the static HTML interactive. + * + * @see https://reactrouter.com/start/framework/entry-client + */ +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); + +// Made with Bob diff --git a/app/navigation.config.js b/app/navigation.config.js new file mode 100644 index 0000000..076ed94 --- /dev/null +++ b/app/navigation.config.js @@ -0,0 +1,121 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { MagicWand, LogoGithub } from '@carbon/icons-react'; + +/** + * Navigation configuration for Carbon UI Shell components. + * This config is separate from routing to allow independent management + * of navigation structure and route definitions. + */ + +/** + * Header navigation items + * These appear in the top navigation bar + */ +export const headerNavigation = [ + { + path: '/dashboard', + label: 'Dashboard', + }, + { + path: '/link-1', + label: 'Link 1', + }, + { + path: '/link-2', + label: 'Link 2', + }, + { + path: '/link-3', + label: 'Link 3', + }, + { + path: '/link-4', + label: 'Link 4', + subMenu: [ + { + path: '/link-4/sub-link-1', + label: 'Sub-link 1', + }, + { + path: '/link-4/sub-link-2', + label: 'Sub-link 2', + }, + { + path: '/link-4/sub-link-3', + label: 'Sub-link 3', + }, + ], + }, +]; + +/** + * Side navigation items + * These appear in the side panel navigation + */ +export const sideNavigation = [ + { + path: '/getting-started', + label: 'Getting Started', + icon: MagicWand, + href: 'https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#get-started', + subMenu: [ + { + path: '/getting-started/how', + label: 'How does this work', + href: 'https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#how-does-this-work', + }, + { + path: '/getting-started/up-to-date', + label: 'Keeping this up to date', + href: 'https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#keeping-this-up-to-date', + }, + { + path: '/getting-started/report', + label: 'Report problems', + href: 'https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#report-problems', + }, + ], + }, + { + path: '/github', + label: 'GitHub', + icon: LogoGithub, + href: 'https://github.com/carbon-design-system/carbon-react-router-starter', + }, +]; + +/** + * Helper function to get all navigation items (header + side) + */ +export const getAllNavigationItems = () => { + return [...headerNavigation, ...sideNavigation]; +}; + +/** + * Helper function to find a navigation item by path + */ +export const findNavigationItem = (path) => { + const allItems = getAllNavigationItems(); + + for (const item of allItems) { + if (item.path === path) { + return item; + } + if (item.subMenu) { + const subItem = item.subMenu.find((sub) => sub.path === path); + if (subItem) { + return subItem; + } + } + } + + return null; +}; + +// Made with Bob diff --git a/app/root.jsx b/app/root.jsx new file mode 100644 index 0000000..5d169be --- /dev/null +++ b/app/root.jsx @@ -0,0 +1,145 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + isRouteErrorResponse, +} from 'react-router'; +import { GlobalTheme, Theme } from '@carbon/react'; +import { ThemeProvider, useThemeContext } from '../src/context/ThemeContext'; + +// Import global styles +import '../src/index.scss'; + +/** + * Root layout component that wraps the entire application. + * Provides document structure, theme management, and error boundaries. + * + * @see https://reactrouter.com/start/framework/routing#root-layout + */ +export function Layout({ children }) { + return ( + + + + + + + + + + {children} + + + + + ); +} + +/** + * Theme wrapper component that applies Carbon Design System themes. + * Uses the ThemeContext to determine which theme to apply. + * Waits for theme to be ready before rendering to prevent flash. + */ +function ThemedApp() { + const { themeMenu, ready } = useThemeContext(); + + // Don't render until theme is ready to prevent flash + if (!ready) { + return null; + } + + return ( + + + + + + ); +} + +/** + * Root component that provides theme context to the entire application. + * This is the entry point for all routes. + * + * @see https://reactrouter.com/start/framework/routing#root-route + */ +export default function Root() { + return ( + + + + ); +} + +/** + * Error boundary for the root route. + * Handles both route errors and unexpected errors. + * + * @see https://reactrouter.com/start/framework/route-module#errorboundary + */ +export function ErrorBoundary({ error }) { + let errorMessage = 'An unexpected error occurred'; + let errorDetails = null; + + if (isRouteErrorResponse(error)) { + // Handle route-specific errors (404, etc.) + errorMessage = `${error.status} ${error.statusText}`; + errorDetails = error.data; + } else if (error instanceof Error) { + // Handle JavaScript errors + errorMessage = error.message; + errorDetails = error.stack; + } + + return ( + + + + + Error - Carbon React Router + + + + +
+

Application Error

+

+ {errorMessage} +

+ {errorDetails && ( +
+ + Error Details + +
+                {typeof errorDetails === 'string'
+                  ? errorDetails
+                  : JSON.stringify(errorDetails, null, 2)}
+              
+
+ )} +
+ + + + ); +} + +// Made with Bob diff --git a/app/routes.js b/app/routes.js new file mode 100644 index 0000000..a7d303d --- /dev/null +++ b/app/routes.js @@ -0,0 +1,61 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { index, route } from '@react-router/dev/routes'; + +/** + * Route configuration for React Router Framework mode. + * + * This configuration defines all application routes using the file-based routing convention. + * Each route maps to a module in the app/routes directory. + * + * Route Naming Conventions: + * - `home.jsx` - Index route (/) + * - `dashboard.jsx` - Static route (/dashboard) + * - `dashboard.$id.jsx` - Dynamic segment (/dashboard/:id) + * - `link-4.jsx` - Layout route for nested routes + * - `link-4.sub-link-1.jsx` - Nested route (/link-4/sub-link-1) + * - `$.jsx` - Catch-all/splat route (*) + * + * @see https://reactrouter.com/start/framework/routing + * @see https://reactrouter.com/start/framework/route-module + */ +export default [ + // Index route - Welcome/Home page + index('routes/home.jsx'), + + // Dashboard routes + route('dashboard', 'routes/dashboard.jsx'), + route('dashboard/:id', 'routes/dashboard.$id.jsx'), + + // Simple link routes (Link 1, 2, 3) + route('link-1', 'routes/link-1.jsx'), + route('link-2', 'routes/link-2.jsx'), + route('link-3', 'routes/link-3.jsx'), + + // API Resource Routes + // These routes return JSON data instead of rendering components + route('api/post/:id', 'routes/api.post.$id.jsx'), + route('api/comments', 'routes/api.comments.jsx'), + route('api/external/post/:id', 'routes/api.external.post.$id.jsx'), + route('api/external/comments', 'routes/api.external.comments.jsx'), + + // Link 4 with nested routes + // The parent route acts as a layout for its children + route('link-4', 'routes/link-4.jsx', [ + index('routes/link-4._index.jsx'), + route('sub-link-1', 'routes/link-4.sub-link-1.jsx'), + route('sub-link-2', 'routes/link-4.sub-link-2.jsx'), + route('sub-link-3', 'routes/link-4.sub-link-3.jsx'), + ]), + + // Catch-all route for 404 pages + // Must be last in the array to act as fallback + route('*', 'routes/$.jsx'), +]; + +// Made with Bob diff --git a/app/routes/$.jsx b/app/routes/$.jsx new file mode 100644 index 0000000..b90fd44 --- /dev/null +++ b/app/routes/$.jsx @@ -0,0 +1,35 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import NotFound from '../../src/pages/not-found/NotFound'; + +/** + * Catch-all/Splat route (*) + * + * This route handles all unmatched URLs and displays a 404 Not Found page. + * The $ in the filename represents the splat parameter in React Router. + * + * @returns {JSX.Element} The not found component + * @see https://reactrouter.com/start/framework/routing#splats + */ +export default function NotFoundRoute() { + return ; +} + +/** + * Metadata for the 404 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: '404 Not Found - Carbon React Router Starter' }, + { name: 'description', content: 'Page not found' }, + ]; +} + +// Made with Bob diff --git a/app/routes/api.comments.jsx b/app/routes/api.comments.jsx new file mode 100644 index 0000000..8261629 --- /dev/null +++ b/app/routes/api.comments.jsx @@ -0,0 +1,51 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { baseUrl } from '../../src/config/server-config.js'; + +/** + * API route for fetching comments for a post + * This is a resource route that returns JSON instead of rendering a component + * + * @see https://reactrouter.com/how-to/resource-routes + */ +export async function loader({ request }) { + const url = new URL(request.url); + const postId = url.searchParams.get('postId'); + + if (!postId) { + return Response.json( + { message: 'Missing postId parameter' }, + { status: 400 }, + ); + } + + try { + // Call the external API endpoint + const response = await fetch( + `${baseUrl}/api/external/comments?postId=${postId}`, + ); + + if (!response.ok) { + const error = await response.json(); + return Response.json(error, { status: response.status }); + } + + const comments = await response.json(); + + // Return the comments as JSON + return Response.json(comments); + } catch (error) { + console.error('Error fetching comments:', error); + return Response.json( + { message: 'Failed to fetch comments' }, + { status: 500 }, + ); + } +} + +// Made with Bob diff --git a/app/routes/api.external.comments.jsx b/app/routes/api.external.comments.jsx new file mode 100644 index 0000000..91e42a4 --- /dev/null +++ b/app/routes/api.external.comments.jsx @@ -0,0 +1,73 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Mock external API route for fetching comments for a post + * This simulates an external service response + * + * @see https://reactrouter.com/how-to/resource-routes + */ + +// Mock database for comments +const mockComments = { + 1: [ + { + id: 1, + postId: 1, + name: 'Great introduction!', + email: 'user1@example.com', + body: 'This is a really helpful overview of Carbon.', + }, + { + id: 2, + postId: 1, + name: 'Thanks for sharing', + email: 'user2@example.com', + body: 'Looking forward to learning more about Carbon.', + }, + ], + 2: [ + { + id: 3, + postId: 2, + name: 'Router questions', + email: 'user3@example.com', + body: 'How does this compare to other routing libraries?', + }, + ], + 3: [ + { + id: 4, + postId: 3, + name: 'Excellent article', + email: 'user4@example.com', + body: 'Very comprehensive guide to modern web development.', + }, + ], +}; + +export async function loader({ request }) { + const url = new URL(request.url); + const postId = url.searchParams.get('postId'); + + // Validate that postId is provided and is a positive integer + if (!postId || !/^\d+$/.test(postId)) { + return Response.json( + { message: 'Invalid or missing postId' }, + { status: 400 }, + ); + } + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 100)); + + const comments = mockComments[postId] || []; + + return Response.json(comments); +} + +// Made with Bob diff --git a/app/routes/api.external.post.$id.jsx b/app/routes/api.external.post.$id.jsx new file mode 100644 index 0000000..7394e96 --- /dev/null +++ b/app/routes/api.external.post.$id.jsx @@ -0,0 +1,60 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Mock external API route for fetching a single post + * This simulates an external service response + * + * @see https://reactrouter.com/how-to/resource-routes + */ + +// Mock database for posts +const mockPosts = { + 1: { + id: 1, + title: 'What is Carbon?', + body: ` + This is just a sample post. Carbon is IBM's open source design system for products and digital experiences. + With the IBM Design Language as its foundation, the system consists of working code, design tools and resources, + human interface guidelines, and a vibrant community of contributors.`, + userId: 1, + }, + 2: { + id: 2, + title: 'Getting started with React router', + body: 'This is just a sample post. React Router is a standard library for routing in React applications.', + userId: 1, + }, + 3: { + id: 3, + title: 'Building modern web applications', + body: 'This is just a sample post. Modern web development requires understanding of various tools and frameworks.', + userId: 2, + }, +}; + +export async function loader({ params }) { + const { id } = params; + + // Validate that id is a positive integer + if (!/^\d+$/.test(id)) { + return Response.json({ message: 'Invalid post id' }, { status: 400 }); + } + + // Simulated network delay + await new Promise((resolve) => setTimeout(resolve, 100)); + + const post = mockPosts[id]; + + if (!post) { + return Response.json({ message: 'Post not found' }, { status: 404 }); + } + + return Response.json(post); +} + +// Made with Bob diff --git a/app/routes/api.post.$id.jsx b/app/routes/api.post.$id.jsx new file mode 100644 index 0000000..fac604d --- /dev/null +++ b/app/routes/api.post.$id.jsx @@ -0,0 +1,43 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { baseUrl } from '../../src/config/server-config.js'; + +/** + * API route for fetching a single post + * This is a resource route that returns JSON instead of rendering a component + * + * @see https://reactrouter.com/how-to/resource-routes + */ +export async function loader({ params }) { + const { id } = params; + + // Validate that id is a positive integer + if (!/^\d+$/.test(id)) { + return Response.json({ message: 'Invalid post id' }, { status: 400 }); + } + + try { + // Call the external API endpoint + const response = await fetch(`${baseUrl}/api/external/post/${id}`); + + if (!response.ok) { + const error = await response.json(); + return Response.json(error, { status: response.status }); + } + + const blogpost = await response.json(); + + // Return the blogpost as JSON + return Response.json(blogpost); + } catch (error) { + console.error('Error fetching post:', error); + return Response.json({ message: 'Failed to fetch post' }, { status: 500 }); + } +} + +// Made with Bob diff --git a/app/routes/dashboard.$id.jsx b/app/routes/dashboard.$id.jsx new file mode 100644 index 0000000..a2af926 --- /dev/null +++ b/app/routes/dashboard.$id.jsx @@ -0,0 +1,43 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Dashboard from '../../src/pages/dashboard/Dashboard'; + +/** + * Dashboard detail route (/dashboard/:id) + * + * Displays the dashboard page with a specific ID parameter. + * The Dashboard component uses useParams() to access the :id parameter. + * + * Example URLs: + * - /dashboard/1234 + * - /dashboard/abc-xyz + * - /dashboard/1234?q=search&name=John + * + * @returns {JSX.Element} The dashboard component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function DashboardDetailRoute() { + return ; +} + +/** + * Metadata for the dashboard detail route + * Uses the route params to customize the page title + * + * @param {Object} params - Route parameters + * @param {string} params.params.id - The dashboard ID from the URL + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta({ params }) { + return [ + { title: `Dashboard ${params.id} - Carbon React Router Starter` }, + { name: 'description', content: `Dashboard detail page for ${params.id}` }, + ]; +} + +// Made with Bob diff --git a/app/routes/dashboard.jsx b/app/routes/dashboard.jsx new file mode 100644 index 0000000..aa71dd9 --- /dev/null +++ b/app/routes/dashboard.jsx @@ -0,0 +1,35 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Dashboard from '../../src/pages/dashboard/Dashboard'; + +/** + * Dashboard route (/dashboard) + * + * Displays the main dashboard page with tiles and visualizations. + * Demonstrates URL parameter handling (both path and query params). + * + * @returns {JSX.Element} The dashboard component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function DashboardRoute() { + return ; +} + +/** + * Metadata for the dashboard route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Dashboard - Carbon React Router Starter' }, + { name: 'description', content: 'Dashboard page with data visualizations' }, + ]; +} + +// Made with Bob diff --git a/app/routes/home.jsx b/app/routes/home.jsx new file mode 100644 index 0000000..8e85f7e --- /dev/null +++ b/app/routes/home.jsx @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Welcome from '../../src/pages/welcome/Welcome'; + +/** + * Home route (index route at /) + * + * Displays the welcome page with getting started information, + * features overview, and data fetching example. + * + * @returns {JSX.Element} The welcome page component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function Home() { + return ; +} + +/** + * Metadata for the home route + * Sets the page title and description for SEO + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Carbon React Router Starter' }, + { + name: 'description', + content: + 'A starter template for building React applications with Carbon Design System and React Router', + }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-1.jsx b/app/routes/link-1.jsx new file mode 100644 index 0000000..ce5a61a --- /dev/null +++ b/app/routes/link-1.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 1 route (/link-1) + * + * A placeholder page demonstrating navigation structure. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function Link1Route() { + return ; +} + +/** + * Metadata for Link 1 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Link 1 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 1 placeholder page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-2.jsx b/app/routes/link-2.jsx new file mode 100644 index 0000000..910f27b --- /dev/null +++ b/app/routes/link-2.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 2 route (/link-2) + * + * A placeholder page demonstrating navigation structure. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function Link2Route() { + return ; +} + +/** + * Metadata for Link 2 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Link 2 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 2 placeholder page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-3.jsx b/app/routes/link-3.jsx new file mode 100644 index 0000000..8c84487 --- /dev/null +++ b/app/routes/link-3.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 3 route (/link-3) + * + * A placeholder page demonstrating navigation structure. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/route-module + */ +export default function Link3Route() { + return ; +} + +/** + * Metadata for Link 3 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Link 3 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 3 placeholder page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-4._index.jsx b/app/routes/link-4._index.jsx new file mode 100644 index 0000000..8d69171 --- /dev/null +++ b/app/routes/link-4._index.jsx @@ -0,0 +1,35 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 4 index route (/link-4) + * + * This is the index route for the /link-4 path. + * It renders when the user navigates to /link-4 without any sub-path. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/routing#index-routes + */ +export default function Link4IndexRoute() { + return ; +} + +/** + * Metadata for Link 4 index route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Link 4 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 4 main page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-4.jsx b/app/routes/link-4.jsx new file mode 100644 index 0000000..daa3457 --- /dev/null +++ b/app/routes/link-4.jsx @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Outlet } from 'react-router'; + +/** + * Link 4 layout route (/link-4) + * + * This is a layout route that renders an Outlet for nested child routes. + * It doesn't render its own content, but provides a container for: + * - /link-4 (index) → link-4._index.jsx + * - /link-4/sub-link-1 → link-4.sub-link-1.jsx + * - /link-4/sub-link-2 → link-4.sub-link-2.jsx + * - /link-4/sub-link-3 → link-4.sub-link-3.jsx + * + * @returns {JSX.Element} Outlet for nested routes + * @see https://reactrouter.com/start/framework/routing#layout-routes + */ +export default function Link4Layout() { + return ; +} + +/** + * Metadata for Link 4 layout route + * This will be inherited by child routes unless they override it + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Link 4 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 4 section with nested routes' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-4.sub-link-1.jsx b/app/routes/link-4.sub-link-1.jsx new file mode 100644 index 0000000..b3b8867 --- /dev/null +++ b/app/routes/link-4.sub-link-1.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 4 Sub-link 1 route (/link-4/sub-link-1) + * + * A nested route under Link 4, demonstrating submenu navigation. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/routing#nested-routes + */ +export default function Link4SubLink1Route() { + return ; +} + +/** + * Metadata for Link 4 Sub-link 1 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Sub-link 1 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 4 Sub-link 1 page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-4.sub-link-2.jsx b/app/routes/link-4.sub-link-2.jsx new file mode 100644 index 0000000..75abe2d --- /dev/null +++ b/app/routes/link-4.sub-link-2.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 4 Sub-link 2 route (/link-4/sub-link-2) + * + * A nested route under Link 4, demonstrating submenu navigation. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/routing#nested-routes + */ +export default function Link4SubLink2Route() { + return ; +} + +/** + * Metadata for Link 4 Sub-link 2 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Sub-link 2 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 4 Sub-link 2 page' }, + ]; +} + +// Made with Bob diff --git a/app/routes/link-4.sub-link-3.jsx b/app/routes/link-4.sub-link-3.jsx new file mode 100644 index 0000000..8a65837 --- /dev/null +++ b/app/routes/link-4.sub-link-3.jsx @@ -0,0 +1,34 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Placeholder from '../../src/pages/placeholder/Placeholder'; + +/** + * Link 4 Sub-link 3 route (/link-4/sub-link-3) + * + * A nested route under Link 4, demonstrating submenu navigation. + * + * @returns {JSX.Element} The placeholder component + * @see https://reactrouter.com/start/framework/routing#nested-routes + */ +export default function Link4SubLink3Route() { + return ; +} + +/** + * Metadata for Link 4 Sub-link 3 route + * + * @see https://reactrouter.com/start/framework/route-module#meta + */ +export function meta() { + return [ + { title: 'Sub-link 3 - Carbon React Router Starter' }, + { name: 'description', content: 'Link 4 Sub-link 3 page' }, + ]; +} + +// Made with Bob diff --git a/eslint.config.js b/eslint.config.js index 519de3d..4e4796d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,10 +25,10 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' import eslintConfigPrettier from 'eslint-config-prettier/flat'; export default [ - { ignores: ['dist', 'vite.config.js'] }, + { ignores: ['dist'] }, { files: ['**/*.{js,mjs,cjs,jsx}'], - ignores: ['dist', 'vite.config.js'], + ignores: ['dist'], languageOptions: { /* added (globals.node) - for server side elements */ globals: { ...globals.browser, ...globals.node, ...globals.jest }, @@ -71,14 +71,38 @@ export default [ 'warn', { allowConstantExport: true }, ], - /* no-irregular-whitespace - - ignore to allow prettier of the text layout. April 10th 2025. + /* no-irregular-whitespace + - ignore to allow prettier of the text layout. April 10th 2025. NOTE: Should be removable after https://github.com/Mikadv/carbon-react-starter/issues/32 */ 'no-irregular-whitespace': ['error', { skipJSXText: true }], // Allow optional chaining 'import/namespace': 'off', + /* + * Disable import/no-unresolved for React Router packages. + * React Router v7 uses package exports that ESLint's import resolver doesn't understand. + * These imports work correctly at runtime and in the build. + */ + 'import/no-unresolved': [ + 'error', + { + ignore: ['^react-router', '^@react-router'], + }, + ], + }, + }, + /* + * Disable react-refresh/only-export-components for React Router route files. + * React Router Framework mode requires route files to export both components (default export) + * and route functions (named exports like meta, loader, action, etc.). + * This is the intended pattern and doesn't affect functionality - it only means HMR will + * do a full page refresh instead of fast refresh when editing route files. + */ + { + files: ['app/routes/**/*.{js,jsx}'], + rules: { + 'react-refresh/only-export-components': 'off', }, }, ]; diff --git a/package-lock.json b/package-lock.json index 05928cc..8822e42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "carbon-react-starter", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "carbon-react-starter", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@carbon-labs/react-theme-settings": "^0.16.0", "@carbon/ibm-products": "^2.74.0", @@ -15,17 +15,17 @@ "@carbon/styles": "^1.72.0", "@ibm/plex": "^6.4.1", "@lhci/cli": "^0.15.0", + "@react-router/node": "^7.9.6", "classnames": "^2.5.1", - "compression": "^1.7.5", - "express": "^5.0.1", + "isbot": "^5", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.5.2", - "sirv": "^3.0.0" + "react-router": "^7.5.2" }, "devDependencies": { "@double-great/stylelint-a11y": "^3.0.4", "@eslint/js": "^9.21.0", + "@react-router/dev": "^7.9.6", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", @@ -221,14 +221,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -237,6 +237,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", @@ -274,6 +287,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -284,6 +329,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -316,6 +375,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -376,6 +490,95 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", @@ -401,18 +604,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -486,7 +689,6 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2782,6 +2984,102 @@ "@swc/helpers": "^0.5.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -3241,6 +3539,12 @@ "tree-kill": "^1.2.1" } }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "license": "MIT" + }, "node_modules/@mswjs/interceptors": { "version": "0.40.0", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", @@ -3297,28 +3601,169 @@ "node": ">= 8" } }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "node_modules/@npmcli/package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true, "license": "MIT" }, @@ -3628,6 +4073,17 @@ "third-party-web": "latest" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", @@ -3641,12 +4097,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "license": "MIT" - }, "node_modules/@puppeteer/browsers": { "version": "2.10.6", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", @@ -3776,6 +4226,119 @@ "node": ">=12" } }, + "node_modules/@react-router/dev": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.6.tgz", + "integrity": "sha512-pBkbczGwI+NcZPcK8JPvWGWdjUpT/+okXYp6IXvt7zI3WLxr5hQLLRox5FkLiVxkykbqARO1hk9NRp9KFwJ2sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.7", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@npmcli/package-json": "^4.0.1", + "@react-router/node": "7.9.6", + "@remix-run/node-fetch-server": "^0.9.0", + "arg": "^5.0.1", + "babel-dead-code-elimination": "^1.0.6", + "chokidar": "^4.0.0", + "dedent": "^1.5.3", + "es-module-lexer": "^1.3.1", + "exit-hook": "2.2.1", + "isbot": "^5.1.11", + "jsesc": "3.0.2", + "lodash": "^4.17.21", + "p-map": "^7.0.3", + "pathe": "^1.1.2", + "picocolors": "^1.1.1", + "prettier": "^3.6.2", + "react-refresh": "^0.14.0", + "semver": "^7.3.7", + "tinyglobby": "^0.2.14", + "valibot": "^1.1.0", + "vite-node": "^3.2.2" + }, + "bin": { + "react-router": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/serve": "^7.9.6", + "@vitejs/plugin-rsc": "*", + "react-router": "^7.9.6", + "typescript": "^5.1.0", + "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", + "wrangler": "^3.28.2 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/dev/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-router/dev/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-router/node": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.6.tgz", + "integrity": "sha512-XzU8gPHwSl2Qh8/bOV30npbpH2fWOO3sFg+SwhX3+IddD1a/0C2KQzRiW/qAngkvZTJVdbca5Qp+FJjCCE7sNw==", + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.9.6", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@remix-run/node-fetch-server": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.9.0.tgz", + "integrity": "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -4738,28 +5301,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4851,6 +5392,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5162,6 +5710,19 @@ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "license": "Apache-2.0" }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5269,26 +5830,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5371,6 +5912,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.2.0.tgz", @@ -5951,18 +6502,6 @@ "node": ">=8" } }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -5979,24 +6518,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -6494,6 +7015,21 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6689,6 +7225,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6791,6 +7334,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7572,6 +8122,19 @@ "dev": true, "license": "MIT" }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -7582,48 +8145,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -7873,23 +8394,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7950,22 +8454,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/fs.realpath": { @@ -8460,6 +8972,19 @@ "dev": true, "license": "MIT" }, + "node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -8573,6 +9098,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -9172,12 +9698,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9348,6 +9868,15 @@ "dev": true, "license": "MIT" }, + "node_modules/isbot": { + "version": "5.1.32", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.32.tgz", + "integrity": "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9437,6 +9966,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -10181,6 +10726,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -10262,15 +10816,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -10290,18 +10835,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -10375,18 +10908,6 @@ "node": ">= 0.6" } }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -10431,6 +10952,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -10449,15 +10980,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10755,6 +11277,22 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10765,6 +11303,61 @@ "node": ">=0.10.0" } }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", + "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11016,6 +11609,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11057,6 +11663,13 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", @@ -11151,6 +11764,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -11416,6 +12053,16 @@ "dev": true, "license": "MIT" }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -11425,6 +12072,27 @@ "node": ">=0.4.0" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -11474,15 +12142,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -11545,21 +12204,6 @@ "node": ">=20" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11590,21 +12234,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -11639,6 +12268,16 @@ "license": "MIT", "peer": true }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "7.9.6", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", @@ -11867,6 +12506,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/rettime": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", @@ -11957,31 +12606,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -12540,43 +13164,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -12768,20 +13355,6 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12892,6 +13465,42 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/speedline-core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", @@ -12923,6 +13532,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -12992,6 +13602,52 @@ "node": ">=4" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -13147,6 +13803,20 @@ "node": ">=6" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -13914,15 +14584,6 @@ "node": ">=0.6" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -14006,20 +14667,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -14235,6 +14882,42 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", @@ -14326,6 +15009,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitest": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.13.tgz", @@ -14650,6 +15356,70 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", diff --git a/package.json b/package.json index b699d5b..3dfa213 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { "name": "carbon-react-starter", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { - "dev": "node src/server", - "build": "npm run build:client && npm run build:server", - "build:client": "vite build --ssrManifest .vite/ssr-manifest.json --outDir dist/client", - "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server", + "dev": "react-router dev", + "build": "react-router build", + "start": "NODE_ENV=production node server.js", "lighthouse": "lhci collect", "lint": "npm run lint:es && npm run lint:style && npm run lint:format && npm run lint:spell", "lint-fix": "npm run lint:es -- --fix && npm run lint:style -- --fix && npm run lint:format", @@ -15,8 +14,6 @@ "lint:style": "stylelint \"./**/*.scss\" --cache --cache-location \"node_modules/.cache/stylelint/\"", "lint:format": "prettier . --write --ignore-unknown --no-error-on-unmatched-pattern --log-level warn --cache --cache-location \"node_modules/.cache/prettier/\"", "lint:spell": "cspell lint --quiet \"**\"", - "preview:prod": "cross-env NODE_ENV=production node src/server.js", - "preview": "cross-env NODE_ENV=local node src/server.js", "test": "vitest run --coverage", "test:watch": "vitest --watch", "prepare": "husky" @@ -29,17 +26,17 @@ "@carbon/styles": "^1.72.0", "@ibm/plex": "^6.4.1", "@lhci/cli": "^0.15.0", + "@react-router/node": "^7.9.6", "classnames": "^2.5.1", - "compression": "^1.7.5", - "express": "^5.0.1", + "isbot": "^5", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.5.2", - "sirv": "^3.0.0" + "react-router": "^7.5.2" }, "devDependencies": { "@double-great/stylelint-a11y": "^3.0.4", "@eslint/js": "^9.21.0", + "@react-router/dev": "^7.9.6", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", @@ -73,7 +70,7 @@ "vitest": "^4.0.0" }, "lint-staged": { - "*.{js,ts,jsx,tsx}": "eslint . --cache --report-unused-disable-directives --max-warnings 0", + "*.{js,ts,jsx,tsx}": "eslint --cache --report-unused-disable-directives --max-warnings 0", ".npmrc": "node -e \"console.error('Are you sure you want to commit .npmrc?'); process.exit(1);\"" }, "overrides": { diff --git a/react-router.config.js b/react-router.config.js new file mode 100644 index 0000000..bb7d220 --- /dev/null +++ b/react-router.config.js @@ -0,0 +1,19 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default { + // Enable server-side rendering + ssr: true, + + // Configure the app directory + appDirectory: 'app', + + // Explicitly specify the server build file + serverBuildFile: 'index.js', +}; + +// Made with Bob diff --git a/server.js b/server.js new file mode 100644 index 0000000..f298a4d --- /dev/null +++ b/server.js @@ -0,0 +1,74 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { createRequestListener } from '@react-router/node'; +import { createServer } from 'node:http'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const build = await import('./build/server/index.js'); + +const server = createServer(async (req, res) => { + // Try to serve static files from build/client + // Only for requests that look like static assets (have file extensions) + const hasFileExtension = /\.[a-zA-Z0-9]+(\?.*)?$/.test(req.url); + + if (hasFileExtension && !req.url.startsWith('/api/')) { + try { + // Remove query string for file path + const urlPath = req.url.split('?')[0]; + + // Sanitize path to prevent directory traversal attacks + const safePath = urlPath.replace(/^\/+/, '').replace(/\.\./g, ''); + const filePath = join(process.cwd(), 'build/client', safePath); + + // Verify the resolved path is still within build/client + const buildDir = join(process.cwd(), 'build/client'); + if (!filePath.startsWith(buildDir)) { + res.statusCode = 403; + res.end('Forbidden'); + return; + } + + const content = readFileSync(filePath); + const ext = urlPath.split('.').pop(); + const contentTypes = { + js: 'application/javascript', + css: 'text/css', + svg: 'image/svg+xml', + woff2: 'font/woff2', + woff: 'font/woff', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + }; + res.setHeader( + 'Content-Type', + contentTypes[ext] || 'application/octet-stream', + ); + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + res.end(content); + return; + } catch { + // File not found - return 404 for static assets + res.statusCode = 404; + res.end('Not Found'); + return; + } + } + + // Handle all other requests (routes) with React Router + const listener = createRequestListener({ build }); + listener(req, res); +}); + +const port = process.env.PORT || 3000; +server.listen(port, () => { + console.log(`Server listening on http://localhost:${port}`); +}); + +// Made with Bob diff --git a/src/__tests__/ProfilePanel.test.jsx b/src/__tests__/ProfilePanel.test.jsx index 3251157..403df8d 100644 --- a/src/__tests__/ProfilePanel.test.jsx +++ b/src/__tests__/ProfilePanel.test.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import { test, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; diff --git a/src/__tests__/ThemeContext.test.jsx b/src/__tests__/ThemeContext.test.jsx index 9e1734a..4f26a20 100644 --- a/src/__tests__/ThemeContext.test.jsx +++ b/src/__tests__/ThemeContext.test.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import { describe, test, expect, vi, beforeEach } from 'vitest'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -46,7 +47,7 @@ describe('ThemeContext', () => { describe('ThemeProvider', () => { test('initializes state from localStorage', () => { // Mock localStorage values - localStorageUtils.getLocalStorageValues.mockReturnValueOnce({ + localStorageUtils.getLocalStorageValues.mockReturnValue({ themeSetting: 'dark', headerInverse: true, }); diff --git a/src/__tests__/Welcome.test.jsx b/src/__tests__/Welcome.test.jsx index 3809fec..7159878 100644 --- a/src/__tests__/Welcome.test.jsx +++ b/src/__tests__/Welcome.test.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import { test, expect } from 'vitest'; import { screen } from '@testing-library/react'; import '@testing-library/jest-dom'; diff --git a/src/__tests__/routes.config.test.js b/src/__tests__/routes.config.test.js deleted file mode 100644 index c4b1182..0000000 --- a/src/__tests__/routes.config.test.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { test, expect, describe } from 'vitest'; -import { routes, routesInHeader, routesInSideNav } from '../routes/config'; -import Dashboard from '../pages/dashboard/Dashboard'; -import NotFound from '../pages/not-found/NotFound'; -import Placeholder from '../pages/placeholder/Placeholder'; -import Welcome from '../pages/welcome/Welcome'; - -describe('routes configuration', () => { - test('routes array contains expected structure', () => { - // Check that routes array exists and is an array - expect(Array.isArray(routes)).toBe(true); - expect(routes.length).toBeGreaterThan(0); - - // Check that the index route is defined correctly - const indexRoute = routes.find((route) => route.index === true); - expect(indexRoute).toBeDefined(); - expect(indexRoute.path).toBe('/'); - expect(indexRoute.element).toBe(Welcome); - - // Check that the NotFound route is defined correctly - const notFoundRoute = routes.find((route) => route.path === '*'); - expect(notFoundRoute).toBeDefined(); - expect(notFoundRoute.element).toBe(NotFound); - expect(notFoundRoute.status).toBe(404); - - // Check that a regular route is defined correctly - const dashboardRoute = routes.find((route) => route.path === '/dashboard'); - expect(dashboardRoute).toBeDefined(); - expect(dashboardRoute.element).toBe(Dashboard); - expect(dashboardRoute.carbon).toBeDefined(); - expect(dashboardRoute.carbon.label).toBe('Dashboard'); - expect(dashboardRoute.carbon.inHeader).toBe(true); - }); - - test('routesInHeader contains only routes with inHeader flag', () => { - expect(Array.isArray(routesInHeader)).toBe(true); - - // All routes in routesInHeader should have carbon.inHeader === true - routesInHeader.forEach((route) => { - expect(route.carbon).toBeDefined(); - expect(route.carbon.inHeader).toBe(true); - expect(route.carbon.inSubMenu).toBeFalsy(); - }); - - // Check that all routes with inHeader flag are included - const headerRoutesCount = routes.filter( - (route) => - route.carbon && route.carbon.inHeader && !route.carbon.inSubMenu, - ).length; - expect(routesInHeader.length).toBe(headerRoutesCount); - }); - - test('routesInSideNav contains only routes with inSideNav flag', () => { - expect(Array.isArray(routesInSideNav)).toBe(true); - - // All routes in routesInSideNav should have carbon.inSideNav === true - routesInSideNav.forEach((route) => { - expect(route.carbon).toBeDefined(); - expect(route.carbon.inSideNav).toBe(true); - expect(route.carbon.inSubMenu).toBeFalsy(); - }); - - // Check that all routes with inSideNav flag are included - const sideNavRoutesCount = routes.filter( - (route) => - route.carbon && route.carbon.inSideNav && !route.carbon.inSubMenu, - ).length; - expect(routesInSideNav.length).toBe(sideNavRoutesCount); - }); - - test('routes with subMenu have their children marked as inSubMenu', () => { - const routesWithSubMenu = routes.filter( - (route) => - route.carbon && route.carbon.subMenu && route.carbon.subMenu.length > 0, - ); - - routesWithSubMenu.forEach((route) => { - route.carbon.subMenu.forEach((subRoute) => { - expect(subRoute.carbon).toBeDefined(); - expect(subRoute.carbon.inSubMenu).toBe(true); - }); - }); - }); - - test('routes support all required properties for React Router', () => { - routes.forEach((route) => { - // Every route should have either a path or a carbon.virtualPath - expect( - route.path || (route.carbon && route.carbon.virtualPath), - ).toBeDefined(); - - // Routes with path should have an element or be a parent route - if (route.path && !route.carbon?.subMenu) { - expect(route.element).toBeDefined(); - } - - // Check that index routes are properly configured - if (route.index) { - expect(route.path).toBeDefined(); - } - - // Check that status is a number if defined - if (route.status !== undefined) { - expect(typeof route.status).toBe('number'); - } - }); - }); -}); diff --git a/src/__tests__/routes.utils.test.js b/src/__tests__/routes.utils.test.js deleted file mode 100644 index d3c41e4..0000000 --- a/src/__tests__/routes.utils.test.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { test, expect, describe, vi } from 'vitest'; -import { findMatchingRoute, getStatusCodeForPath } from '../routes/utils'; -import * as configModule from '../routes/config'; - -// Mock the routes from config.js -vi.mock('../routes/config', () => { - return { - routes: [ - { - path: '/', - index: true, - element: vi.fn(), - }, - { - path: '/dashboard', - element: vi.fn(), - carbon: { - label: 'Dashboard', - inHeader: true, - }, - }, - { - path: '/nested/*', - element: vi.fn(), - }, - { - path: '*', - element: vi.fn(), - status: 404, - }, - ], - }; -}); - -describe('routes utils', () => { - describe('findMatchingRoute', () => { - test('finds exact match for root path', () => { - const route = findMatchingRoute('/'); - expect(route).toBeDefined(); - expect(route.path).toBe('/'); - expect(route.index).toBe(true); - }); - - test('finds exact match for dashboard path', () => { - const route = findMatchingRoute('/dashboard'); - expect(route).toBeDefined(); - expect(route.path).toBe('/dashboard'); - expect(route.carbon.label).toBe('Dashboard'); - }); - - test('finds nested wildcard match', () => { - const route = findMatchingRoute('/nested/something'); - expect(route).toBeDefined(); - expect(route.path).toBe('/nested/*'); - }); - - test('returns wildcard route for non-existent path', () => { - const route = findMatchingRoute('/non-existent'); - expect(route).toBeDefined(); - expect(route.path).toBe('*'); - expect(route.status).toBe(404); - }); - - test('handles paths with or without leading slash', () => { - const routeWithSlash = findMatchingRoute('/dashboard'); - const routeWithoutSlash = findMatchingRoute('dashboard'); - - expect(routeWithSlash).toEqual(routeWithoutSlash); - expect(routeWithSlash.path).toBe('/dashboard'); - }); - }); - - describe('getStatusCodeForPath', () => { - test('returns 200 for existing routes', () => { - expect(getStatusCodeForPath('/')).toBe(200); - expect(getStatusCodeForPath('/dashboard')).toBe(200); - }); - - test('returns 404 for non-existent routes', () => { - expect(getStatusCodeForPath('/non-existent')).toBe(404); - }); - - test('handles undefined status gracefully', () => { - // Temporarily modify the wildcard route to remove status - const originalRoutes = [...configModule.routes]; - const modifiedRoutes = [...originalRoutes]; - const wildcardIndex = modifiedRoutes.findIndex( - (route) => route.path === '*', - ); - - if (wildcardIndex !== -1) { - const wildcardRoute = { ...modifiedRoutes[wildcardIndex] }; - delete wildcardRoute.status; - modifiedRoutes[wildcardIndex] = wildcardRoute; - - // Replace routes with our modified version - vi.spyOn(configModule, 'routes', 'get').mockReturnValue(modifiedRoutes); - - // Should return default 200 when status is undefined - expect(getStatusCodeForPath('/non-existent')).toBe(200); - - // Restore original routes - vi.spyOn(configModule, 'routes', 'get').mockReturnValue(originalRoutes); - } - }); - }); -}); diff --git a/src/components/commonHeader/CommonHeader.jsx b/src/components/commonHeader/CommonHeader.jsx index aac4b8f..e9815d8 100644 --- a/src/components/commonHeader/CommonHeader.jsx +++ b/src/components/commonHeader/CommonHeader.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import { AspectRatio, Column, Grid, Heading } from '@carbon/react'; export const CommonHeader = ({ title, paragraphs }) => { diff --git a/src/components/footer/Footer.jsx b/src/components/footer/Footer.jsx index ce61522..05bcab7 100644 --- a/src/components/footer/Footer.jsx +++ b/src/components/footer/Footer.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import { AspectRatio, Column } from '@carbon/react'; export const Footer = () => { diff --git a/src/components/nav/Nav.jsx b/src/components/nav/Nav.jsx index 719bb36..d7f798b 100644 --- a/src/components/nav/Nav.jsx +++ b/src/components/nav/Nav.jsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { useState } from 'react'; +import React, { useState } from 'react'; import { Header, HeaderGlobalAction, @@ -30,7 +30,10 @@ import { import { Link as RouterLink, useLocation } from 'react-router'; import ProfilePanel from '../profilePanel/ProfilePanel'; -import { routesInHeader, routesInSideNav } from '../../routes/config'; +import { + headerNavigation, + sideNavigation, +} from '../../../app/navigation.config'; import { NavHeaderItems } from './NavHeaderItems'; import { NavSideItems } from './NavSideItems'; @@ -46,6 +49,7 @@ export const Nav = () => { }; const handleProfileOpen = () => { + console.log('Profile button clicked, current state:', isProfileOpen); setIsProfileOpen((prev) => !prev); }; @@ -63,10 +67,10 @@ export const Nav = () => { React starter template - {routesInHeader.length > 0 && ( + {headerNavigation.length > 0 && ( @@ -97,17 +101,17 @@ export const Nav = () => { isPersistent={false} > - {routesInHeader.length > 0 && ( + {headerNavigation.length > 0 && ( )} diff --git a/src/components/nav/NavHeaderItems.jsx b/src/components/nav/NavHeaderItems.jsx index 8138d1b..8097177 100644 --- a/src/components/nav/NavHeaderItems.jsx +++ b/src/components/nav/NavHeaderItems.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { HeaderMenu, HeaderMenuItem } from '@carbon/react'; import { Link as RouterLink } from 'react-router'; @@ -13,35 +14,35 @@ const isPathActive = (menuPath, currentPath) => { return currentPath.startsWith(`${menuPath}/`); }; -export const NavHeaderItems = ({ routesInHeader, currentPath }) => ( +export const NavHeaderItems = ({ navigationItems, currentPath }) => ( <> - {routesInHeader.map(({ path, carbon }) => - !carbon.inSubMenu && carbon?.label ? ( - carbon.subMenu ? ( + {navigationItems.map((item) => + item.label ? ( + item.subMenu ? ( - {carbon.subMenu.map((subRoute) => ( + {item.subMenu.map((subItem) => ( - {subRoute.carbon.label} + {subItem.label} ))} ) : ( - {carbon?.label} + {item.label} ) ) : null, diff --git a/src/components/nav/NavSideItems.jsx b/src/components/nav/NavSideItems.jsx index 431d18d..5062985 100644 --- a/src/components/nav/NavSideItems.jsx +++ b/src/components/nav/NavSideItems.jsx @@ -1,46 +1,44 @@ +import React from 'react'; import { SideNavLink, SideNavMenu, SideNavMenuItem } from '@carbon/react'; import { Link as RouterLink } from 'react-router'; -const destinationProps = (path, carbon, currentPath) => - path +const destinationProps = (item, currentPath) => + item.path && !item.href ? { as: RouterLink, - isActive: path === currentPath, + to: item.path, + isActive: item.path === currentPath, } : { - href: carbon.href, + href: item.href, }; -export const NavSideItems = ({ routesInSideNav, currentPath }) => ( +export const NavSideItems = ({ navigationItems, currentPath }) => ( <> - {routesInSideNav.map(({ path, carbon }) => - !carbon.inSubMenu && carbon?.label ? ( - carbon.subMenu ? ( + {navigationItems.map((item) => + item.label ? ( + item.subMenu ? ( - {carbon.subMenu.map((subRoute) => ( + {item.subMenu.map((subItem) => ( - {subRoute.carbon.label} + {subItem.label} ))} ) : ( - {carbon.label} + {item.label} ) ) : null, diff --git a/src/components/profilePanel/ProfilePanel.jsx b/src/components/profilePanel/ProfilePanel.jsx index c43a103..bcf5c7c 100644 --- a/src/components/profilePanel/ProfilePanel.jsx +++ b/src/components/profilePanel/ProfilePanel.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; diff --git a/src/config/server-config.js b/src/config/server-config.js index 02c09c8..73447ec 100644 --- a/src/config/server-config.js +++ b/src/config/server-config.js @@ -7,6 +7,8 @@ // Server configuration constants // Extracted to avoid importing the full server during tests -export const port = process.env.PORT || 5173; +// Use 5173 for dev (Vite default), 3000 for production +export const port = + process.env.PORT || (process.env.NODE_ENV === 'production' ? 3000 : 5173); export const base = process.env.BASE || '/'; export const baseUrl = `http://localhost:${port}`; diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx index 8f3c806..bc30a2d 100644 --- a/src/context/ThemeContext.jsx +++ b/src/context/ThemeContext.jsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { +import React, { createContext, useContext, useState, @@ -32,16 +32,12 @@ const ThemeContext = createContext({ export const ThemeProvider = ({ children }) => { const prefersDark = usePrefersDarkScheme(); - const [ready, setReady] = useState(false); - // Initialize state from local storage - const storedValues = getLocalStorageValues(); - const [themeSetting, setThemeSettingState] = useState( - storedValues.themeSetting || 'system', - ); - const [themeMenuCompliment, setThemeMenuComplimentState] = useState( - storedValues.headerInverse || false, - ); + // Initialize with defaults for SSR + const [themeSetting, setThemeSettingState] = useState('system'); + const [themeMenuCompliment, setThemeMenuComplimentState] = useState(false); + const [ready, setReady] = useState(false); + const [initialized, setInitialized] = useState(false); // Wrapper functions to update both state and local storage const setThemeSetting = useCallback((value) => { @@ -82,8 +78,49 @@ export const ThemeProvider = ({ children }) => { const theme = calculateTheme(); const themeMenu = calculateMenuTheme(theme); + // Load stored values from localStorage on mount (client-side only) + useEffect(() => { + // Only run on client side + if (typeof window === 'undefined') { + // eslint-disable-next-line react-hooks/set-state-in-effect -- Setting initialized on SSR is intentional + setInitialized(true); + return; + } + + const storedValues = getLocalStorageValues(); + + // Update state with stored values + if ( + storedValues.themeSetting && + storedValues.themeSetting !== themeSetting + ) { + setThemeSettingState(storedValues.themeSetting); + } + + if ( + storedValues.headerInverse !== undefined && + storedValues.headerInverse !== themeMenuCompliment + ) { + setThemeMenuComplimentState(storedValues.headerInverse); + } + + // Mark as initialized after loading stored values + + setInitialized(true); + }, []); // eslint-disable-line react-hooks/exhaustive-deps -- Only run once on mount + + // Set ready after theme values have been updated + useEffect(() => { + if (initialized) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- Setting ready after theme state updates is intentional to prevent flash + setReady(true); + } + }, [initialized, themeSetting, themeMenuCompliment]); + // Update the DOM when theme changes useEffect(() => { + if (!ready) return; // Don't update DOM until ready + const root = document.documentElement; // Remove any existing theme data attribute @@ -93,10 +130,7 @@ export const ThemeProvider = ({ children }) => { if (themeSetting !== 'system') { root.setAttribute('cs--theme', theme); } - - // eslint-disable-next-line react-hooks/set-state-in-effect -- Setting ready state after DOM synchronization is intentional - setReady(true); - }, [theme, themeSetting]); + }, [theme, themeSetting, ready]); const value = { themeSetting, diff --git a/src/entry-client.jsx b/src/entry-client.jsx deleted file mode 100644 index e986609..0000000 --- a/src/entry-client.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -// Third-party imports -import { StrictMode } from 'react'; -import { hydrateRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router'; -import { Router } from './routes'; - -// App level imports -import { ThemeProvider } from './context/ThemeContext'; - -hydrateRoot( - document.getElementById('root'), - - - - - - - , -); diff --git a/src/entry-server.jsx b/src/entry-server.jsx deleted file mode 100644 index 7c0e951..0000000 --- a/src/entry-server.jsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -// Third-party imports -import { StrictMode } from 'react'; -import { renderToPipeableStream } from 'react-dom/server'; -import { StaticRouter } from 'react-router'; - -// App level imports -import { Router } from './routes/index.jsx'; -import { getStatusCodeForPath } from './routes/utils.js'; - -/** - * @param {string} url - * @param {import('react-dom/server').RenderToPipeableStreamOptions} [options] - */ -export function render(_url, options) { - const url = `/${_url}`; - const statusCode = getStatusCodeForPath(url); - - const { pipe, abort } = renderToPipeableStream( - - - - - , - options, - ); - - const head = ''; - - return { pipe, head, abort, statusCode }; -} diff --git a/src/layouts/page-layout.jsx b/src/layouts/page-layout.jsx index aab2cfd..f0fd828 100644 --- a/src/layouts/page-layout.jsx +++ b/src/layouts/page-layout.jsx @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import React, { Children, Suspense } from 'react'; import { Content, Theme } from '@carbon/react'; -import { Children, Suspense } from 'react'; import { Nav } from '../components/nav/Nav'; import classNames from 'classnames'; import { useThemeContext } from '../context/ThemeContext'; diff --git a/src/layouts/theme-layout.jsx b/src/layouts/theme-layout.jsx deleted file mode 100644 index 7e00061..0000000 --- a/src/layouts/theme-layout.jsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { GlobalTheme, Theme } from '@carbon/react'; -import { Outlet } from 'react-router'; -import { useThemeContext } from '../context/ThemeContext'; - -export const ThemeLayout = () => { - const { themeMenu, ready } = useThemeContext(); - - return ( - ready && ( - - - - - - ) - ); -}; diff --git a/src/pages/welcome/Welcome.jsx b/src/pages/welcome/Welcome.jsx index 7b6d34e..418dbfa 100644 --- a/src/pages/welcome/Welcome.jsx +++ b/src/pages/welcome/Welcome.jsx @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import React, { Suspense } from 'react'; import { CodeSnippet, Column, @@ -16,7 +17,6 @@ import { Section, Stack, } from '@carbon/react'; -import { Suspense } from 'react'; import { Footer } from '../../components/footer/Footer'; import { WelcomeHeader } from './WelcomeHeader.jsx'; diff --git a/src/pages/welcome/WelcomeHeader.jsx b/src/pages/welcome/WelcomeHeader.jsx index 7bf70d8..e75588e 100644 --- a/src/pages/welcome/WelcomeHeader.jsx +++ b/src/pages/welcome/WelcomeHeader.jsx @@ -8,6 +8,7 @@ import { Button } from '@carbon/react'; import { ArrowRight } from '@carbon/icons-react'; +import React from 'react'; import { CommonHeader } from '../../components/commonHeader/CommonHeader'; export const WelcomeHeader = () => { diff --git a/src/pages/welcome/post/PostComponent.jsx b/src/pages/welcome/post/PostComponent.jsx index a15d034..32c116d 100644 --- a/src/pages/welcome/post/PostComponent.jsx +++ b/src/pages/welcome/post/PostComponent.jsx @@ -1,6 +1,6 @@ +import React, { useEffect, useState } from 'react'; import { getComments, getPost } from '../../../api/message.js'; import { Heading, Section, Tile, Stack, Layer } from '@carbon/react'; -import { useEffect, useState } from 'react'; /** * Renders a single blog post and its comments. diff --git a/src/routes/config.js b/src/routes/config.js deleted file mode 100644 index 86b529e..0000000 --- a/src/routes/config.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ -import { MagicWand, LogoGithub } from '@carbon/icons-react'; -import Dashboard from '../pages/dashboard/Dashboard'; -import NotFound from '../pages/not-found/NotFound'; -import Placeholder from '../pages/placeholder/Placeholder'; -import Welcome from '../pages/welcome/Welcome'; - -// type carbonRouteType = { -// virtualPath: string; // related to path, used for arranging Carbon menu when no path exists -// label: string; -// inHeader?: boolean; -// inSideNav?: boolean; -// separator?: boolean; -// icon?: CarbonIconType; -// subMenu?: routesType[]; -// inSubMenu?: boolean; -// href?: string, -// }; - -// type routesType = { -// path: string; -// index?: boolean; -// element?: ({ usingOutlet }: { usingOutlet?: boolean }) => JSX.Element; -// status?: number; -// carbon?: carbonRouteType; -// }; - -export const routes = [ - { - index: true, - path: '/', - element: Welcome, - }, - { - path: '/dashboard', - element: Dashboard, - carbon: { - label: 'Dashboard', - inHeader: true, - }, - }, - { - path: '/dashboard/:id', - element: Dashboard, - }, - { - path: '/link-1', - element: Placeholder, - carbon: { - label: 'Link 1', - inHeader: true, - }, - }, - { - path: '/link-2', - element: Placeholder, - carbon: { - label: 'Link 2', - inHeader: true, - }, - }, - { - path: '/link-3', - element: Placeholder, - carbon: { - label: 'Link 3', - inHeader: true, - }, - }, - { - path: '/link-4', - carbon: { - label: 'Link 4', - inHeader: true, - }, - }, - { - path: '/link-4/sub-link-1', - element: Placeholder, - carbon: { - label: 'Sub-link 1', - }, - }, - { - path: '/link-4/sub-link-2', - element: Placeholder, - carbon: { - label: 'Sub-link 2', - }, - }, - { - path: '/link-4/sub-link-3', - element: Placeholder, - carbon: { - label: 'Sub-link 3', - }, - }, - { - carbon: { - virtualPath: '/getting-started', - inSideNav: true, - label: 'Getting Started', - icon: MagicWand, - href: `https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#get-started`, - }, - }, - { - carbon: { - virtualPath: '/getting-started/how', - label: 'How does this work', - href: `https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#how-does-this-work`, - }, - }, - { - carbon: { - virtualPath: '/getting-started/up-to-date', - label: 'Keeping this up to date', - href: `https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#keeping-this-up-to-date`, - }, - }, - { - carbon: { - virtualPath: '/getting-started/report', - label: 'Report problems', - href: `https://github.com/carbon-design-system/carbon-react-router-starter?tab=readme-ov-file#report-problems`, - }, - }, - { - carbon: { - virtualPath: '/github', - inSideNav: true, - label: 'GitHub', - icon: LogoGithub, - href: `https://github.com/carbon-design-system/carbon-react-router-starter`, - }, - }, - - { - path: '*', - element: NotFound, - status: 404, - }, -]; - -// The routes config is a flat structure defined for use with react-router. -// Here we organize the routes into a hierarchy for use by the Carbon header and sidenav -// NOTE: The routes are processed outside of a component as they are not dynamic. -const routesProcessed = routes.map((route) => { - if (!route.carbon) { - return route; - } - - const path = route.path || route.carbon.virtualPath; - - const subMenu = routes.filter((subRoute) => { - // Only include routes with carbon config in navigation menus - if (!subRoute.carbon) return false; - - const subPath = subRoute.path || subRoute.carbon.virtualPath; - const childPath = new RegExp(`^${path}/[^/]+$`); // match direct parent only - - return !route.index && subPath && childPath.test(subPath); - }); - - if (subMenu && subMenu.length > 0) { - // add sub menu to parent - route.carbon.subMenu = subMenu; - - // mark child as in sub menu - subMenu.forEach((menu) => { - const subPath = menu.path || menu.carbon.virtualPath; - // Carbon should never be blank - menu.carbon = menu.carbon || { label: subPath }; - menu.carbon.inSubMenu = true; - }); - } - - return route; -}); - -export const routesInHeader = routesProcessed.filter( - (route) => route.carbon && route.carbon.inHeader && !route.carbon.inSubMenu, -); - -export const routesInSideNav = routesProcessed.filter( - (route) => route.carbon && route.carbon.inSideNav && !route.carbon.inSubMenu, -); diff --git a/src/routes/index.jsx b/src/routes/index.jsx deleted file mode 100644 index 1eb1434..0000000 --- a/src/routes/index.jsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { Route, Routes } from 'react-router'; - -import { routes } from './config.js'; -import { ThemeLayout } from '../layouts/theme-layout.jsx'; - -export const Router = () => { - return ( - - }> - {routes.map(({ element: Element, ...rest }) => ( - } /> - ))} - - - ); -}; diff --git a/src/routes/routes.js b/src/routes/routes.js deleted file mode 100644 index 597166f..0000000 --- a/src/routes/routes.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ -import { getPost, getComments } from '../service/postHandlers.js'; -import { - getExternalPost, - getExternalComments, -} from '../service/externalHandlers.js'; - -/** - * Registers all API routes on the given Express app instance. - * - * This function is reusable in both production and unit tests, - * and allows swapping out route handlers for mocks if needed. - * @param app - Express app instance OR msw router in case of unit testing - * @param handlers - Route handlers (can be mocked for testing) - * @param externalHandlers - External API mock handlers (can be mocked for testing) - */ -export const getRoutes = ( - app, - handlers = { getPost, getComments }, - externalHandlers = { getExternalPost, getExternalComments }, -) => { - // Client-facing API routes (these call the external routes below) - app.get('/api/post/:id', handlers.getPost); - app.get('/api/comments', handlers.getComments); - - // Mock "external" API routes (simulate external services) - app.get('/api/external/post/:id', externalHandlers.getExternalPost); - app.get('/api/external/comments', externalHandlers.getExternalComments); -}; diff --git a/src/routes/utils.js b/src/routes/utils.js deleted file mode 100644 index 4e99354..0000000 --- a/src/routes/utils.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { routes } from './config.js'; - -/** - * Converts a route pattern to a regex that matches dynamic parameters - * @param {string} pattern - Route pattern like '/dashboard/:id' - * @returns {RegExp} Regular expression to match the pattern - */ -function patternToRegex(pattern) { - // Escape special regex characters except for :param patterns - const regexPattern = pattern - .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars - .replace(/\*/g, '.*') // Convert * to match anything - .replace(/:([^/]+)/g, '([^/]+)'); // Convert :param to capture group - - return new RegExp(`^${regexPattern}$`); -} - -/** - * Finds the matching route for a given URL path - * @param {string} pathname - The URL path to match against - * @returns {Object|null} The matched route or null if no match is found - */ -export function findMatchingRoute(pathname) { - // Clean up the pathname - const path = pathname.startsWith('/') ? pathname : `/${pathname}`; - - // Try to find a match - for (const route of routes) { - if (!route.path) continue; - - // Skip the wildcard route for now - if (route.path === '*') continue; - - // Check for exact match first (most efficient) - if (route.path === path) { - return route; - } - - // Check for pattern match (handles :param and *) - const regex = patternToRegex(route.path); - if (regex.test(path)) { - return route; - } - } - - // If no match, find the wildcard route (404) - return routes.find((route) => route.path === '*') || null; -} - -/** - * Gets the HTTP status code for a given URL path - * @param {string} pathname - The URL path to get the status code for - * @returns {number} The HTTP status code (default: 200) - */ -export function getStatusCodeForPath(pathname) { - const route = findMatchingRoute(pathname); - return route?.status || 200; -} diff --git a/src/server.js b/src/server.js deleted file mode 100644 index 01689db..0000000 --- a/src/server.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import fs from 'node:fs/promises'; -import express from 'express'; -import { Transform } from 'node:stream'; -import { getRoutes } from './routes/routes.js'; -import { port, base, baseUrl } from './config/server-config.js'; - -// Constants -const isProduction = process.env.NODE_ENV === 'production'; -const ABORT_DELAY = 10000; - -// Create http server -const app = express(); - -// Add Vite or respective production middlewares -/** @type {import('vite').ViteDevServer | undefined} */ -let vite; -if (!isProduction) { - const { createServer } = await import('vite'); - vite = await createServer({ - server: { middlewareMode: true }, - appType: 'custom', - base, - }); - app.use(vite.middlewares); -} else { - const compression = (await import('compression')).default; - const sirv = (await import('sirv')).default; - app.use(compression()); - app.use(base, sirv('./dist/client', { extensions: [] })); -} - -// Register API routes -getRoutes(app); - -// Serve HTML -app.use('*all', async (req, res) => { - try { - const url = req.originalUrl.replace(base, ''); - - /** @type {string} */ - let template; - /** @type {import('./entry-server.jsx').render} */ - let render; - if (!isProduction) { - // Always read fresh template in development - template = await fs.readFile('./index.html', 'utf-8'); - template = await vite.transformIndexHtml(url, template); - render = (await vite.ssrLoadModule('/src/entry-server.jsx')).render; - } else { - const templateHtml = isProduction - ? await fs.readFile('./dist/client/index.html', 'utf-8') - : ''; - template = templateHtml; - render = (await import('../dist/server/entry-server.js')).render; - } - - let didError = false; - - const { pipe, abort, statusCode } = render(url, { - onShellError() { - res.status(500); - res.set({ 'Content-Type': 'text/html' }); - res.send('

Something went wrong

'); - }, - onShellReady() { - res.status(didError ? 500 : statusCode); - res.set({ 'Content-Type': 'text/html' }); - - const transformStream = new Transform({ - transform(chunk, encoding, callback) { - res.write(chunk, encoding); - callback(); - }, - }); - - const [htmlStart, htmlEnd] = template.split(``); - - res.write(htmlStart); - - transformStream.on('finish', () => { - res.end(htmlEnd); - }); - - pipe(transformStream); - }, - onError(error) { - didError = true; - console.error(error); - }, - }); - - setTimeout(() => { - abort(); - }, ABORT_DELAY); - } catch (e) { - vite?.ssrFixStacktrace(e); - console.log(e.stack); - res.status(500).end(e.stack); - } -}); - -// Start http server -app.listen(port, () => { - console.log(`Server started at: ${baseUrl}`); -}); diff --git a/src/service/externalHandlers.js b/src/service/externalHandlers.js deleted file mode 100644 index 4d6f521..0000000 --- a/src/service/externalHandlers.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * This file contains mock "external" API handlers that simulate external services. - * These are called by the main API handlers to demonstrate API-to-API communication. - * Just for demonstration purposes. - * You can remove/replace these with actual external API handlers in your actual application. - */ - -// Mock database for posts -const mockPosts = { - 1: { - id: 1, - title: 'What is Carbon?', - body: ` - This is just a sample post. Carbon is IBM's open source design system for products and digital experiences. - With the IBM Design Language as its foundation, the system consists of working code, design tools and resources, - human interface guidelines, and a vibrant community of contributors.`, - userId: 1, - }, - 2: { - id: 2, - title: 'Getting started with React router', - body: 'This is just a sample post. React Router is a standard library for routing in React applications.', - userId: 1, - }, - 3: { - id: 3, - title: 'Building modern web applications', - body: 'This is just a sample post. Modern web development requires understanding of various tools and frameworks.', - userId: 2, - }, -}; - -// Mock database for comments -const mockComments = { - 1: [ - { - id: 1, - postId: 1, - name: 'Great introduction!', - email: 'user1@example.com', - body: 'This is a really helpful overview of Carbon.', - }, - { - id: 2, - postId: 1, - name: 'Thanks for sharing', - email: 'user2@example.com', - body: 'Looking forward to learning more about Carbon.', - }, - ], - 2: [ - { - id: 3, - postId: 2, - name: 'Router questions', - email: 'user3@example.com', - body: 'How does this compare to other routing libraries?', - }, - ], - 3: [ - { - id: 4, - postId: 3, - name: 'Excellent article', - email: 'user4@example.com', - body: 'Very comprehensive guide to modern web development.', - }, - ], -}; - -/** - * Mock external API handler for fetching a single post - * Simulates an external service response - */ -export const getExternalPost = async (req, res) => { - const { id } = req.params; - - // Validate that id is a positive integer - if (!/^\d+$/.test(id)) { - return res.status(400).json({ message: 'Invalid post id' }); - } - - // Simulated network delay - await new Promise((resolve) => setTimeout(resolve, 100)); - const post = mockPosts[id]; - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - res.json(post); -}; - -/** - * Mock external API handler for fetching comments for a post - * Simulates an external service response - */ -export const getExternalComments = async (req, res) => { - const { postId } = req.query; - - // Validate that postId is provided and is a positive integer - if (!postId || !/^\d+$/.test(postId)) { - return res.status(400).json({ message: 'Invalid or missing postId' }); - } - - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 100)); - - const comments = mockComments[postId] || []; - - res.json(comments); -}; - -export default { - getExternalComments, - getExternalPost, -}; diff --git a/src/service/postHandlers.js b/src/service/postHandlers.js deleted file mode 100644 index c4cd030..0000000 --- a/src/service/postHandlers.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * This file contains the functions that do async network requests - * These handlers call our "external" API routes to simulate API-to-API communication - */ - -/** - * Get the base URL for the server - * In production, this would be configured via environment variables - */ -import { baseUrl } from '../config/server-config.js'; - -const getBaseUrl = () => { - return baseUrl; -}; - -export const getPost = async (req, res) => { - const { id } = req.params; - - // Validate that id is a positive integer - if (!/^\d+$/.test(id)) { - return res.status(400).json({ message: 'Invalid post id' }); - } - - try { - // Call our mock "external" API endpoint - const response = await fetch(`${getBaseUrl()}/api/external/post/${id}`); - - if (!response.ok) { - const error = await response.json(); - return res.status(response.status).json(error); - } - - const blogpost = await response.json(); - - // Return the blogpost - res.json(blogpost); - } catch (error) { - console.error('Error fetching post:', error); - res.status(500).json({ message: 'Failed to fetch post' }); - } -}; - -export const getComments = async (req, res) => { - const { postId } = req.query; - - if (!postId) { - return res.status(400).json({ message: 'Missing postId parameter' }); - } - - try { - // Call our mock "external" API endpoint - const response = await fetch( - `${getBaseUrl()}/api/external/comments?postId=${postId}`, - ); - - if (!response.ok) { - const error = await response.json(); - return res.status(response.status).json(error); - } - - const comments = await response.json(); - - // Return the comments - return res.json(comments); - } catch (error) { - console.error('Error fetching comments:', error); - return res.status(500).json({ message: 'Failed to fetch comments' }); - } -}; - -export default { - getComments, - getPost, -}; diff --git a/src/test/server.js b/src/test/server.js index c94e480..05a15cc 100644 --- a/src/test/server.js +++ b/src/test/server.js @@ -7,22 +7,18 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { getNetworking } from './networking'; -import { getRouter } from './router'; -import { getRoutes } from '../routes/routes'; -import { port, base } from '../config/server-config.js'; +/** + * Mock Service Worker (MSW) server for testing + * + * In React Router Framework mode, API routes are handled by the framework. + * This server provides mock responses for external API calls made during tests. + */ const _setupServer = (...args) => { - const mocks = []; - const networking = getNetworking(); - - // Set up internal API routes (including external mock routes) - getRoutes(getRouter(mocks, networking)); - - // Mock external API calls from postHandlers to our local external endpoints - // These intercept the fetch calls made by postHandlers.js + // Mock external API endpoints that the application might call const externalMocks = [ - http.get(`${base}:${port}/api/external/post/:id`, ({ params }) => { + // Mock external post API + http.get('/api/external/post/:id', ({ params }) => { return HttpResponse.json({ id: params.id, title: 'Test post title', @@ -30,7 +26,9 @@ const _setupServer = (...args) => { userId: 1, }); }), - http.get(`${base}:${port}/api/external/comments`, ({ request }) => { + + // Mock external comments API + http.get('/api/external/comments', ({ request }) => { const url = new URL(request.url); const postId = url.searchParams.get('postId'); return HttpResponse.json([ @@ -52,9 +50,7 @@ const _setupServer = (...args) => { }), ]; - const server = setupServer(...mocks, ...externalMocks, ...args); - server.networking = networking; - return server; + return setupServer(...externalMocks, ...args); }; export const server = _setupServer(); diff --git a/src/test/setup.js b/src/test/setup.js index 7faa37e..6251095 100644 --- a/src/test/setup.js +++ b/src/test/setup.js @@ -33,25 +33,22 @@ global.ResizeObserver = class { }; export function setupBeforeAll(server) { - console.log('[Test Setup: BeforeAll] Starting server'); + console.log('[Test Setup: BeforeAll] Starting MSW server'); server.listen(); } export function setupBeforeEach(server) { console.log(`[Test Setup: BeforeEach] server?: ${!!server}`); - // TODO: + // Reset handlers before each test to ensure clean state } export async function setupAfterEach(server) { console.log('[Test Setup: AfterEach] Cleaning up after test'); - - // FIXME: the inflight requests don't settle at this point, need to investigate why and fix - // await expect(server.networking.getRunningRequestCount()).toBe(0); - server.networking.clearRunningRequests(); + // Reset any request handlers that were added during the test server.resetHandlers(); } export function setupAfterAll(server) { - console.log('[Test Setup: AfterAll] Closing server'); + console.log('[Test Setup: AfterAll] Closing MSW server'); server.close(); } diff --git a/src/test/test-utils.jsx b/src/test/test-utils.jsx index 0c350cc..1e18010 100644 --- a/src/test/test-utils.jsx +++ b/src/test/test-utils.jsx @@ -5,15 +5,17 @@ * LICENSE file in the root directory of this source tree. */ +import React, { StrictMode } from 'react'; import { render as rtlRender } from '@testing-library/react'; import { BrowserRouter } from 'react-router'; -import { StrictMode } from 'react'; -import { Router } from '../routes'; import { ThemeProvider } from '../context/ThemeContext'; /** * Renders a component with all providers (Theme, Router, etc.) * Use this for page components or components that need routing + * + * Note: In React Router Framework mode, we use BrowserRouter for testing + * instead of the full application router since tests focus on individual components. */ export function renderWithAllProviders( ui, @@ -22,13 +24,11 @@ export function renderWithAllProviders( // Push the route we want to test window.history.pushState({}, 'Test page', route); - function Wrapper() { + function Wrapper({ children }) { return ( - - - + {children} ); diff --git a/src/utils/local-storage.js b/src/utils/local-storage.js index 6522a6e..b633c97 100644 --- a/src/utils/local-storage.js +++ b/src/utils/local-storage.js @@ -3,7 +3,20 @@ const localStorageKeys = { headerInverse: 'app-header-inverse', }; +/** + * Check if we're running in a browser environment (not SSR) + */ +const isBrowser = typeof window !== 'undefined'; + export const getLocalStorageValues = () => { + // Return defaults during SSR + if (!isBrowser) { + return { + themeSetting: 'system', + headerInverse: false, + }; + } + const themeSetting = window.localStorage.getItem(localStorageKeys.themeSetting) || 'system'; const headerInverse = @@ -17,6 +30,11 @@ export const getLocalStorageValues = () => { }; export const setLocalStorageValues = (values) => { + // Skip during SSR + if (!isBrowser) { + return; + } + if (values) { const keys = Object.keys(localStorageKeys); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c6a0117 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@react-router/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + }, + "noEmit": true, + + // Enable type generation for route modules + "rootDirs": [".", "./.react-router/types"] + } +} diff --git a/vite.config.js b/vite.config.js index 98e9c6f..ac2e6d1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,12 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import react from '@vitejs/plugin-react-swc'; +import { reactRouter } from '@react-router/dev/vite'; import { defineConfig } from 'vite'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], +export default defineConfig(({ mode }) => ({ + // Only use React Router plugin in non-test modes + // The plugin interferes with Vitest's module resolution + plugins: mode !== 'test' ? [reactRouter()] : [], test: { globals: true, environment: 'jsdom', @@ -20,4 +22,4 @@ export default defineConfig({ reporter: ['text', 'html'], }, }, -}); +}));