245 lines
9.0 KiB
JavaScript
245 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Fixes Internal Server Error (Server/Client boundary + DI init), then builds.
|
|
* Run from project root: node fix-and-build.mjs
|
|
*/
|
|
import { spawnSync } from 'child_process';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
const logPath = path.join(root, 'agent-build-log.txt');
|
|
|
|
const HEADER_CONSTANTS = `/** Shared header layout constants — safe for Server Components (no 'use client'). */
|
|
|
|
export const LANGUAGE_BANNER_HEIGHT = 48;
|
|
export const NAVBAR_HEIGHT = 68;
|
|
export const HEADER_HEIGHT = LANGUAGE_BANNER_HEIGHT + NAVBAR_HEIGHT; // 116px
|
|
export const HEADER_HEIGHT_MOBILE = HEADER_HEIGHT;
|
|
export const HEADER_OFFSET_VAR = '--header-height';
|
|
|
|
/** @deprecated Use HEADER_HEIGHT */
|
|
export const headerHeight = HEADER_HEIGHT;
|
|
`;
|
|
|
|
const HEADER_SPACER = `import { HEADER_HEIGHT } from '@/lib/header-constants';
|
|
|
|
/** Pushes page content below the fixed language banner + navbar. */
|
|
export function HeaderSpacer() {
|
|
return <div aria-hidden style={{ height: HEADER_HEIGHT, flexShrink: 0 }} />;
|
|
}
|
|
`;
|
|
|
|
function log(msg) {
|
|
console.log(msg);
|
|
fs.appendFileSync(logPath, msg + '\n');
|
|
}
|
|
|
|
function read(file) {
|
|
try {
|
|
return fs.readFileSync(path.join(root, file), 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function write(file, content) {
|
|
const full = path.join(root, file);
|
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
fs.writeFileSync(full, content);
|
|
log(` wrote ${file}`);
|
|
}
|
|
|
|
function walk(dir, acc = []) {
|
|
const full = path.join(root, dir);
|
|
if (!fs.existsSync(full)) return acc;
|
|
for (const name of fs.readdirSync(full)) {
|
|
if (name === 'node_modules' || name === '.next' || name === '.git') continue;
|
|
const rel = path.join(dir, name);
|
|
const stat = fs.statSync(path.join(root, rel));
|
|
if (stat.isDirectory()) walk(rel, acc);
|
|
else if (/\.(tsx?|jsx?|mjs|css)$/.test(name)) acc.push(rel);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
fs.writeFileSync(logPath, `=== fix-and-build ${new Date().toISOString()} ===\n`);
|
|
|
|
const fixes = [];
|
|
|
|
// Always sync server-safe header constants
|
|
const prevLib = read('lib/header-constants.ts');
|
|
if (prevLib !== HEADER_CONSTANTS) {
|
|
write('lib/header-constants.ts', HEADER_CONSTANTS);
|
|
fixes.push('Synced lib/header-constants.ts (116px header)');
|
|
}
|
|
|
|
// Force-fix HeaderSpacer if it imports from client LanguageSwitcher
|
|
for (const spacerPath of ['components/HeaderSpacer.tsx', 'components/layout/HeaderSpacer.tsx']) {
|
|
const spacer = read(spacerPath);
|
|
if (!spacer) continue;
|
|
if (/LanguageSwitcher/.test(spacer)) {
|
|
write(spacerPath, HEADER_SPACER);
|
|
fixes.push(`${spacerPath}: replaced — imports from @/lib/header-constants`);
|
|
}
|
|
}
|
|
|
|
const files = walk('.');
|
|
|
|
for (const file of files) {
|
|
let content = read(file);
|
|
if (!content) continue;
|
|
const original = content;
|
|
const isClient = /^\s*['"]use client['"]/.test(content);
|
|
|
|
// Any file importing layout constants from LanguageSwitcher → header-constants
|
|
if (/LanguageSwitcher/.test(content) && /HEADER|LANGUAGE_BANNER|NAVBAR|headerHeight/i.test(content)) {
|
|
content = content
|
|
.replace(
|
|
/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*LanguageSwitcher['"];?\s*\n/g,
|
|
(m, imports) => {
|
|
const names = imports
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.filter((n) => /HEADER|LANGUAGE_BANNER|NAVBAR|headerHeight/i.test(n));
|
|
if (!names.length) return m;
|
|
return `import { ${names.join(', ')} } from '@/lib/header-constants';\n`;
|
|
}
|
|
)
|
|
.replace(
|
|
/import\s*\{([^}]+)\}\s*from\s*['"]\.\/LanguageSwitcher['"];?\s*\n/g,
|
|
(m, imports) => {
|
|
const names = imports
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.filter((n) => /HEADER|LANGUAGE_BANNER|NAVBAR|headerHeight/i.test(n));
|
|
if (!names.length) return m;
|
|
return `import { ${names.join(', ')} } from '@/lib/header-constants';\n`;
|
|
}
|
|
);
|
|
if (content !== original) {
|
|
fixes.push(`${file}: header constants from @/lib/header-constants`);
|
|
}
|
|
}
|
|
|
|
// LanguageSwitcher: move exported layout constants to lib
|
|
if (/LanguageSwitcher/i.test(file) && isClient) {
|
|
const constExports = [...content.matchAll(/^export const (\w+)\s*=/gm)].map((m) => m[1]);
|
|
const headerConsts = constExports.filter((n) =>
|
|
/HEADER|LOCALE|SUPPORTED|DEFAULT_LOCALE|LANGUAGE_BANNER|NAVBAR|headerHeight/i.test(n)
|
|
);
|
|
if (headerConsts.length) {
|
|
let lib = read('lib/header-constants.ts') || HEADER_CONSTANTS;
|
|
for (const name of headerConsts) {
|
|
const block = content.match(new RegExp(`export const ${name}\\s*=\\s*[^;]+;`, 'm'));
|
|
if (block && !lib.includes(`export const ${name}`)) {
|
|
lib += `\n${block[0]}`;
|
|
}
|
|
content = content.replace(new RegExp(`export const ${name}\\s*=\\s*[^;]+;\\s*\\n?`, 'm'), '');
|
|
}
|
|
const layoutConsts = headerConsts.filter((n) =>
|
|
/HEADER|LANGUAGE_BANNER|NAVBAR|headerHeight/i.test(n)
|
|
);
|
|
if (layoutConsts.length && !/from ['"]@\/lib\/header-constants['"]/.test(content)) {
|
|
content = `import { ${layoutConsts.join(', ')} } from '@/lib/header-constants';\n` + content;
|
|
}
|
|
write('lib/header-constants.ts', lib.trim() + '\n');
|
|
fixes.push(`${file}: moved ${headerConsts.join(', ')} to lib/header-constants.ts`);
|
|
}
|
|
}
|
|
|
|
// SignatureMenuMarquee: client boundary
|
|
if (/SignatureMenuMarquee/i.test(file) && !isClient) {
|
|
const needsClient = /\b(useState|useEffect|useRef|useCallback|useMemo|onClick|onMouse|framer-motion|useInView)\b/.test(
|
|
content
|
|
);
|
|
if (needsClient) {
|
|
content = `'use client';\n\n${content.replace(/^\s*['"]use client['"];?\s*\n?/, '')}`;
|
|
fixes.push(`${file}: added 'use client' directive`);
|
|
}
|
|
}
|
|
|
|
// container.ts: lazy init
|
|
if (file.endsWith('infrastructure/di/container.ts') || file.endsWith('lib/di/container.ts')) {
|
|
if (/export const container\s*=/.test(content) && !/function getContainer/.test(content)) {
|
|
content = content.replace(
|
|
/export const container\s*=\s*(\w+)\(\);?/,
|
|
`let _container = null;\n\nexport function getContainer() {\n if (!_container) _container = $1();\n return _container;\n}\n\n/** @deprecated Prefer getContainer() */\nexport const container = new Proxy({}, {\n get(_, prop) {\n return getContainer()[prop];\n },\n});`
|
|
);
|
|
fixes.push(`${file}: lazy container init via getContainer()`);
|
|
}
|
|
content = content.replace(
|
|
/if\s*\(\s*!process\.env\.(\w+)\s*\)\s*throw new Error\([^)]+\);/g,
|
|
`if (!process.env.$1) {\n if (process.env.NODE_ENV === 'production') throw new Error('Missing env: $1');\n }`
|
|
);
|
|
}
|
|
|
|
// App pages: getContainer() instead of eager container
|
|
if (/^app\/.*\.(tsx|ts)$/.test(file) && /container/.test(content)) {
|
|
if (/from ['"][^'"]*\/di\/container['"]/.test(content) && !/getContainer/.test(content)) {
|
|
content = content.replace(
|
|
/import\s*\{([^}]*)\bcontainer\b([^}]*)\}\s*from\s*['"]([^'"]*\/di\/container)['"]/,
|
|
(m, before, after, mod) => {
|
|
const names = `${before}${after}`
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.filter((n) => n !== 'container');
|
|
const extra = names.length ? `, ${names.join(', ')}` : '';
|
|
const importPath = mod.startsWith('@') ? mod : `@/${mod.replace(/^\//, '')}`;
|
|
return `import { getContainer${extra} } from '${importPath}'`;
|
|
}
|
|
);
|
|
content = content.replace(/\bcontainer\./g, 'getContainer().');
|
|
if (content !== original) fixes.push(`${file}: use getContainer() instead of eager container`);
|
|
}
|
|
}
|
|
|
|
// globals.css: --header-height
|
|
if (file === 'app/globals.css' || file.endsWith('/globals.css')) {
|
|
if (/--header-height:\s*\d+px/.test(content)) {
|
|
content = content.replace(/--header-height:\s*\d+px/g, '--header-height: 116px');
|
|
if (content !== original) fixes.push(`${file}: --header-height set to 116px`);
|
|
} else if (!content.includes('--header-height')) {
|
|
content = `:root {\n --header-height: 116px;\n}\n\n` + content;
|
|
fixes.push(`${file}: added --header-height: 116px`);
|
|
}
|
|
}
|
|
|
|
if (content !== original) write(file, content);
|
|
}
|
|
|
|
// Clear stale Next.js cache
|
|
const nextDir = path.join(root, '.next');
|
|
if (fs.existsSync(nextDir)) {
|
|
fs.rmSync(nextDir, { recursive: true, force: true });
|
|
fixes.push('Removed .next cache');
|
|
}
|
|
|
|
log('\n--- Fixes ---');
|
|
if (!fixes.length) log('(no automatic fixes applied)');
|
|
else fixes.forEach((f) => log(` • ${f}`));
|
|
|
|
log('\n--- npm run build ---\n');
|
|
const build = spawnSync('npm', ['run', 'build'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
maxBuffer: 20 * 1024 * 1024,
|
|
shell: true,
|
|
});
|
|
|
|
if (build.stdout) {
|
|
process.stdout.write(build.stdout);
|
|
fs.appendFileSync(logPath, build.stdout);
|
|
}
|
|
if (build.stderr) {
|
|
process.stderr.write(build.stderr);
|
|
fs.appendFileSync(logPath, build.stderr);
|
|
}
|
|
|
|
const ok = build.status === 0;
|
|
log(ok ? '\nBUILD PASSED — run: npm run dev' : `\nBUILD FAILED (exit ${build.status})`);
|
|
process.exit(ok ? 0 : 1); |