// @ts-check /** * PromptJS v1.0.0 — CLI: `build` Command / Perintah `build` * ============================================================================ * * Build production: compile + minify + prerender HTML via jsdom. * v0.4.0: Multi-file project build with prompt.js + prompt.css output. * v0.8.0: Config loading, plugin support, adapter flag (--adapter). * * Output: folder `dist/` berisi `.html`, `prompt.js`, `prompt.css`, dan static assets. */ 'use strict'; const fs = require('fs'); const path = require('path'); const { PromptJSEngine } = require('../../engine/promptjs'); const Builder = require('../../engine/builder'); const Config = require('../../engine/config'); const { findPjsFiles, printDiagnostics, ensureDirForFile, formatSize, formatElapsed, makeColors, } = require('../utils'); /** * Jalankan command `pjs build`. * * Algoritma: cari semua file `.pjs` di `src/`, compile masing-masing, * prerender ke HTML via jsdom (jika ada), minify JS, copy static assets * ke `dist/`. * * @param {Object} argv - Parsed args dari `parseArgs` * @returns {void} */ function runBuild(argv) { const inputDir = argv._[0] || '.'; const outDir = argv['out-dir'] || argv.outDir || null; const prerender = argv.prerender || false; const minify = argv.minify || false; const adapterFlag = argv.adapter || null; const rootDir = path.resolve(inputDir); // v0.8: Load project config const { config: projectConfig, errors: _configErrors, rootDir: _configRootDir, } = Config.loadProjectConfig(rootDir, argv); const csp = argv.csp || projectConfig.csp || false; const distDir = path.resolve(outDir || projectConfig.outDir || 'dist'); if (!fs.existsSync(rootDir)) { process.stderr.write(`Error: Directory '${inputDir}' does not exist.\n`); process.exit(1); } const { green, cyan, red, gray, bold, reset } = makeColors({ stream: process.stdout }); // ── v0.4.0: Project mode — if src/pages/ exists, use multi-file builder ── const srcDir = path.join(rootDir, 'src'); const pagesInSrc = path.join(srcDir, 'pages'); const pagesInRoot = path.join(rootDir, 'pages'); const pagesDir = fs.existsSync(pagesInSrc) ? pagesInSrc : fs.existsSync(pagesInRoot) ? pagesInRoot : null; // v0.8: Determine adapter (CLI flag > config file > none) const adapter = adapterFlag || projectConfig.adapter || null; if (pagesDir && fs.statSync(pagesDir).isDirectory()) { const projectRoot = fs.existsSync(pagesInSrc) ? srcDir : rootDir; process.stderr.write(`\n${bold}PromptJS v1.0.0 — Project Build${reset}\n`); process.stderr.write(` Source: ${cyan}${projectRoot}${reset}\n`); process.stderr.write(` Output: ${cyan}${distDir}${reset}\n`); if (adapter) { process.stderr.write(` Adapter: ${cyan}${adapter}${reset}\n`); } if (csp) { process.stderr.write(` CSP: ${cyan}enabled${reset} (nonce injection)\n`); } if (projectConfig.plugins.length > 0) { process.stderr.write(` Plugins: ${cyan}${projectConfig.plugins.length}${reset}\n`); } process.stderr.write('\n'); const startTotal = process.hrtime(); const result = Builder.buildProject({ rootDir: projectRoot, outDir: distDir, pagesDir: 'pages', adapter: adapter, plugins: projectConfig.plugins, meta: projectConfig.meta, siteUrl: projectConfig.siteUrl, apiUrl: projectConfig.apiUrl, csp: csp, }); if (result.errors.length > 0) { printDiagnostics(result.errors, 'Build Errors'); } // Report const elapsed = formatElapsed(startTotal); let jsSize = 0; let cssSize = 0; try { jsSize = fs.statSync(path.join(distDir, 'prompt.js')).size; } catch {} try { cssSize = fs.statSync(path.join(distDir, 'prompt.css')).size; } catch {} process.stderr.write(`\n${green}✓${reset} Built ${bold}${result.pages.length}${reset} pages\n`); if (jsSize) process.stderr.write(` ${gray}prompt.js${reset} ${formatSize(jsSize)}\n`); if (cssSize) process.stderr.write(` ${gray}prompt.css${reset} ${formatSize(cssSize)}\n`); for (const page of result.pages) { process.stderr.write(` ${green}✓${reset} ${page.htmlFile} (${page.route})\n`); } // Report adapter results if (result.adapter) { if (result.adapter.hashedAssets) { const ha = result.adapter.hashedAssets; if (ha.js) process.stderr.write(` ${gray}Asset hash:${reset} ${ha.js}\n`); if (ha.css) process.stderr.write(` ${gray}Asset hash:${reset} ${ha.css}\n`); } if (result.adapter.nonce) { process.stderr.write(` ${gray}CSP nonce:${reset} ${result.adapter.nonce}\n`); } if (result.adapter.serverPath) { process.stderr.write(` ${gray}Generated:${reset} server.js + Dockerfile\n`); } if (result.adapter.vercelJsonPath) { process.stderr.write(` ${gray}Generated:${reset} vercel.json + .vercel/output/\n`); } if (result.adapter.sitemap) { process.stderr.write(` ${gray}Generated:${reset} sitemap.xml\n`); } } process.stderr.write(`\n ${gray}Done in ${elapsed}${reset}\n\n`); if (result.errors.some((e) => e.severity === 'error')) { process.exit(1); } return; } // ── Legacy: single-file build mode ────────────────────────────────────── const startTotal = process.hrtime(); process.stderr.write(`\n${bold}PromptJS Build${reset}\n`); process.stderr.write(` Input: ${cyan}${rootDir}${reset}\n`); process.stderr.write(` Output: ${cyan}${distDir}${reset}\n\n`); // Clean dist directory if (fs.existsSync(distDir)) { rmDirRecursive(distDir); } fs.mkdirSync(distDir, { recursive: true }); // Find all .pjs files const pjsFiles = findPjsFiles(rootDir); if (pjsFiles.length === 0) { process.stderr.write(`No .pjs files found in ${rootDir}\n`); process.exit(1); } process.stderr.write(`${gray}Compiling ${pjsFiles.length} file(s)...${reset}\n`); // Compile all .pjs files let compiled = 0; let failed = 0; let totalJsSize = 0; const compiledResults = []; for (const filePath of pjsFiles) { const start = process.hrtime(); const engine = new PromptJSEngine(); const result = engine.compileFile(filePath, { dev: false, loadDataFiles: true, dataDir: path.dirname(filePath), source: path.basename(filePath), }); const elapsed = formatElapsed(start); if (result.errors && result.errors.length > 0) { printDiagnostics(result.errors, 'error', true); } if (result.warnings && result.warnings.length > 0) { printDiagnostics(result.warnings, 'warning', true); } if (!result.success) { process.stderr.write( ` ${red}✗${reset} ${path.relative(rootDir, filePath)} ${gray}(${elapsed})${reset}\n` ); failed++; continue; } const js = minify ? minifyJs(result.js) : result.js; totalJsSize += js.length; // Determine output paths const relPath = path.relative(rootDir, filePath); const jsOutPath = path.join(distDir, relPath.replace(/\.pjs$/, '.js')); const htmlOutPath = path.join(distDir, relPath.replace(/\.pjs$/, '.html')); // Write JS file ensureDirForFile(jsOutPath); fs.writeFileSync(jsOutPath, js, 'utf-8'); // Write HTML file const htmlContent = buildHtml(js, filePath, { prerender: false }); ensureDirForFile(htmlOutPath); fs.writeFileSync(htmlOutPath, htmlContent, 'utf-8'); process.stderr.write( ` ${green}✓${reset} ${path.relative(rootDir, filePath)} ${gray}→ ${path.relative(distDir, jsOutPath)} (${formatSize(js.length)} ${elapsed})${reset}\n` ); compiled++; compiledResults.push({ filePath, js, jsOutPath, htmlOutPath }); } // Copy static assets (non-.pjs files) process.stderr.write(`\n${gray}Copying static assets...${reset}\n`); const staticFiles = copyStaticAssets(rootDir, distDir); process.stderr.write(` ${green}${staticFiles}${reset} asset(s) copied\n`); // Prerender with jsdom if requested if (prerender) { process.stderr.write(`\n${gray}Prerendering HTML pages...${reset}\n`); try { const jsdom = require('jsdom'); const { JSDOM } = jsdom; for (const { filePath, js, htmlOutPath } of compiledResults) { try { const dom = new JSDOM( `
`, { runScripts: 'dangerously', resources: 'usable' } ); const rendered = dom.window.document.getElementById('app').innerHTML; // Write prerendered HTML const prerenderedHtml = buildPrerenderedHtml(rendered, filePath); fs.writeFileSync(htmlOutPath, prerenderedHtml, 'utf-8'); process.stderr.write(` ${green}✓${reset} ${path.basename(htmlOutPath)} (prerendered)\n`); dom.window.close(); } catch (e) { process.stderr.write( ` ${red}✗${reset} ${path.basename(htmlOutPath)} prerender failed: ${e.message}\n` ); } } } catch { process.stderr.write( ` ${red}jsdom not available${reset} — install with: npm install jsdom\n` ); process.stderr.write(` ${gray}Falling back to client-side rendering${reset}\n`); } } // Summary const totalElapsed = formatElapsed(startTotal); process.stderr.write( `\n${bold}${green}Build complete${reset} ${gray}(${totalElapsed})${reset}\n` ); process.stderr.write(` ${compiled} compiled, ${failed} failed, ${staticFiles} assets\n`); process.stderr.write( ` Output: ${cyan}${distDir}${reset} ${gray}(${formatSize(totalJsSize + staticFiles * 500)})${reset}\n\n` ); process.exit(failed > 0 ? 1 : 0); } /** * Build an HTML page wrapping the compiled JS. */ /** * Bungkus kode JS hasil compile menjadi file HTML lengkap dengan `