mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
♻️ refactor: migrate frontend from Next.js App Router to Vite SPA (#12404)
* init plan
* 📝 docs: update SPA plan for dev mode Worker cross-origin handling
- Clarified the handling of Worker cross-origin issues in dev mode, emphasizing the need for `workerPatch` to wrap cross-origin URLs as blob URLs.
- Enhanced the explanation of the dev mode's resource URL rewriting process for better understanding.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: Phase 1 - 环境变量整治
- Fix Pyodide env var mismatch (NEXT_PUBLIC_PYPI_INDEX_URL → pythonEnv.NEXT_PUBLIC_PYODIDE_PIP_INDEX_URL)
- Consolidate python.ts to use pythonEnv instead of direct process.env
- Remove NEXT_PUBLIC_ prefix from server-side MARKET_BASE_URL (5 files)
* 🏗️ chore: Phase 2 - Vite 工程搭建
- Add vite.config.ts with dual build (desktop/mobile via MOBILE env)
- Add index.html SPA template with __SERVER_CONFIG__ placeholder
- Add entry.desktop.tsx and entry.mobile.tsx SPA entry points
- Add dev:spa, dev:spa:mobile, build:spa, build:spa:copy scripts
- Install @vitejs/plugin-react and linkedom
* ♻️ refactor: Phase 3 - 第一方包 Next.js 解耦
- Replace next/link with <a> in builtin-tool-web-browsing (4 files, external links)
- Replace next/image with <img> in builtin-tool-agent-builder/InstallPlugin.tsx
- Add Vite import.meta.env compat for isDesktop in const/version.ts, builtin-tool-gtd, builtin-tool-group-management
* ♻️ refactor: Phase 4a - Auth 页面改用直接 next/navigation 和 next/link
- 9 auth files: @/libs/next/navigation → next/navigation
- 5 auth files: @/libs/next/Link → next/link
- Auth pages remain in Next.js App Router, need direct Next.js imports
* ♻️ refactor: Phase 4b - Next.js 抽象层替换为 react-router-dom/vanilla React
- navigation.ts: useRouter/usePathname/useSearchParams/useParams → react-router-dom
- navigation.ts: redirect/notFound → custom error throws
- navigation.ts: useServerInsertedHTML → no-op for SPA
- Link.tsx: next/link → react-router-dom Link adapter (href→to, external→<a>)
- Image.tsx: next/image → <img> wrapper with fill/style support
- dynamic.tsx: next/dynamic → React.lazy + Suspense wrapper
* ✨ feat: Phase 5 - 新建 SPAGlobalProvider
- Create SPAServerConfig type (analyticsConfig, clientEnv, theme, featureFlags, locale)
- Add window.__SERVER_CONFIG__ and __MOBILE__ to global.d.ts
- Create SPAGlobalProvider (client-only Provider tree mirroring GlobalProvider)
- Includes AuthProvider for user session support
- Update entry.desktop.tsx and entry.mobile.tsx to wrap with SPAGlobalProvider
* ♻️ refactor: add SPA catch-all route handler with Vite dev proxy
- Create (spa)/[[...path]]/route.ts for serving SPA HTML
- Dev mode: proxy Vite dev server, rewrite asset URLs, inject Worker patch
- Prod mode: read pre-built HTML templates
- Build SPAServerConfig with analytics, theme, clientEnv, featureFlags
- Update middleware to pass SPA routes through to catch-all
* ♻️ refactor: skip auth checks for SPA routes in middleware
SPA pages are all public (no sensitive data in HTML).
Auth is handled client-side by SPAGlobalProvider's AuthProvider.
Only Next.js auth routes and API endpoints go through session checks.
* ♻️ refactor: replace Next.js-specific analytics with vanilla JS
- Google.tsx: replace @next/third-parties/google with direct gtag script
- ReactScan.tsx: replace react-scan/monitoring/next with generic script
- Desktop.tsx: replace next/script with native script injection
* ♻️ refactor: migrate @t3-oss/env-nextjs to @t3-oss/env-core
Replace framework-specific env validation with framework-agnostic version.
Add clientPrefix where client schemas exist.
* ♻️ refactor: replace next-mdx-remote/rsc with react-markdown
Use client-side react-markdown for MDX rendering instead of
Next.js RSC-dependent next-mdx-remote.
* 🔧 chore: update build scripts and Dockerfile for SPA integration
- build:docker now includes SPA build + copy steps
- dev defaults to Vite SPA, dev:next for Next.js backend
- Dockerfile copies public/spa/ assets for production
- Add public/spa/ to .gitignore (build artifact)
* 🗑️ chore: remove old Next.js route segment files and serwist PWA
- Delete [variants] page.tsx, error.tsx, not-found.tsx, loading.tsx
- Delete root loading.tsx and empty [[...path]] directory
- Delete unused loaders directory
- Remove @serwist/next PWA wrapper from Next.js config
* plan2
* ✨ feat: add locale detection script to index.html for SPA dev mode
* ♻️ refactor: remove locale and theme from SPAServerConfig
* ✨ feat: add [locale] segment with force-static and SEO meta generation
* ♻️ refactor: remove theme/locale reads from SPAGlobalProvider
* ✨ feat: set vite base to /spa/ for production builds
* ✨ feat: auto-generate spaHtmlTemplates from vite build output
* 🔧 chore: register dev:next task in turbo.json for parallel dev startup
* ♻️ refactor: rename (spa) route group to spa segment, rewrite SPA routes via middleware
* ✨ feat: add Vite-compatible i18n/locale modules with import.meta.glob and resolve aliases
* 🔧 fix: use custom Vite plugin for module redirects instead of resolve.alias
* very important
* build
* 🔧 chore: update build scripts and clean up Vite configuration by removing unused plugin and code
Signed-off-by: Innei <tukon479@gmail.com>
* 🗑️ refactor: remove all electron modifier scripts
Modifiers are no longer needed with Vite SPA renderer build.
* ✨ feat: add Vite renderer entry to electron-vite config
Add renderer build configuration to electron-vite, replacing the old
Next.js shadow workspace build flow. Delete buildNextApp.mts and
moveNextExports.ts, update package.json scripts accordingly.
* ✨ feat: add .desktop suffix files for eager i18n loading
Create 4 .desktop files that use import.meta.glob({ eager: true })
for synchronous locale access in Electron desktop builds, replacing
the async lazy-loading used in web SPA builds.
* 🔧 refactor: adapt Electron main process for Vite renderer
Replace nextExportDir with rendererDir, update protocol from
app://next to app://renderer, simplify file resolution to SPA
fallback pattern, update _next/ asset paths to /assets/.
* 🔧 chore: update electron-builder files config for Vite renderer
Replace dist/next references with dist/renderer, remove Next.js
specific exclusion rules no longer applicable to Vite output.
* 🗑️ chore: remove @ast-grep/napi dependency
No longer needed after removing electron modifier scripts.
* 🔧 refactor: unify isDesktop to __ELECTRON__ compile-time constant
Remove NEXT_PUBLIC_IS_DESKTOP_APP and VITE_IS_DESKTOP_APP env vars.
Unify isDesktop in @lobechat/const using __ELECTRON__ defined by Vite.
Re-export from builtin-tool packages. Scripts use DESKTOP_BUILD.
* update
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: use electron-vite ELECTRON_RENDERER_URL instead of hardcoded port 3015
Replace hardcoded http://localhost:3015 with process.env.ELECTRON_RENDERER_URL
injected by electron-vite dev server. Clean up stale Next.js references.
* 🐛 fix: use local renderer-entry shim to resolve Vite root path issue
HTML entry ../../src/entry.desktop.tsx resolves to /src/entry.desktop.tsx
in URL space, which Vite cannot find within apps/desktop/ root. Add a
local shim that imports across root via module resolver instead.
* 🔧 refactor: extract shared renderer Vite config into sharedRendererConfig
Deduplicate plugins (nodeModuleStub, platformResolve, tsconfigPaths) and
define (__MOBILE__, __ELECTRON__, process.env) between root vite.config.ts
and electron.vite.config.ts renderer section.
* 🔧 refactor: move all renderer plugins and optimizeDeps into shared config
sharedRendererPlugins now includes react, codeInspectorPlugin alongside
nodeModuleStub, platformResolve, tsconfigPaths. Add sharedOptimizeDeps
for pre-bundling list. Both root and electron configs consume shared only.
* 🐛 fix: set electron renderer root to monorepo root for correct glob resolution
import.meta.glob with absolute paths (e.g. /node_modules/antd/...) resolved
within apps/desktop/ instead of monorepo root. Change renderer root to ROOT_DIR,
add electronDesktopHtmlPlugin middleware to rewrite / to /apps/desktop/index.html,
and remove the now-unnecessary renderer-entry.ts shim.
* desktop vite !!
Signed-off-by: Innei <tukon479@gmail.com>
* sync import !!
Signed-off-by: Innei <tukon479@gmail.com>
* clean ci!!
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: update SPA path structure and clean up dependencies
- Changed the path in .gitignore and related files from [locale] to [variants] for SPA templates.
- Updated index.html to set body height to 100%.
- Cleaned up package.json by removing unused dependencies and reorganizing devDependencies.
- Refactored RendererUrlManager to use a constant for SPA entry HTML path.
- Removed obsolete route.ts file from the SPA structure.
- Adjusted proxy configuration to reflect the new SPA path structure.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: update build script to include mobile SPA build
- Modified the build script in package.json to add the mobile SPA build step.
- Ensured the build process accommodates both desktop and mobile SPA versions.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: update build scripts and improve file encoding consistency
- Modified the build script in package.json to ensure the SPA copy step runs after the build.
- Updated file encoding in generateSpaTemplates.mts from 'utf-8' to 'utf8' for consistency.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 fix: correct Blob import syntax and update global server config type
- Fixed the Blob import syntax in route.ts to ensure proper module loading.
- Updated the global server configuration type in global.d.ts for improved type safety.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 test: update RendererUrlManager test to reflect new file path
- Modified the mock implementation in RendererUrlManager.test.ts to check for the updated file path '/mock/export/out/apps/desktop/index.html'.
- Adjusted the expected resolved path in the test to match the new structure.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: remove catch-all example file and update imports
- Deleted the catch-all example file `catch-all.eg.ts` to streamline the codebase.
- Updated import paths in `ClientResponsiveLayout.tsx` and `ClientResponsiveContent/index.tsx` to use the new dynamic import location.
- Added type declarations for HTML templates in `spaHtmlTemplates.d.ts`.
- Adjusted `tsconfig.json` to include the updated file structure.
- Enhanced type definitions in `global.d.ts` and fixed locale loading in `locale.vite.ts`.
Signed-off-by: Innei <tukon479@gmail.com>
* e2e
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: remove unused build script for Vercel deployment
- Deleted the `build:vercel` script from package.json to streamline the build process.
- Ensured the remaining build scripts are organized and relevant.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 config: update Vite build input for mobile support
- Changed the build input path in vite.config.ts to conditionally use 'index.mobile.html' for mobile builds, enhancing support for mobile SPA versions.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 feat: add compatibility checks for import maps and cascade layers
- Implemented functions to check for browser support of import maps and CSS cascade layers.
- Redirected users to a compatibility page if their browser does not support the required features.
- Updated the build script in package.json to use the experimental analyze command for better performance.
Signed-off-by: Innei <tukon479@gmail.com>
* chore: rename
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 feat: refactor authentication layout and introduce global providers
- Created a new `RootLayout` component to streamline the layout structure.
- Removed the old layout file for variants and integrated necessary features into the new layout.
- Added `AuthGlobalProvider` to manage authentication context and server configurations.
- Introduced language and theme selection components for enhanced user experience.
- Updated various components to utilize the new context and improve modularity.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 config: exclude build artifacts from serverless functions
- Updated the `next.config.ts` to exclude SPA, desktop, and mobile build artifacts from serverless functions.
- Added paths for `public/spa/**`, `dist/**`, `apps/desktop/build/**`, and `packages/database/migrations/**` to the exclusion list.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 config: refine exclusion of build artifacts from serverless functions
- Updated `next.config.ts` to specify exclusion paths for desktop and mobile build artifacts.
- Changed exclusions from `dist/**` and `apps/desktop/build/**` to `dist/desktop/**`, `dist/mobile/**`, and `apps/desktop/**` for better clarity and organization.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 fix: update BrowserRouter basename for local development
- Modified the `ClientRouter` component to conditionally set the `basename` of `BrowserRouter` based on the `__DEBUG_PROXY__` variable, improving local development experience.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 feat: implement mobile SPA workflow and S3 asset management
- Added a new workflow for building and uploading mobile SPA assets to S3, including environment variable configurations in `.env.example`.
- Updated `package.json` to include a new script for the mobile SPA workflow.
- Enhanced the Vite configuration to support dynamic CDN base paths.
- Refactored the template generation script to handle mobile HTML templates more effectively.
- Introduced new modules for uploading assets to S3 and generating mobile HTML templates.
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix: extract origin from MOBILE_S3_PUBLIC_DOMAIN to prevent double key prefix
* 🔧 fix: update mobile HTML template to use the latest asset versions
- Modified the mobile HTML template to reference the updated JavaScript asset version for improved functionality.
- Ensured consistency in the template structure while maintaining existing styles and scripts.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: update dependencies and refine service worker integration
- Removed outdated dependencies related to Serwist from package.json and tsconfig.json.
- Added vite-plugin-pwa to enhance PWA capabilities in the Vite configuration.
- Updated service worker registration logic in the PWA installation component.
- Introduced a new local development proxy route for debugging purposes.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: refactor development scripts and remove Turbo configuration
- Updated the `dev` script in `package.json` to use a new startup sequence script for improved development workflow.
- Removed the outdated `turbo.json` configuration file as it is no longer needed.
- Introduced `devStartupSequence.mts` to manage the startup of Next.js and Vite processes concurrently.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 feat: update entry points and introduce debug proxy for local development
- Changed the main entry point in `index.html` from `entry.desktop.tsx` to `entry.web.tsx` for improved web compatibility.
- Added an `initialize.ts` file to enable `immer`'s `enableMapSet` functionality.
- Introduced a new `__DEBUG_PROXY__` variable in global types to support local development proxy features.
- Implemented a debug proxy route to facilitate local development with dynamic HTML injection and script handling.
- Removed outdated mobile routing components to streamline the codebase.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: replace BrowserRouter with RouterProvider for improved routing
- Updated entry points for desktop, mobile, and web to utilize RouterProvider and createAppRouter for better routing management.
- Removed the deprecated renderRoutes function in favor of a more streamlined router configuration.
- Enhanced router setup to support error boundaries and dynamic routing.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: remove direct access handling for SPA routes in proxy configuration
- Eliminated the handling of direct access to pre-rendered SPA pages in the proxy configuration.
- Simplified the request processing logic by removing checks for SPA routes, streamlining the middleware response flow.
Signed-off-by: Innei <tukon479@gmail.com>
* update
* 🔧 refactor: enhance Worker instantiation logic in mobile HTML template
* 🐛 fix: remove duplicate waitForPageWorkspaceReady calls in page CRUD e2e steps
* 🔧 refactor: simplify createTracePayload function by using btoa for base64 encoding
* 🔧 refactor: specify locales in import.meta.glob for dayjs and antd
* 🔧 refactor: replace Node.js Buffer with web-compatible btoa for base64 encoding in file upload
* 🐛 fix: disable consistent-type-imports rule for mdx files to prevent eslint crash
* 🔧 refactor: add height style to root div for consistent layout
* 🔧 refactor: replace btoa with Buffer for base64 encoding in trace and file upload handling
* 🔧 refactor: extract nextjsOnlyRoutes to a separate file for better organization
* 🔧 refactor: enable Immer MapSet plugin in tests for better state management
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 refactor: integrate sharedRollupOutput configuration and increase cache size for better performance
Signed-off-by: Innei <tukon479@gmail.com>
* 🗑️ chore: remove obsolete desktop.routes.test.ts file as it is no longer needed
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix: use cross-env for env vars in npm scripts (Windows CI)
Co-authored-by: Cursor <cursoragent@cursor.com>
* 🔧 chore: update Dockerfile for web-only build and adjust npm scripts to use pnpm
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: enhance Dockerfile prebuild process with environment checks and add new dependencies
- Updated Dockerfile to include environment checks before removing desktop-only code.
- Added new dependencies in package.json: @aws-sdk/client-bedrock-runtime, @opentelemetry/auto-instrumentations-node, @opentelemetry/resources, @opentelemetry/sdk-metrics, and ajv.
- Configured Rollup to exclude @aws-sdk/client-bedrock-runtime from the SPA bundle.
- Introduced dockerPrebuild.mts script for environment variable validation and information logging.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: enhance Vite and Electron configurations with environment loading and trace encoding improvements
- Updated Vite and Electron configurations to load environment variables using loadEnv.
- Modified trace encoding in utils to use TextEncoder for better compatibility.
- Adjusted sharedRendererConfig to expose only necessary public environment variables.
Signed-off-by: Innei <tukon479@gmail.com>
* 🗑️ chore: remove plans directory (migrated to discussion)
* ♻️ refactor: inject NEXT_PUBLIC_* env per key in Vite define
Co-authored-by: Cursor <cursoragent@cursor.com>
* ✨ feat: add loading screen with animation to enhance user experience
- Introduced a loading screen with a brand logo and animations for better visual feedback during loading times.
- Implemented CSS styles for the loading screen and animations in index.html.
- Removed the loading screen from the DOM once the layout is ready using useLayoutEffect in SPAGlobalProvider.
Signed-off-by: Innei <tukon479@gmail.com>
* 🗑️ chore: remove unnecessary external dependency from Vite configuration
- Eliminated the external dependency '@aws-sdk/client-bedrock-runtime' from the Vite configuration to streamline the build process for the SPA bundle.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat: add web app manifest link in index.html and enable PWA support in Vite configuration
- Added a link to the web app manifest in index.html to enhance PWA capabilities.
- Enabled manifest support in Vite configuration for improved service worker functionality.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔧 chore: update link rel attributes for improved SEO and consistency
- Modified link rel attributes in multiple components to remove 'noreferrer' and standardize to 'nofollow'.
- Adjusted imports in PageContent components for better organization.
Signed-off-by: Innei <tukon479@gmail.com>
* update provider
* ✨ feat: enhance loading experience and update package dependencies
- Added a loading screen with animations and a brand logo in index.html for improved user feedback during loading times.
- Introduced CSS styles for the loading screen and animations.
- Updated package.json files across multiple packages to include "@lobechat/const" as a dependency.
Signed-off-by: Innei <tukon479@gmail.com>
* fix: update proxy
Signed-off-by: Innei <tukon479@gmail.com>
* 🗑️ chore: remove GlobalLayout and Locale components
- Deleted GlobalLayout and Locale components from the GlobalProvider directory to streamline the codebase.
- This removal is part of a refactor to simplify the layout structure and improve maintainability.
Signed-off-by: Innei <tukon479@gmail.com>
* chore: clean up console logs and improve component structure
- Removed unnecessary console log statements from AgentForkTag components in both agent and community directories to enhance code cleanliness.
- Refactored UserAgentList component for better readability by restructuring the useUserDetailContext hook and adjusting the layout of Flexbox components.
Signed-off-by: Innei <tukon479@gmail.com>
* chore: remove console log from MemoryAnalysis component
* chore: update mobile HTML template with new asset links
- Replaced the previous asset links in the mobile HTML template with updated versions to ensure the latest resources are utilized.
- Adjusted the link rel attributes for module preloading to enhance performance and loading efficiency.
Signed-off-by: Innei <tukon479@gmail.com>
* fix: correct variable assignment in createClientTaskThread integration test
- Updated the assignment of the second parent message in the createClientTaskThread integration test to improve clarity and ensure proper data handling.
- Changed the variable name from 'secondParentMsg' to 'inserted' for better context before extracting the first message from the inserted results.
Signed-off-by: Innei <tukon479@gmail.com>
* refactor: simplify authentication check in define-config
- Removed the dependency on the isDesktop variable in the authentication check to streamline the logic.
- Enhanced the clarity of the redirection process for protected routes by focusing solely on the isLoggedIn status.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(dev): enhance local development setup with debug proxy instructions
- Added detailed instructions for starting the development environment in CLAUDE.md, including commands for SPA and full-stack modes.
- Updated README.md and README.zh-CN.md to reflect new commands and the debug proxy URL for local development.
- Introduced a Vite plugin to print the debug proxy URL upon server start, facilitating easier local development against the production backend.
- Corrected the debug proxy route in entry.web.tsx and define-config.ts for consistency.
This improves the developer experience by providing clear guidance and tools for local development.
Signed-off-by: Innei <tukon479@gmail.com>
* optimize perf
* optimize perf
* optimize perf
* remove speedy plugin
* add dayjs vendor
* Revert "remove speedy plugin"
This reverts commit bf986afeb1.
---------
Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -219,14 +219,9 @@ const config = {
|
||||
files: [
|
||||
'dist',
|
||||
'resources',
|
||||
// Ensure Next export assets are packaged
|
||||
'dist/next/**/*',
|
||||
'dist/renderer/**/*',
|
||||
'!resources/locales',
|
||||
'!resources/dmg.png',
|
||||
'!dist/next/docs',
|
||||
'!dist/next/packages',
|
||||
'!dist/next/.next/server/app/sitemap',
|
||||
'!dist/next/.next/static/media',
|
||||
// Exclude all node_modules first
|
||||
'!node_modules',
|
||||
// Then explicitly include native modules using object form (handles pnpm symlinks)
|
||||
|
||||
@@ -1,14 +1,46 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { defineConfig } from 'electron-vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import { defineConfig } from 'electron-vite';
|
||||
import type { PluginOption, ViteDevServer } from 'vite';
|
||||
import { loadEnv } from 'vite';
|
||||
|
||||
import {
|
||||
sharedOptimizeDeps,
|
||||
sharedRendererDefine,
|
||||
sharedRendererPlugins,
|
||||
sharedRollupOutput,
|
||||
} from '../../plugins/vite/sharedRendererConfig';
|
||||
import { getExternalDependencies } from './native-deps.config.mjs';
|
||||
|
||||
/**
|
||||
* Rewrite `/` to `/apps/desktop/index.html` so the electron-vite dev server
|
||||
* serves the desktop HTML entry when root is the monorepo root.
|
||||
*/
|
||||
function electronDesktopHtmlPlugin(): PluginOption {
|
||||
return {
|
||||
configureServer(server: ViteDevServer) {
|
||||
server.middlewares.use((req, _res, next) => {
|
||||
if (req.url === '/' || req.url === '/index.html') {
|
||||
req.url = '/apps/desktop/index.html';
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
name: 'electron-desktop-html',
|
||||
};
|
||||
}
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const ROOT_DIR = resolve(__dirname, '../..');
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
|
||||
Object.assign(process.env, loadEnv(mode, ROOT_DIR, ''));
|
||||
const updateChannel = process.env.UPDATE_CHANNEL;
|
||||
console.log(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`); // 添加日志确认
|
||||
|
||||
console.info(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`);
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
@@ -61,4 +93,23 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
root: ROOT_DIR,
|
||||
build: {
|
||||
outDir: resolve(__dirname, 'dist/renderer'),
|
||||
rollupOptions: {
|
||||
input: resolve(__dirname, 'index.html'),
|
||||
output: sharedRollupOutput,
|
||||
},
|
||||
},
|
||||
define: sharedRendererDefine({ isMobile: false, isElectron: true }),
|
||||
optimizeDeps: sharedOptimizeDeps,
|
||||
plugins: [
|
||||
electronDesktopHtmlPlugin(),
|
||||
...(sharedRendererPlugins({ platform: 'desktop' }) as PluginOption[]),
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
90
apps/desktop/index.html
Normal file
90
apps/desktop/index.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
html body {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
html[data-theme='dark'] body {
|
||||
background-color: #000;
|
||||
}
|
||||
#loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: inherit;
|
||||
gap: 12px;
|
||||
}
|
||||
@keyframes loading-draw {
|
||||
0% { stroke-dashoffset: 1000; }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
@keyframes loading-fill {
|
||||
30% { fill-opacity: 0.05; }
|
||||
100% { fill-opacity: 1; }
|
||||
}
|
||||
#loading-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
#loading-brand svg path {
|
||||
fill: currentcolor;
|
||||
fill-opacity: 0;
|
||||
stroke: currentcolor;
|
||||
stroke-dasharray: 1000;
|
||||
stroke-dashoffset: 1000;
|
||||
stroke-width: 0.25em;
|
||||
animation:
|
||||
loading-draw 2s cubic-bezier(0.4, 0, 0.2, 1) infinite,
|
||||
loading-fill 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
}
|
||||
html[data-theme='dark'] #loading-brand {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="height: 100%">
|
||||
<script>
|
||||
(function () {
|
||||
var theme = 'system';
|
||||
try {
|
||||
theme = localStorage.getItem('theme') || 'system';
|
||||
} catch (_) {}
|
||||
var systemTheme =
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
var resolvedTheme = theme === 'system' ? systemTheme : theme;
|
||||
if (resolvedTheme === 'dark' || resolvedTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', resolvedTheme);
|
||||
}
|
||||
var locale = navigator.language || 'en-US';
|
||||
document.documentElement.lang = locale;
|
||||
var rtl = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
|
||||
document.documentElement.dir =
|
||||
rtl.indexOf(locale.split('-')[0].toLowerCase()) >= 0 ? 'rtl' : 'ltr';
|
||||
})();
|
||||
</script>
|
||||
<div id="loading-screen">
|
||||
<div id="loading-brand" aria-label="Loading" role="status">
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="40" style="flex:none;line-height:1" viewBox="0 0 940 320" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>LobeHub</title>
|
||||
<path d="M15 240.035V87.172h39.24V205.75h66.192v34.285H15zM183.731 242c-11.759 0-22.196-2.621-31.313-7.862-9.116-5.241-16.317-12.447-21.601-21.619-5.153-9.317-7.729-19.945-7.729-31.883 0-11.937 2.576-22.492 7.729-31.664 5.164-8.963 12.159-15.98 20.982-21.05l.619-.351c9.117-5.241 19.554-7.861 31.313-7.861s22.196 2.62 31.313 7.861c9.248 5.096 16.449 12.229 21.601 21.401 5.153 9.172 7.729 19.727 7.729 31.664 0 11.938-2.576 22.566-7.729 31.883-5.152 9.172-12.353 16.378-21.601 21.619-9.117 5.241-19.554 7.862-31.313 7.862zm0-32.975c4.36 0 8.191-1.092 11.494-3.275 3.436-2.184 6.144-5.387 8.126-9.609 1.982-4.367 2.973-9.536 2.973-15.505 0-5.968-.991-10.991-2.973-15.067-1.906-4.06-4.483-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.134-3.276-11.494-3.276-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275zM295.508 78l-.001 54.042a34.071 34.071 0 016.541-5.781c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.557 7.424 7.872 4.835 14.105 11.684 18.7 20.546l.325.637c4.756 9.026 7.135 19.799 7.135 32.319 0 12.666-2.379 23.585-7.135 32.757-4.624 9.026-10.966 16.087-19.025 21.182-7.928 4.95-16.78 7.425-26.557 7.425-9.644 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.355-7.532-7.226l.001 11.812h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.494 3.276-3.303 2.184-6.012 5.387-8.126 9.609-1.982 4.076-2.972 9.099-2.972 15.067 0 5.969.99 11.138 2.972 15.505 2.114 4.222 4.823 7.425 8.126 9.609 3.435 2.183 7.266 3.275 11.494 3.275s7.994-1.092 11.297-3.275c3.435-2.184 6.143-5.387 8.125-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.483-7.177-7.732-9.352l-.393-.257c-3.303-2.184-7.069-3.276-11.297-3.276zm105.335 38.653l.084.337a27.857 27.857 0 002.057 5.559c2.246 4.222 5.417 7.498 9.513 9.827 4.096 2.184 8.984 3.276 14.665 3.276 5.285 0 9.777-.801 13.477-2.403 3.579-1.632 7.1-4.025 10.564-7.182l.732-.679 19.818 22.711c-5.153 6.26-11.494 11.064-19.025 14.413-7.531 3.203-16.449 4.804-26.755 4.804-12.683 0-23.782-2.621-33.294-7.862-9.381-5.386-16.713-12.665-21.998-21.837-5.153-9.317-7.729-19.872-7.729-31.665 0-11.792 2.51-22.274 7.53-31.446 5.036-9.105 11.902-16.195 20.596-21.268l.61-.351c8.984-5.241 19.091-7.861 30.322-7.861 10.311 0 19.743 2.286 28.294 6.859l.64.347c8.72 4.659 15.656 11.574 20.809 20.746 5.153 9.172 7.729 20.309 7.729 33.411 0 1.294-.052 2.761-.156 4.4l-.042.623-.17 2.353c-.075 1.01-.151 1.973-.227 2.888h-78.044zm21.365-42.147c-4.492 0-8.456 1.092-11.891 3.276-3.303 2.184-5.879 5.314-7.729 9.39a26.04 26.04 0 00-1.117 2.79 30.164 30.164 0 00-1.121 4.499l-.058.354h43.96l-.015-.106c-.401-2.638-1.122-5.055-2.163-7.252l-.246-.503c-1.776-3.774-4.282-6.742-7.519-8.906l-.409-.266c-3.303-2.184-7.2-3.276-11.692-3.276zm111.695-62.018l-.001 57.432h53.51V87.172h39.24v152.863h-39.24v-59.617H555.9l.001 59.617h-39.24V87.172h39.24zM715.766 242c-8.72 0-16.581-1.893-23.583-5.678-6.87-3.785-12.287-9.681-16.251-17.688-3.832-8.153-5.747-18.417-5.747-30.791v-66.168h37.654v59.398c0 9.172 1.519 15.723 4.558 19.654 3.171 3.931 7.597 5.896 13.278 5.896 3.7 0 7.069-.946 10.108-2.839 3.038-1.892 5.483-4.877 7.332-8.953 1.85-4.222 2.775-9.609 2.775-16.16v-56.996h37.654v118.36h-35.871l.004-12.38c-2.642 3.197-5.682 5.868-9.12 8.012-7.002 4.222-14.599 6.333-22.791 6.333zM841.489 78l-.001 54.041a34.1 34.1 0 016.541-5.78c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.556 7.424 7.873 4.835 14.106 11.684 18.701 20.546l.325.637c4.756 9.026 7.134 19.799 7.134 32.319 0 12.666-2.378 23.585-7.134 32.757-4.624 9.026-10.966 16.087-19.026 21.182-7.927 4.95-16.779 7.425-26.556 7.425-9.645 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.354-7.531-7.224v11.81h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275 4.228 0 7.993-1.092 11.296-3.275 3.435-2.184 6.144-5.387 8.126-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.484-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.068-3.276-11.296-3.276z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="root" style="height: 100%;"></div>
|
||||
<script>
|
||||
window.__SERVER_CONFIG__ = undefined;
|
||||
</script>
|
||||
<script type="module" src="../../src/entry.desktop.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "LobeHub",
|
||||
"main": "./dist/main/index.js",
|
||||
"scripts": {
|
||||
"build:main": "electron-vite build",
|
||||
"build:main": "cross-env NODE_OPTIONS=--max-old-space-size=8192 electron-vite build",
|
||||
"build:run-unpack": "electron .",
|
||||
"dev": "electron-vite dev",
|
||||
"dev:static": "cross-env DESKTOP_RENDERER_STATIC=1 npm run dev",
|
||||
@@ -30,7 +30,7 @@
|
||||
"package:local": "npm run build:main && electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
||||
"package:local:reuse": "electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
||||
"package:mac": "npm run build:main && electron-builder --mac --config electron-builder.mjs --publish never",
|
||||
"package:mac:local": "npm run build:main && UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.mjs --publish never",
|
||||
"package:mac:local": "npm run build:main && cross-env UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.mjs --publish never",
|
||||
"package:win": "npm run build:main && electron-builder --win --config electron-builder.mjs --publish never",
|
||||
"start": "electron-vite preview",
|
||||
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
|
||||
@@ -41,12 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "^0.1.70",
|
||||
"electron-liquid-glass": "^1.1.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"get-port-please": "^3.2.0",
|
||||
"superjson": "^2.2.6"
|
||||
"electron-liquid-glass": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
@@ -69,6 +64,7 @@
|
||||
"async-retry": "^1.3.3",
|
||||
"consola": "^3.4.2",
|
||||
"cookie": "^1.1.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"diff": "^8.0.2",
|
||||
"electron": "^38.7.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
@@ -76,12 +72,16 @@
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-vite": "^4.0.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"electron-vite": "^5.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"es-toolkit": "^1.43.0",
|
||||
"eslint": "10.0.0",
|
||||
"execa": "^9.6.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"fix-path": "^5.0.0",
|
||||
"get-port-please": "^3.2.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
@@ -93,11 +93,12 @@
|
||||
"semver": "^7.7.3",
|
||||
"set-cookie-parser": "^2.7.2",
|
||||
"stylelint": "^15.11.0",
|
||||
"superjson": "^2.2.6",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "^7.16.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.2.7",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^3.2.4",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -110,6 +111,10 @@
|
||||
"electron",
|
||||
"electron-builder",
|
||||
"node-mac-permissions"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { app } from 'electron';
|
||||
import { pathExistsSync } from 'fs-extra';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const mainDir = join(__dirname);
|
||||
@@ -12,12 +11,7 @@ export const buildDir = join(mainDir, '../../build');
|
||||
|
||||
const appPath = app.getAppPath();
|
||||
|
||||
const nextExportOutDir = join(appPath, 'dist', 'next', 'out');
|
||||
const nextExportDefaultDir = join(appPath, 'dist', 'next');
|
||||
|
||||
export const nextExportDir = pathExistsSync(nextExportOutDir)
|
||||
? nextExportOutDir
|
||||
: nextExportDefaultDir;
|
||||
export const rendererDir = join(appPath, 'dist', 'renderer');
|
||||
|
||||
export const userDataDir = app.getPath('userData');
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ vi.mock('@/env', () => ({
|
||||
|
||||
vi.mock('@/const/dir', () => ({
|
||||
buildDir: '/mock/build',
|
||||
nextExportDir: '/mock/export/out',
|
||||
rendererDir: '/mock/export/out',
|
||||
appStorageDir: '/mock/storage/path',
|
||||
userDataDir: '/mock/user/data',
|
||||
FILE_STORAGE_DIR: 'file-storage',
|
||||
|
||||
@@ -491,7 +491,7 @@ export default class Browser {
|
||||
|
||||
/**
|
||||
* Setup CORS bypass for ALL requests
|
||||
* In production, the renderer uses app://next protocol which triggers CORS
|
||||
* In production, the renderer uses app://renderer protocol which triggers CORS
|
||||
*/
|
||||
private setupCORSBypass(browserWindow: BrowserWindow): void {
|
||||
logger.debug(`[${this.identifier}] Setting up CORS bypass for all requests`);
|
||||
|
||||
@@ -19,25 +19,25 @@ const RENDERER_PROTOCOL_PRIVILEGES = {
|
||||
|
||||
interface RendererProtocolManagerOptions {
|
||||
host?: string;
|
||||
nextExportDir: string;
|
||||
rendererDir: string;
|
||||
resolveRendererFilePath: ResolveRendererFilePath;
|
||||
scheme?: string;
|
||||
}
|
||||
|
||||
const RENDERER_DIR = 'next';
|
||||
const RENDERER_DIR = 'renderer';
|
||||
export class RendererProtocolManager {
|
||||
private readonly scheme: string;
|
||||
private readonly host: string;
|
||||
private readonly nextExportDir: string;
|
||||
private readonly rendererDir: string;
|
||||
private readonly resolveRendererFilePath: ResolveRendererFilePath;
|
||||
private handlerRegistered = false;
|
||||
|
||||
constructor(options: RendererProtocolManagerOptions) {
|
||||
const { nextExportDir, resolveRendererFilePath } = options;
|
||||
const { rendererDir, resolveRendererFilePath } = options;
|
||||
|
||||
this.scheme = 'app';
|
||||
this.host = RENDERER_DIR;
|
||||
this.nextExportDir = nextExportDir;
|
||||
this.rendererDir = rendererDir;
|
||||
this.resolveRendererFilePath = resolveRendererFilePath;
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ export class RendererProtocolManager {
|
||||
registerHandler() {
|
||||
if (this.handlerRegistered) return;
|
||||
|
||||
if (!pathExistsSync(this.nextExportDir)) {
|
||||
if (!pathExistsSync(this.rendererDir)) {
|
||||
createLogger('core:RendererProtocolManager').warn(
|
||||
`Next export directory not found, skip static handler: ${this.nextExportDir}`,
|
||||
`Renderer directory not found, skip static handler: ${this.rendererDir}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export class RendererProtocolManager {
|
||||
const ext = extname(normalizedPathname);
|
||||
|
||||
return (
|
||||
pathname.startsWith('/_next/') ||
|
||||
pathname.startsWith('/assets/') ||
|
||||
pathname.startsWith('/static/') ||
|
||||
pathname === '/favicon.ico' ||
|
||||
pathname === '/manifest.json' ||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { pathExistsSync } from 'fs-extra';
|
||||
import { extname, join } from 'node:path';
|
||||
|
||||
import { nextExportDir } from '@/const/dir';
|
||||
import { pathExistsSync } from 'fs-extra';
|
||||
|
||||
import { rendererDir } from '@/const/dir';
|
||||
import { isDev } from '@/const/env';
|
||||
import { getDesktopEnv } from '@/env';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
@@ -9,7 +10,10 @@ import { createLogger } from '@/utils/logger';
|
||||
import { RendererProtocolManager } from './RendererProtocolManager';
|
||||
|
||||
const logger = createLogger('core:RendererUrlManager');
|
||||
const devDefaultRendererUrl = 'http://localhost:3015';
|
||||
|
||||
// Vite build with root=monorepo preserves input path structure,
|
||||
// so index.html ends up at apps/desktop/index.html in outDir.
|
||||
const SPA_ENTRY_HTML = join(rendererDir, 'apps', 'desktop', 'index.html');
|
||||
|
||||
export class RendererUrlManager {
|
||||
private readonly rendererProtocolManager: RendererProtocolManager;
|
||||
@@ -18,7 +22,7 @@ export class RendererUrlManager {
|
||||
|
||||
constructor() {
|
||||
this.rendererProtocolManager = new RendererProtocolManager({
|
||||
nextExportDir,
|
||||
rendererDir,
|
||||
resolveRendererFilePath: this.resolveRendererFilePath,
|
||||
});
|
||||
|
||||
@@ -33,12 +37,18 @@ export class RendererUrlManager {
|
||||
* Configure renderer loading strategy for dev/prod
|
||||
*/
|
||||
configureRendererLoader() {
|
||||
if (isDev && !this.rendererStaticOverride) {
|
||||
this.rendererLoadedUrl = devDefaultRendererUrl;
|
||||
const electronRendererUrl = process.env['ELECTRON_RENDERER_URL'];
|
||||
|
||||
if (isDev && !this.rendererStaticOverride && electronRendererUrl) {
|
||||
this.rendererLoadedUrl = electronRendererUrl;
|
||||
this.setupDevRenderer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDev && !this.rendererStaticOverride && !electronRendererUrl) {
|
||||
logger.warn('Dev mode: ELECTRON_RENDERER_URL not set, falling back to protocol handler');
|
||||
}
|
||||
|
||||
if (isDev && this.rendererStaticOverride) {
|
||||
logger.warn('Dev mode: DESKTOP_RENDERER_STATIC enabled, using static renderer handler');
|
||||
}
|
||||
@@ -56,68 +66,32 @@ export class RendererUrlManager {
|
||||
|
||||
/**
|
||||
* Resolve renderer file path in production.
|
||||
* Static assets map directly; app routes fall back to index.html.
|
||||
* Static assets map directly; all routes fall back to index.html (SPA).
|
||||
*/
|
||||
resolveRendererFilePath = async (url: URL): Promise<string | null> => {
|
||||
const pathname = url.pathname;
|
||||
const normalizedPathname = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
||||
|
||||
// Static assets should be resolved from root
|
||||
if (
|
||||
pathname.startsWith('/_next/') ||
|
||||
pathname.startsWith('/static/') ||
|
||||
pathname === '/favicon.ico' ||
|
||||
pathname === '/manifest.json'
|
||||
) {
|
||||
return this.resolveExportFilePath(pathname);
|
||||
// Static assets: direct file mapping
|
||||
if (pathname.startsWith('/assets/') || extname(pathname)) {
|
||||
const filePath = join(rendererDir, pathname);
|
||||
return pathExistsSync(filePath) ? filePath : null;
|
||||
}
|
||||
|
||||
// If the incoming path already contains an extension (like .html or .ico),
|
||||
// treat it as a direct asset lookup.
|
||||
const extension = extname(normalizedPathname);
|
||||
if (extension) {
|
||||
return this.resolveExportFilePath(pathname);
|
||||
}
|
||||
|
||||
return this.resolveExportFilePath('/');
|
||||
// All routes fallback to index.html (SPA)
|
||||
return SPA_ENTRY_HTML;
|
||||
};
|
||||
|
||||
private resolveExportFilePath(pathname: string) {
|
||||
// Normalize by removing leading/trailing slashes so extname works as expected
|
||||
const normalizedPath = decodeURIComponent(pathname).replace(/^\/+/, '').replace(/\/$/, '');
|
||||
|
||||
if (!normalizedPath) return join(nextExportDir, 'index.html');
|
||||
|
||||
const basePath = join(nextExportDir, normalizedPath);
|
||||
const ext = extname(normalizedPath);
|
||||
|
||||
// If the request explicitly includes an extension (e.g. html, ico, txt),
|
||||
// treat it as a direct asset.
|
||||
if (ext) {
|
||||
return pathExistsSync(basePath) ? basePath : null;
|
||||
}
|
||||
|
||||
const candidates = [`${basePath}.html`, join(basePath, 'index.html'), basePath];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (pathExistsSync(candidate)) return candidate;
|
||||
}
|
||||
|
||||
const fallback404 = join(nextExportDir, '404.html');
|
||||
if (pathExistsSync(fallback404)) return fallback404;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Development: use Next dev server directly
|
||||
* Development: use electron-vite renderer dev server
|
||||
*/
|
||||
private setupDevRenderer() {
|
||||
logger.info('Development mode: renderer served from Next dev server, no protocol hook');
|
||||
logger.info(
|
||||
`Development mode: renderer served from electron-vite dev server at ${this.rendererLoadedUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Production: serve static Next export assets
|
||||
* Production: serve static renderer assets via protocol handler
|
||||
*/
|
||||
private setupProdRenderer() {
|
||||
this.rendererLoadedUrl = this.rendererProtocolManager.getRendererUrl();
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('RendererProtocolManager', () => {
|
||||
mockReadFile.mockImplementation(async (path: string) => Buffer.from(`content:${path}`));
|
||||
|
||||
const manager = new RendererProtocolManager({
|
||||
nextExportDir: '/export',
|
||||
rendererDir: '/export',
|
||||
resolveRendererFilePath,
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('RendererProtocolManager', () => {
|
||||
const response = await handler({
|
||||
headers: new Headers(),
|
||||
method: 'GET',
|
||||
url: 'app://next/missing',
|
||||
url: 'app://renderer/missing',
|
||||
} as any);
|
||||
const body = await response.text();
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('RendererProtocolManager', () => {
|
||||
mockReadFile.mockImplementation(async (path: string) => Buffer.from(`content:${path}`));
|
||||
|
||||
const manager = new RendererProtocolManager({
|
||||
nextExportDir: '/export',
|
||||
rendererDir: '/export',
|
||||
resolveRendererFilePath,
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('RendererProtocolManager', () => {
|
||||
const response = await handler({
|
||||
headers: new Headers(),
|
||||
method: 'GET',
|
||||
url: 'app://next/404.html',
|
||||
url: 'app://renderer/404.html',
|
||||
} as any);
|
||||
|
||||
expect(resolveRendererFilePath).toHaveBeenCalledTimes(1);
|
||||
@@ -123,14 +123,14 @@ describe('RendererProtocolManager', () => {
|
||||
const resolveRendererFilePath = vi.fn(async (_url: URL) => null);
|
||||
|
||||
const manager = new RendererProtocolManager({
|
||||
nextExportDir: '/export',
|
||||
rendererDir: '/export',
|
||||
resolveRendererFilePath,
|
||||
});
|
||||
|
||||
manager.registerHandler();
|
||||
const handler = protocolHandlerRef.current;
|
||||
|
||||
const response = await handler({ url: 'app://next/logo.png' } as any);
|
||||
const response = await handler({ url: 'app://renderer/logo.png' } as any);
|
||||
|
||||
expect(resolveRendererFilePath).toHaveBeenCalledTimes(1);
|
||||
expect(response.status).toBe(404);
|
||||
@@ -144,7 +144,7 @@ describe('RendererProtocolManager', () => {
|
||||
mockReadFile.mockImplementation(async () => payload);
|
||||
|
||||
const manager = new RendererProtocolManager({
|
||||
nextExportDir: '/export',
|
||||
rendererDir: '/export',
|
||||
resolveRendererFilePath,
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('RendererProtocolManager', () => {
|
||||
const response = await handler({
|
||||
headers: new Headers({ Range: 'bytes=0-1' }),
|
||||
method: 'GET',
|
||||
url: 'app://next/_next/static/media/intro-video.mp4',
|
||||
url: 'app://renderer/assets/intro-video.mp4',
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(206);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { RendererUrlManager } from '../RendererUrlManager';
|
||||
|
||||
const mockPathExistsSync = vi.fn();
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
@@ -19,11 +17,15 @@ vi.mock('fs-extra', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/const/dir', () => ({
|
||||
nextExportDir: '/mock/export/out',
|
||||
rendererDir: '/mock/export/out',
|
||||
}));
|
||||
|
||||
let mockIsDev = false;
|
||||
|
||||
vi.mock('@/const/env', () => ({
|
||||
isDev: false,
|
||||
get isDev() {
|
||||
return mockIsDev;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/env', () => ({
|
||||
@@ -40,33 +42,80 @@ vi.mock('@/utils/logger', () => ({
|
||||
}));
|
||||
|
||||
describe('RendererUrlManager', () => {
|
||||
let manager: RendererUrlManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPathExistsSync.mockReset();
|
||||
manager = new RendererUrlManager();
|
||||
mockIsDev = false;
|
||||
delete process.env['ELECTRON_RENDERER_URL'];
|
||||
});
|
||||
|
||||
describe('resolveRendererFilePath', () => {
|
||||
it('should resolve asset requests directly', async () => {
|
||||
const { RendererUrlManager } = await import('../RendererUrlManager');
|
||||
const manager = new RendererUrlManager();
|
||||
|
||||
mockPathExistsSync.mockImplementation(
|
||||
(p: string) => p === '/mock/export/out/en-US__0__light.txt',
|
||||
);
|
||||
|
||||
const resolved = await manager.resolveRendererFilePath(
|
||||
new URL('app://next/en-US__0__light.txt'),
|
||||
new URL('app://renderer/en-US__0__light.txt'),
|
||||
);
|
||||
|
||||
expect(resolved).toBe('/mock/export/out/en-US__0__light.txt');
|
||||
});
|
||||
|
||||
it('should fall back to index.html for app routes', async () => {
|
||||
mockPathExistsSync.mockImplementation((p: string) => p === '/mock/export/out/index.html');
|
||||
const { RendererUrlManager } = await import('../RendererUrlManager');
|
||||
const manager = new RendererUrlManager();
|
||||
|
||||
const resolved = await manager.resolveRendererFilePath(new URL('app://next/settings'));
|
||||
mockPathExistsSync.mockImplementation(
|
||||
(p: string) => p === '/mock/export/out/apps/desktop/index.html',
|
||||
);
|
||||
|
||||
expect(resolved).toBe('/mock/export/out/index.html');
|
||||
const resolved = await manager.resolveRendererFilePath(new URL('app://renderer/settings'));
|
||||
|
||||
expect(resolved).toBe('/mock/export/out/apps/desktop/index.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureRendererLoader (dev mode)', () => {
|
||||
it('should use ELECTRON_RENDERER_URL when available in dev mode', async () => {
|
||||
mockIsDev = true;
|
||||
process.env['ELECTRON_RENDERER_URL'] = 'http://localhost:5173';
|
||||
|
||||
const { RendererUrlManager } = await import('../RendererUrlManager');
|
||||
const manager = new RendererUrlManager();
|
||||
manager.configureRendererLoader();
|
||||
|
||||
expect(manager.buildRendererUrl('/')).toBe('http://localhost:5173/');
|
||||
expect(manager.buildRendererUrl('/settings')).toBe('http://localhost:5173/settings');
|
||||
});
|
||||
|
||||
it('should fall back to protocol handler when ELECTRON_RENDERER_URL is not set', async () => {
|
||||
mockIsDev = true;
|
||||
|
||||
const { RendererUrlManager } = await import('../RendererUrlManager');
|
||||
const manager = new RendererUrlManager();
|
||||
mockPathExistsSync.mockReturnValue(true);
|
||||
manager.configureRendererLoader();
|
||||
|
||||
expect(manager.buildRendererUrl('/')).toBe('app://renderer/');
|
||||
});
|
||||
|
||||
it('should use protocol handler when DESKTOP_RENDERER_STATIC is enabled regardless of ELECTRON_RENDERER_URL', async () => {
|
||||
mockIsDev = true;
|
||||
process.env['ELECTRON_RENDERER_URL'] = 'http://localhost:5173';
|
||||
|
||||
const { getDesktopEnv } = await import('@/env');
|
||||
vi.mocked(getDesktopEnv).mockReturnValue({ DESKTOP_RENDERER_STATIC: true } as any);
|
||||
|
||||
const { RendererUrlManager } = await import('../RendererUrlManager');
|
||||
const manager = new RendererUrlManager();
|
||||
mockPathExistsSync.mockReturnValue(true);
|
||||
manager.configureRendererLoader();
|
||||
|
||||
expect(manager.buildRendererUrl('/')).toBe('app://renderer/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user