Files
lobehub__lobehub/plugins/vite/sharedRendererConfig.test.ts
Innei 6329f97e14 🐛 fix(vite): stop assuming node_modules sits at the repo root (#19325)
* 🐛 fix(vite): resolve the editor provider without assuming node_modules layout

GlobalProvider/Editor.tsx reached @lobehub/editor's provider through a
relative `../../../node_modules/...` path, which assumes node_modules sits at
the repo root. Business-overlay desktop builds mount this repo as the overlay
root's `lobehub/` submodule, so the install hoists to the parent directory and
the path does not exist — every desktop platform failed with UNRESOLVED_IMPORT.

The provider subpath is not in the package's export map, so it still cannot be
imported on its own; the dev-only resolver now runs in builds too and derives
the provider file from the resolved package entry, wherever it lives.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* 🐛 fix(vite): resolve the @lobehub/ui barrel through the package

The barrel parser reached the package's `es` directory through
`../../../node_modules/@lobehub/ui/es`, which assumes node_modules sits at the
repo root — the same assumption that broke the desktop build. Business-overlay
share builds hoist the install above this checkout, where that path does not
exist and `realpathSync` throws before the plugin can load.

Resolving the package entry instead finds it wherever it is installed. The
plugin is not loaded on the desktop platform, so this never surfaced there.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* ♻️ refactor(vite): take the editor provider entry @lobehub/editor now exports

@lobehub/editor 4.27.0 exports `./react/EditorProvider`, so the provider no
longer has to be reached by file path and the build-time resolver and its
ambient shim can go. The dev redirect stays: Vite prebundles the provider entry
separately from `@lobehub/editor/react`, which the editors use, and the app
would end up with two EditorContexts.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* 🐛 fix(desktop): let pnpm find the business-const stub

pnpm does not match the `./stubs/business-const` workspace entry, so an OSS
install from apps/desktop fails to resolve `@lobechat/business-const` — the
package the stub exists to provide — and the desktop build cannot be run
locally at all. Cloud builds resolve the real package from the overlay, and
the renderer redirects the module to `@cloud/business-const` either way.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S
2026-09-09 14:54:22 +08:00

286 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
__testing,
sharedModulePreload,
sharedOptimizeDeps,
sharedRendererDedupe,
sharedRendererPlugins,
} from './sharedRendererConfig';
const getPluginNames = (platform: 'desktop' | 'web') =>
sharedRendererPlugins({ platform })
.flat(Number.POSITIVE_INFINITY)
.filter((plugin): plugin is { name: string } => Boolean(plugin) && typeof plugin === 'object')
.map((plugin) => plugin.name);
describe('sharedRendererPlugins', () => {
it('keeps the icon barrel transform out of the Electron renderer', () => {
expect(getPluginNames('desktop')).not.toContain('lobe-icon-named-export-proxy');
expect(getPluginNames('web')).toContain('lobe-icon-named-export-proxy');
});
});
describe('lobe-dev-editor-provider', () => {
it('sends the provider entry back to the one prebundled editor bundle', async () => {
const plugin = sharedRendererPlugins({ platform: 'web' })
.flat(Number.POSITIVE_INFINITY)
.find(
(
item,
): item is { name: string; resolveId: (source: string, importer: string) => unknown } =>
Boolean(item) &&
typeof item === 'object' &&
(item as { name?: string }).name === 'lobe-dev-editor-provider',
);
const resolve = async (source: string) =>
plugin!.resolveId.call(
{ resolve: async (id: string) => ({ id }) },
source,
'/repo/src/layout/GlobalProvider/Editor.tsx',
);
await expect(resolve('@lobehub/editor/react/EditorProvider')).resolves.toEqual({
id: '@lobehub/editor/react',
});
await expect(resolve('@lobehub/editor/react')).resolves.toBeNull();
});
});
describe('sharedOptimizeDeps', () => {
it('pre-bundles the root and base-ui entrypoints together', () => {
expect(sharedOptimizeDeps.include).toEqual(
expect.arrayContaining(['@lobehub/ui', '@lobehub/ui/base-ui']),
);
});
});
describe('sharedRendererDedupe', () => {
it('keeps editor entrypoints on one shared context instance', () => {
expect(sharedRendererDedupe).toContain('@lobehub/editor');
});
});
describe('sharedModulePreload', () => {
it('keeps regular dependencies while excluding deferred i18n and devtools chunks', () => {
const resolveDependencies = sharedModulePreload.resolveDependencies!;
expect(
resolveDependencies(
'assets/index.js',
[
'assets/vendor-icons.js',
'vendor/vendor-react.js',
'i18n/i18n-default.js',
'assets/i18n-en-US.js',
'devtools/devtools-abc.js',
'/_spa/devtools/DevDock-abc.js',
'assets/devtools-legacy.js',
'assets/page.js',
],
{ hostId: 'index.html', hostType: 'html' },
),
).toEqual(['assets/vendor-icons.js', 'vendor/vendor-react.js', 'assets/page.js']);
});
});
describe('sharedManualChunks', () => {
it('isolates synchronously bundled and heavy namespaces from deferred locale data', () => {
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/auth.json')).toBe('i18n-zh-CN-auth');
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/common.json')).toBe(
'i18n-zh-CN-common',
);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/default/oauth.ts')).toBe(
'i18n-default-oauth',
);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/default/chat.ts')).toBe(
'i18n-default-app-shell',
);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/default/home.ts')).toBe(
'i18n-default-app-shell',
);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/default/hotkey.ts')).toBe(
'i18n-default-hotkey',
);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/default/models.ts')).toBe(
'i18n-default-models',
);
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/chat.json')).toBe(
'i18n-zh-CN-app-shell',
);
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/home.json')).toBe(
'i18n-zh-CN-app-shell',
);
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/models.json')).toBe(
'i18n-zh-CN-models',
);
expect(__testing.sharedManualChunks('/repo/locales/zh-CN/setting.json')).toBe('i18n-zh-CN');
});
it('keeps UI and date locale runtimes outside deferred namespace data', () => {
expect(__testing.sharedManualChunks('/repo/node_modules/antd/es/locale/zh_CN.js')).toBe(
'i18n-zh-CN-ui-runtime',
);
expect(__testing.sharedManualChunks('/repo/node_modules/dayjs/locale/zh-cn.js')).toBe(
'i18n-zh-CN-ui-runtime',
);
});
it('keeps locale runtime helpers out of the default locale chunk', () => {
expect(__testing.sharedManualChunks('/repo/packages/locales/src/resources.ts')).toBe(undefined);
expect(__testing.sharedManualChunks('/repo/packages/locales/src/create.ts')).toBe(undefined);
});
it('groups shared constants into a dedicated chunk', () => {
expect(__testing.sharedManualChunks('/repo/packages/const/src/url.ts')).toBe('app-const');
});
it('keeps DevDock source boundaries intact and groups only dedicated packages', () => {
expect(__testing.sharedManualChunks('/repo/src/features/DevDock/index.tsx')).toBeUndefined();
expect(__testing.sharedManualChunks('/repo/src/utils/devDockUnlock.ts')).toBeUndefined();
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/react-scan/node_modules/react-scan/dist/index.js',
),
).toBe('devtools-react-scan');
});
it('places natural DevDock chunks in the specialized devtools asset directory', () => {
expect(
__testing.sharedChunkFileNames({
moduleIds: ['/repo/src/features/DevDock/index.tsx'],
name: 'index',
}),
).toBe('devtools/[name]-[hash].js');
expect(
__testing.sharedChunkFileNames({
moduleIds: ['/repo/src/features/Conversation/ChatList/components/Message.tsx'],
name: 'message',
}),
).toBe('assets/[name]-[hash].js');
});
it('groups stable runtime packages into coarse vendor chunks', () => {
expect(
__testing.sharedManualChunks('/repo/node_modules/.pnpm/react@19/node_modules/react/index.js'),
).toBe('vendor-react');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/react-dom@19/node_modules/react-dom/client.js',
),
).toBe('vendor-react');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/@emotion+react/node_modules/@emotion/react/dist/index.js',
),
).toBe('vendor-ui-runtime');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/motion@12/node_modules/motion/react/dist/index.js',
),
).toBe('vendor-ui-runtime');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/lucide-react/node_modules/lucide-react/dist/index.js',
),
).toBe('vendor-icons');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/zustand@5/node_modules/zustand/esm/index.mjs',
),
).toBe('vendor-data-runtime');
expect(
__testing.sharedManualChunks(
'/repo/node_modules/.pnpm/openai@4/node_modules/openai/index.mjs',
),
).toBe('vendor-ai-runtime');
});
it('lets model-runtime follow dynamic import boundaries', () => {
expect(
__testing.sharedManualChunks('/repo/packages/model-runtime/src/providers/openai/index.ts'),
).toBeUndefined();
expect(
__testing.sharedManualChunks('/repo/packages/model-runtime/src/helpers/parseToolCalls.ts'),
).toBe('model-runtime-client');
expect(
__testing.sharedManualChunks('/repo/packages/model-runtime/src/types/toolsCalling.ts'),
).toBe('model-runtime-client');
expect(
__testing.sharedManualChunks(
'/repo/packages/model-runtime/src/utils/getFallbackModelProperty.ts',
),
).toBeUndefined();
expect(__testing.sharedManualChunks('/repo/packages/model-bank/src/index.ts')).toBeUndefined();
});
});
describe('isUiCoreModule', () => {
it('folds first-screen @lobehub/ui members into vendor-ui-core and leaves heavy ones lazy', () => {
const es = '/repo/node_modules/.pnpm/@lobehub+ui@5/node_modules/@lobehub/ui/es/';
expect(__testing.isUiCoreModule(`${es}Flex/FlexBasic.mjs`)).toBe(true);
expect(__testing.isUiCoreModule(`${es}base-ui/Button/Button.mjs`)).toBe(true);
expect(__testing.isUiCoreModule(`${es}hooks/useIsClient.mjs`)).toBe(true);
expect(__testing.isUiCoreModule(`${es}Markdown/Markdown.mjs`)).toBe(false);
expect(__testing.isUiCoreModule(`${es}hooks/useMarkdown/index.mjs`)).toBe(false);
expect(__testing.isUiCoreModule(`${es}base-ui/Select/Select.mjs`)).toBe(false);
expect(__testing.isUiCoreModule('/repo/node_modules/antd/es/index.js')).toBe(false);
});
});
describe('sharedChunkFileNames', () => {
it('routes only the complete deferred model catalog to on-demand assets', () => {
expect(
__testing.sharedChunkFileNames({
moduleIds: [
'/repo/packages/model-bank/src/aiModels/index.ts',
'/repo/packages/model-bank/src/aiModels/openai.ts',
],
name: 'src',
}),
).toBe('model-bank/[name]-[hash].js');
expect(
__testing.sharedChunkFileNames({
moduleIds: ['/repo/packages/model-bank/src/aiModels/opencodeZen.ts'],
name: 'opencodeZen',
}),
).toBe('assets/[name]-[hash].js');
});
it('routes only self-contained Shiki language, theme, and WASM chunks to on-demand assets', () => {
expect(
__testing.sharedChunkFileNames({
moduleIds: ['/repo/node_modules/@shikijs/langs/dist/python.mjs'],
name: 'python',
}),
).toBe('shiki/[name]-[hash].js');
expect(
__testing.sharedChunkFileNames({
moduleIds: ['/repo/node_modules/@shikijs/themes/dist/github-dark.mjs'],
name: 'github-dark',
}),
).toBe('shiki/[name]-[hash].js');
expect(
__testing.sharedChunkFileNames({
moduleIds: [
'/repo/node_modules/@shikijs/engine-oniguruma/dist/wasm-inlined.mjs',
'/repo/node_modules/shiki/dist/wasm.mjs',
],
name: 'wasm',
}),
).toBe('shiki/[name]-[hash].js');
expect(
__testing.sharedChunkFileNames({
moduleIds: [
'/repo/node_modules/@shikijs/core/dist/index.mjs',
'/repo/src/features/Conversation/Markdown/index.tsx',
],
name: 'markdown',
}),
).toBe('assets/[name]-[hash].js');
});
});