HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/scripts/build-client.mjs
import { spawnSync } from 'node:child_process';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

const projectRoot = process.cwd();

function argValue(flag) {
    const idx = process.argv.indexOf(flag);
    if (idx === -1) return undefined;
    return process.argv[idx + 1];
}

function hasFlag(flag) {
    return process.argv.includes(flag);
}

const zipEnabled = hasFlag('--zip');
const zipClean = hasFlag('--clean');
const outDir = path.resolve(projectRoot, argValue('--out') ?? 'client-build');

function run(command, args, options = {}) {
    const result = spawnSync(command, args, {
        stdio: 'inherit',
        shell: false,
        ...options,
    });

    if (result.error) throw result.error;
    if (result.status !== 0) {
        throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
    }
}

function runCapture(command, args, options = {}) {
    const result = spawnSync(command, args, {
        stdio: ['ignore', 'pipe', 'inherit'],
        shell: false,
        encoding: 'utf8',
        ...options,
    });

    if (result.error) throw result.error;
    if (result.status !== 0) {
        throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
    }

    return (result.stdout ?? '').trim();
}

async function pathExists(p) {
    try {
        await fs.access(p);
        return true;
    } catch {
        return false;
    }
}

async function ensureDir(p) {
    await fs.mkdir(p, { recursive: true });
}

async function safeReadFile(p) {
    try {
        return await fs.readFile(p, 'utf8');
    } catch {
        return null;
    }
}

