Merge remote-tracking branch 'origin/main' into feat/model-plugins-implementing

This commit is contained in:
yyh
2026-03-11 11:57:11 +08:00
10 changed files with 198 additions and 274 deletions

View File

@ -35,7 +35,7 @@ COPY --from=packages /app/web/ .
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm build:docker
RUN pnpm build
# production stage

View File

@ -20,17 +20,21 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
}) => {
const [editor] = useLexicalComposerContext()
const ref = useRef<any>(null)
const ref = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
return mergeRegister(
const clearHideMenuTimeout = () => {
if (ref.current) {
clearTimeout(ref.current)
ref.current = null
}
}
const unregister = mergeRegister(
editor.registerCommand(
CLEAR_HIDE_MENU_TIMEOUT,
() => {
if (ref.current) {
clearTimeout(ref.current)
ref.current = null
}
clearHideMenuTimeout()
return true
},
COMMAND_PRIORITY_EDITOR,
@ -41,6 +45,7 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
// Check if the clicked target element is var-search-input
const target = event?.relatedTarget as HTMLElement
if (!target?.classList?.contains('var-search-input')) {
clearHideMenuTimeout()
ref.current = setTimeout(() => {
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
}, 200)
@ -61,6 +66,11 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
COMMAND_PRIORITY_EDITOR,
),
)
return () => {
clearHideMenuTimeout()
unregister()
}
}, [editor, onBlur, onFocus])
return null

View File

@ -3565,11 +3565,6 @@
"count": 2
}
},
"app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/base/prompt-editor/plugins/query-block/index.tsx": {
"react-refresh/only-export-components": {
"count": 2
@ -12614,14 +12609,6 @@
"count": 2
}
},
"scripts/optimize-standalone.js": {
"e18e/prefer-static-regex": {
"count": 1
},
"unused-imports/no-unused-vars": {
"count": 2
}
},
"scripts/refactor-component.js": {
"e18e/prefer-static-regex": {
"count": 14

View File

@ -218,3 +218,6 @@ export default antfu(
},
},
)
.disableRulesFix([
'e18e/prefer-array-at',
])

View File

