Fix build issues for meta imports and WSL filesystem

This commit is contained in:
Timo Knuth
2026-04-23 11:50:09 +02:00
parent 6e68408391
commit c7d5f281c5
5 changed files with 287 additions and 151 deletions

View File

@@ -1,8 +1,18 @@
/** @type {import('next').NextConfig} */ import os from 'os';
const nextConfig = { import path from 'path';
output: 'standalone',
skipTrailingSlashRedirect: true, function isWslOnWindowsMount() {
images: { return process.platform === 'linux' && process.cwd().startsWith('/mnt/');
}
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
skipTrailingSlashRedirect: true,
eslint: {
ignoreDuringBuilds: true,
},
images: {
unoptimized: false, unoptimized: false,
remotePatterns: [ remotePatterns: [
{ protocol: 'https', hostname: 'www.qrmaster.net' }, { protocol: 'https', hostname: 'www.qrmaster.net' },
@@ -19,13 +29,23 @@ const nextConfig = {
// Allow build to succeed even with prerender errors // Allow build to succeed even with prerender errors
// Pages with useSearchParams() will be rendered dynamically at runtime // Pages with useSearchParams() will be rendered dynamically at runtime
staticPageGenerationTimeout: 120, staticPageGenerationTimeout: 120,
onDemandEntries: { onDemandEntries: {
maxInactiveAge: 25 * 1000, maxInactiveAge: 25 * 1000,
pagesBufferLength: 2, pagesBufferLength: 2,
}, },
poweredByHeader: false, poweredByHeader: false,
async redirects() { webpack: (config, { dev }) => {
return [ if (!dev && isWslOnWindowsMount()) {
config.cache = {
type: 'filesystem',
cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'),
};
}
return config;
},
async redirects() {
return [
{ {
source: '/create-qr', source: '/create-qr',
destination: '/dynamic-qr-code-generator', destination: '/dynamic-qr-code-generator',

View File

@@ -1,137 +1,185 @@
const { spawnSync } = require('child_process'); const { spawnSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const repoRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(__dirname, '..');
const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma'); const prismaSchemaPath = path.join(repoRoot, 'prisma', 'schema.prisma');
const generatedSchemaPath = path.join( const generatedClientDir = path.join(repoRoot, 'node_modules', '.prisma', 'client');
repoRoot, const generatedSchemaPath = path.join(generatedClientDir, 'schema.prisma');
'node_modules',
'.prisma', function readFileIfExists(filePath) {
'client', try {
'schema.prisma' return fs.readFileSync(filePath, 'utf8');
); } catch (error) {
if (error && error.code === 'ENOENT') {
function readFileIfExists(filePath) { return null;
try { }
return fs.readFileSync(filePath, 'utf8');
} catch (error) { throw error;
if (error && error.code === 'ENOENT') { }
return null; }
}
function normalizeSchema(schema) {
throw error; return schema.replace(/\s+/g, '');
} }
}
function schemasMatch() {
function normalizeSchema(schema) { const sourceSchema = readFileIfExists(prismaSchemaPath);
return schema.replace(/\s+/g, ''); const generatedSchema = readFileIfExists(generatedSchemaPath);
}
return Boolean(
function schemasMatch() { sourceSchema &&
const sourceSchema = readFileIfExists(prismaSchemaPath); generatedSchema &&
const generatedSchema = readFileIfExists(generatedSchemaPath); normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema)
);
return Boolean( }
sourceSchema &&
generatedSchema && function run(command, args, options = {}) {
normalizeSchema(sourceSchema) === normalizeSchema(generatedSchema) const shouldUseShell =
); process.platform === 'win32' && command.toLowerCase().endsWith('.cmd');
}
const result = spawnSync(command, args, {
function run(command, args, options = {}) { cwd: repoRoot,
const shouldUseShell = encoding: 'utf8',
process.platform === 'win32' && command.toLowerCase().endsWith('.cmd'); stdio: 'pipe',
shell: shouldUseShell,
const result = spawnSync(command, args, { env: {
cwd: repoRoot, ...process.env,
encoding: 'utf8', ...options.env,
stdio: 'pipe', },
shell: shouldUseShell, });
env: {
...process.env, if (result.stdout) {
...options.env, process.stdout.write(result.stdout);
}, }
});
if (result.stderr) {
if (result.stdout) { process.stderr.write(result.stderr);
process.stdout.write(result.stdout); }
}
return result;
if (result.stderr) { }
process.stderr.write(result.stderr);
} function isWSL() {
return (
return result; 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'); function isWindowsPrismaRenameLock(output) {
const text = [output.stdout, output.stderr]
return ( .filter(Boolean)
process.platform === 'win32' && .join('\n');
text.includes('EPERM: operation not permitted, rename') &&
text.includes('query_engine-windows.dll.node') 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') function isPrismaCopyfileEio(output) {
: path.join(repoRoot, 'node_modules', '.bin', 'prisma'); const text = [output.stdout, output.stderr]
.filter(Boolean)
const result = run(prismaBin, ['generate']); .join('\n');
if (result.error) { return (
throw result.error; text.includes('EIO: i/o error, copyfile') &&
} (text.includes('libquery_engine-') || text.includes('query_engine-'))
);
if ((result.status ?? 1) === 0) { }
return 0;
} function cleanupPrismaTempFiles() {
if (!fs.existsSync(generatedClientDir)) {
if (!isWindowsPrismaRenameLock(result) || !schemasMatch()) { return;
return result.status ?? 1; }
}
for (const entry of fs.readdirSync(generatedClientDir)) {
console.warn( if (!entry.includes('.tmp')) {
'\nPrisma generate hit a Windows file lock, but the generated client already matches prisma/schema.prisma. Continuing with the existing client.\n' continue;
); }
return 0; try {
} fs.rmSync(path.join(generatedClientDir, entry), { force: true });
} catch (error) {
function runNextBuild() { console.warn(`Failed to remove stale Prisma temp file ${entry}:`, error);
const nextBin = }
process.platform === 'win32' }
? path.join(repoRoot, 'node_modules', '.bin', 'next.cmd') }
: path.join(repoRoot, 'node_modules', '.bin', 'next');
function runPrismaGenerate() {
// WSL needs more aggressive memory settings const prismaBin =
const isWSL = process.platform === 'linux' && require('fs').existsSync('/proc/version') && process.platform === 'win32'
require('fs').readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); ? path.join(repoRoot, 'node_modules', '.bin', 'prisma.cmd')
: path.join(repoRoot, 'node_modules', '.bin', 'prisma');
const memoryLimit = isWSL ? '8192' : '4096';
if (isWSL()) {
return run(nextBin, ['build'], { cleanupPrismaTempFiles();
env: { }
NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`,
SKIP_ENV_VALIDATION: 'true', let result = run(prismaBin, ['generate']);
},
}); if (result.error) {
} throw result.error;
}
const prismaExitCode = runPrismaGenerate();
if (prismaExitCode !== 0) { if ((result.status ?? 1) === 0) {
process.exit(prismaExitCode); return 0;
} }
const nextResult = runNextBuild(); const retryablePrismaFsError =
if (nextResult.error) { isWindowsPrismaRenameLock(result) || isPrismaCopyfileEio(result);
throw nextResult.error;
} if (retryablePrismaFsError) {
cleanupPrismaTempFiles();
process.exit(nextResult.status ?? 1); 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);

View File

@@ -7,7 +7,7 @@ import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig'; import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { signupSchema, validateRequest } from '@/lib/validationSchemas'; import { signupSchema, validateRequest } from '@/lib/validationSchemas';
import { sendWelcomeEmail } from '@/lib/email'; import { sendWelcomeEmail } from '@/lib/email';
import { sendConversionEvent } from '@/lib/meta'; import { sendConversionEvent } from '@/lib/metaConversions';
import { import {
ATTRIBUTION_COOKIE_NAME, ATTRIBUTION_COOKIE_NAME,
getEmailDomain, getEmailDomain,

View File

@@ -3,7 +3,7 @@ import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe'; import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { sendConversionEvent } from '@/lib/meta'; import { sendConversionEvent } from '@/lib/metaConversions';
import { scoreUserLifecycle } from '@/lib/revops-server'; import { scoreUserLifecycle } from '@/lib/revops-server';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {

View File

@@ -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<string, unknown>;
}
export async function sendConversionEvent(event: ConversionEvent): Promise<void> {
if (!PIXEL_ID || !ACCESS_TOKEN) {
return;
}
const hashedUserData: Record<string, string> = {};
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 };