// @ts-check /** * PromptJS v1.0.0 — Compiler (Tahap 5) / Kompilator * ============================================================================ * * Lowers the AST into Vanilla JavaScript DOM API calls. * Melakukan lowering AST menjadi Vanilla JavaScript DOM API. * * v0.5 changes: * - Source map generation (V3 + VLQ encoding) * - Tree shaking: this.helpers starts empty, filled by visitors * - emit() tracks source mappings when node.loc is provided * * Features / Fitur: Proxy-based reactivity, lifecycle management, zero dependencies. */ const { BaseVisitor, accept } = require('../utils/visitor'); const RuntimeEmitter = require('./emitters/runtime'); const Codegen = require('./utils/codegen'); const ExpressionLowering = require('./lower/expression'); const StatementEmitters = require('./emitters/statements'); /** * Constructor PromptJSCompiler — codegen berbasis visitor. * * State compiler: * - `output` — array baris kode JS yang sedang di-emit * - `indent` — level indentasi saat ini * - `varCounter` — counter untuk generate nama variabel unik * - `componentCount` — counter komponen yang sudah di-emit * - `helpers` — Set nama runtime helper yang PERLU di-emit (diisi oleh StatementEmitters) * - `sourceMapData` — array mapping { sourceLine, sourceCol, outputLine } untuk source map * - `currentSource` — nama file source saat ini (untuk source map) * * @constructor * @this {PromptJSCompiler} */ function PromptJSCompiler() { BaseVisitor.call(this); this.output = []; this.indent = 0; this.varCounter = 0; this.componentCount = 0; // v0.5: helpers starts EMPTY — visitors add what they need this.helpers = new Set(); // v0.5: source map tracking this.sourceMapData = []; this.currentSource = ''; } PromptJSCompiler.prototype = Object.create(BaseVisitor.prototype); PromptJSCompiler.prototype.constructor = PromptJSCompiler; // TypeScript hint: BaseVisitor.call(this) inherits genericVisit/visit* methods. /** @type {(node: Object) => void} */ PromptJSCompiler.prototype.genericVisit; /** * Entry point compiler — lower AST menjadi string JavaScript. * * Algoritma: * 1. Reset state (output, varCounter, componentCount). * 2. Emit header comment. * 3. Traverse AST — visitors populate `this.helpers` Set dan emit user code. * Setiap top-level node dengan `loc` juga di-record untuk source map. * 4. Generate runtime helpers (tree-shaken) berdasarkan `this.helpers`. * 5. Splice helpers ke posisi yang benar (setelah header, sebelum user code). * 6. v0.6: Jika SPA mode, wrap user code dalam factory function dengan * mount/unmount lifecycle. Jika bukan, wrap dalam IIFE (v0.5 behavior). * 7. Return seluruh output yang di-join dengan `\n`. * * @param {Object} ast - Root AST node (Program) * @returns {string} Kode JavaScript hasil kompilasi */ PromptJSCompiler.prototype.compile = function (ast) { // v1.0.0: Validate unsupported AST node types (E5001) this._validateNodeTypes(ast); this.output = []; this.varCounter = 0; this.componentCount = 0; this.helpers = new Set(); this.sourceMapData = []; this.currentSource = ast.source || 'program.pjs'; // v0.6: SPA mode flags from engine (set via ast properties) this.isSPA = !!ast.isSPA; this._spaPageName = ast.pageName || 'page'; this._spaPageRoot = null; // v0.9: Auth directive flags from engine this.butuhAuth = !!ast.butuhAuth; this.authRedirect = ast.authRedirect || '/login'; this.authToken = ast.authToken || 'localStorage'; this.authTokenKey = ast.authTokenKey || 'token'; this.authPeran = ast.authPeran || null; // Phase 1: Emit header this.emit('// Generated by PromptJS Compiler v1.0.0'); this.emit(`// Source: ${ast.source || 'program.pjs'}`); if (this.isSPA) { this.emit('// Mode: SPA (router active)'); } if (this.butuhAuth) { this.emit('// Auth: Protected by authentication guard'); } this.emit(''); // Record where helpers will be inserted (after header) const helperInsertIdx = this.output.length; this.output.push(''); // Phase 2: Traverse AST — this populates this.helpers via visitors this.emit('// === User Code ==='); // v0.9.9: Auth guard wrapper (before SPA factory or IIFE) if (this.butuhAuth) { // S-1 (v1.0.0): `authToken` di-emit sebagai identifier telanjang — aman // karena sudah lolos whitelist {localStorage, sessionStorage} di engine. // Semua nilai string (key, redirect, peran) WAJIB lewat escapeString agar // tidak bisa keluar dari konteks string literal (code-injection). const esc = Codegen.escapeString; this.emit(`// Auth guard: check ${this.authToken} for token`); this.emit(`(function() {`); this.emit(` var __token = ${this.authToken}.getItem(${esc(this.authTokenKey)});`); this.emit(` if (!__token) {`); this.emit(` window.location.href = ${esc(this.authRedirect)};`); this.emit(` return;`); this.emit(` }`); // v0.9.9: Role-based access check (peran directive) // S-5 (v1.0.0): Pemeriksaan peran ini bersifat CLIENT-SIDE/ADVISORY — nilai // `__peran` di localStorage/sessionStorage dapat dipalsukan pengguna lewat // devtools. Ini BUKAN kontrol keamanan; otorisasi sesungguhnya WAJIB di // server. Hardening yang dilakukan tanpa mengingkari batas itu: // • Sediakan seam `window.__pjs_verifyPeran(peran, allowed)` agar developer // dapat memasang verifikasi server-side (mis. cek klaim JWT) — bila ada // dan mengembalikan false, akses ditolak. // • Dukung banyak peran (dipisah koma) + normalisasi (trim/lowercase). // • Tegaskan sifat advisory via console.warn sekali jalan. if (this.authPeran) { this.emit(` var __peran = ${this.authToken}.getItem('__peran');`); this.emit(` var __allowedPeran = ${esc(this.authPeran)};`); this.emit( ` var __allowedList = String(__allowedPeran).split(',').map(function(s){return s.trim().toLowerCase();});` ); this.emit(` var __peranNorm = __peran == null ? '' : String(__peran).trim().toLowerCase();`); this.emit(` var __peranOk = __allowedList.indexOf(__peranNorm) !== -1;`); this.emit( ` if (typeof window !== 'undefined' && typeof window.__pjs_verifyPeran === 'function') {` ); this.emit(` __peranOk = !!window.__pjs_verifyPeran(__peran, __allowedPeran);`); this.emit(` } else if (typeof console !== 'undefined') {`); this.emit( ` console.warn('[PromptJS] Pemeriksaan peran bersifat client-side/advisory — verifikasi otorisasi WAJIB di server. Pasang window.__pjs_verifyPeran untuk validasi sungguhan.');` ); this.emit(` }`); this.emit(` if (!__peranOk) {`); this.emit(` window.location.href = ${esc(this.authRedirect)};`); this.emit(` return;`); this.emit(` }`); } this.emit(''); this.indent++; } // v0.6: SPA mode wraps code in factory function instead of IIFE if (this.isSPA) { this.emit('var __cleanupFns = [];'); this.emit('var __dipasangFns = [];'); this.emit('var __dilepasFns = [];'); this.emit(''); } // When butuhAuth is on, the auth guard IIFE already wraps everything. // No need for an extra IIFE — the auth guard body itself is the wrapper. // For SPA+auth: the factory function body sits inside the auth guard. // For non-SPA+auth: user code sits directly inside the auth guard. if (!this.isSPA && !this.butuhAuth) { this.emit('(function() {'); } this.indent++; if (ast.body && ast.body.length > 0) { for (let i = 0; i < ast.body.length; i++) { const node = ast.body[i]; if (node && node.loc && node.loc.start) { this.emit(`// @source ${node.loc.start.line}:${node.loc.start.column} ${node.type}`); this.sourceMapData.push({ sourceLine: node.loc.start.line, sourceCol: node.loc.start.column, outputLine: this.output.length - 1, }); } const result = accept(node, this); if (typeof result === 'string' && result.length > 0) { this.emit(result + ';'); } } } this.indent--; // v0.6: Close factory function or IIFE if (this.isSPA) { const pageRoot = this._spaPageRoot || 'null'; this.emit(''); this.emit('return {'); this.emit(' el: ' + pageRoot + ','); this.emit(' mount: function(__parent) {'); this.emit(' (__parent || document.body).appendChild(' + pageRoot + ');'); this.emit(' __dipasangFns.forEach(function(fn) { fn(); });'); this.emit(' },'); this.emit(' unmount: function() {'); this.emit(' __dilepasFns.forEach(function(fn) { fn(); });'); this.emit(' __cleanupFns.forEach(function(fn) { fn(); });'); this.emit(' if (' + pageRoot + ') ' + pageRoot + '.remove();'); this.emit(' }'); this.emit('};'); } else if (!this.butuhAuth) { // Only close IIFE if we opened one (not in auth guard mode) this.emit('})();'); } // v0.9: Close auth guard wrapper (if present) if (this.butuhAuth) { this.indent--; this.emit('})();'); } // Phase 3: Generate tree-shaken runtime helpers const savedOutput = this.output; const savedIndent = this.indent; this.output = []; this.indent = 0; RuntimeEmitter.emitRuntimeHelpers(this); const helperLines = this.output; this.output = savedOutput; this.indent = savedIndent; // Phase 4: Splice helpers into output at placeholder position if (helperLines.length > 0) { this.output.splice(helperInsertIdx, 1, ...helperLines); const smOffset = helperLines.length - 1; for (let s = 0; s < this.sourceMapData.length; s++) { this.sourceMapData[s].outputLine += smOffset; } } else { this.output.splice(helperInsertIdx, 1); for (let s2 = 0; s2 < this.sourceMapData.length; s2++) { this.sourceMapData[s2].outputLine -= 1; } } return this.output.join('\n'); }; // --- Emitter Helpers --- /** * Emit satu baris kode ke `this.output` dengan indentasi yang sesuai. * Jika `loc` diberikan, catat mapping untuk source map. * * @param {string} code - Kode yang akan di-emit * @param {Object} [loc] - Source location { start: { line, column } } * @returns {void} */ PromptJSCompiler.prototype.emit = function (code, loc) { if (loc && loc.start) { this.sourceMapData.push({ sourceLine: loc.start.line, sourceCol: loc.start.column, outputLine: this.output.length, }); } return Codegen.emit(this, code); }; /** * Generate nama variabel unik dengan prefix (mis. `v1`, `v2`, `__el3`). * Delegasi ke `Codegen.genVar`. * * @param {string} [prefix='v'] - Prefix nama variabel * @returns {string} Nama variabel unik */ PromptJSCompiler.prototype.genVar = function (prefix = 'v') { return Codegen.genVar(this, prefix); }; /** * Resolve target element menjadi ekspresi JavaScript. * * Menangani empat jenis node target: * - `Identifier` — jika reaktif (`data`/`turunan`), emit `.value`; * jika tidak, emit `` mentah. * - `Selector` — emit `document.querySelector("tag.class#id")`. * - `Literal` — emit `document.querySelector("")`. * - `SelfReference` — emit `compiledVarName` dari node yang direferensikan. * * @param {Object | null} targetNode - AST node target (Identifier/Selector/Literal/SelfReference) * @returns {string} Ekspresi JavaScript yang merepresentasikan target */ PromptJSCompiler.prototype.resolveTarget = function (targetNode) { if (!targetNode) return 'null'; if (targetNode.type === 'Identifier') { // Check if it's a compiled DOM variable (has compiledVarName) // or reactive data/turunan — use .value for reactive, direct name otherwise. if ( targetNode.resolved && (targetNode.resolved.kind === 'data' || targetNode.resolved.kind === 'turunan') ) { return `${targetNode.name}.value`; } // Fallback: check targetSymbol (set by Resolver._trackWrite) for reactivity. // targetSymbol is set on the parent statement (e.g. SimpanStatement.targetSymbol), // not on the Identifier itself — but we can check the identifier's symbol. return targetNode.name; } if (targetNode.type === 'Selector') { let selectorStr = targetNode.tag || ''; if (targetNode.id) selectorStr += '#' + targetNode.id; if (targetNode.classes && targetNode.classes.length > 0) { selectorStr += '.' + targetNode.classes.join('.'); } return `document.querySelector("${selectorStr}")`; } if (targetNode.type === 'Literal') { return `document.querySelector("${targetNode.value}")`; } if (targetNode.type === 'SelfReference') { if (targetNode.referencedNode && targetNode.referencedNode.compiledVarName) { return targetNode.referencedNode.compiledVarName; } return 'null'; } // Fallback return targetNode.name || 'null'; }; /** * Emit runtime helpers (tree-shaken) ke output. * Implementasi sebenarnya di `compiler/emitters/runtime.js`. * * @returns {void} */ PromptJSCompiler.prototype.emitRuntimeHelpers = function () { RuntimeEmitter.emitRuntimeHelpers(this); }; /** * Lower expression AST node menjadi string ekspresi JavaScript. * Delegasi ke `ExpressionLowering.lowerExpression`. * * @param {Object | null} node - AST node expression * @returns {string} Ekspresi JavaScript */ PromptJSCompiler.prototype.lowerExpression = function (node) { return ExpressionLowering.lowerExpression(this, node); }; /** * Generate a V3 source map JSON string from collected mapping data. * * @returns {string} JSON string of Source Map V3 */ PromptJSCompiler.prototype.generateSourceMap = function () { return Codegen.generateSourceMap(this); }; // Statement emitters dipasang dari compiler/emitters/statements.js. // ═══════════════════════════════════════════════════════════════════════════════ // AST VALIDATION (v1.0.0) // ═══════════════════════════════════════════════════════════════════════════════ /** * Validasi tipe node AST — throw E5001 jika tipe tidak dikenal. * Rekursif traversing semua child nodes via getChildKeys. * * @param {Object} node - AST node * @throws {Error} E5001 jika node type tidak dikenali */ PromptJSCompiler.prototype._validateNodeTypes = function (node) { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) this._validateNodeTypes(node[i]); return; } if (!node.type) return; const { getChildKeys } = require('../utils/visitor'); // Build valid types from getChildKeys registry + known leaf types const validTypes = new Set(); // Known node types in the AST pipeline (including leaf/synthetic types) const allTypes = [ 'Program', 'BlockStatement', 'BuatStatement', 'TampilkanStatement', 'SembunyikanStatement', 'HapusStatement', 'KosongkanStatement', 'PerbaruiStatement', 'KetikaStatement', 'SaatStatement', 'LifecycleStatement', 'SetelahStatement', 'JikaStatement', 'UlangiStatement', 'SelamaStatement', 'KembalikanStatement', 'SimpanStatement', 'TambahkanStatement', 'KurangiStatement', 'SisipkanStatement', 'AmbilDomStatement', 'AmbilLuarStatement', 'KomponenDeclaration', 'FungsiDeclaration', 'GunakanStatement', 'JalankanExpression', 'RantaiAksi', 'BinaryExpression', 'UnaryExpression', 'ConditionalExpression', 'MemberExpression', 'CallExpression', 'PanggilNativeExpression', 'ObjectLiteral', 'ArrayLiteral', 'Selector', 'PropertyNode', 'AttributeNode', 'Parameter', 'SelfReference', 'DataDeclaration', 'TetapDeclaration', 'UbahDeclaration', 'TurunanDeclaration', 'DefinisikanDeclaration', 'HapusDariStatement', 'ArahkanStatement', 'BerhentiStatement', 'LewatiStatement', 'PassStatement', 'Identifier', 'Literal', 'ErrorNode', 'DocString', 'AmbilStatement', 'FetchBranch', 'FetchOption', 'UbahStatement', 'TextNode', 'TernaryExpression', 'HapusDariExpression', 'SimpanExpression', ]; for (let i = 0; i < allTypes.length; i++) { validTypes.add(allTypes[i]); } if (!validTypes.has(node.type)) { const err = new Error(`[E5001] Node AST bertipe "${node.type}" tidak didukung oleh compiler`); throw err; } // Rekursi ke child nodes const childKeys = getChildKeys(node.type); if (childKeys) { for (let i = 0; i < childKeys.length; i++) { const child = node[childKeys[i]]; if (child) this._validateNodeTypes(child); } } }; // ═══════════════════════════════════════════════════════════════════════════════ // EXPRESSION LOWERING // ═══════════════════════════════════════════════════════════════════════════════ PromptJSCompiler.prototype.lowerExpression = function (node) { return ExpressionLowering.lowerExpression(this, node); }; // Install statement visitor emitters after core compiler helpers are defined. StatementEmitters.install(PromptJSCompiler, accept); module.exports = PromptJSCompiler;