aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.env.example5
-rw-r--r--.gitignore5
-rw-r--r--README.md42
-rw-r--r--src/router.tsx12
-rw-r--r--src/vite-env.d.ts4
-rw-r--r--vite.config.ts58
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=/
diff --git a/.gitignore b/.gitignore
index a547bf3..e6def8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Local environment configuration
+.env
+.env.*
+!.env.example
diff --git a/README.md b/README.md
index 9299ae6..69b3b7e 100644
--- a/README.md
+++ b/README.md
@@ -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';
+ }
}
}
}