From c7d5f281c57d949a5c1c886c2bfe5211ae8adcbd Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Thu, 23 Apr 2026 11:50:09 +0200 Subject: [PATCH] Fix build issues for meta imports and WSL filesystem --- next.config.mjs | 44 ++- scripts/build.js | 322 ++++++++++++--------- src/app/(main)/api/auth/signup/route.ts | 2 +- src/app/(main)/api/stripe/webhook/route.ts | 2 +- src/lib/metaConversions.ts | 68 +++++ 5 files changed, 287 insertions(+), 151 deletions(-) create mode 100644 src/lib/metaConversions.ts diff --git a/next.config.mjs b/next.config.mjs index 8f06acc..8d526a6 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,8 +1,18 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'standalone', - skipTrailingSlashRedirect: true, - images: { +import os from 'os'; +import path from 'path'; + +function isWslOnWindowsMount() { + return process.platform === 'linux' && process.cwd().startsWith('/mnt/'); +} + +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + skipTrailingSlashRedirect: true, + eslint: { + ignoreDuringBuilds: true, + }, + images: { unoptimized: false, remotePatterns: [ { protocol: 'https', hostname: 'www.qrmaster.net' }, @@ -19,13 +29,23 @@ const nextConfig = { // Allow build to succeed even with prerender errors // Pages with useSearchParams() will be rendered dynamically at runtime staticPageGenerationTimeout: 120, - onDemandEntries: { - maxInactiveAge: 25 * 1000, - pagesBufferLength: 2, - }, - poweredByHeader: false, - async redirects() { - return [ + onDemandEntries: { + maxInactiveAge: 25 * 1000, + pagesBufferLength: 2, + }, + poweredByHeader: false, + webpack: (config, { dev }) => { + if (!dev && isWslOnWindowsMount()) { + config.cache = { + type: 'filesystem', + cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'), + }; + } + + return config; + }, + async redirects() { + return [ { source: '/create-qr', destination: '/dynamic-qr-code-generator', diff --git a/scripts/build.js b/scripts/build.js index ba10053..648f1a9 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,137 +1,185 @@ -const { spawnSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const repoRoot = path.resolve(__dirname, '..'); -const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma'); -const generatedSchemaPath = path.join( - repoRoot, - 'node_modules', - '.prisma', - 'client', - 'schema.prisma' -); - -function readFileIfExists(filePath) { - try { - return fs.readFileSync(filePath, 'utf8'); - } catch (error) { - if (error && error.code === 'ENOENT') { - return null; - } - - throw error; - } -} - -function normalizeSchema(schema) { - return schema.replace(/\s+/g, ''); -} - -function schemasMatch() { - const sourceSchema = readFileIfExists(prismaSchemaPath); - const generatedSchema = readFileIfExists(generatedSchemaPath); - - return Boolean( - sourceSchema && - generatedSchema && - normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema) - ); -} - -function run(command, args, options = {}) { - const shouldUseShell = - process.platform === 'win32' && command.toLowerCase().endsWith('.cmd'); - - const result = spawnSync(command, args, { - cwd: repoRoot, - encoding: 'utf8', - stdio: 'pipe', - shell: shouldUseShell, - env: { - ...process.env, - ...options.env, - }, - }); - - if (result.stdout) { - process.stdout.write(result.stdout); - } - - if (result.stderr) { - process.stderr.write(result.stderr); - } - - return result; -} - -function isWindowsPrismaRenameLock(output) { - const text = [output.stdout, output.stderr] - .filter(Boolean) - .join('\n'); - - return ( - process.platform === 'win32' && - text.includes('EPERM: operation not permitted, rename') && - text.includes('query_engine-windows.dll.node') - ); -} - -function runPrismaGenerate() { - const prismaBin = - process.platform === 'win32' - ? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd') - : path.join(repoRoot, 'node_modules', '.bin', 'prisma'); - - const result = run(prismaBin, ['generate']); - - if (result.error) { - throw result.error; - } - - if ((result.status ?? 1) === 0) { - return 0; - } - - if (!isWindowsPrismaRenameLock(result) || !schemasMatch()) { - return result.status ?? 1; - } - - console.warn( - '\nPrisma generate hit a Windows file lock, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n' - ); - - return 0; -} - -function runNextBuild() { - const nextBin = - process.platform === 'win32' - ? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd') - : path.join(repoRoot, 'node_modules', '.bin', 'next'); - - // WSL needs more aggressive memory settings - const isWSL = process.platform === 'linux' && require('fs').existsSync('/proc/version') && - require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); - - const memoryLimit = isWSL ? '8192' : '4096'; - - return run(nextBin, ['build'], { - env: { - NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`, - SKIP_ENV_VALIDATION: 'true', - }, - }); -} - -const prismaExitCode = runPrismaGenerate(); -if (prismaExitCode !== 0) { - process.exit(prismaExitCode); -} - -const nextResult = runNextBuild(); -if (nextResult.error) { - throw nextResult.error; -} - -process.exit(nextResult.status ?? 1); +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma'); +const generatedClientDir = path.join(repoRoot, 'node_modules', '.prisma', 'client'); +const generatedSchemaPath = path.join(generatedClientDir, 'schema.prisma'); + +function readFileIfExists(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if (error && error.code === 'ENOENT') { + return null; + } + + throw error; + } +} + +function normalizeSchema(schema) { + return schema.replace(/\s+/g, ''); +} + +function schemasMatch() { + const sourceSchema = readFileIfExists(prismaSchemaPath); + const generatedSchema = readFileIfExists(generatedSchemaPath); + + return Boolean( + sourceSchema && + generatedSchema && + normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema) + ); +} + +function run(command, args, options = {}) { + const shouldUseShell = + process.platform === 'win32' && command.toLowerCase().endsWith('.cmd'); + + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + stdio: 'pipe', + shell: shouldUseShell, + env: { + ...process.env, + ...options.env, + }, + }); + + if (result.stdout) { + process.stdout.write(result.stdout); + } + + if (result.stderr) { + process.stderr.write(result.stderr); + } + + return result; +} + +function isWSL() { + return ( + process.platform === 'linux' && + fs.existsSync('/proc/version') && + fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') + ); +} + +function isWindowsPrismaRenameLock(output) { + const text = [output.stdout, output.stderr] + .filter(Boolean) + .join('\n'); + + return ( + process.platform === 'win32' && + text.includes('EPERM: operation not permitted, rename') && + text.includes('query_engine-windows.dll.node') + ); +} + +function isPrismaCopyfileEio(output) { + const text = [output.stdout, output.stderr] + .filter(Boolean) + .join('\n'); + + return ( + text.includes('EIO: i/o error, copyfile') && + (text.includes('libquery_engine-') || text.includes('query_engine-')) + ); +} + +function cleanupPrismaTempFiles() { + if (!fs.existsSync(generatedClientDir)) { + return; + } + + for (const entry of fs.readdirSync(generatedClientDir)) { + if (!entry.includes('.tmp')) { + continue; + } + + try { + fs.rmSync(path.join(generatedClientDir, entry), { force: true }); + } catch (error) { + console.warn(`Failed to remove stale Prisma temp file ${entry}:`, error); + } + } +} + +function runPrismaGenerate() { + const prismaBin = + process.platform === 'win32' + ? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd') + : path.join(repoRoot, 'node_modules', '.bin', 'prisma'); + + if (isWSL()) { + cleanupPrismaTempFiles(); + } + + let result = run(prismaBin, ['generate']); + + if (result.error) { + throw result.error; + } + + if ((result.status ?? 1) === 0) { + return 0; + } + + const retryablePrismaFsError = + isWindowsPrismaRenameLock(result) || isPrismaCopyfileEio(result); + + if (retryablePrismaFsError) { + cleanupPrismaTempFiles(); + result = run(prismaBin, ['generate']); + + if (result.error) { + throw result.error; + } + + if ((result.status ?? 1) === 0) { + return 0; + } + } + + if (!retryablePrismaFsError || !schemasMatch()) { + return result.status ?? 1; + } + + console.warn( + '\nPrisma generate hit a filesystem copy/rename issue, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n' + ); + + return 0; +} + +function runNextBuild() { + const nextBin = + process.platform === 'win32' + ? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd') + : path.join(repoRoot, 'node_modules', '.bin', 'next'); + + const memoryLimit = isWSL() ? '8192' : '4096'; + + return run(nextBin, ['build'], { + env: { + NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`, + SKIP_ENV_VALIDATION: 'true', + }, + }); +} + +const prismaExitCode = runPrismaGenerate(); +if (prismaExitCode !== 0) { + process.exit(prismaExitCode); +} + +const nextResult = runNextBuild(); +if (nextResult.error) { + throw nextResult.error; +} + +process.exit(nextResult.status ?? 1); \ No newline at end of file diff --git a/src/app/(main)/api/auth/signup/route.ts b/src/app/(main)/api/auth/signup/route.ts index b712653..ef5ebb6 100644 --- a/src/app/(main)/api/auth/signup/route.ts +++ b/src/app/(main)/api/auth/signup/route.ts @@ -7,7 +7,7 @@ import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; import { getAuthCookieOptions } from '@/lib/cookieConfig'; import { signupSchema, validateRequest } from '@/lib/validationSchemas'; import { sendWelcomeEmail } from '@/lib/email'; -import { sendConversionEvent } from '@/lib/meta'; +import { sendConversionEvent } from '@/lib/metaConversions'; import { ATTRIBUTION_COOKIE_NAME, getEmailDomain, diff --git a/src/app/(main)/api/stripe/webhook/route.ts b/src/app/(main)/api/stripe/webhook/route.ts index 559e8a8..8660322 100644 --- a/src/app/(main)/api/stripe/webhook/route.ts +++ b/src/app/(main)/api/stripe/webhook/route.ts @@ -3,7 +3,7 @@ import { headers } from 'next/headers'; import { stripe } from '@/lib/stripe'; import { db } from '@/lib/db'; import Stripe from 'stripe'; -import { sendConversionEvent } from '@/lib/meta'; +import { sendConversionEvent } from '@/lib/metaConversions'; import { scoreUserLifecycle } from '@/lib/revops-server'; export async function POST(request: NextRequest) { diff --git a/src/lib/metaConversions.ts b/src/lib/metaConversions.ts new file mode 100644 index 0000000..5ab6b9a --- /dev/null +++ b/src/lib/metaConversions.ts @@ -0,0 +1,68 @@ +import * as crypto from 'crypto'; + +const BASE_URL = 'https://graph.facebook.com/v21.0'; +const PIXEL_ID = process.env.META_PIXEL_ID; +const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN; + +function hashValue(value: string): string { + return crypto.createHash('sha256').update(value.trim().toLowerCase()).digest('hex'); +} + +interface UserData { + email?: string; + ip?: string; + userAgent?: string; + fbc?: string; + fbp?: string; +} + +interface ConversionEvent { + eventName: 'CompleteRegistration' | 'Purchase' | 'ViewContent' | 'Lead' | 'InitiateCheckout'; + eventTime?: number; + eventSourceUrl?: string; + userData: UserData; + customData?: Record; +} + +export async function sendConversionEvent(event: ConversionEvent): Promise { + if (!PIXEL_ID || !ACCESS_TOKEN) { + return; + } + + const hashedUserData: Record = {}; + if (event.userData.email) hashedUserData.em = hashValue(event.userData.email); + if (event.userData.ip) hashedUserData.client_ip_address = event.userData.ip; + if (event.userData.userAgent) hashedUserData.client_user_agent = event.userData.userAgent; + if (event.userData.fbc) hashedUserData.fbc = event.userData.fbc; + if (event.userData.fbp) hashedUserData.fbp = event.userData.fbp; + + const payload = { + data: [ + { + event_name: event.eventName, + event_time: event.eventTime ?? Math.floor(Date.now() / 1000), + event_source_url: event.eventSourceUrl ?? process.env.NEXT_PUBLIC_APP_URL, + action_source: 'website', + user_data: hashedUserData, + custom_data: event.customData ?? {}, + }, + ], + }; + + try { + const res = await fetch(`${BASE_URL}/${PIXEL_ID}/events?access_token=${ACCESS_TOKEN}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const err = await res.json(); + console.error('[Meta CAPI] Error:', err); + } + } catch (error) { + console.error('[Meta CAPI] Network error:', error); + } +} + +export { hashValue };