@ -26,39 +26,36 @@
"node": "^22.22.1"
},
"scripts": {
"analyze": "next experimental-analyze",
"analyze-component": "node ./scripts/analyze-component.js",
"build": "next build",
"build:vinext": "vinext build",
"dev": "next dev",
"dev:inspect": "next dev --inspect",
"dev:vinext": "vinext dev",
"build": "next build",
"build:docker": "next build && node scripts/optimize-standalone.js",
"build:vinext": "vinext build",
"start": "node ./scripts/copy-and-start.mjs",
"start:vinext": "vinext start",
"gen-doc-paths": "tsx ./scripts/gen-doc-paths.ts",
"gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/",
"i18n:check": "tsx ./scripts/check-i18n.js",
"knip": "knip",
"lint": "eslint --cache --concurrency=auto",
"lint:ci": "eslint --cache --concurrency 2",
"lint:fix": "pnpm lint --fix",
"lint:quiet": "pnpm lint --quiet",
"lint:complexity": "pnpm lint --rule 'complexity: [error, {max: 15}]' --quiet",
"lint:report": "pnpm lint --output-file eslint_report.json --format json",
"lint:tss": "tsslint --project tsconfig.json",
"type-check": "tsc --noEmit",
"type-check:tsgo": "tsgo --noEmit",
"preinstall": "npx only-allow pnpm",
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky",
"gen-icons": "node ./scripts/gen-icons.mjs && eslint --fix app/components/base/icons/src/",
"gen-doc-paths": "tsx ./scripts/gen-doc-paths.ts",
"uglify-embed": "node ./bin/uglify-embed",
"i18n:check": "tsx ./scripts/check-i18n.js",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --coverage --silent=passed-only",
"test:watch": "vitest --watch",
"analyze-component": "node ./scripts/analyze-component.js",
"refactor-component": "node ./scripts/refactor-component.js",
"start": "node ./scripts/copy-and-start.mjs",
"start:vinext": "vinext start",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build",
"preinstall": "npx only-allow pnpm",
"analyze": "next experimental-analyze",
"knip": "knip"
"test": "vitest run",
"test:ci": "vitest run --coverage --silent=passed-only",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"type-check": "tsc --noEmit",
"type-check:tsgo": "tsgo --noEmit",
"uglify-embed": "node ./bin/uglify-embed"
},
"dependencies": {
"@amplitude/analytics-browser": "2.36.3",

View File

@ -1,38 +0,0 @@
# Production Build Optimization Scripts
## optimize-standalone.js
This script removes unnecessary development dependencies from the Next.js standalone build output to reduce the production Docker image size.
### What it does
The script specifically targets and removes `jest-worker` packages that are bundled with Next.js but not needed in production. These packages are included because:
1. Next.js includes jest-worker in its compiled dependencies
1. terser-webpack-plugin (used by Next.js for minification) depends on jest-worker
1. pnpm's dependency resolution creates symlinks to jest-worker in various locations
### Usage
The script is automatically run during Docker builds via the `build:docker` npm script:
```bash
# Docker build (removes jest-worker after build)
pnpm build:docker
```
To run the optimization manually:
```bash
node scripts/optimize-standalone.js
```
### What gets removed
- `node_modules/.pnpm/next@*/node_modules/next/dist/compiled/jest-worker`
- `node_modules/.pnpm/terser-webpack-plugin@*/node_modules/jest-worker` (symlinks)
- `node_modules/.pnpm/jest-worker@*` (actual packages)
### Impact
Removing jest-worker saves approximately 36KB per instance from the production image. While this may seem small, it helps ensure production images only contain necessary runtime dependencies.

View File

@ -1,163 +0,0 @@
/**
* Script to optimize Next.js standalone output for production
* Removes unnecessary files like jest-worker that are bundled with Next.js
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
console.log('🔧 Optimizing standalone output...')
const standaloneDir = path.join(__dirname, '..', '.next', 'standalone')
// Check if standalone directory exists
if (!fs.existsSync(standaloneDir)) {
console.error('❌ Standalone directory not found. Please run "next build" first.')
process.exit(1)
}
// List of paths to remove (relative to standalone directory)
const pathsToRemove = [
// Remove jest-worker from Next.js compiled dependencies
'node_modules/.pnpm/next@*/node_modules/next/dist/compiled/jest-worker',
// Remove jest-worker symlinks from terser-webpack-plugin
'node_modules/.pnpm/terser-webpack-plugin@*/node_modules/jest-worker',
// Remove actual jest-worker packages (directories only, not symlinks)
'node_modules/.pnpm/jest-worker@*',
]
// Function to safely remove a path
function removePath(basePath, relativePath) {
const fullPath = path.join(basePath, relativePath)
// Handle wildcard patterns
if (relativePath.includes('*')) {
const parts = relativePath.split('/')
let currentPath = basePath
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (part.includes('*')) {
// Find matching directories
if (fs.existsSync(currentPath)) {
const entries = fs.readdirSync(currentPath)
// replace '*' with '.*'
const regexPattern = part.replace(/\*/g, '.*')
const regex = new RegExp(`^${regexPattern}$`)
for (const entry of entries) {
if (regex.test(entry)) {
const remainingPath = parts.slice(i + 1).join('/')
const matchedPath = path.join(currentPath, entry, remainingPath)
try {
// Use lstatSync to check if path exists (works for both files and symlinks)
const stats = fs.lstatSync(matchedPath)
if (stats.isSymbolicLink()) {
// Remove symlink
fs.unlinkSync(matchedPath)
console.log(`✅ Removed symlink: ${path.relative(basePath, matchedPath)}`)
}
else {
// Remove directory/file
fs.rmSync(matchedPath, { recursive: true, force: true })
console.log(`✅ Removed: ${path.relative(basePath, matchedPath)}`)
}
}
catch (error) {
// Silently ignore ENOENT (path not found) errors
if (error.code !== 'ENOENT') {
console.error(`❌ Failed to remove ${matchedPath}: ${error.message}`)
}
}
}
}
}
return
}
else {
currentPath = path.join(currentPath, part)
}
}
}
else {
// Direct path removal
if (fs.existsSync(fullPath)) {
try {
fs.rmSync(fullPath, { recursive: true, force: true })
console.log(`✅ Removed: ${relativePath}`)
}
catch (error) {
console.error(`❌ Failed to remove ${fullPath}: ${error.message}`)
}
}
}
}
// Remove unnecessary paths
console.log('🗑️ Removing unnecessary files...')
for (const pathToRemove of pathsToRemove) {
removePath(standaloneDir, pathToRemove)
}
// Calculate size reduction
console.log('\n📊 Optimization complete!')
// Optional: Display the size of remaining jest-related files (if any)
const checkForJest = (dir) => {
const jestFiles = []
function walk(currentPath) {
if (!fs.existsSync(currentPath))
return
try {
const entries = fs.readdirSync(currentPath)
for (const entry of entries) {
const fullPath = path.join(currentPath, entry)
try {
const stat = fs.lstatSync(fullPath) // Use lstatSync to handle symlinks
if (stat.isDirectory() && !stat.isSymbolicLink()) {
// Skip node_modules subdirectories to avoid deep traversal
if (entry === 'node_modules' && currentPath !== standaloneDir) {
continue
}
walk(fullPath)
}
else if (stat.isFile() && entry.includes('jest')) {
jestFiles.push(path.relative(standaloneDir, fullPath))
}
}
catch (err) {
// Skip files that can't be accessed
continue
}
}
}
catch (err) {
// Skip directories that can't be read
}
}
walk(dir)
return jestFiles
}
const remainingJestFiles = checkForJest(standaloneDir)
if (remainingJestFiles.length > 0) {
console.log('\n⚠ Warning: Some jest-related files still remain:')
remainingJestFiles.forEach(file => console.log(` - ${file}`))
}
else {
console.log('\n✨ No jest-related files found in standalone output!')
}