2025-08-25 14:44:29 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Script to optimize Next.js standalone output for production
|
|
|
|
|
|
* Removes unnecessary files like jest-worker that are bundled with Next.js
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-12-22 16:35:22 +08:00
|
|
|
|
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)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
2025-12-23 16:58:55 +08:00
|
|
|
|
console.log('🔧 Optimizing standalone output...')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const standaloneDir = path.join(__dirname, '..', '.next', 'standalone')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
// Check if standalone directory exists
|
|
|
|
|
|
if (!fs.existsSync(standaloneDir)) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
console.error('❌ Standalone directory not found. Please run "next build" first.')
|
|
|
|
|
|
process.exit(1)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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@*',
|
2025-12-23 16:58:55 +08:00
|
|
|
|
]
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
// Function to safely remove a path
|
|
|
|
|
|
function removePath(basePath, relativePath) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const fullPath = path.join(basePath, relativePath)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
// Handle wildcard patterns
|
|
|
|
|
|
if (relativePath.includes('*')) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const parts = relativePath.split('/')
|
|
|
|
|
|
let currentPath = basePath
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const part = parts[i]
|
2025-08-25 14:44:29 +08:00
|
|
|
|
if (part.includes('*')) {
|
|
|
|
|
|
// Find matching directories
|
|
|
|
|
|
if (fs.existsSync(currentPath)) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const entries = fs.readdirSync(currentPath)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
// replace '*' with '.*'
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const regexPattern = part.replace(/\*/g, '.*')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const regex = new RegExp(`^${regexPattern}$`)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
if (regex.test(entry)) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const remainingPath = parts.slice(i + 1).join('/')
|
|
|
|
|
|
const matchedPath = path.join(currentPath, entry, remainingPath)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Use lstatSync to check if path exists (works for both files and symlinks)
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const stats = fs.lstatSync(matchedPath)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
if (stats.isSymbolicLink()) {
|
|
|
|
|
|
// Remove symlink
|
2025-12-23 16:58:55 +08:00
|
|
|
|
fs.unlinkSync(matchedPath)
|
|
|
|
|
|
console.log(`✅ Removed symlink: ${path.relative(basePath, matchedPath)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
2025-08-25 14:44:29 +08:00
|
|
|
|
// Remove directory/file
|
2025-12-23 16:58:55 +08:00
|
|
|
|
fs.rmSync(matchedPath, { recursive: true, force: true })
|
|
|
|
|
|
console.log(`✅ Removed: ${path.relative(basePath, matchedPath)}`)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
2025-08-25 14:44:29 +08:00
|
|
|
|
// Silently ignore ENOENT (path not found) errors
|
|
|
|
|
|
if (error.code !== 'ENOENT') {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
console.error(`❌ Failed to remove ${matchedPath}: ${error.message}`)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
currentPath = path.join(currentPath, part)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
else {
|
2025-08-25 14:44:29 +08:00
|
|
|
|
// Direct path removal
|
|
|
|
|
|
if (fs.existsSync(fullPath)) {
|
|
|
|
|
|
try {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
fs.rmSync(fullPath, { recursive: true, force: true })
|
|
|
|
|
|
console.log(`✅ Removed: ${relativePath}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error(`❌ Failed to remove ${fullPath}: ${error.message}`)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove unnecessary paths
|
2025-12-23 16:58:55 +08:00
|
|
|
|
console.log('🗑️ Removing unnecessary files...')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
for (const pathToRemove of pathsToRemove) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
removePath(standaloneDir, pathToRemove)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate size reduction
|
2025-12-23 16:58:55 +08:00
|
|
|
|
console.log('\n📊 Optimization complete!')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
// Optional: Display the size of remaining jest-related files (if any)
|
|
|
|
|
|
const checkForJest = (dir) => {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const jestFiles = []
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
function walk(currentPath) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
if (!fs.existsSync(currentPath))
|
|
|
|
|
|
return
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const entries = fs.readdirSync(currentPath)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
for (const entry of entries) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const fullPath = path.join(currentPath, entry)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const stat = fs.lstatSync(fullPath) // Use lstatSync to handle symlinks
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
|
|
|
|
|
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
|
|
|
|
// Skip node_modules subdirectories to avoid deep traversal
|
|
|
|
|
|
if (entry === 'node_modules' && currentPath !== standaloneDir) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
continue
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
walk(fullPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (stat.isFile() && entry.includes('jest')) {
|
|
|
|
|
|
jestFiles.push(path.relative(standaloneDir, fullPath))
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (err) {
|
2025-08-25 14:44:29 +08:00
|
|
|
|
// Skip files that can't be accessed
|
2025-12-23 16:58:55 +08:00
|
|
|
|
continue
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-23 16:58:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (err) {
|
2025-08-25 14:44:29 +08:00
|
|
|
|
// Skip directories that can't be read
|
2025-12-23 16:58:55 +08:00
|
|
|
|
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 16:58:55 +08:00
|
|
|
|
walk(dir)
|
|
|
|
|
|
return jestFiles
|
|
|
|
|
|
}
|
2025-08-25 14:44:29 +08:00
|
|
|
|
|
2025-12-23 16:58:55 +08:00
|
|
|
|
const remainingJestFiles = checkForJest(standaloneDir)
|
2025-08-25 14:44:29 +08:00
|
|
|
|
if (remainingJestFiles.length > 0) {
|
2025-12-23 16:58:55 +08:00
|
|
|
|
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!')
|
2025-08-25 14:44:29 +08:00
|
|
|
|
}
|