// @ts-check
/**
* PromptJS v1.0.0 — Adapter: Node Server
* ============================================================================
*
* Generates a self-contained Node.js HTTP server (`server.js`) from
* compiled output. The server serves static files, matches routes,
* and optionally proxies API requests.
*
* Usage:
* pjs build --adapter node
* node dist/server.js # Start on port 3000
* PORT=8080 node dist/server.js
*
* Zero-dependency — generated server.js only uses Node.js built-ins.
*/
'use strict';
const fs = require('fs');
const path = require('path');
/**
* Generate the server.js content for a Node.js adapter.
*
* @param {Object} [opts] - Adapter options
* @param {string} [opts.outDir] - Output directory (where files were written)
* @param {boolean} [opts.isSPA] - Whether this is an SPA build
* @param {string[]} [opts.routes] - Route paths (for SPA route table)
* @param {string} [opts.apiUrl] - Backend API URL for proxying
* @returns {string} server.js content
*/
function generateServerJS(opts) {
opts = opts || {};
const isSPA = opts.isSPA;
const apiUrl = opts.apiUrl || '';
let serverCode = '';
serverCode += '// PromptJS v1.0.0 — Node.js Server\n';
serverCode += '// Auto-generated. Do not edit.\n';
serverCode += '// Usage: node server.js [PORT]\n\n';
serverCode += 'var http = require("http");\n';
serverCode += 'var fs = require("fs");\n';
serverCode += 'var path = require("path");\n';
serverCode += 'var url = require("url");\n\n';
serverCode += 'var PORT = parseInt(process.env.PORT || "3000", 10);\n';
serverCode += 'var STATIC_DIR = path.join(__dirname);\n';
serverCode += 'var MIME_TYPES = {\n';
serverCode += ' ".html": "text/html; charset=utf-8",\n';
serverCode += ' ".js": "application/javascript; charset=utf-8",\n';
serverCode += ' ".css": "text/css; charset=utf-8",\n';
serverCode += ' ".json": "application/json; charset=utf-8",\n';
serverCode += ' ".png": "image/png",\n';
serverCode += ' ".jpg": "image/jpeg",\n';
serverCode += ' ".jpeg": "image/jpeg",\n';
serverCode += ' ".gif": "image/gif",\n';
serverCode += ' ".svg": "image/svg+xml",\n';
serverCode += ' ".ico": "image/x-icon",\n';
serverCode += ' ".woff": "font/woff",\n';
serverCode += ' ".woff2": "font/woff2",\n';
serverCode += ' ".txt": "text/plain; charset=utf-8",\n';
serverCode += ' ".xml": "application/xml; charset=utf-8",\n';
serverCode += ' ".webmanifest": "application/manifest+json"\n';
serverCode += '};\n\n';
if (isSPA) {
// SPA: all routes serve index.html (client-side router handles it)
serverCode += '// SPA mode: serve index.html for all non-static routes\n';
serverCode += 'var INDEX_HTML = null;\n';
serverCode +=
'try { INDEX_HTML = fs.readFileSync(path.join(STATIC_DIR, "index.html"), "utf-8"); }\n';
serverCode += 'catch(e) { INDEX_HTML = "
Error: index.html not found
"; }\n\n';
serverCode += 'function serveSPA(req, res) {\n';
serverCode += ' var parsed = url.parse(req.url);\n';
serverCode += ' var pathname = parsed.pathname;\n\n';
// Serve static files first (js, css, assets)
serverCode += ' // Try static file first\n';
serverCode += ' var filePath = path.join(STATIC_DIR, pathname);\n';
serverCode +=
' if (pathname !== "/" && fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n';
serverCode += ' serveStaticFile(filePath, res);\n';
serverCode += ' return;\n';
serverCode += ' }\n\n';
// Fallback to index.html for all routes
serverCode += ' // SPA fallback: serve index.html\n';
serverCode += ' res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });\n';
serverCode += ' res.end(INDEX_HTML);\n';
serverCode += '}\n\n';
} else {
// MPA: serve per-page HTML files
serverCode += '// MPA mode: serve HTML files per route\n\n';
serverCode += 'function serveMPA(req, res) {\n';
serverCode += ' var parsed = url.parse(req.url);\n';
serverCode += ' var pathname = parsed.pathname;\n\n';
serverCode += ' // Root → index.html\n';
serverCode += ' if (pathname === "/") {\n';
serverCode += ' pathname = "/index.html";\n';
serverCode +=
' } else if (!pathname.endsWith(".html") && !pathname.endsWith(".js") && !pathname.endsWith(".css") && !path.extname(pathname)) {\n';
serverCode += ' pathname = pathname + ".html";\n';
serverCode += ' }\n\n';
serverCode += ' var filePath = path.join(STATIC_DIR, pathname);\n';
serverCode += ' if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {\n';
serverCode += ' serveStaticFile(filePath, res);\n';
serverCode += ' } else {\n';
serverCode += ' // Try 404.html\n';
serverCode += ' var notFoundPath = path.join(STATIC_DIR, "404.html");\n';
serverCode += ' if (fs.existsSync(notFoundPath)) {\n';
serverCode += ' res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });\n';
serverCode += ' res.end(fs.readFileSync(notFoundPath, "utf-8"));\n';
serverCode += ' } else {\n';
serverCode += ' res.writeHead(404, { "Content-Type": "text/plain" });\n';
serverCode += ' res.end("404 Not Found");\n';
serverCode += ' }\n';
serverCode += ' }\n';
serverCode += '}\n\n';
}
// Static file server
serverCode += 'function serveStaticFile(filePath, res) {\n';
serverCode += ' var ext = path.extname(filePath).toLowerCase();\n';
serverCode += ' var contentType = MIME_TYPES[ext] || "application/octet-stream";\n';
serverCode += ' try {\n';
serverCode += ' var content = fs.readFileSync(filePath);\n';
serverCode += ' res.writeHead(200, {\n';
serverCode += ' "Content-Type": contentType,\n';
serverCode += ' "Cache-Control": "public, max-age=31536000, immutable"\n';
serverCode += ' });\n';
serverCode += ' res.end(content);\n';
serverCode += ' } catch(e) {\n';
serverCode += ' res.writeHead(500, { "Content-Type": "text/plain" });\n';
serverCode += ' res.end("Internal Server Error");\n';
serverCode += ' }\n';
serverCode += '}\n\n';
// API proxy (if configured)
if (apiUrl) {
serverCode += '// API proxy\n';
serverCode += 'var API_URL = "' + apiUrl + '";\n\n';
serverCode += 'function proxyApi(req, res) {\n';
serverCode += ' var parsed = url.parse(req.url);\n';
serverCode += ' var proxyUrl = API_URL + parsed.pathname + (parsed.search || "");\n\n';
serverCode += ' var body = [];\n';
serverCode += ' req.on("data", function(chunk) { body.push(chunk); });\n';
serverCode += ' req.on("end", function() {\n';
serverCode += ' var options = url.parse(proxyUrl);\n';
serverCode += ' options.method = req.method;\n';
serverCode += ' options.headers = Object.assign({}, req.headers, { host: options.host });\n';
serverCode +=
' if (body.length > 0) options.headers["Content-Length"] = Buffer.concat(body).length;\n\n';
serverCode += ' var proxyReq = http.request(options, function(proxyRes) {\n';
serverCode += ' res.writeHead(proxyRes.statusCode, proxyRes.headers);\n';
serverCode += ' proxyRes.pipe(res);\n';
serverCode += ' });\n';
serverCode += ' proxyReq.on("error", function(e) {\n';
serverCode += ' res.writeHead(502, { "Content-Type": "application/json" });\n';
serverCode += ' res.end(JSON.stringify({ error: "Bad Gateway: " + e.message }));\n';
serverCode += ' });\n';
serverCode += ' if (body.length > 0) proxyReq.write(Buffer.concat(body));\n';
serverCode += ' proxyReq.end();\n';
serverCode += ' });\n';
serverCode += '}\n\n';
}
// Main server
serverCode += 'var server = http.createServer(function(req, res) {\n';
if (apiUrl) {
serverCode += ' // API proxy\n';
serverCode += ' if (req.url.startsWith("/api/")) {\n';
serverCode += ' proxyApi(req, res);\n';
serverCode += ' return;\n';
serverCode += ' }\n\n';
}
serverCode += ' ' + (isSPA ? 'serveSPA' : 'serveMPA') + '(req, res);\n';
serverCode += '});\n\n';
serverCode += 'server.listen(PORT, function() {\n';
serverCode += ' console.log("PromptJS server running at http://localhost:" + PORT);\n';
serverCode += '});\n\n';
return serverCode;
}
/**
* Run the Node adapter: write server.js + Dockerfile.
*
* @param {Object} [opts] - Adapter options
* @param {string} [opts.outDir] - Output directory
* @param {boolean} [opts.isSPA] - SPA or MPA
* @param {string[]} [opts.routes] - Route paths
* @param {string} [opts.apiUrl] - Backend API URL
* @returns {{ serverPath: string, dockerfilePath: string, errors: Object[] }}
*/
function runNodeAdapter(opts) {
opts = opts || {};
const errors = [];
const outDir = path.resolve(opts.outDir || 'dist');
const serverContent = generateServerJS(opts);
const serverPath = path.join(outDir, 'server.js');
fs.writeFileSync(serverPath, serverContent, 'utf-8');
// Generate Dockerfile
const dockerContent =
'FROM node:20-slim\n' +
'WORKDIR /app\n' +
'COPY dist/ .\n' +
'EXPOSE 3000\n' +
'CMD ["node", "server.js"]\n';
const dockerfilePath = path.join(outDir, 'Dockerfile');
fs.writeFileSync(dockerfilePath, dockerContent, 'utf-8');
return {
serverPath: serverPath,
dockerfilePath: dockerfilePath,
errors: errors,
};
}
module.exports = {
generateServerJS,
runNodeAdapter,
};