function extractVersionFromMailpurseConfig(configContents) {
    if (!configContents) return null;

    const directMatch = configContents.match(/['"]version['"]\s*=>\s*['"]([^'"]+)['"]/);
    if (directMatch && directMatch[1]) return directMatch[1].trim();

    const envFallbackMatch = configContents.match(
        /['"]version['"]\s*=>\s*env\([^,]+,\s*['"]([^'"]+)['"]\s*\)/,
    );
    if (envFallbackMatch && envFallbackMatch[1]) return envFallbackMatch[1].trim();

    return null;
}

async function patchFile(p, transform) {
    const current = await safeReadFile(p);
    if (current === null) return;
    const next = transform(current);
    if (next !== current) {
        await fs.writeFile(p, next, 'utf8');
    }
}

async function copyPath(src, dest, shouldCopy) {
    let stat;
    try {
        stat = await fs.lstat(src);
    } catch (err) {
        if (err && typeof err === 'object' && err.code === 'ENOENT') return;
        throw err;
    }

    if (!shouldCopy(src, stat)) return;

    if (stat.isSymbolicLink()) {
        let link;
        try {
            link = await fs.readlink(src);
        } catch (err) {
            if (err && typeof err === 'object' && err.code === 'ENOENT') return;
            throw err;
        }
        await ensureDir(path.dirname(dest));
        await fs.symlink(link, dest);
        return;
    }

    if (stat.isDirectory()) {
        await ensureDir(dest);
        let entries;
        try {
            entries = await fs.readdir(src);
        } catch (err) {
            if (err && typeof err === 'object' && err.code === 'ENOENT') return;
            throw err;
        }
        await Promise.all(
            entries.map((name) => copyPath(path.join(src, name), path.join(dest, name), shouldCopy)),
        );
        return;
    }

    await ensureDir(path.dirname(dest));
    try {
        await fs.copyFile(src, dest);
    } catch (err) {
        if (err && typeof err === 'object' && err.code === 'ENOENT') return;
        throw err;
    }
}

function shouldCopyFactory() {
    const ignored = new Set(['.git', '.cursor', 'node_modules', '.vite', 'client-build', 'packages']);

    return (src, stat) => {
        const rel = path.relative(projectRoot, src);
        if (rel === '') return true;

        const first = rel.split(path.sep)[0];
        if (ignored.has(first)) return false;

        if (first === 'public') return false;

        // Ignore installed addons directory
        if (rel.startsWith('storage' + path.sep + 'app' + path.sep + 'addons')) return false;

        if (stat.isFile() && path.extname(src).toLowerCase() === '.zip') return false;
        if (stat.isFile() && /^mailpurse_build_.*\.zip$/i.test(path.basename(src))) return false;

        if (stat.isFile()) {
            const base = path.basename(src);
            if (base === '.env') return false;
            if (base.startsWith('.env.') && base !== '.env.example') return false;
        }

        return true;
    };
}

async function main() {
    await fs.rm(outDir, { recursive: true, force: true });
    await ensureDir(outDir);

    const vendorAutoload = path.join(projectRoot, 'vendor', 'autoload.php');
    if (!(await pathExists(vendorAutoload))) {
        run('composer', ['install', '--no-dev', '--optimize-autoloader']);
    }

    run('npm', ['run', 'build']);

    const manifestSrc = path.join(projectRoot, 'public', 'build', 'manifest.json');
    const assetsSrc = path.join(projectRoot, 'public', 'build', 'assets');

    if (!(await pathExists(manifestSrc))) {
        throw new Error('Vite manifest not found at public/build/manifest.json after build');
    }

    if (!(await pathExists(assetsSrc))) {
        throw new Error('Vite assets folder not found at public/build/assets after build');
    }

    const shouldCopy = shouldCopyFactory();

    const entries = await fs.readdir(projectRoot);
    await Promise.all(
        entries.map((name) =>
            copyPath(path.join(projectRoot, name), path.join(outDir, name), shouldCopy),
        ),
    );

    const publicBuildDir = path.join(outDir, 'public', 'build');
    await ensureDir(publicBuildDir);

    await fs.copyFile(manifestSrc, path.join(publicBuildDir, 'manifest.json'));

    const rootBuildDir = path.join(outDir, 'build');
    await ensureDir(rootBuildDir);
    await fs.copyFile(manifestSrc, path.join(rootBuildDir, 'manifest.json'));

    const buildAssetsDest = path.join(publicBuildDir, 'assets');
    await ensureDir(buildAssetsDest);
    await fs.cp(assetsSrc, buildAssetsDest, { recursive: true });

    const rootBuildAssetsDest = path.join(rootBuildDir, 'assets');
    await ensureDir(rootBuildAssetsDest);
    await fs.cp(assetsSrc, rootBuildAssetsDest, { recursive: true });

    const publicIndexSrc = path.join(projectRoot, 'public', 'index.php');
    const indexDest = path.join(outDir, 'index.php');

    const indexContents = await fs.readFile(publicIndexSrc, 'utf8');
    const updatedIndex = indexContents
        .replace("require __DIR__.'/../vendor/autoload.php';", "require __DIR__.'/vendor/autoload.php';")
        .replace("(require_once __DIR__.'/../bootstrap/app.php')", "(require_once __DIR__.'/bootstrap/app.php')")
        .replace("__DIR__.'/../storage/framework/maintenance.php'", "__DIR__.'/storage/framework/maintenance.php'");

    await fs.writeFile(indexDest, updatedIndex, 'utf8');

    const htaccessDest = path.join(outDir, '.htaccess');
    await fs.copyFile(path.join(projectRoot, '.htaccess'), htaccessDest);

    run('php', [
        'artisan',
        'translations:export',
        '--locale',
        'en',
        '--dir',
        path.join(outDir, 'language'),
        '--format',
        'json',
        '--force',
    ], { cwd: projectRoot });

    if (zipEnabled) {
        const zipFlat = hasFlag('--zip-flat');
        const zipFolderName = zipFlat ? null : (argValue('--zip-folder') ?? 'mailpurse');

        let version = null;

        const mailpurseConfig = await safeReadFile(path.join(projectRoot, 'config', 'mailpurse.php'));
        version = extractVersionFromMailpurseConfig(mailpurseConfig);

        if (!version) {
            const pkgJson = await safeReadFile(path.join(projectRoot, 'package.json'));
            if (pkgJson) {
                try {
                    const parsed = JSON.parse(pkgJson);
                    if (typeof parsed?.version === 'string' && parsed.version.trim() !== '') {
                        version = parsed.version.trim();
                    }
                } catch {
                    // ignore
                }
            }
        }

        if (!version) version = '0.0.0';

        const envSha =
            process.env.GIT_SHA ??
            process.env.COMMIT_SHA ??
            process.env.GITHUB_SHA ??
            process.env.VERCEL_GIT_COMMIT_SHA ??
            process.env.CF_PAGES_COMMIT_SHA ??
            process.env.RAILWAY_GIT_COMMIT_SHA;

        let hash = typeof envSha === 'string' && envSha.trim() !== '' ? envSha.trim() : '';
        if (!hash) {
            try {
                hash = runCapture('git', ['rev-parse', '--short', 'HEAD'], { cwd: projectRoot });
            } catch {
                hash = 'nogit';
            }
        }
        hash = hash.replace(/[^a-zA-Z0-9]/g, '').slice(0, 12) || 'nogit';

        const zipFileName =
            argValue('--zip-file') ?? `mailpurse_build_${version.replace(/[^a-zA-Z0-9._-]/g, '')}_${hash}.zip`;
        const zipPath = path.resolve(projectRoot, zipFileName);

        const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mailpurse-client-zip-'));
        const stagedDir = zipFlat ? tmpDir : path.join(tmpDir, zipFolderName);

        await fs.rm(zipPath, { force: true });
        await ensureDir(stagedDir);
        await fs.cp(outDir, stagedDir, { recursive: true });

        const stagedHtaccessPath = path.join(stagedDir, '.htaccess');
        if (!(await pathExists(stagedHtaccessPath))) {
            throw new Error('Missing .htaccess in staged client zip directory');
        }

        const stagedHtaccessContents = await fs.readFile(stagedHtaccessPath, 'utf8');
        if (!stagedHtaccessContents.includes('FilesMatch "^\\\\."')) {
            throw new Error('Staged .htaccess does not contain dotfile deny rule (FilesMatch "^\\.")');
        }

        await fs.rm(path.join(stagedDir, 'storage', 'logs'), { recursive: true, force: true });
        await fs.rm(path.join(stagedDir, 'storage', 'framework', 'sessions'), { recursive: true, force: true });
        await fs.rm(path.join(stagedDir, 'storage', 'framework', 'views'), { recursive: true, force: true });
        await fs.rm(path.join(stagedDir, 'storage', 'framework', 'cache'), { recursive: true, force: true });
        await fs.rm(path.join(stagedDir, 'storage', 'app', 'private'), { recursive: true, force: true });

        await ensureDir(path.join(stagedDir, 'storage', 'logs'));
        await ensureDir(path.join(stagedDir, 'storage', 'framework', 'sessions'));
        await ensureDir(path.join(stagedDir, 'storage', 'framework', 'views'));
        await ensureDir(path.join(stagedDir, 'storage', 'framework', 'cache'));
        await ensureDir(path.join(stagedDir, 'storage', 'app', 'private'));

        run('zip', ['-r', zipPath, zipFlat ? '.' : zipFolderName], { cwd: tmpDir });

        console.log(`Created ${zipFileName}`);

        const masterZipFileName = argValue('--master-zip-file') ?? 'Mailpurse-master.zip';
        const masterZipPath = path.resolve(projectRoot, masterZipFileName);

        await fs.rm(masterZipPath, { force: true });

        const masterTmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mailpurse-master-zip-'));

        await fs.copyFile(zipPath, path.join(masterTmpDir, path.basename(zipPath)));
        await fs.writeFile(path.join(masterTmpDir, 'docs.txt'), 'https://getmailpurse.com/docs\n', 'utf8');

        const wpBaseDir = path.join(projectRoot, 'wordpress-plugin');
        const wpPluginDir = path.join(wpBaseDir, 'mailpurse-integration');

        if (!(await pathExists(wpPluginDir))) {
            throw new Error('Missing wordpress-plugin/mailpurse-integration directory');
        }

        const wpPluginZipPath = path.join(masterTmpDir, 'wp-mailpurse-integration.zip');
        run('zip', ['-r', wpPluginZipPath, 'mailpurse-integration'], { cwd: wpBaseDir });

        run('zip', ['-r', masterZipPath, '.'], { cwd: masterTmpDir });

        console.log(`Created ${masterZipFileName}`);

        await fs.rm(masterTmpDir, { recursive: true, force: true });

        await fs.rm(tmpDir, { recursive: true, force: true });

        // Zip each addon from packages/ into the project root
        const packagesDir = path.join(projectRoot, 'packages');
        if (await pathExists(packagesDir)) {
            const addonDirs = await fs.readdir(packagesDir);
            for (const addonName of addonDirs) {
                const addonPath = path.join(packagesDir, addonName);
                const addonStat = await fs.lstat(addonPath);
                if (!addonStat.isDirectory()) continue;
                const addonZipPath = path.join(packagesDir, `${addonName}.zip`);
                await fs.rm(addonZipPath, { force: true });
                run('zip', ['-r', addonZipPath, addonName], { cwd: packagesDir });
                console.log(`Created ${addonName}.zip`);
            }
        }

        if (zipClean) {
            await fs.rm(outDir, { recursive: true, force: true });
        }
    }
}

main().catch((err) => {
    console.error(err);
    process.exit(1);
});