Files

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);