diff options
| -rw-r--r-- | .env.example | 5 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | README.md | 42 | ||||
| -rw-r--r-- | src/router.tsx | 12 | ||||
| -rw-r--r-- | src/vite-env.d.ts | 4 | ||||
| -rw-r--r-- | vite.config.ts | 58 |
6 files changed, 98 insertions, 28 deletions
diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..579ded3 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Router mode: history or hash (default: history) +VITE_ROUTER_MODE=history + +# Deployment base pathname (default: /), e.g. /app/ +VITE_BASE_URL=/ @@ -22,3 +22,8 @@ dist-ssr *.njsproj *.sln *.sw? + +# Local environment configuration +.env +.env.* +!.env.example @@ -2,12 +2,47 @@ This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. +## Environment variables + +Copy `.env.example` to `.env` and adjust the values: + +```dotenv +VITE_ROUTER_MODE=hash +VITE_BASE_URL=/app/ +``` + +| Variable | Default | Description | +| --- | --- | --- | +| `VITE_ROUTER_MODE` | `history` | `history` or `hash`; other values fail startup/build. | +| `VITE_BASE_URL` | `/` | Deployment pathname for assets and routing, such as `/app/`. Must start with a single `/`, without query strings, fragments, whitespace, or backslashes. A trailing `/` is added automatically. | + +With `/app/`, the About page is `/app/about` in history mode and +`/app/#/about` in hash mode. History mode requires the host to serve +`/app/index.html` for application paths under `/app/`. Hash mode does not +require a server-side route fallback. + +Shell environment variables override `.env` values: + +```sh +VITE_ROUTER_MODE=hash VITE_BASE_URL=/app/ pnpm build +``` + +Vite also loads `.env.local`, `.env.[mode]`, and `.env.[mode].local`. +Mode-specific files override generic files; shell variables have highest priority. +For example, `pnpm build --mode staging` loads `.env.staging`. +Environment files are ignored by Git except for `.env.example`. +Restart the development server after changing variables. Production values are +embedded during the build; changing server environment variables requires rebuilding. +Custom frontend variables use the `VITE_` prefix and are read through +`import.meta.env`. These values are public, so do not put secrets in them. +See [Vite environment variables](https://vite.dev/guide/env-and-mode). + ## Routing Routing uses [React Router Data Mode](https://reactrouter.com/start/data/installation). `src/route-config.ts` is the single source for page paths, components, titles, icons, menu groups, and default expanded groups. It generates the page routes and -menu data. `src/router.tsx` creates the router with `createBrowserRouter`, and +menu data. `src/router.tsx` creates the router with `createBrowserRouter` or `createHashRouter`, and `src/main.tsx` renders it with `RouterProvider`. `src/App.tsx` configures the Spectrum Provider and renders the route `Outlet` inside `BasicLayout`. `src/components/layout-basic.tsx` owns the sidebar, top bar, collapse state, and content area, @@ -65,8 +100,9 @@ absolute `path`, `Component`, and optional `Icon`. Use `children` for menu group and `defaultExpanded` to control their initial expansion. Menu groups do not change page URLs. Set `hidden: true` to keep a route out of navigation, as with the 404 page. Desktop, collapsed, and mobile menus all use this configuration. -When deploying, configure your host to serve `index.html` for application URLs -so direct visits and refreshes on nested routes work. +When deploying in history mode, configure your host to serve `index.html` from +the configured base directory for application URLs so direct visits and refreshes +on nested routes work. Currently, two official plugins are available: diff --git a/src/router.tsx b/src/router.tsx index 8128f45..77158f4 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,11 +1,17 @@ -import { createBrowserRouter } from 'react-router' +import { createBrowserRouter, createHashRouter } from 'react-router' import App from './App' import { pageRoutes } from './route-config' -export const router = createBrowserRouter([ +const isHashRouter = import.meta.env.VITE_ROUTER_MODE === 'hash' +const createRouter = isHashRouter ? createHashRouter : createBrowserRouter + +export const router = createRouter([ { path: '/', Component: App, children: pageRoutes, }, -]) +], { + // The deployment base precedes the hash and must not be repeated inside it. + basename: isHashRouter ? '/' : import.meta.env.BASE_URL, +}) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..f0d802a --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,4 @@ +interface ImportMetaEnv { + readonly VITE_ROUTER_MODE?: 'hash' | 'history' + readonly VITE_BASE_URL?: string +} diff --git a/vite.config.ts b/vite.config.ts index c012b56..6771fca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,30 +1,44 @@ import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import macros from 'unplugin-parcel-macros'; import optimizeLocales from '@react-aria/optimize-locales-plugin'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - macros.vite(), // Must be first! - react(), - { - ...optimizeLocales.vite({ locales: ['en-US', 'zh-CN'] }), - enforce: 'pre' - } - ], - build: { - target: ['es2022'], - // Lightning CSS produces a much smaller CSS bundle than the default minifier. - cssMinify: 'lightningcss', - rollupOptions: { - output: { - // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. - // Because atomic CSS has so much overlap between components, loading all CSS up front results in - // smaller bundles instead of producing duplication between pages. - manualChunks(id) { - if (/macro-(.*)\.css$/.test(id) || /@react-spectrum\/s2\/.*\.css$/.test(id)) { - return 's2-styles'; +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()) + const routerMode = env.VITE_ROUTER_MODE ?? 'history' + const base = env.VITE_BASE_URL ?? '/' + + if (routerMode !== 'hash' && routerMode !== 'history') { + throw new Error('VITE_ROUTER_MODE must be "hash" or "history".') + } + if (!base.startsWith('/') || base.startsWith('//') || /[?#\\\s]/.test(base)) { + throw new Error('VITE_BASE_URL must be an absolute pathname, such as "/" or "/app/".') + } + + return { + base: base.endsWith('/') ? base : `${base}/`, + plugins: [ + macros.vite(), // Must be first! + react(), + { + ...optimizeLocales.vite({ locales: ['en-US', 'zh-CN'] }), + enforce: 'pre' + } + ], + build: { + target: ['es2022'], + // Lightning CSS produces a much smaller CSS bundle than the default minifier. + cssMinify: 'lightningcss', + rollupOptions: { + output: { + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + manualChunks(id) { + if (/macro-(.*)\.css$/.test(id) || /@react-spectrum\/s2\/.*\.css$/.test(id)) { + return 's2-styles'; + } } } } |
