diff --git a/dev-server/app.tsx b/dev-server/app.tsx new file mode 100644 index 0000000000..4f4cc2fc05 --- /dev/null +++ b/dev-server/app.tsx @@ -0,0 +1,269 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { createContext, Suspense, useContext, useEffect, useState } from 'react'; + +import ErrorBoundary from './error-boundary'; +import { defaultURLParams, formatURLParams, URLParams } from './url-params'; + +/** + * App context for sharing URL parameters and page state + */ +export interface AppContextType { + pageId?: string; + urlParams: URLParams; + isServer: boolean; + setUrlParams: (newParams: Partial) => void; +} + +const AppContext = createContext({ + pageId: undefined, + urlParams: defaultURLParams, + isServer: true, + setUrlParams: () => {}, +}); + +export const useAppContext = () => useContext(AppContext); + +/** + * Props for the App shell component + */ +export interface AppProps { + pageId?: string; + urlParams: URLParams; + isServer: boolean; + children: React.ReactNode; +} + +/** + * Check if a page is an AppLayout page (needs special handling) + */ +function isAppLayoutPage(pageId?: string): boolean { + if (!pageId) { + return false; + } + const appLayoutPages = [ + 'app-layout', + 'content-layout', + 'grid-navigation-custom', + 'expandable-rows-test', + 'container/sticky-permutations', + 'copy-to-clipboard/scenario-split-panel', + 'prompt-input/simple', + 'funnel-analytics/static-single-page-flow', + 'funnel-analytics/static-multi-page-flow', + 'charts.test', + 'error-boundary/demo-async-load', + 'error-boundary/demo-components', + ]; + return appLayoutPages.some(match => pageId.includes(match)); +} + +/** + * Theme switcher component with toggles for dark mode, density, motion, etc. + */ +function ThemeSwitcher({ + urlParams, + setUrlParams, +}: { + urlParams: URLParams; + setUrlParams: (p: Partial) => void; +}) { + const switcherStyle: React.CSSProperties = { + display: 'flex', + gap: '12px', + alignItems: 'center', + fontSize: '12px', + }; + + const labelStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: '4px', + cursor: 'pointer', + }; + + return ( +
+ + + + +
+ ); +} + +/** + * Header component for the demo pages + */ +function Header({ + sticky, + urlParams, + setUrlParams, +}: { + sticky?: boolean; + urlParams: URLParams; + setUrlParams: (p: Partial) => void; +}) { + const headerStyle: React.CSSProperties = { + boxSizing: 'border-box', + background: '#232f3e', + paddingBlockStart: '12px', + paddingBlockEnd: '11px', + paddingInline: '12px', + fontSize: '15px', + fontWeight: 700, + lineHeight: '17px', + display: 'flex', + color: '#eee', + justifyContent: 'space-between', + alignItems: 'center', + ...(sticky && { + inlineSize: '100%', + zIndex: 1000, + insetBlockStart: 0, + position: 'sticky' as const, + }), + }; + + const linkStyle: React.CSSProperties = { + textDecoration: 'none', + color: '#eee', + }; + + return ( +
+ + Demo Assets + + +
+ ); +} + +/** + * Client-side effects for applying global styles and modes + * These only run on the client after hydration + */ +function ClientEffects({ urlParams }: { urlParams: URLParams }) { + useEffect(() => { + // Apply mode (light/dark) + import('@cloudscape-design/global-styles').then(({ applyMode, Mode }) => { + applyMode(urlParams.mode === 'dark' ? Mode.Dark : Mode.Light); + }); + }, [urlParams.mode]); + + useEffect(() => { + // Apply density + import('@cloudscape-design/global-styles').then(({ applyDensity, Density }) => { + applyDensity(urlParams.density === 'compact' ? Density.Compact : Density.Comfortable); + }); + }, [urlParams.density]); + + useEffect(() => { + // Apply motion disabled + import('@cloudscape-design/global-styles').then(({ disableMotion }) => { + disableMotion(urlParams.motionDisabled); + }); + }, [urlParams.motionDisabled]); + + useEffect(() => { + // Apply direction + document.documentElement.setAttribute('dir', urlParams.direction); + }, [urlParams.direction]); + + return null; +} + +/** + * App shell component that wraps demo pages + * Similar to pages/app/index.tsx but for SSR + */ +export default function App({ pageId, urlParams: initialUrlParams, isServer, children }: AppProps) { + const [urlParams, setUrlParamsState] = useState(initialUrlParams); + const isAppLayout = isAppLayoutPage(pageId); + const ContentTag = isAppLayout ? 'div' : 'main'; + + // Update URL params and sync to URL + const setUrlParams = (newParams: Partial) => { + const updatedParams = { ...urlParams, ...newParams }; + setUrlParamsState(updatedParams); + + // Update URL without reload (except for visualRefresh which needs reload) + if (!isServer && !('visualRefresh' in newParams)) { + const newUrl = pageId ? `/${pageId}${formatURLParams(updatedParams)}` : `/${formatURLParams(updatedParams)}`; + window.history.replaceState({}, '', newUrl); + } + }; + + // Header is always rendered outside the error boundary so users can still + // toggle settings (like visual refresh) even when a page throws an error + const header = ( +
+ ); + + // Page content is wrapped in error boundary (client-side only) + const pageContent = isServer ? children : {children}; + + // Wrap in Suspense on client side only (not supported in React 16 SSR) + const suspenseWrapped = isServer ? ( + pageContent + ) : ( + Loading...}>{pageContent} + ); + + return ( + + {/* Client-side effects for applying global styles */} + {!isServer && } + + + {header} + {suspenseWrapped} + + + ); +} diff --git a/dev-server/collect-styles.mjs b/dev-server/collect-styles.mjs new file mode 100644 index 0000000000..43b41f53cf --- /dev/null +++ b/dev-server/collect-styles.mjs @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { glob } from 'glob'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); +const componentsDir = path.resolve(rootDir, 'lib/components'); + +let cachedStyles = null; + +/** + * Gets the global styles CSS from @cloudscape-design/global-styles + */ +function getGlobalStyles() { + try { + // Find the global-styles package in node_modules + const globalStylesPath = path.resolve(rootDir, 'node_modules/@cloudscape-design/global-styles/index.css'); + return fs.readFileSync(globalStylesPath, 'utf-8'); + } catch (e) { + console.warn('Warning: Could not read global styles:', e.message); + return ''; + } +} + +/** + * Collects all scoped CSS files from the built component library + * and combines them into a single CSS string for SSR injection. + * Also includes global styles from @cloudscape-design/global-styles. + */ +export function collectStyles() { + // Return cached styles if available (for performance) + if (cachedStyles !== null) { + return cachedStyles; + } + + // Start with global styles + const globalStyles = getGlobalStyles(); + + const cssFiles = glob.sync('**/*.scoped.css', { + cwd: componentsDir, + absolute: true, + }); + + const componentStyles = cssFiles + .map(file => { + try { + return fs.readFileSync(file, 'utf-8'); + } catch (e) { + console.warn(`Warning: Could not read CSS file ${file}:`, e.message); + return ''; + } + }) + .filter(Boolean) + .join('\n'); + + // Combine global styles first, then component styles + cachedStyles = globalStyles + '\n' + componentStyles; + return cachedStyles; +} + +/** + * Clears the cached styles (useful for development when styles change) + */ +export function clearStyleCache() { + cachedStyles = null; +} diff --git a/dev-server/dev.mjs b/dev-server/dev.mjs new file mode 100644 index 0000000000..e66706fccd --- /dev/null +++ b/dev-server/dev.mjs @@ -0,0 +1,129 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Development startup script that: + * 1. Starts the build watcher (gulp watch) + * 2. Waits for the initial build to complete + * 3. Starts the demo server + * 4. Handles graceful shutdown + */ + +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); + +// Track child processes for cleanup +const childProcesses = []; + +/** + * Spawns a child process and tracks it for cleanup + */ +function spawnProcess(command, args, options = {}) { + const proc = spawn(command, args, { + stdio: 'inherit', + shell: true, + cwd: rootDir, + ...options, + }); + + childProcesses.push(proc); + + proc.on('error', err => { + console.error(`Error starting ${command}:`, err.message); + }); + + return proc; +} + +/** + * Gracefully shuts down all child processes + */ +function shutdown() { + console.log('\nShutting down...'); + + for (const proc of childProcesses) { + if (proc && !proc.killed) { + proc.kill('SIGTERM'); + } + } + + // Force exit after a timeout if processes don't terminate + setTimeout(() => { + console.log('Force exiting...'); + // eslint-disable-next-line no-undef + process.exit(0); + }, 3000); +} + +/** + * Waits for the lib/components directory to exist (initial build complete) + */ +async function waitForBuild() { + const { existsSync } = await import('node:fs'); + const componentsPath = path.resolve(rootDir, 'lib/components'); + + // If lib/components already exists, no need to wait + if (existsSync(componentsPath)) { + console.log('Build artifacts found, starting demo server...'); + return; + } + + console.log('Waiting for initial build to complete...'); + + return new Promise(resolve => { + const checkInterval = setInterval(() => { + if (existsSync(componentsPath)) { + clearInterval(checkInterval); + console.log('Build complete, starting demo server...'); + resolve(); + } + }, 1000); + }); +} + +/** + * Main entry point + */ +async function main() { + // Set up signal handlers for graceful shutdown + // eslint-disable-next-line no-undef + process.on('SIGINT', shutdown); + // eslint-disable-next-line no-undef + process.on('SIGTERM', shutdown); + + console.log('Starting development environment...\n'); + + // Start the build watcher + console.log('Starting build watcher (gulp watch)...'); + const watchProc = spawnProcess('npx', ['gulp', 'watch']); + + watchProc.on('exit', code => { + if (code !== null && code !== 0) { + console.error(`Build watcher exited with code ${code}`); + shutdown(); + } + }); + + // Wait for initial build to complete + await waitForBuild(); + + // Start the demo server + console.log('Starting demo server...'); + const serverProc = spawnProcess('node', ['dev-server/server.mjs']); + + serverProc.on('exit', code => { + if (code !== null && code !== 0) { + console.error(`Demo server exited with code ${code}`); + shutdown(); + } + }); +} + +main().catch(err => { + console.error('Failed to start development environment:', err); + shutdown(); +}); diff --git a/dev-server/entry-client.tsx b/dev-server/entry-client.tsx new file mode 100644 index 0000000000..75a59b0c62 --- /dev/null +++ b/dev-server/entry-client.tsx @@ -0,0 +1,246 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import App from './app'; +import IndexPage, { TreeItem } from './index-page'; +import { parseURLParams, URLParams } from './url-params'; + +/** + * Handle legacy hash-based routes from the old dev server. + * Redirects URLs like /#/page-id to /page-id for backwards compatibility. + */ +function handleHashRedirect(): boolean { + const hash = window.location.hash; + if (hash && hash.startsWith('#/')) { + // Extract the path from the hash (e.g., "#/button/simple" -> "/button/simple") + const newPath = hash.slice(1); // Remove the leading "#" + // Preserve any query parameters from the original URL + const search = window.location.search; + window.location.replace(newPath + search); + return true; // Redirect in progress + } + return false; +} + +// Global flags symbols (same as in pages/app/index.tsx and entry-server.tsx) +const awsuiVisualRefreshFlag = Symbol.for('awsui-visual-refresh-flag'); +const awsuiGlobalFlagsSymbol = Symbol.for('awsui-global-flags'); + +interface GlobalFlags { + appLayoutWidget?: boolean; + appLayoutToolbar?: boolean; +} + +interface ExtendedWindow extends Window { + [awsuiVisualRefreshFlag]?: () => boolean; + [awsuiGlobalFlagsSymbol]?: GlobalFlags; +} + +declare const window: ExtendedWindow; + +/** + * Set up global flags before hydration + * These flags must be set before any React rendering occurs + */ +function setupGlobalFlags(urlParams: URLParams): void { + // Set visual refresh flag + window[awsuiVisualRefreshFlag] = () => urlParams.visualRefresh; + + // Set global flags for app layout + if (!window[awsuiGlobalFlagsSymbol]) { + window[awsuiGlobalFlagsSymbol] = {}; + } + window[awsuiGlobalFlagsSymbol].appLayoutWidget = urlParams.appLayoutWidget; + window[awsuiGlobalFlagsSymbol].appLayoutToolbar = urlParams.appLayoutToolbar; + + // Apply direction to document + document.documentElement.setAttribute('dir', urlParams.direction); +} + +/** + * Parse the current URL to get page ID and parameters + */ +function parseCurrentURL(): { pageId: string | undefined; urlParams: URLParams } { + const url = new URL(window.location.href); + + // Get page ID from pathname (remove leading slash) + let pathname = url.pathname; + // Remove trailing slash (except for root) + if (pathname !== '/' && pathname.endsWith('/')) { + pathname = pathname.slice(0, -1); + } + const pageId = pathname === '/' ? undefined : pathname.slice(1); + + // Parse URL parameters + const urlParams = parseURLParams(url.searchParams); + + return { pageId, urlParams }; +} + +/** + * Get the page tree from the embedded script tag (for index page hydration) + */ +function getPageTreeFromScript(): TreeItem | undefined { + const scriptElement = document.getElementById('ssr-page-tree'); + if (scriptElement) { + try { + return JSON.parse(scriptElement.textContent || ''); + } catch (e) { + console.warn('Failed to parse page tree from script:', e); + } + } + return undefined; +} + +/** + * Pre-load all page modules using Vite's glob import + * This allows dynamic imports with nested paths (e.g., "app-layout-toolbar/with-content-layout") + */ +const pageModules = import.meta.glob('../pages/**/*.page.tsx'); + +/** + * Load a page component dynamically + */ +async function loadPageComponent(pageId: string): Promise { + const pagePath = `../pages/${pageId}.page.tsx`; + const loader = pageModules[pagePath]; + + if (!loader) { + throw new Error(`Page not found: ${pageId}`); + } + + try { + const pageModule = (await loader()) as { default: React.ComponentType }; + return pageModule.default; + } catch (error) { + console.error(`Failed to load page "${pageId}":`, error); + throw error; + } +} + +/** + * Error fallback component for pages that fail to load + */ +function PageLoadError({ pageId, error }: { pageId: string; error: Error }): React.ReactElement { + return ( +
+

Failed to Load Page

+

+ Could not load page: {pageId} +

+
+ Error Details +
+          {error.message}
+          {'\n\n'}
+          {error.stack}
+        
+
+

+ ← Back to index +

+
+ ); +} + +/** + * Check if the page was rendered with an SSR error (client-side only fallback) + */ +function wasSSRError(): boolean { + const errorMarker = document.querySelector('[data-ssr-error="true"]'); + return errorMarker !== null; +} + +/** + * Main hydration function + * Detects React version and uses appropriate hydration method + */ +async function hydrate(): Promise { + // Handle legacy hash-based routes first + if (handleHashRedirect()) { + return; // Redirect in progress, don't hydrate + } + + const { pageId, urlParams } = parseCurrentURL(); + + // Set up global flags BEFORE any React rendering + setupGlobalFlags(urlParams); + + // Get the app container + const container = document.getElementById('app'); + if (!container) { + console.error('Could not find #app container for hydration'); + return; + } + + // Check if this page had an SSR error and needs client-side only rendering + const hadSSRError = wasSSRError(); + if (hadSSRError && pageId) { + console.log(`Page "${pageId}" was not SSR'd, rendering client-side only`); + } + + // Load the page component if needed + let PageComponent: React.ComponentType | null = null; + let loadError: Error | null = null; + + if (pageId) { + try { + PageComponent = await loadPageComponent(pageId); + } catch (error) { + loadError = error instanceof Error ? error : new Error(String(error)); + } + } + + // Create the app element + const appElement = ( + + {loadError ? ( + + ) : PageComponent ? ( + + ) : ( + + )} + + ); + + // Detect React version and hydrate accordingly + const reactVersion = React.version; + const isReact18 = reactVersion.startsWith('18'); + + console.log(`Hydrating with React ${reactVersion}${hadSSRError ? ' (client-side only)' : ''}`); + + if (isReact18) { + // React 18: Use hydrateRoot for SSR'd pages, createRoot for client-only pages + // Use a variable to prevent Vite from statically analyzing this import + // (react-dom/client doesn't exist in React 16) + const reactDomClient = 'react-dom/client'; + // eslint-disable-next-line no-unsanitized/method + const { hydrateRoot, createRoot } = (await import(/* @vite-ignore */ reactDomClient)) as any; + if (hadSSRError) { + // For pages that weren't SSR'd, use createRoot instead of hydrateRoot + // to avoid hydration mismatch warnings + createRoot(container).render(appElement); + } else { + hydrateRoot(container, appElement); + } + } else { + // React 16/17: Use ReactDOM.hydrate for SSR'd pages, render for client-only + const ReactDOM = (await import('react-dom')) as any; + if (hadSSRError) { + // For pages that weren't SSR'd, use render instead of hydrate + ReactDOM.render(appElement, container); + } else { + ReactDOM.hydrate(appElement, container); + } + } +} + +// Start hydration when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', hydrate); +} else { + hydrate(); +} diff --git a/dev-server/entry-server.tsx b/dev-server/entry-server.tsx new file mode 100644 index 0000000000..6348fd0171 --- /dev/null +++ b/dev-server/entry-server.tsx @@ -0,0 +1,155 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// IMPORTANT: Global flags are set by server.mjs BEFORE this module is loaded. +// Do NOT set the visual refresh flag here - it must be set per-request. + +// These imports are safe - they don't touch the component library +import type { TreeItem } from './index-page'; +import { parseURLParams } from './url-params'; + +// Render options interface (accepts raw query params) +export interface RenderOptions { + pageId?: string; + urlParams: Record; + // Page utilities passed from server.mjs (to avoid importing Node.js modules) + pageTree?: TreeItem; + pageExists?: boolean; + pageList?: string[]; +} + +// Render result interface +export interface RenderResult { + html: string; + status: number; +} + +// Main render function +export async function render(options: RenderOptions): Promise { + const { pageId, urlParams: rawUrlParams, pageTree, pageExists, pageList } = options; + + // Parse URL parameters with defaults + const urlParams = parseURLParams(rawUrlParams); + + // Global flags are already set by server.mjs before this module is loaded + // Just clear the cached visual refresh state to ensure it's re-evaluated + const { clearVisualRefreshState } = await import('@cloudscape-design/component-toolkit/internal/testing'); + clearVisualRefreshState(); + + // Now dynamically import React and components (after flags are set) + const React = await import('react'); + const ReactDOMServer = await import('react-dom/server'); + const { default: App } = await import('./app'); + const { default: IndexPage } = await import('./index-page'); + + // 404 Not Found page component + function NotFoundPage({ pageId, pageList = [] }: { pageId: string; pageList?: string[] }): React.ReactElement { + return React.createElement( + 'div', + { style: { maxWidth: 800, margin: '0 auto', padding: '20px' } }, + React.createElement('h1', { style: { color: '#d32f2f' } }, 'Page Not Found'), + React.createElement('p', null, 'The page ', React.createElement('code', null, pageId), ' does not exist.'), + React.createElement('p', null, React.createElement('a', { href: '/' }, '← Back to index')), + React.createElement( + 'details', + null, + React.createElement('summary', null, `Available pages (${pageList.length})`), + React.createElement( + 'ul', + null, + pageList + .slice(0, 50) + .map((page: string) => + React.createElement('li', { key: page }, React.createElement('a', { href: `/${page}` }, page)) + ), + pageList.length > 50 && React.createElement('li', null, `... and ${pageList.length - 50} more`) + ) + ) + ); + } + + // Handle index page + if (!pageId) { + // Embed the page tree as JSON for client-side hydration + const pageTreeScript = pageTree + ? `` + : ''; + const appHtml = ReactDOMServer.renderToString( + React.createElement( + App, + { pageId: undefined, urlParams, isServer: true }, + React.createElement(IndexPage, { pageTree }) + ) + ); + return { html: pageTreeScript + appHtml, status: 200 }; + } + + // Check if page exists + if (pageExists === false) { + const html = ReactDOMServer.renderToString( + React.createElement( + App, + { pageId, urlParams, isServer: true }, + React.createElement(NotFoundPage, { pageId, pageList }) + ) + ); + return { html, status: 404 }; + } + + // Try to dynamically import and render the page + try { + // Dynamic import of the page module + const pages = import.meta.glob('../pages/**/*.page.tsx'); + const pagePath = `../pages/${pageId}.page.tsx`; + + if (!pages[pagePath]) { + throw new Error(`Page module not found: ${pagePath}`); + } + + const pageModule = (await pages[pagePath]()) as { default: React.ComponentType }; + const PageComponent = pageModule.default; + + if (!PageComponent) { + throw new Error(`Page module "${pageId}" does not have a default export`); + } + + const html = ReactDOMServer.renderToString( + React.createElement(App, { pageId, urlParams, isServer: true }, React.createElement(PageComponent)) + ); + return { html, status: 200 }; + } catch (error) { + // If the page can't be rendered, return a placeholder for client-side rendering + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + console.warn(`āš ļø SSR Warning for page "${pageId}": Page will be rendered client-side only`); + console.warn(` Reason: ${errorMessage}`); + if (errorStack) { + console.warn(` Stack: ${errorStack.split('\n').slice(1, 4).join('\n ')}`); + } + + const errorPlaceholder = React.createElement( + 'div', + { 'data-ssr-error': 'true', 'data-page-id': pageId, 'data-error-message': errorMessage }, + React.createElement( + 'div', + { style: { padding: '20px', textAlign: 'center' } }, + React.createElement('p', { style: { color: '#666' } }, `Loading ${pageId}...`), + React.createElement( + 'p', + { style: { fontSize: '12px', color: '#999' } }, + '(This page is rendered client-side only)' + ) + ) + ); + + const html = ReactDOMServer.renderToString( + React.createElement(App, { pageId, urlParams, isServer: true }, errorPlaceholder) + ); + return { html, status: 200 }; + } +} + +// Re-export URL params utilities +export { parseURLParams, formatURLParams, defaultURLParams } from './url-params'; +export type { URLParams } from './url-params'; diff --git a/dev-server/error-boundary.tsx b/dev-server/error-boundary.tsx new file mode 100644 index 0000000000..044349f976 --- /dev/null +++ b/dev-server/error-boundary.tsx @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryProps { + children: ReactNode; + pageId?: string; +} + +interface ErrorBoundaryState { + hasError: boolean; + errorMessage: string; +} + +/** + * Error Boundary component for catching client-side rendering errors + * + * This component wraps page content and catches any errors that occur during + * React rendering on the client side. When an error is caught, it displays + * the error message in red text, matching the behavior of the non-SSR demo server. + * + * Note: Error boundaries only catch errors in their child component tree. + * They do not catch errors in event handlers, async code, or server-side rendering. + */ +export default class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + errorMessage: '', + }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + // Update state so the next render will show the fallback UI + return { + hasError: true, + errorMessage: error.stack || error.message, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + // Log the error for debugging + console.error('ErrorBoundary caught an error:', error); + console.error('Component stack:', errorInfo.componentStack); + } + + render(): ReactNode { + if (this.state.hasError) { + // Match the non-SSR demo server's error display: red text with the error + return {this.state.errorMessage}; + } + + return this.props.children; + } +} diff --git a/dev-server/index-page.tsx b/dev-server/index-page.tsx new file mode 100644 index 0000000000..71b705c7b5 --- /dev/null +++ b/dev-server/index-page.tsx @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +/** + * Tree item interface for the page tree structure + */ +export interface TreeItem { + name: string; + href?: string; + items: TreeItem[]; +} + +/** + * Props for IndexPage component + */ +export interface IndexPageProps { + pageTree?: TreeItem; +} + +/** + * Renders a tree item with its children + */ +function TreeItemView({ item }: { item: TreeItem }) { + return ( +
  • + {item.href ? {item.name} : {item.name}} + {item.items.length > 0 && ( +
      + {item.items.map(child => ( + + ))} +
    + )} +
  • + ); +} + +/** + * Index page component that lists all available demo pages + * Similar to pages/app/components/index-page.tsx but for SSR + * + * The pageTree prop is passed from the server to avoid importing + * Node.js-only modules (page-loader.js) on the client side. + */ +export default function IndexPage({ pageTree }: IndexPageProps) { + // If no pageTree is provided (client-side hydration), show a loading message + // The server will always provide the pageTree + if (!pageTree) { + return ( +
    +

    Cloudscape Demo Pages

    +

    Loading page list...

    +
    + ); + } + + return ( +
    +

    Cloudscape Demo Pages

    +

    Select a page:

    +
      + {pageTree.items.map(item => ( + + ))} +
    +
    + ); +} diff --git a/dev-server/index.html b/dev-server/index.html new file mode 100644 index 0000000000..fbdfbca21f --- /dev/null +++ b/dev-server/index.html @@ -0,0 +1,16 @@ + + + + + + Cloudscape Demo Pages + + + + +
    +
    +
    + + + diff --git a/dev-server/page-loader.d.ts b/dev-server/page-loader.d.ts new file mode 100644 index 0000000000..33f18ea3ba --- /dev/null +++ b/dev-server/page-loader.d.ts @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Tree item interface for the page tree structure + */ +export interface TreeItem { + name: string; + href?: string; + items: TreeItem[]; +} + +/** + * Scans for all demo pages matching the pattern pages/*.page.tsx + * and returns a list of page IDs (e.g., "alert/simple", "app-layout/with-drawers") + */ +export function getPageList(): string[]; + +/** + * Checks if a page exists by its page ID + */ +export function pageExists(pageId: string): boolean; + +/** + * Gets the file path for a page ID + */ +export function getPagePath(pageId: string): string; + +/** + * Clears the cached page list (useful for development when pages are added/removed) + */ +export function clearPageCache(): void; + +/** + * Creates a tree structure from the page list for display in the index page + */ +export function createPagesTree(pages: string[]): TreeItem; diff --git a/dev-server/page-loader.js b/dev-server/page-loader.js new file mode 100644 index 0000000000..140564abee --- /dev/null +++ b/dev-server/page-loader.js @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { glob } from 'glob'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); +const pagesDir = path.resolve(rootDir, 'pages'); + +// Cache for page list to avoid repeated filesystem scans +let cachedPageList = null; + +// Scans for all demo pages matching the pattern pages/**/*.page.tsx +// and returns a list of page IDs (e.g., "alert/simple", "app-layout/with-drawers") +export function getPageList() { + if (cachedPageList !== null) { + return cachedPageList; + } + + const pageFiles = glob.sync('**/*.page.tsx', { + cwd: pagesDir, + ignore: ['app/**'], // Ignore the app directory itself + }); + + cachedPageList = pageFiles.map(file => file.replace(/\.page\.tsx$/, '')); + return cachedPageList; +} + +// Checks if a page exists by its page ID +export function pageExists(pageId) { + const pageList = getPageList(); + return pageList.includes(pageId); +} + +// Gets the file path for a page ID +export function getPagePath(pageId) { + return path.resolve(pagesDir, `${pageId}.page.tsx`); +} + +// Clears the cached page list (useful for development when pages are added/removed) +export function clearPageCache() { + cachedPageList = null; +} + +// Creates a tree structure from the page list for display in the index page +export function createPagesTree(pages) { + const tree = { name: '.', items: [] }; + + function putInTree(segments, node, item) { + if (segments.length === 0) { + node.href = item; + } else { + let match = node.items.find(child => child.name === segments[0]); + if (!match) { + match = { name: segments[0], items: [] }; + node.items.push(match); + } + putInTree(segments.slice(1), match, item); + // Make directories display above files + node.items.sort((a, b) => Math.min(b.items.length, 1) - Math.min(a.items.length, 1)); + } + } + + for (const page of pages) { + const segments = page.split('/'); + putInTree(segments, tree, page); + } + + return tree; +} diff --git a/dev-server/server.mjs b/dev-server/server.mjs new file mode 100644 index 0000000000..ebdbde42e4 --- /dev/null +++ b/dev-server/server.mjs @@ -0,0 +1,158 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import express from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createServer as createViteServer } from 'vite'; + +// Import page-loader directly (Node.js module, not through Vite) +import { createPagesTree, getPageList, pageExists } from './page-loader.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); + +// eslint-disable-next-line no-undef +const port = process.env.PORT || 8080; + +async function createServer() { + const app = express(); + + // Create Vite server in middleware mode + const vite = await createViteServer({ + configFile: path.resolve(__dirname, 'vite.config.js'), + server: { middlewareMode: true }, + appType: 'custom', + }); + + // Use Vite's connect instance as middleware + app.use(vite.middlewares); + + // Serve static files from lib directory + app.use('/lib', express.static(path.resolve(rootDir, 'lib'))); + + // Handle all routes - use wildcard for Express 4.x + app.use('*', async (req, res) => { + const url = req.originalUrl; + + try { + // Read the HTML template + let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8'); + + // Apply Vite HTML transforms (handles HMR injection, etc.) + template = await vite.transformIndexHtml(url, template); + + // Parse URL to get page ID and query parameters BEFORE loading modules + // Handle both /page and /page/ formats + let urlPath = url.split('?')[0]; + // Remove trailing slash (except for root) + if (urlPath !== '/' && urlPath.endsWith('/')) { + urlPath = urlPath.slice(0, -1); + } + const pageId = urlPath === '/' ? undefined : urlPath.slice(1); + const urlParams = Object.fromEntries(new URL(url, `http://localhost:${port}`).searchParams); + + // Clear the visual refresh state BEFORE loading any component modules + // This ensures the visual refresh flag is re-evaluated per request + const { clearVisualRefreshState } = await vite.ssrLoadModule( + '@cloudscape-design/component-toolkit/internal/testing' + ); + clearVisualRefreshState(); + + // Set the visual refresh flag based on URL params BEFORE loading entry-server + const visualRefresh = urlParams.visualRefresh !== 'false'; + globalThis[Symbol.for('awsui-visual-refresh-flag')] = () => visualRefresh; + + // Set up other global flags + const globalFlagsSymbol = Symbol.for('awsui-global-flags'); + if (!globalThis[globalFlagsSymbol]) { + globalThis[globalFlagsSymbol] = {}; + } + globalThis[globalFlagsSymbol].appLayoutWidget = urlParams.appLayoutWidget === 'true'; + globalThis[globalFlagsSymbol].appLayoutToolbar = urlParams.appLayoutToolbar === 'true'; + + // Load the server entry module (path relative to Vite root which is dev-server/) + const { render } = await vite.ssrLoadModule('/entry-server.tsx'); + + // Collect CSS styles from built components + let styles = ''; + try { + const { collectStyles: getStyles } = await vite.ssrLoadModule('/collect-styles.mjs'); + styles = getStyles(); + } catch (e) { + console.warn('Warning: Could not collect styles:', e.message); + } + + // Get page utilities (using Node.js module directly, not through Vite) + const pageList = getPageList(); + const pageTree = createPagesTree(pageList); + const pageExistsResult = pageId ? pageExists(pageId) : true; + + // Render the app HTML + const { html: appHtml, status } = await render({ + pageId, + urlParams, + pageTree, + pageExists: pageExistsResult, + pageList, + }); + + // Inject styles and rendered HTML into template + // Build body classes based on URL params for SSR + const bodyClasses = []; + if (visualRefresh) { + bodyClasses.push('awsui-visual-refresh'); + } + if (urlParams.mode === 'dark') { + bodyClasses.push('awsui-dark-mode'); + } + if (urlParams.density === 'compact') { + bodyClasses.push('awsui-compact-mode'); + } + if (urlParams.motionDisabled === 'true') { + bodyClasses.push('awsui-motion-disabled'); + } + const bodyClass = bodyClasses.join(' '); + + const finalHtml = template + .replace('', styles ? `` : '') + .replace('', bodyClass) + .replace('', appHtml); + + // Send the response + res.status(status).set({ 'Content-Type': 'text/html' }).end(finalHtml); + } catch (e) { + // Fix stack trace for Vite + vite.ssrFixStacktrace(e); + + console.error('Render Error:', e.stack); + + // Return error page + res.status(500).set({ 'Content-Type': 'text/html' }).end(` + + + + Render Error + + + +

    Rendering Error

    +

    ${e.message}

    +
    ${e.stack}
    + + + `); + } + }); + + app.listen(port, () => { + console.log(`Demo server running at http://localhost:${port}`); + }); +} + +createServer(); diff --git a/dev-server/ssr-utils.ts b/dev-server/ssr-utils.ts new file mode 100644 index 0000000000..b4d840b5fc --- /dev/null +++ b/dev-server/ssr-utils.ts @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility functions for SSR compatibility in demo pages + */ + +/** + * Check if code is running on the server (SSR) or client + */ +export const isServer = typeof window === 'undefined'; + +/** + * Check if code is running on the client (browser) + */ +export const isClient = typeof window !== 'undefined'; + +/** + * Safely access window object, returns undefined on server + */ +export function getWindow(): Window | undefined { + return isClient ? window : undefined; +} + +/** + * Safely access document object, returns undefined on server + */ +export function getDocument(): Document | undefined { + return isClient ? document : undefined; +} + +/** + * Safely access navigator object, returns undefined on server + */ +export function getNavigator(): Navigator | undefined { + return isClient ? navigator : undefined; +} + +/** + * Execute a function only on the client side + * Returns undefined on the server + */ +export function clientOnly(fn: () => T): T | undefined { + return isClient ? fn() : undefined; +} diff --git a/dev-server/tsconfig.json b/dev-server/tsconfig.json new file mode 100644 index 0000000000..d1ceb792e6 --- /dev/null +++ b/dev-server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["ES2020", "DOM"], + "target": "ES2019", + "declaration": false, + "declarationMap": false, + "rootDir": ".", + "incremental": true, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "importHelpers": true, + "strict": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": false, + "paths": { + "~components": ["../lib/components"], + "~components/*": ["../lib/components/*"], + "~design-tokens": ["../lib/design-tokens"] + }, + "types": ["node", "vite/client"] + }, + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.mjs"] +} diff --git a/dev-server/url-params.ts b/dev-server/url-params.ts new file mode 100644 index 0000000000..d187c61f00 --- /dev/null +++ b/dev-server/url-params.ts @@ -0,0 +1,90 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * URL parameters interface for configuring demo page rendering + */ +export interface URLParams { + visualRefresh: boolean; + density: 'comfortable' | 'compact'; + motionDisabled: boolean; + direction: 'ltr' | 'rtl'; + mode: 'light' | 'dark'; + appLayoutWidget: boolean; + appLayoutToolbar: boolean; +} + +/** + * Default values for URL parameters + * Note: visualRefresh defaults to true to match the behavior of the non-SSR dev server + * when THEME === 'default' (which is the case for the built components) + */ +export const defaultURLParams: URLParams = { + visualRefresh: true, + density: 'comfortable', + motionDisabled: false, + direction: 'ltr', + mode: 'light', + appLayoutWidget: false, + appLayoutToolbar: false, +}; + +/** + * Parse a boolean value from a string or boolean + */ +function parseBoolean(value: string | boolean | undefined, defaultValue: boolean): boolean { + if (value === undefined) { + return defaultValue; + } + if (typeof value === 'boolean') { + return value; + } + return value === 'true'; +} + +/** + * Parse URL query parameters into URLParams object + * @param query - Query string or URLSearchParams or plain object + * @returns Parsed URL parameters with defaults applied + */ +export function parseURLParams(query: string | URLSearchParams | Record): URLParams { + let params: Record; + + if (typeof query === 'string') { + // Handle query string (with or without leading ?) + const searchParams = new URLSearchParams(query.startsWith('?') ? query.slice(1) : query); + params = Object.fromEntries(searchParams.entries()); + } else if (query instanceof URLSearchParams) { + params = Object.fromEntries(query.entries()); + } else { + params = query; + } + + return { + visualRefresh: parseBoolean(params.visualRefresh, defaultURLParams.visualRefresh), + density: params.density === 'compact' ? 'compact' : defaultURLParams.density, + motionDisabled: parseBoolean(params.motionDisabled, defaultURLParams.motionDisabled), + direction: params.direction === 'rtl' ? 'rtl' : defaultURLParams.direction, + mode: params.mode === 'dark' ? 'dark' : defaultURLParams.mode, + appLayoutWidget: parseBoolean(params.appLayoutWidget, defaultURLParams.appLayoutWidget), + appLayoutToolbar: parseBoolean(params.appLayoutToolbar, defaultURLParams.appLayoutToolbar), + }; +} + +/** + * Format URL parameters back to a query string + * Only includes non-default values + */ +export function formatURLParams(params: Partial): string { + const query = new URLSearchParams(); + + for (const [key, value] of Object.entries(params)) { + const defaultValue = defaultURLParams[key as keyof URLParams]; + if (value !== defaultValue && value !== undefined) { + query.set(key, String(value)); + } + } + + const queryString = query.toString(); + return queryString ? `?${queryString}` : ''; +} diff --git a/dev-server/vite.config.js b/dev-server/vite.config.js new file mode 100644 index 0000000000..287b0b5299 --- /dev/null +++ b/dev-server/vite.config.js @@ -0,0 +1,347 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Vite configuration for the demo development server + * + * React 18 Support: + * When REACT_VERSION=18 is set, this config uses a custom plugin to rewrite + * react/react-dom imports to react18/react-dom18 packages. The packages are + * externalized so Node.js loads them directly (handling CommonJS properly). + * + * We use Vite 5.x because: + * - Vite 5's legacy.proxySsrExternalModules option provides CommonJS interop + * - This allows the react18 package (CommonJS-only) to work correctly in SSR + * - Vite 6+ removed this option and has stricter ESM requirements + */ + +import react from '@vitejs/plugin-react'; +import { createRequire } from 'module'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { defineConfig } from 'vite'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); +const require = createRequire(import.meta.url); + +// eslint-disable-next-line no-undef +const react18 = process.env.REACT_VERSION === '18'; + +if (react18) { + console.log('\nšŸ”„ React 18 mode enabled - using react18/react-dom18 packages\n'); +} + +/** + * Plugin to handle React 18 aliasing + * - For SSR: resolve to react18/react-dom18 and mark as external + * - For client: resolve to the actual file paths for bundling + */ +function react18Plugin() { + // Pre-resolve the package entry points + let react18Entry = null; + let reactDom18Entry = null; + + if (react18) { + try { + react18Entry = require.resolve('react18'); + reactDom18Entry = require.resolve('react-dom18'); + } catch (e) { + console.error('Failed to resolve react18/react-dom18 packages:', e.message); + } + } + + return { + name: 'react18-resolver', + enforce: 'pre', + resolveId(source, importer, options) { + if (!react18) { + return null; + } + + const isSSR = options?.ssr; + + // Handle react imports + if (source === 'react') { + if (isSSR) { + return { id: 'react18', external: true }; + } else { + return react18Entry; + } + } + if (source.startsWith('react/')) { + const subpath = source.slice('react/'.length); + if (isSSR) { + return { id: `react18/${subpath}`, external: true }; + } else { + // Resolve the subpath relative to react18 package + try { + return require.resolve(`react18/${subpath}`); + } catch { + return null; // Let Vite handle it + } + } + } + + // Handle react-dom imports + if (source === 'react-dom') { + if (isSSR) { + return { id: 'react-dom18', external: true }; + } else { + return reactDom18Entry; + } + } + if (source.startsWith('react-dom/')) { + const subpath = source.slice('react-dom/'.length); + if (isSSR) { + return { id: `react-dom18/${subpath}`, external: true }; + } else { + // For react-dom/client, resolve to the actual react-dom18 package's client entry + // require.resolve doesn't work with package subpath exports, so we resolve manually + if (subpath === 'client') { + const reactDom18Dir = path.dirname(reactDom18Entry); + return path.join(reactDom18Dir, 'client.js'); + } + try { + return require.resolve(`react-dom18/${subpath}`); + } catch { + return null; // Let Vite handle it + } + } + } + + // Handle already-rewritten imports (from JSX transform with jsxImportSource) + if (source === 'react18') { + if (isSSR) { + return { id: source, external: true }; + } else { + return react18Entry; + } + } + if (source.startsWith('react18/')) { + const subpath = source.slice('react18/'.length); + if (isSSR) { + return { id: source, external: true }; + } else { + try { + return require.resolve(`react18/${subpath}`); + } catch { + return null; + } + } + } + + if (source === 'react-dom18') { + if (isSSR) { + return { id: source, external: true }; + } else { + return reactDom18Entry; + } + } + if (source.startsWith('react-dom18/')) { + const subpath = source.slice('react-dom18/'.length); + if (isSSR) { + return { id: source, external: true }; + } else { + // For react-dom18/client, resolve to the actual package's client entry + if (subpath === 'client') { + const reactDom18Dir = path.dirname(reactDom18Entry); + return path.join(reactDom18Dir, 'client.js'); + } + try { + return require.resolve(`react-dom18/${subpath}`); + } catch { + return null; + } + } + } + + return null; + }, + }; +} + +/** + * Plugin to skip CSS imports from lib/components since they're already + * collected and injected during SSR via collect-styles.mjs. + * + * The styles.css.js files import .scoped.css - we strip that import + * since the CSS is already in the SSR response. + */ +function skipLibComponentsCssPlugin() { + return { + name: 'skip-lib-components-css', + enforce: 'pre', + + resolveId(source, importer) { + // Skip .scoped.css files from lib/components - they're already in the SSR styles + if (source.endsWith('.scoped.css') && importer && importer.includes('lib/components')) { + return { id: `\0skip-css:${source}`, external: false }; + } + if (source.endsWith('.scoped.css') && source.includes('lib/components')) { + return { id: `\0skip-css:${source}`, external: false }; + } + return null; + }, + + load(id) { + // Return empty module for skipped CSS + if (id.startsWith('\0skip-css:')) { + return 'export default {}'; + } + return null; + }, + + transform(code, id) { + // For styles.css.js files in lib/components, remove the CSS import + if (id.includes('lib/components') && id.endsWith('styles.css.js')) { + return code.replace(/^\s*import\s+['"]\.\/styles\.scoped\.css['"];\s*$/m, ''); + } + return null; + }, + }; +} + +/** + * Plugin to treat all .scss files in pages/ as CSS modules + * Vite only treats .module.scss as CSS modules by default. + * This plugin creates virtual .module.scss files that load the real .scss content. + */ +function pagesScssModulesPlugin() { + const virtualToReal = new Map(); + + return { + name: 'pages-scss-modules', + enforce: 'pre', + + async resolveId(source, importer, options) { + // Only handle .scss files that aren't already .module.scss + if (!source.endsWith('.scss') || source.endsWith('.module.scss')) { + return null; + } + + // Only handle relative imports from within pages/ + if (!importer || !importer.includes('/pages/')) { + return null; + } + + if (!source.startsWith('./') && !source.startsWith('../')) { + return null; + } + + // Resolve the actual path + const resolved = await this.resolve(source, importer, { ...options, skipSelf: true }); + if (!resolved) { + return null; + } + + // Create a virtual .module.scss path + const virtualPath = resolved.id.replace(/\.scss$/, '.virtual.module.scss'); + virtualToReal.set(virtualPath, resolved.id); + + return virtualPath; + }, + + async load(id) { + // Handle our virtual .module.scss files + const realPath = virtualToReal.get(id); + if (realPath) { + const fs = await import('fs/promises'); + const content = await fs.readFile(realPath, 'utf-8'); + return content; + } + return null; + }, + }; +} + +// Build resolve aliases (non-React) +const aliases = [ + // Map @cloudscape-design/components to built lib/components + { find: '@cloudscape-design/components', replacement: path.resolve(rootDir, 'lib/components') }, + { find: '~components', replacement: path.resolve(rootDir, 'lib/components') }, + { find: '~design-tokens', replacement: path.resolve(rootDir, 'lib/design-tokens') }, + { find: '@cloudscape-design/design-tokens', replacement: path.resolve(rootDir, 'lib/design-tokens') }, +]; + +export default defineConfig({ + root: path.resolve(__dirname), + + plugins: [ + skipLibComponentsCssPlugin(), + pagesScssModulesPlugin(), + react18Plugin(), + react({ + // Configure JSX runtime for React 18 mode + jsxImportSource: react18 ? 'react18' : 'react', + }), + ], + + resolve: { + alias: aliases, + extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx'], + }, + + // SSR configuration + ssr: { + // Target Node.js for SSR + target: 'node', + // Include these packages in the SSR bundle (don't externalize them) + noExternal: [ + '@cloudscape-design/components', + '@cloudscape-design/design-tokens', + '@cloudscape-design/collection-hooks', + '@cloudscape-design/component-toolkit', + '@cloudscape-design/theming-runtime', + '@cloudscape-design/global-styles', + ], + // Externalize Node.js modules + external: ['glob', 'fs', 'path', 'node:fs', 'node:path', 'node:url'], + }, + + // Legacy options for better CommonJS interop in SSR + legacy: { + proxySsrExternalModules: true, + }, + + // Optimize dependencies for faster dev startup + optimizeDeps: { + include: react18 + ? ['react18', 'react-dom18', 'react-router-dom', 'prop-types', 'react-is', 'react-keyed-flatten-children'] + : ['react', 'react-dom', 'react-router-dom', 'prop-types', 'react-is', 'react-keyed-flatten-children'], + // Don't try to pre-bundle local lib/components - it has TypeScript type exports + // that esbuild can't handle, and we want HMR for development anyway + exclude: ['@cloudscape-design/components', '~components'], + // Don't scan pages for dependencies - they import from local lib + entries: [], + }, + + // CSS handling + css: { + modules: { + localsConvention: 'camelCase', + }, + preprocessorOptions: { + scss: { + // Add the design-tokens path for @use '~design-tokens' + includePaths: [path.resolve(rootDir, 'lib')], + }, + }, + }, + + // Build configuration (for potential production builds) + build: { + outDir: path.resolve(rootDir, 'dist/ssr'), + emptyOutDir: true, + }, + + // Server configuration + server: { + port: 3000, + strictPort: false, + fs: { + // Allow serving files from the entire project + allow: [rootDir], + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 19b7fbdb38..83daa27b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "@types/react-test-renderer": "^16.9.12", "@types/react-transition-group": "^4.4.4", "@types/webpack-env": "^1.16.3", + "@vitejs/plugin-react": "^4.3.4", "axe-core": "^4.7.2", "babel-jest": "^29.7.0", "change-case": "^4.1.2", @@ -77,6 +78,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^60.0.0", "execa": "^4.1.0", + "express": "^4.21.2", "fs-extra": "^11.2.0", "glob": "^11.1.0", "globals": "^16.1.0", @@ -121,6 +123,7 @@ "types-react18": "npm:@types/react@^18.3.24", "typescript": "^5.9.2", "typescript-eslint": "^8.44.0", + "vite": "^5.4.21", "wait-on": "^8.0.2", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", @@ -143,18 +146,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "dev": true, @@ -766,20 +757,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -795,14 +788,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", + "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.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@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" }, "engines": { @@ -824,6 +819,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "dev": true, @@ -837,13 +842,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -869,7 +876,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -885,23 +894,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.5", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1144,6 +1157,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "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-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "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/runtime": { "version": "7.27.6", "license": "MIT", @@ -1165,39 +1210,33 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", + "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.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.6", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2784,28 +2823,29 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", "dev": true, "license": "MIT", "engines": { @@ -2827,7 +2867,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3374,6 +3416,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", "dev": true, @@ -3418,127 +3467,435 @@ } } }, - "node_modules/@sideway/address": { - "version": "4.1.5", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@sideway/formula": { - "version": "3.0.1", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sidvind/better-ajv-errors": { - "version": "2.1.3", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "chalk": "^4.1.0" - }, - "engines": { - "node": ">= 16.14" - }, - "peerDependencies": { - "ajv": "4.11.8 - 8" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sidvind/better-ajv-errors/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@size-limit/esbuild": { - "version": "11.2.0", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "nanoid": "^5.1.0" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "size-limit": "11.2.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@size-limit/file": { - "version": "11.2.0", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "size-limit": "11.2.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@size-limit/preset-small-lib": { - "version": "11.2.0", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@size-limit/esbuild": "11.2.0", - "@size-limit/file": "11.2.0", - "size-limit": "11.2.0" - }, - "peerDependencies": { - "size-limit": "11.2.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@smithy/abort-controller": { - "version": "4.0.4", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sidvind/better-ajv-errors": { + "version": "2.1.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/@sidvind/better-ajv-errors/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@size-limit/esbuild": { + "version": "11.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "nanoid": "^5.1.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "size-limit": "11.2.0" + } + }, + "node_modules/@size-limit/file": { + "version": "11.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "size-limit": "11.2.0" + } + }, + "node_modules/@size-limit/preset-small-lib": { + "version": "11.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@size-limit/esbuild": "11.2.0", + "@size-limit/file": "11.2.0", + "size-limit": "11.2.0" + }, + "peerDependencies": { + "size-limit": "11.2.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { "version": "4.1.4", "dev": true, "license": "Apache-2.0", @@ -4907,6 +5264,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", @@ -17195,6 +17573,16 @@ "version": "18.3.1", "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "5.3.4", "dev": true, @@ -20717,6 +21105,538 @@ "node": ">=10.13.0" } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "dev": true, diff --git a/package.json b/package.json index 952123f6f0..9d76898c4b 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,11 @@ "build:react18": "cross-env NODE_ENV=production REACT_VERSION=18 gulp build", "start": "npm-run-all --parallel start:watch start:dev", "start:watch": "gulp watch", - "start:dev": "cross-env NODE_ENV=development webpack serve --config pages/webpack.config.cjs", + "start:dev": "node dev-server/dev.mjs", + "start:dev:server": "node dev-server/server.mjs", "start:integ": "cross-env NODE_ENV=development webpack serve --config pages/webpack.config.integ.cjs", "start:react18": "npm-run-all --parallel start:watch start:react18:dev", - "start:react18:dev": "cross-env NODE_ENV=development REACT_VERSION=18 webpack serve --config pages/webpack.config.cjs", + "start:react18:dev": "cross-env REACT_VERSION=18 node dev-server/server.mjs", "prepare": "husky" }, "dependencies": { @@ -80,6 +81,7 @@ "@types/react-test-renderer": "^16.9.12", "@types/react-transition-group": "^4.4.4", "@types/webpack-env": "^1.16.3", + "@vitejs/plugin-react": "^4.3.4", "axe-core": "^4.7.2", "babel-jest": "^29.7.0", "change-case": "^4.1.2", @@ -100,6 +102,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^60.0.0", "execa": "^4.1.0", + "express": "^4.21.2", "fs-extra": "^11.2.0", "glob": "^11.1.0", "globals": "^16.1.0", @@ -144,6 +147,7 @@ "types-react18": "npm:@types/react@^18.3.24", "typescript": "^5.9.2", "typescript-eslint": "^8.44.0", + "vite": "^5.4.21", "wait-on": "^8.0.2", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", diff --git a/pages/common/flush-response.ts b/pages/common/flush-response.ts index 697de7e450..f56684f2ee 100644 --- a/pages/common/flush-response.ts +++ b/pages/common/flush-response.ts @@ -8,6 +8,10 @@ export interface WindowWithFlushResponse extends Window { declare const window: WindowWithFlushResponse; export function enhanceWindow() { + // Guard for SSR - only run on client + if (typeof window === 'undefined') { + return; + } window.__pendingCallbacks = []; window.__flushServerResponse = () => { for (const cb of window.__pendingCallbacks) { diff --git a/pages/drag-handle/drag-handle-interaction-state-hook.page.tsx b/pages/drag-handle/drag-handle-interaction-state-hook.page.tsx index b1bee04708..47323722ec 100644 --- a/pages/drag-handle/drag-handle-interaction-state-hook.page.tsx +++ b/pages/drag-handle/drag-handle-interaction-state-hook.page.tsx @@ -136,7 +136,11 @@ const TestBoardItemButton: React.FC = () => {

    - {renderInPortal ?
    {createPortal(buttonComp, document.body)}
    : buttonComp} + {renderInPortal ? ( +
    {typeof document !== 'undefined' && createPortal(buttonComp, document.body)}
    + ) : ( + buttonComp + )} ); diff --git a/pages/funnel-analytics/with-async-table.page.tsx b/pages/funnel-analytics/with-async-table.page.tsx index c36c17b618..b3f053d79b 100644 --- a/pages/funnel-analytics/with-async-table.page.tsx +++ b/pages/funnel-analytics/with-async-table.page.tsx @@ -31,7 +31,10 @@ import { } from '../table/shared-configs'; const componentMetricsLog: any[] = []; -(window as any).__awsuiComponentlMetrics__ = componentMetricsLog; +// Guard for SSR - only set on client +if (typeof window !== 'undefined') { + (window as any).__awsuiComponentlMetrics__ = componentMetricsLog; +} setComponentMetrics({ componentMounted: props => { diff --git a/pages/onboarding/with-app-layout.page.tsx b/pages/onboarding/with-app-layout.page.tsx index 6fdb2cade0..84826caad4 100644 --- a/pages/onboarding/with-app-layout.page.tsx +++ b/pages/onboarding/with-app-layout.page.tsx @@ -96,7 +96,10 @@ export default function OnboardingDemoPage() { }, []); const onFeedbackClick = useCallback(() => { - window.prompt('Please enter your feedback here:'); + // Guard for SSR + if (typeof window !== 'undefined') { + window.prompt('Please enter your feedback here:'); + } }, []); return ( @@ -127,7 +130,7 @@ export default function OnboardingDemoPage() { i18nStrings={tutorialPanelStrings} tutorials={tutorials} onFeedbackClick={onFeedbackClick} - downloadUrl={window.location.href} + downloadUrl={typeof window !== 'undefined' ? window.location.href : ''} /> ), }, diff --git a/pages/select/select.test.async.page.tsx b/pages/select/select.test.async.page.tsx index 84b03413ec..74d32bcad0 100644 --- a/pages/select/select.test.async.page.tsx +++ b/pages/select/select.test.async.page.tsx @@ -22,7 +22,8 @@ interface ExtendedWindow extends Window { __pendingRequests: Array; } declare const window: ExtendedWindow; -const pendingRequests: Array = (window.__pendingRequests = []); +// Guard for SSR - only initialize on client +const pendingRequests: Array = typeof window !== 'undefined' ? (window.__pendingRequests = []) : []; const ITEMS_PER_PAGE = 25; const MAX_PAGES = 3; diff --git a/pages/select/select.test.events.page.tsx b/pages/select/select.test.events.page.tsx index b18de8a519..c20bfc278c 100644 --- a/pages/select/select.test.events.page.tsx +++ b/pages/select/select.test.events.page.tsx @@ -17,13 +17,26 @@ interface ExtendedWindow extends Window { } declare const window: ExtendedWindow; const appendLog = (text: string) => { + // Guard for SSR + if (typeof window === 'undefined') { + return; + } if (!window.__eventsLog) { window.__eventsLog = []; } window.__eventsLog.push(text); }; -const clearLog = () => (window.__eventsLog = []); -window.__clearEvents = clearLog; +const clearLog = () => { + // Guard for SSR + if (typeof window === 'undefined') { + return; + } + window.__eventsLog = []; +}; +// Guard for SSR - only set on client +if (typeof window !== 'undefined') { + window.__clearEvents = clearLog; +} export default function SelectEventsPage() { const [selectedOption, setValue] = useState(null); diff --git a/pages/select/virtual-resize.page.tsx b/pages/select/virtual-resize.page.tsx index d9d30ed1bc..6d07314354 100644 --- a/pages/select/virtual-resize.page.tsx +++ b/pages/select/virtual-resize.page.tsx @@ -24,7 +24,10 @@ const options = [ export default function () { const [selectedOption, setSelectedOption] = useState(null); const [shrunk, setShrunk] = useState(false); - window.__shrinkComponent = setShrunk; + // Guard for SSR - only set on client + if (typeof window !== 'undefined') { + window.__shrinkComponent = setShrunk; + } const style = shrunk ? { width: '100px' } : undefined; return ( diff --git a/pages/theming/themed-stroke-width.page.tsx b/pages/theming/themed-stroke-width.page.tsx index e9b0ee7a3f..762409ee98 100644 --- a/pages/theming/themed-stroke-width.page.tsx +++ b/pages/theming/themed-stroke-width.page.tsx @@ -55,6 +55,10 @@ export default function ThemedStrokeWidthPage() { // Reload page once after initial load to fix theme application useLayoutEffect(() => { + // Guard for SSR + if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') { + return; + } const hasReloaded = sessionStorage.getItem('themed-stroke-width-reloaded'); if (!hasReloaded) { sessionStorage.setItem('themed-stroke-width-reloaded', 'true'); diff --git a/pages/tree-view/permutations.page.tsx b/pages/tree-view/permutations.page.tsx index ca0d4f564c..5d29a2dd8c 100644 --- a/pages/tree-view/permutations.page.tsx +++ b/pages/tree-view/permutations.page.tsx @@ -54,7 +54,10 @@ export default function TreeViewPermuations() { checked={urlParams.expandAll ?? false} onChange={event => { setUrlParams({ expandAll: event.detail.checked }); - window.location.reload(); + // Guard for SSR + if (typeof window !== 'undefined') { + window.location.reload(); + } }} > Expand all