1268 lines
37 KiB
TypeScript
1268 lines
37 KiB
TypeScript
/// <reference types='vitest' />
|
||
import { defineConfig, Plugin } from 'vite';
|
||
import react from '@vitejs/plugin-react';
|
||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||
import type { OutputAsset, OutputBundle, OutputChunk } from 'rollup';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import crypto from 'crypto';
|
||
import { createRequire } from 'module';
|
||
import { visualizer } from 'rollup-plugin-visualizer';
|
||
|
||
const require = createRequire(import.meta.url);
|
||
const workspaceRoot = path.resolve(__dirname, '../..');
|
||
|
||
// Read version from public/version.json
|
||
const versionPath = path.resolve(__dirname, 'public/version.json');
|
||
let appVersion = '0.0.0';
|
||
|
||
try {
|
||
if (fs.existsSync(versionPath)) {
|
||
const versionContent = fs.readFileSync(versionPath, 'utf-8');
|
||
const versionJson = JSON.parse(versionContent);
|
||
appVersion = versionJson.version || '0.0.0';
|
||
console.log(`[Vite] Loaded version from version.json: ${appVersion}`);
|
||
} else {
|
||
console.warn('[Vite] version.json not found at', versionPath);
|
||
}
|
||
} catch (e) {
|
||
console.error('[Vite] Failed to read version.json', e);
|
||
}
|
||
|
||
const IDLE_PREFETCH_GROUPS = [
|
||
'ai-chat',
|
||
'tool-windows',
|
||
'diagram-engines',
|
||
'office-data',
|
||
'editor-engines',
|
||
'media-viewer',
|
||
'external-skills',
|
||
'runtime-static-assets',
|
||
'offline-static-assets',
|
||
] as const;
|
||
|
||
const IDLE_PREFETCH_DEFAULTS = [
|
||
'tool-windows',
|
||
'runtime-static-assets',
|
||
] as const;
|
||
|
||
type IdlePrefetchGroup = (typeof IDLE_PREFETCH_GROUPS)[number];
|
||
|
||
type ManifestEntry = { url: string; revision: string };
|
||
type ViteOutputChunk = OutputChunk & {
|
||
viteMetadata?: {
|
||
importedCss?: Set<string>;
|
||
};
|
||
};
|
||
|
||
const STATIC_SCAN_EXCLUDED_DIRS = new Set([
|
||
'product_showcase',
|
||
'help_tooltips',
|
||
'sw-debug',
|
||
]);
|
||
|
||
const STATIC_SCAN_EXCLUDED_PATTERNS = [
|
||
/stats\.html$/,
|
||
/\.map$/,
|
||
/precache-manifest\.json$/,
|
||
/idle-prefetch-manifest\.json$/,
|
||
/sw\.js$/,
|
||
/sw-debug\.html$/,
|
||
/changelog\.json$/,
|
||
/version\.json$/,
|
||
];
|
||
|
||
const PRECACHE_EXTENSIONS = new Set(['.js', '.css', '.json', '.svg', '.ico']);
|
||
const POST_BOOT_PREFETCH_EXTENSIONS = new Set([
|
||
'.png',
|
||
'.jpg',
|
||
'.jpeg',
|
||
'.gif',
|
||
'.webp',
|
||
'.avif',
|
||
'.woff',
|
||
'.woff2',
|
||
'.ttf',
|
||
'.eot',
|
||
'.wasm',
|
||
'.txt',
|
||
'.md',
|
||
]);
|
||
const PRECACHE_ALWAYS_INCLUDE = new Set([
|
||
'/index.html',
|
||
'/manifest.json',
|
||
'/favicon.ico',
|
||
]);
|
||
|
||
function isFileDescriptorLimitError(error: unknown): boolean {
|
||
return (
|
||
typeof error === 'object' &&
|
||
error !== null &&
|
||
'code' in error &&
|
||
((error as NodeJS.ErrnoException).code === 'EMFILE' ||
|
||
(error as NodeJS.ErrnoException).code === 'ENFILE')
|
||
);
|
||
}
|
||
|
||
function waitForFileDescriptorRetry(attempt: number): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
setTimeout(resolve, 120 * (attempt + 1));
|
||
});
|
||
}
|
||
|
||
async function readFileWithFdRetry(
|
||
fullPath: string,
|
||
encoding?: BufferEncoding
|
||
): Promise<string | Buffer> {
|
||
let lastError: unknown;
|
||
|
||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||
try {
|
||
return encoding
|
||
? await fs.promises.readFile(fullPath, encoding)
|
||
: await fs.promises.readFile(fullPath);
|
||
} catch (error) {
|
||
lastError = error;
|
||
if (!isFileDescriptorLimitError(error) || attempt === 5) {
|
||
throw error;
|
||
}
|
||
await waitForFileDescriptorRetry(attempt);
|
||
}
|
||
}
|
||
|
||
throw lastError;
|
||
}
|
||
|
||
async function writeFileWithFdRetry(
|
||
fullPath: string,
|
||
content: string
|
||
): Promise<void> {
|
||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||
try {
|
||
await fs.promises.writeFile(fullPath, content);
|
||
return;
|
||
} catch (error) {
|
||
if (!isFileDescriptorLimitError(error) || attempt === 5) {
|
||
throw error;
|
||
}
|
||
await waitForFileDescriptorRetry(attempt);
|
||
}
|
||
}
|
||
}
|
||
|
||
function createRevision(fullPath: string): string {
|
||
const content = fs.readFileSync(fullPath);
|
||
return crypto
|
||
.createHash('md5')
|
||
.update(new Uint8Array(content))
|
||
.digest('hex')
|
||
.substring(0, 8);
|
||
}
|
||
|
||
async function createRevisionWithFdRetry(fullPath: string): Promise<string> {
|
||
const content = await readFileWithFdRetry(fullPath);
|
||
return crypto
|
||
.createHash('md5')
|
||
.update(new Uint8Array(content as Buffer))
|
||
.digest('hex')
|
||
.substring(0, 8);
|
||
}
|
||
|
||
function createContentRevision(content: string | Uint8Array): string {
|
||
return crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
|
||
}
|
||
|
||
function createBundleFileRevision(file: OutputAsset | OutputChunk): string {
|
||
if (file.type === 'asset') {
|
||
const source = file.source ?? '';
|
||
return createContentRevision(
|
||
typeof source === 'string' || source instanceof Uint8Array
|
||
? source
|
||
: String(source)
|
||
);
|
||
}
|
||
|
||
return createContentRevision(file.code);
|
||
}
|
||
|
||
function shouldExcludeStaticScanUrl(url: string): boolean {
|
||
return STATIC_SCAN_EXCLUDED_PATTERNS.some((pattern) => pattern.test(url));
|
||
}
|
||
|
||
function isRuntimeStaticAssetUrl(url: string): boolean {
|
||
// 仅覆盖构建产物中的运行时附属资源,避免把手册截图等大文件带进默认 idle 预取。
|
||
return url.startsWith('/assets/');
|
||
}
|
||
|
||
function collectStaticEntries(
|
||
outDir: string,
|
||
shouldInclude: (url: string, ext: string) => boolean
|
||
): ManifestEntry[] {
|
||
const manifest: ManifestEntry[] = [];
|
||
|
||
function scanDir(dir: string, base = '') {
|
||
if (!fs.existsSync(dir)) return;
|
||
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
const relativePath = path.join(base, entry.name).replace(/\\/g, '/');
|
||
|
||
if (entry.isDirectory()) {
|
||
if (!STATIC_SCAN_EXCLUDED_DIRS.has(entry.name)) {
|
||
scanDir(fullPath, relativePath);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (!entry.isFile()) {
|
||
continue;
|
||
}
|
||
|
||
const ext = path.extname(entry.name).toLowerCase();
|
||
const url = '/' + relativePath;
|
||
|
||
if (shouldExcludeStaticScanUrl(url) || !shouldInclude(url, ext)) {
|
||
continue;
|
||
}
|
||
|
||
manifest.push({
|
||
url,
|
||
revision: createRevision(fullPath),
|
||
});
|
||
}
|
||
}
|
||
|
||
scanDir(outDir);
|
||
manifest.sort((a, b) => a.url.localeCompare(b.url));
|
||
return manifest;
|
||
}
|
||
|
||
function normalizeStaticUrlCandidate(candidate: string): string | null {
|
||
if (!candidate) {
|
||
return null;
|
||
}
|
||
|
||
if (
|
||
candidate.startsWith('http://') ||
|
||
candidate.startsWith('https://') ||
|
||
candidate.startsWith('data:') ||
|
||
candidate.startsWith('blob:')
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
const [withoutQuery] = candidate.split(/[?#]/, 1);
|
||
if (!withoutQuery) {
|
||
return null;
|
||
}
|
||
|
||
if (withoutQuery.startsWith('./')) {
|
||
return `/${withoutQuery.slice(2)}`;
|
||
}
|
||
|
||
if (withoutQuery.startsWith('/')) {
|
||
return withoutQuery;
|
||
}
|
||
|
||
return `/${withoutQuery}`;
|
||
}
|
||
|
||
async function collectSelectedEntries(
|
||
outDir: string,
|
||
urls: Iterable<string>,
|
||
shouldInclude: (url: string, ext: string) => boolean
|
||
): Promise<ManifestEntry[]> {
|
||
const manifest: ManifestEntry[] = [];
|
||
const normalizedUrls = new Set<string>();
|
||
|
||
for (const url of urls) {
|
||
const normalizedUrl = normalizeStaticUrlCandidate(url);
|
||
if (!normalizedUrl) {
|
||
continue;
|
||
}
|
||
|
||
normalizedUrls.add(normalizedUrl);
|
||
}
|
||
|
||
for (const url of normalizedUrls) {
|
||
const ext = path.extname(url).toLowerCase();
|
||
if (shouldExcludeStaticScanUrl(url) || !shouldInclude(url, ext)) {
|
||
continue;
|
||
}
|
||
|
||
const fullPath = path.join(outDir, url.replace(/^\//, ''));
|
||
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
||
continue;
|
||
}
|
||
|
||
manifest.push({
|
||
url,
|
||
revision: await createRevisionWithFdRetry(fullPath),
|
||
});
|
||
}
|
||
|
||
manifest.sort((a, b) => a.url.localeCompare(b.url));
|
||
return manifest;
|
||
}
|
||
|
||
async function collectHtmlShellPrecacheUrls(
|
||
indexHtmlPath: string
|
||
): Promise<Set<string>> {
|
||
const urls = new Set<string>(PRECACHE_ALWAYS_INCLUDE);
|
||
|
||
if (!fs.existsSync(indexHtmlPath)) {
|
||
return urls;
|
||
}
|
||
|
||
const html = (await readFileWithFdRetry(indexHtmlPath, 'utf8')) as string;
|
||
const attrPattern = /\b(?:data-local-(?:src|href)|src|href)="([^"]+)"/g;
|
||
|
||
for (const match of html.matchAll(attrPattern)) {
|
||
const normalizedUrl = normalizeStaticUrlCandidate(match[1]);
|
||
if (normalizedUrl) {
|
||
urls.add(normalizedUrl);
|
||
}
|
||
}
|
||
|
||
return urls;
|
||
}
|
||
|
||
function normalizePathForChunking(id: string): string {
|
||
return id.replace(/\\/g, '/');
|
||
}
|
||
|
||
function escapeRegExp(value: string): string {
|
||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
function findPackageRoot(entryPath: string): string {
|
||
let current = fs.statSync(entryPath).isDirectory()
|
||
? entryPath
|
||
: path.dirname(entryPath);
|
||
|
||
while (current !== path.dirname(current)) {
|
||
if (fs.existsSync(path.join(current, 'package.json'))) {
|
||
return current;
|
||
}
|
||
|
||
current = path.dirname(current);
|
||
}
|
||
|
||
throw new Error(`Unable to resolve package root for ${entryPath}`);
|
||
}
|
||
|
||
function resolvePackageRoot(packageName: string, fromPaths: string[]): string {
|
||
return findPackageRoot(require.resolve(packageName, { paths: fromPaths }));
|
||
}
|
||
|
||
function createPackageRootAlias(packageName: string, packageRoot: string) {
|
||
const escapedName = escapeRegExp(packageName);
|
||
|
||
return [
|
||
{ find: new RegExp(`^${escapedName}$`), replacement: packageRoot },
|
||
{
|
||
find: new RegExp(`^${escapedName}/(.+)$`),
|
||
replacement: `${packageRoot}/$1`,
|
||
},
|
||
];
|
||
}
|
||
|
||
const mermaidToDrawnixPackageRoot = resolvePackageRoot(
|
||
'@plait-board/mermaid-to-drawnix',
|
||
[workspaceRoot]
|
||
);
|
||
const diagramEngineAliases = [
|
||
...createPackageRootAlias(
|
||
'mermaid',
|
||
resolvePackageRoot('mermaid', [mermaidToDrawnixPackageRoot])
|
||
),
|
||
];
|
||
|
||
function normalizeBundleFileName(fileName: string): string {
|
||
return normalizePathForChunking(fileName).replace(/^\.\//, '');
|
||
}
|
||
|
||
function toManifestUrl(fileName: string): string {
|
||
const normalizedFileName = normalizeBundleFileName(fileName);
|
||
return normalizedFileName.startsWith('/')
|
||
? normalizedFileName
|
||
: `/${normalizedFileName}`;
|
||
}
|
||
|
||
function shouldIncludeIdlePrefetchBundleFile(fileName: string): boolean {
|
||
const normalizedFileName = normalizeBundleFileName(fileName).toLowerCase();
|
||
return (
|
||
normalizedFileName.endsWith('.js') || normalizedFileName.endsWith('.css')
|
||
);
|
||
}
|
||
|
||
function isNamedIdlePrefetchGroupFile(
|
||
fileName: string,
|
||
group: IdlePrefetchGroup
|
||
): boolean {
|
||
const baseName = path.posix
|
||
.basename(normalizeBundleFileName(fileName))
|
||
.toLowerCase();
|
||
return baseName.startsWith(`${group.toLowerCase()}-`);
|
||
}
|
||
|
||
function getNamedIdlePrefetchGroupForFile(
|
||
fileName: string
|
||
): IdlePrefetchGroup | undefined {
|
||
return IDLE_PREFETCH_GROUPS.find((group) =>
|
||
isNamedIdlePrefetchGroupFile(fileName, group)
|
||
);
|
||
}
|
||
|
||
function collectNamedGroupEntriesFromOutputDir(
|
||
assetsDir: string,
|
||
group: IdlePrefetchGroup
|
||
): ManifestEntry[] {
|
||
if (!fs.existsSync(assetsDir)) {
|
||
return [];
|
||
}
|
||
|
||
const files = fs.readdirSync(assetsDir);
|
||
const entries: ManifestEntry[] = [];
|
||
|
||
for (const file of files) {
|
||
const normalizedFileName = normalizeBundleFileName(file);
|
||
if (
|
||
!isNamedIdlePrefetchGroupFile(normalizedFileName, group) ||
|
||
!shouldIncludeIdlePrefetchBundleFile(normalizedFileName)
|
||
) {
|
||
continue;
|
||
}
|
||
|
||
const fullPath = path.join(assetsDir, file);
|
||
entries.push({
|
||
url: toManifestUrl(path.posix.join('assets', normalizedFileName)),
|
||
revision: createRevision(fullPath),
|
||
});
|
||
}
|
||
|
||
entries.sort((a, b) => a.url.localeCompare(b.url));
|
||
return entries;
|
||
}
|
||
|
||
function chunkBelongsToIdlePrefetchGroup(
|
||
chunk: ViteOutputChunk,
|
||
group: IdlePrefetchGroup
|
||
): boolean {
|
||
if (isNamedIdlePrefetchGroupFile(chunk.fileName, group)) {
|
||
return true;
|
||
}
|
||
|
||
const moduleIds = [chunk.facadeModuleId, ...Object.keys(chunk.modules)];
|
||
return moduleIds.some(
|
||
(moduleId) =>
|
||
typeof moduleId === 'string' &&
|
||
resolveIdlePrefetchGroup(moduleId) === group
|
||
);
|
||
}
|
||
|
||
function collectIdlePrefetchGroupEntriesFromBundle(
|
||
bundle: OutputBundle,
|
||
group: IdlePrefetchGroup
|
||
): ManifestEntry[] {
|
||
if (group === 'runtime-static-assets' || group === 'offline-static-assets') {
|
||
return [];
|
||
}
|
||
|
||
const chunksByFileName = new Map<string, ViteOutputChunk>();
|
||
const assetsByFileName = new Map<string, OutputAsset>();
|
||
|
||
for (const output of Object.values(bundle)) {
|
||
const normalizedFileName = normalizeBundleFileName(output.fileName);
|
||
|
||
if (output.type === 'chunk') {
|
||
chunksByFileName.set(normalizedFileName, output as ViteOutputChunk);
|
||
continue;
|
||
}
|
||
|
||
assetsByFileName.set(normalizedFileName, output);
|
||
}
|
||
|
||
const pendingChunkFileNames = Array.from(chunksByFileName.values())
|
||
.filter((chunk) => chunkBelongsToIdlePrefetchGroup(chunk, group))
|
||
.map((chunk) => normalizeBundleFileName(chunk.fileName));
|
||
|
||
if (pendingChunkFileNames.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const visitedChunkFileNames = new Set<string>();
|
||
const pendingCssFileNames = new Set<string>();
|
||
const entriesByUrl = new Map<string, ManifestEntry>();
|
||
|
||
const addBundleEntry = (file: OutputAsset | OutputChunk) => {
|
||
if (!shouldIncludeIdlePrefetchBundleFile(file.fileName)) {
|
||
return;
|
||
}
|
||
|
||
const entry = {
|
||
url: toManifestUrl(file.fileName),
|
||
revision: createBundleFileRevision(file),
|
||
};
|
||
entriesByUrl.set(entry.url, entry);
|
||
};
|
||
|
||
while (pendingChunkFileNames.length > 0) {
|
||
const currentFileName = pendingChunkFileNames.pop();
|
||
if (!currentFileName || visitedChunkFileNames.has(currentFileName)) {
|
||
continue;
|
||
}
|
||
|
||
visitedChunkFileNames.add(currentFileName);
|
||
|
||
const chunk = chunksByFileName.get(currentFileName);
|
||
if (!chunk) {
|
||
continue;
|
||
}
|
||
|
||
addBundleEntry(chunk);
|
||
|
||
chunk.viteMetadata?.importedCss?.forEach((cssFileName) => {
|
||
pendingCssFileNames.add(normalizeBundleFileName(cssFileName));
|
||
});
|
||
|
||
chunk.referencedFiles.forEach((referencedFileName) => {
|
||
const normalizedReferencedFileName =
|
||
normalizeBundleFileName(referencedFileName);
|
||
if (normalizedReferencedFileName.toLowerCase().endsWith('.css')) {
|
||
pendingCssFileNames.add(normalizedReferencedFileName);
|
||
}
|
||
});
|
||
|
||
for (const importedFileName of [
|
||
...chunk.imports,
|
||
...chunk.dynamicImports,
|
||
]) {
|
||
const normalizedImportedFileName =
|
||
normalizeBundleFileName(importedFileName);
|
||
const importedGroup = getNamedIdlePrefetchGroupForFile(
|
||
normalizedImportedFileName
|
||
);
|
||
|
||
if (importedGroup && importedGroup !== group) {
|
||
continue;
|
||
}
|
||
|
||
if (!visitedChunkFileNames.has(normalizedImportedFileName)) {
|
||
pendingChunkFileNames.push(normalizedImportedFileName);
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const cssFileName of pendingCssFileNames) {
|
||
const cssAsset = assetsByFileName.get(cssFileName);
|
||
if (cssAsset) {
|
||
addBundleEntry(cssAsset);
|
||
}
|
||
}
|
||
|
||
for (const asset of assetsByFileName.values()) {
|
||
if (
|
||
isNamedIdlePrefetchGroupFile(asset.fileName, group) &&
|
||
asset.fileName.toLowerCase().endsWith('.css')
|
||
) {
|
||
addBundleEntry(asset);
|
||
}
|
||
}
|
||
|
||
return Array.from(entriesByUrl.values()).sort((a, b) =>
|
||
a.url.localeCompare(b.url)
|
||
);
|
||
}
|
||
|
||
function resolveIdlePrefetchGroup(id: string): IdlePrefetchGroup | undefined {
|
||
const normalizedId = normalizePathForChunking(id);
|
||
|
||
if (
|
||
normalizedId.includes('/packages/drawnix/src/components/chat-drawer/') ||
|
||
normalizedId.includes('/packages/drawnix/src/hooks/useChatHandler') ||
|
||
normalizedId.includes('/packages/drawnix/src/services/chat-service') ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/services/chat-storage-service'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/types/chat.types') ||
|
||
normalizedId.includes('/packages/drawnix/src/types/chat-ui.types')
|
||
) {
|
||
return 'ai-chat';
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes('/node_modules/.pnpm/mermaid@') ||
|
||
normalizedId.includes('/node_modules/mermaid/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/elkjs@') ||
|
||
normalizedId.includes('/node_modules/elkjs/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/cytoscape@') ||
|
||
normalizedId.includes('/node_modules/cytoscape/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/layout-base@') ||
|
||
normalizedId.includes('/node_modules/layout-base/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/cose-base@') ||
|
||
normalizedId.includes('/node_modules/cose-base/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/cytoscape-cose-bilkent@') ||
|
||
normalizedId.includes('/node_modules/cytoscape-cose-bilkent/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/dagre-d3-es@') ||
|
||
normalizedId.includes('/node_modules/dagre-d3-es/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/dompurify@') ||
|
||
normalizedId.includes('/node_modules/dompurify/')
|
||
) {
|
||
return 'diagram-engines';
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes('/node_modules/.pnpm/xlsx@') ||
|
||
normalizedId.includes('/node_modules/xlsx/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/jszip@') ||
|
||
normalizedId.includes('/node_modules/jszip/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/pptxgenjs@') ||
|
||
normalizedId.includes('/node_modules/pptxgenjs/')
|
||
) {
|
||
return 'office-data';
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes('/node_modules/.pnpm/@milkdown+') ||
|
||
normalizedId.includes('/node_modules/@milkdown/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/@codemirror+') ||
|
||
normalizedId.includes('/node_modules/@codemirror/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/@lezer+') ||
|
||
normalizedId.includes('/node_modules/@lezer/') ||
|
||
normalizedId.includes('/node_modules/.pnpm/prosemirror-') ||
|
||
normalizedId.includes('/node_modules/prosemirror-') ||
|
||
normalizedId.includes('/node_modules/.pnpm/katex@') ||
|
||
normalizedId.includes('/node_modules/katex/')
|
||
) {
|
||
return 'editor-engines';
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes('/node_modules/.pnpm/viewerjs@') ||
|
||
normalizedId.includes('/node_modules/viewerjs/')
|
||
) {
|
||
return 'media-viewer';
|
||
}
|
||
|
||
if (normalizedId.includes('/packages/drawnix/src/tools/tool-ids.ts')) {
|
||
return undefined;
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/startup/DrawnixDeferredRuntime.tsx'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/startup/DeferredMediaLibraryModal.tsx'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/startup/DeferredSyncSettings.tsx'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/services/asset-integration-service'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/services/font-manager-service'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/hooks/useTaskStorage') ||
|
||
normalizedId.includes('/packages/drawnix/src/hooks/useTaskExecutor') ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/hooks/useAutoInsertToCanvas'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/hooks/useImageGenerationAnchorSync'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/hooks/useBeforeUnload') ||
|
||
normalizedId.includes('/packages/drawnix/src/hooks/useProviderProfiles') ||
|
||
normalizedId.includes('/packages/drawnix/src/components/toolbox-drawer/') ||
|
||
normalizedId.includes('/packages/drawnix/src/components/project-drawer/') ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/toolbar/minimized-tools-bar/'
|
||
) ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/services/tool-window-service'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/services/toolbox-service') ||
|
||
normalizedId.includes('/packages/drawnix/src/tools/') ||
|
||
normalizedId.includes('/packages/drawnix/src/components/backup-restore/') ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/performance-panel/'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/components/version-update/') ||
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/components/command-palette/'
|
||
) ||
|
||
normalizedId.includes('/packages/drawnix/src/components/canvas-search/')
|
||
) {
|
||
return 'tool-windows';
|
||
}
|
||
|
||
if (
|
||
normalizedId.includes(
|
||
'/packages/drawnix/src/generated/external-skills-bundle.json'
|
||
)
|
||
) {
|
||
return 'external-skills';
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
function isStartupRuntimeModule(id: string): boolean {
|
||
const normalizedId = normalizePathForChunking(id);
|
||
return (
|
||
normalizedId.includes('vite/preload-helper') ||
|
||
normalizedId.includes('commonjs-dynamic-modules') ||
|
||
normalizedId.includes('commonjsHelpers')
|
||
);
|
||
}
|
||
|
||
interface ManualChunkModuleInfo {
|
||
importers: string[];
|
||
}
|
||
|
||
interface ManualChunkContext {
|
||
getModuleInfo: (id: string) => ManualChunkModuleInfo | null;
|
||
}
|
||
|
||
function isWebMainEntry(id: string): boolean {
|
||
const normalizedId = normalizePathForChunking(id);
|
||
return (
|
||
normalizedId.endsWith('/apps/web/src/main.tsx') ||
|
||
normalizedId.endsWith('/src/main.tsx')
|
||
);
|
||
}
|
||
|
||
function isStaticallyReachableFromWebMain(
|
||
id: string,
|
||
getModuleInfo: ManualChunkContext['getModuleInfo'],
|
||
visited = new Set<string>()
|
||
): boolean {
|
||
if (visited.has(id)) {
|
||
return false;
|
||
}
|
||
visited.add(id);
|
||
|
||
if (isWebMainEntry(id)) {
|
||
return true;
|
||
}
|
||
|
||
const moduleInfo = getModuleInfo(id);
|
||
if (!moduleInfo) {
|
||
return false;
|
||
}
|
||
|
||
return moduleInfo.importers.some((importerId) =>
|
||
isStaticallyReachableFromWebMain(importerId, getModuleInfo, visited)
|
||
);
|
||
}
|
||
|
||
function resolveManualChunk(
|
||
id: string,
|
||
context?: ManualChunkContext
|
||
): string | undefined {
|
||
if (isStartupRuntimeModule(id)) {
|
||
return 'startup-runtime';
|
||
}
|
||
|
||
const group = resolveIdlePrefetchGroup(id);
|
||
if (
|
||
(group === undefined || group === 'tool-windows') &&
|
||
context &&
|
||
isStaticallyReachableFromWebMain(id, context.getModuleInfo)
|
||
) {
|
||
return 'startup-app';
|
||
}
|
||
|
||
return group;
|
||
}
|
||
|
||
/**
|
||
* Vite 插件:生成 precache-manifest.json
|
||
* 在构建完成后扫描输出目录,生成需要预缓存的静态资源清单
|
||
*/
|
||
function precacheManifestPlugin(): Plugin {
|
||
return {
|
||
name: 'precache-manifest',
|
||
apply: 'build',
|
||
closeBundle: {
|
||
sequential: true,
|
||
order: 'post',
|
||
async handler() {
|
||
const outDir = path.resolve(__dirname, '../../dist/apps/web');
|
||
const indexHtmlPath = path.join(outDir, 'index.html');
|
||
const precacheUrls = await collectHtmlShellPrecacheUrls(indexHtmlPath);
|
||
const manifest = await collectSelectedEntries(
|
||
outDir,
|
||
precacheUrls,
|
||
(url, ext) =>
|
||
PRECACHE_ALWAYS_INCLUDE.has(url) ||
|
||
(ext !== '.html' && PRECACHE_EXTENSIONS.has(ext))
|
||
);
|
||
|
||
// 写入 manifest 文件
|
||
const manifestPath = path.join(outDir, 'precache-manifest.json');
|
||
const manifestContent = {
|
||
version: appVersion,
|
||
timestamp: new Date().toISOString(),
|
||
files: manifest,
|
||
};
|
||
|
||
await writeFileWithFdRetry(
|
||
manifestPath,
|
||
JSON.stringify(manifestContent, null, 2)
|
||
);
|
||
console.log(
|
||
`[Precache] Generated manifest with ${manifest.length} files`
|
||
);
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function idlePrefetchManifestPlugin(): Plugin {
|
||
let bundleGroupEntries = Object.fromEntries(
|
||
IDLE_PREFETCH_GROUPS.map((group) => [group, [] as ManifestEntry[]])
|
||
) as Record<IdlePrefetchGroup, ManifestEntry[]>;
|
||
|
||
return {
|
||
name: 'idle-prefetch-manifest',
|
||
apply: 'build',
|
||
generateBundle(_options, bundle) {
|
||
bundleGroupEntries = Object.fromEntries(
|
||
IDLE_PREFETCH_GROUPS.map((group) => [
|
||
group,
|
||
collectIdlePrefetchGroupEntriesFromBundle(bundle, group),
|
||
])
|
||
) as Record<IdlePrefetchGroup, ManifestEntry[]>;
|
||
},
|
||
closeBundle: {
|
||
sequential: true,
|
||
order: 'post',
|
||
async handler() {
|
||
const outDir = path.resolve(__dirname, '../../dist/apps/web');
|
||
const assetsDir = path.join(outDir, 'assets');
|
||
const precacheManifestPath = path.join(
|
||
outDir,
|
||
'precache-manifest.json'
|
||
);
|
||
const groups = Object.fromEntries(
|
||
IDLE_PREFETCH_GROUPS.map((group) => [
|
||
group,
|
||
[] as Array<{ url: string; revision: string }>,
|
||
])
|
||
) as Record<
|
||
IdlePrefetchGroup,
|
||
Array<{ url: string; revision: string }>
|
||
>;
|
||
const precachedUrls = new Set<string>();
|
||
|
||
if (fs.existsSync(precacheManifestPath)) {
|
||
try {
|
||
const precacheManifest = JSON.parse(
|
||
(await readFileWithFdRetry(
|
||
precacheManifestPath,
|
||
'utf8'
|
||
)) as string
|
||
) as { files?: Array<{ url?: string }> };
|
||
for (const entry of precacheManifest.files || []) {
|
||
if (typeof entry.url === 'string' && entry.url.length > 0) {
|
||
precachedUrls.add(entry.url);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(
|
||
'[IdlePrefetch] Failed to read precache-manifest.json:',
|
||
error
|
||
);
|
||
}
|
||
}
|
||
|
||
if (fs.existsSync(assetsDir)) {
|
||
for (const group of IDLE_PREFETCH_GROUPS) {
|
||
if (group === 'offline-static-assets') {
|
||
continue;
|
||
}
|
||
|
||
const expandedEntries =
|
||
bundleGroupEntries[group].length > 0
|
||
? bundleGroupEntries[group]
|
||
: collectNamedGroupEntriesFromOutputDir(assetsDir, group);
|
||
|
||
groups[group] = expandedEntries.filter((entry) => {
|
||
const fullPath = path.join(outDir, entry.url.replace(/^\//, ''));
|
||
return fs.existsSync(fullPath) && !precachedUrls.has(entry.url);
|
||
});
|
||
}
|
||
}
|
||
|
||
groups['runtime-static-assets'] = collectStaticEntries(
|
||
outDir,
|
||
(url, ext) =>
|
||
!precachedUrls.has(url) &&
|
||
ext !== '.html' &&
|
||
POST_BOOT_PREFETCH_EXTENSIONS.has(ext) &&
|
||
isRuntimeStaticAssetUrl(url)
|
||
);
|
||
|
||
const runtimeStaticUrls = new Set(
|
||
groups['runtime-static-assets'].map((entry) => entry.url)
|
||
);
|
||
|
||
groups['offline-static-assets'] = collectStaticEntries(
|
||
outDir,
|
||
(url, ext) =>
|
||
!precachedUrls.has(url) &&
|
||
!runtimeStaticUrls.has(url) &&
|
||
ext !== '.html' &&
|
||
POST_BOOT_PREFETCH_EXTENSIONS.has(ext)
|
||
);
|
||
|
||
const manifestPath = path.join(outDir, 'idle-prefetch-manifest.json');
|
||
fs.writeFileSync(
|
||
manifestPath,
|
||
JSON.stringify(
|
||
{
|
||
version: appVersion,
|
||
timestamp: new Date().toISOString(),
|
||
defaults: [...IDLE_PREFETCH_DEFAULTS],
|
||
groups,
|
||
},
|
||
null,
|
||
2
|
||
)
|
||
);
|
||
console.log('[IdlePrefetch] Generated idle-prefetch-manifest.json');
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function deferEntryAssetsPlugin(): Plugin {
|
||
return {
|
||
name: 'defer-entry-assets',
|
||
apply: 'build',
|
||
closeBundle: {
|
||
sequential: true,
|
||
order: 'post',
|
||
async handler() {
|
||
const outDir = path.resolve(__dirname, '../../dist/apps/web');
|
||
const indexHtmlPath = path.join(outDir, 'index.html');
|
||
|
||
if (!fs.existsSync(indexHtmlPath)) {
|
||
return;
|
||
}
|
||
|
||
const html = (await readFileWithFdRetry(
|
||
indexHtmlPath,
|
||
'utf8'
|
||
)) as string;
|
||
const deferredTags: string[] = [];
|
||
const assetTagPattern =
|
||
/^[ \t]*(<script\b[^>]*type="module"[^>]*src="\.\/assets\/[^"]+"[^>]*><\/script>|<link\b[^>]*rel="stylesheet"[^>]*href="\.\/assets\/[^"]+"[^>]*>)\s*$/gm;
|
||
|
||
const strippedHtml = html.replace(assetTagPattern, (match, tag) => {
|
||
deferredTags.push(tag.trim());
|
||
return '';
|
||
});
|
||
|
||
if (deferredTags.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const injection = ` ${deferredTags.join('\n ')}\n`;
|
||
const nextHtml = strippedHtml.replace('</body>', `${injection}</body>`);
|
||
|
||
await writeFileWithFdRetry(indexHtmlPath, nextHtml);
|
||
console.log(
|
||
`[EntryAssets] Deferred ${deferredTags.length} entry asset tag(s) to body end`
|
||
);
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function rewriteEntryAssetsToCDNPlugin(): Plugin {
|
||
const preferLocalAssets =
|
||
process.env.TRUEGROWTH_DESKTOP_LOCAL_ASSETS === '1' ||
|
||
process.env.VITE_DESKTOP_LOCAL_ASSETS === '1';
|
||
|
||
return {
|
||
name: 'rewrite-entry-assets-to-cdn',
|
||
apply: 'build',
|
||
closeBundle: {
|
||
sequential: true,
|
||
order: 'post',
|
||
async handler() {
|
||
const outDir = path.resolve(__dirname, '../../dist/apps/web');
|
||
const indexHtmlPath = path.join(outDir, 'index.html');
|
||
|
||
if (!fs.existsSync(indexHtmlPath)) {
|
||
return;
|
||
}
|
||
|
||
const html = (await readFileWithFdRetry(
|
||
indexHtmlPath,
|
||
'utf8'
|
||
)) as string;
|
||
const cdnBaseUrl = `https://cdn.jsdelivr.net/npm/aitu-app@${appVersion}`;
|
||
let rewrittenCount = 0;
|
||
|
||
const rewriteAssetUrl = (localPath: string) => {
|
||
if (preferLocalAssets) {
|
||
return localPath;
|
||
}
|
||
|
||
const [pathname, suffix = ''] = localPath.split(/([?#].*)/, 2);
|
||
return `${cdnBaseUrl}/${pathname.replace(/^\.\//, '')}${suffix}`;
|
||
};
|
||
|
||
const rewriteManagedLinkTag = (
|
||
beforeHref: string,
|
||
localHref: string,
|
||
afterHref: string
|
||
) => {
|
||
const hasSelfClosingSlash = /\/\s*$/.test(afterHref);
|
||
const normalizedAfterHref = afterHref.replace(/\/\s*$/, '');
|
||
rewrittenCount += 1;
|
||
return `<link${beforeHref}href="${rewriteAssetUrl(
|
||
localHref
|
||
)}" data-local-href="${localHref}" data-cdn-fallback-managed="1"${normalizedAfterHref} onerror="window.__OPENTU_BOOT_ASSET_FALLBACK__&&window.__OPENTU_BOOT_ASSET_FALLBACK__(this)"${
|
||
hasSelfClosingSlash ? ' /' : ''
|
||
}>`;
|
||
};
|
||
|
||
let nextHtml = html.replace(
|
||
/<script\b([^>]*\btype="module"[^>]*)\bsrc="(\.\/assets\/[^"]+)"([^>]*)><\/script>/g,
|
||
(_match, beforeSrc, localSrc, afterSrc) => {
|
||
rewrittenCount += 1;
|
||
return `<script${beforeSrc}src="${rewriteAssetUrl(
|
||
localSrc
|
||
)}" data-local-src="${localSrc}" data-cdn-fallback-managed="1"${afterSrc} onerror="window.__OPENTU_BOOT_ASSET_FALLBACK__&&window.__OPENTU_BOOT_ASSET_FALLBACK__(this)"></script>`;
|
||
}
|
||
);
|
||
|
||
nextHtml = nextHtml.replace(
|
||
/<link\b([^>]*\brel="stylesheet"[^>]*)\bhref="(\.\/assets\/[^"]+)"([^>]*)>/g,
|
||
(_match, beforeHref, localHref, afterHref) =>
|
||
rewriteManagedLinkTag(beforeHref, localHref, afterHref)
|
||
);
|
||
|
||
nextHtml = nextHtml.replace(
|
||
/<link\b([^>]*\brel="(?:manifest|icon|apple-touch-icon)"[^>]*)\bhref="(\.\/[^"]+)"([^>]*)>/g,
|
||
(_match, beforeHref, localHref, afterHref) =>
|
||
rewriteManagedLinkTag(beforeHref, localHref, afterHref)
|
||
);
|
||
|
||
if (rewrittenCount === 0) {
|
||
return;
|
||
}
|
||
|
||
await writeFileWithFdRetry(indexHtmlPath, nextHtml);
|
||
console.log(
|
||
`[EntryAssets] Rewrote ${rewrittenCount} entry asset tag(s) to prefer ${
|
||
preferLocalAssets ? 'local files' : 'CDN'
|
||
}`
|
||
);
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function rewriteManifestAssetsToCDNPlugin(): Plugin {
|
||
return {
|
||
name: 'rewrite-manifest-assets-to-cdn',
|
||
apply: 'build',
|
||
closeBundle: {
|
||
sequential: true,
|
||
order: 'post',
|
||
async handler() {
|
||
const outDir = path.resolve(__dirname, '../../dist/apps/web');
|
||
const manifestPath = path.join(outDir, 'manifest.json');
|
||
|
||
if (!fs.existsSync(manifestPath)) {
|
||
return;
|
||
}
|
||
|
||
const manifest = JSON.parse(
|
||
(await readFileWithFdRetry(manifestPath, 'utf8')) as string
|
||
);
|
||
const cdnBaseUrl = `https://cdn.jsdelivr.net/npm/aitu-app@${appVersion}`;
|
||
let rewrittenCount = 0;
|
||
|
||
const rewriteManifestAssetUrl = (assetUrl: string) => {
|
||
if (
|
||
typeof assetUrl !== 'string' ||
|
||
!assetUrl ||
|
||
/^https?:\/\//.test(assetUrl)
|
||
) {
|
||
return assetUrl;
|
||
}
|
||
|
||
rewrittenCount += 1;
|
||
return `${cdnBaseUrl}/${assetUrl.replace(/^\.\//, '')}`;
|
||
};
|
||
|
||
if (Array.isArray(manifest.icons)) {
|
||
manifest.icons = manifest.icons.map(
|
||
(icon: Record<string, unknown>) => ({
|
||
...icon,
|
||
src: rewriteManifestAssetUrl(String(icon.src || '')),
|
||
})
|
||
);
|
||
}
|
||
|
||
if (Array.isArray(manifest.shortcuts)) {
|
||
manifest.shortcuts = manifest.shortcuts.map(
|
||
(shortcut: Record<string, unknown>) => ({
|
||
...shortcut,
|
||
icons: Array.isArray(shortcut.icons)
|
||
? shortcut.icons.map((icon: Record<string, unknown>) => ({
|
||
...icon,
|
||
src: rewriteManifestAssetUrl(String(icon.src || '')),
|
||
}))
|
||
: shortcut.icons,
|
||
})
|
||
);
|
||
}
|
||
|
||
if (rewrittenCount === 0) {
|
||
return;
|
||
}
|
||
|
||
fs.writeFileSync(
|
||
manifestPath,
|
||
JSON.stringify(manifest, null, 2) + '\n'
|
||
);
|
||
console.log(
|
||
`[ManifestAssets] Rewrote ${rewrittenCount} manifest asset url(s) to prefer CDN`
|
||
);
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
// 检测是否在 watch 模式下运行(命令行包含 --watch)
|
||
const isWatchMode = process.argv.includes('--watch');
|
||
const isServeMode = process.argv.includes('serve');
|
||
const isVitestMode = process.argv.some((arg) => arg.includes('vitest'));
|
||
const reactNodeEnv =
|
||
isWatchMode || isServeMode || isVitestMode ? 'development' : 'production';
|
||
const TRUEMODEL_API_PROXY_PREFIX = '/api/truemodel';
|
||
const LOCAL_API_PROXY_TARGET =
|
||
process.env.VITE_LOCAL_API_PROXY_TARGET ||
|
||
`http://127.0.0.1:${process.env.TRUEGROWTH_LOCAL_API_PORT || 48177}`;
|
||
const appDevProxy = {
|
||
[TRUEMODEL_API_PROXY_PREFIX]: {
|
||
target: 'https://truemodel.benchu.cloud',
|
||
changeOrigin: true,
|
||
secure: true,
|
||
rewrite: (requestPath: string) =>
|
||
requestPath.replace(/^\/api\/truemodel(?=\/|$)/, '') || '/',
|
||
},
|
||
'/local-api': {
|
||
target: LOCAL_API_PROXY_TARGET,
|
||
changeOrigin: true,
|
||
secure: false,
|
||
},
|
||
};
|
||
|
||
export default defineConfig({
|
||
root: __dirname,
|
||
cacheDir: '../../node_modules/.vite/apps/web',
|
||
|
||
// 使用相对路径,源站始终可用,CDN 加速由 SW 层处理
|
||
// SW 的 handleStaticRequest: cache → CDN → 源站回退
|
||
base: process.env.VITE_BASE_URL || './',
|
||
|
||
define: {
|
||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
|
||
'process.env.NODE_ENV': JSON.stringify(reactNodeEnv),
|
||
__APP_VERSION__: JSON.stringify(appVersion),
|
||
// Vue feature flags - @milkdown/crepe 内部使用了 Vue,需要定义这些编译时标志
|
||
__VUE_OPTIONS_API__: JSON.stringify(false),
|
||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
|
||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(false),
|
||
},
|
||
|
||
esbuild: {
|
||
jsxDev: reactNodeEnv === 'development',
|
||
},
|
||
|
||
server: {
|
||
port: 7200,
|
||
host: 'localhost',
|
||
proxy: appDevProxy,
|
||
headers: {
|
||
'Content-Security-Policy':
|
||
"default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://us.i.posthog.com https://us-assets.i.posthog.com https://wiki.tu-zi.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: http: https:; media-src 'self' data: blob: http: https:; connect-src 'self' http: https: ws: wss: data:; frame-ancestors 'self' localhost:* 127.0.0.1:* https://truemodel.benchu.cloud;",
|
||
},
|
||
},
|
||
|
||
preview: {
|
||
port: 4300,
|
||
host: 'localhost',
|
||
proxy: appDevProxy,
|
||
headers: {
|
||
'Content-Security-Policy':
|
||
"upgrade-insecure-requests; default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://us.i.posthog.com https://us-assets.i.posthog.com https://wiki.tu-zi.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: http: https:; media-src 'self' data: blob: http: https:; connect-src 'self' http: https: ws: wss: data:; frame-ancestors 'self' localhost:* 127.0.0.1:* https://truemodel.benchu.cloud;",
|
||
},
|
||
},
|
||
|
||
plugins: [
|
||
react(),
|
||
nxViteTsPaths(),
|
||
visualizer({
|
||
open: false,
|
||
filename: path.resolve(__dirname, '../../dist/apps/web/stats.html'),
|
||
gzipSize: true,
|
||
brotliSize: true,
|
||
}),
|
||
deferEntryAssetsPlugin(),
|
||
rewriteEntryAssetsToCDNPlugin(),
|
||
rewriteManifestAssetsToCDNPlugin(),
|
||
precacheManifestPlugin(),
|
||
idlePrefetchManifestPlugin(),
|
||
],
|
||
|
||
resolve: {
|
||
alias: [
|
||
...diagramEngineAliases,
|
||
{
|
||
find: /^tdesign-react$/,
|
||
replacement: path.resolve(
|
||
__dirname,
|
||
'../../packages/drawnix/src/utils/tdesign.ts'
|
||
),
|
||
},
|
||
],
|
||
dedupe: ['react', 'react-dom'],
|
||
},
|
||
|
||
// Uncomment this if you are using workers.
|
||
// worker: {
|
||
// plugins: [ nxViteTsPaths() ],
|
||
// },
|
||
|
||
build: {
|
||
outDir: '../../dist/apps/web',
|
||
// watch 模式下不清空输出目录,避免 index.html 丢失导致 serve 失败
|
||
emptyOutDir: !isWatchMode,
|
||
reportCompressedSize: true,
|
||
// 首屏只注入壳层资源,懒加载分组改由运行时按需拉取/空闲预取。
|
||
modulePreload: false,
|
||
commonjsOptions: {
|
||
transformMixedEsModules: true,
|
||
},
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks(id, context) {
|
||
return resolveManualChunk(id, context);
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|