"use strict";
/**
 * Build-time expansion of Hot.include(path, literalArgs) calls.
 *
 * Given a partial path and a JSON-serializable args object, this module
 * compiles the partial via the legacy HotFile.parseContent pipeline and
 * executes it inside a Node-side sandbox with a minimal Hot global that
 *
 *   - echoUnsafe(str) / echo(str): appends to an Output buffer
 *   - include(path, args): recursive, reuses the same sandbox
 *   - Cookies, CurrentPage, Data: no-op stubs
 *   - getJSON / fetch / anything network: throws so the caller can
 *     detect "this partial has runtime dependencies" and fall back.
 *
 * The output HTML is fully static: interleaved `<* for/if *>` control
 * flow unrolled, `${TITLE}` substitutions resolved, nested partials
 * expanded recursively. Matches the admin-panel shape end to end.
 *
 * When execution throws, the caller falls back to the HS090-15 raw-
 * template-inline behaviour and emits a warning so the site author can
 * refactor or declare the route as staticRender: true.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.expandPartial = expandPartial;
exports.stripDocumentWrappers = stripDocumentWrappers;
exports.evalInstalledModuleIndex = evalInstalledModuleIndex;
exports.isLiteralAst = isLiteralAst;
exports.materialiseLiteral = materialiseLiteral;
const fs = __importStar(require("fs"));
const fsp = __importStar(require("fs/promises"));
const ppath = __importStar(require("path"));
const ts_morph_1 = require("ts-morph");
const HotFile_1 = require("../HotFile");
const Hot_1 = require("../Hot");
const HotStaq_1 = require("../HotStaq");
/**
 * Render a partial at build time with the given literal args bound.
 *
 * Throws (with a descriptive message) when the partial's preamble
 * touches a runtime-only API (getJSON, fetch, Hot.API.*, etc.), when
 * the sandbox exceeds a cycle limit, or when the compiled .hott raises
 * a SyntaxError the sandbox can't wrap.
 */
function expandPartial(opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const visited = [];
        const inflightCycleGuard = new Set();
        const registry = opts.moduleRegistry || new Map();
        const publicDir = opts.publicDir || ppath.dirname(opts.absPath);
        const assets = opts.assets || null;
        const rawHtml = yield renderOne(opts.absPath, opts.args, opts.resolve, visited, inflightCycleGuard, registry, publicDir, assets);
        // admin-panel partials (and other legacy SSR partials that assume
        // they emit a full HTML document) are cleaned up into fragment-safe
        // HTML so the output drops straight into a <template> stash node.
        const html = stripDocumentWrappers(rawHtml);
        return ({ html, visited });
    });
}
/**
 * Remove <!doctype html>, <html>, <head>, <body> wrappers from a rendered
 * fragment so it embeds cleanly into a <template> stash. Content inside
 * the wrappers (links, scripts, style, actual body markup) is preserved.
 *
 * Intentionally conservative: only strips wrappers when the fragment
 * actually starts like a full document. Leaves ordinary fragments
 * untouched.
 */
function stripDocumentWrappers(html) {
    let out = html;
    // Strip leading whitespace + <!doctype ...>
    out = out.replace(/^\s*<!doctype[^>]*>\s*/i, "");
    // If there's a full <html>...</html> wrapper, unwrap it. Tolerate
    // attributes on the opening tag (`<html lang="en">` etc).
    const htmlOpen = out.match(/^\s*<html\b[^>]*>/i);
    if (htmlOpen) {
        const openEnd = htmlOpen.index + htmlOpen[0].length;
        const closeIdx = out.lastIndexOf("</html>");
        if (closeIdx > openEnd)
            out = out.substring(openEnd, closeIdx);
        else
            out = out.substring(openEnd);
    }
    // Drop stray <head>/</head> and <body>/</body> tags — their contents
    // stay. Stylesheets + scripts inside <head> still apply when the
    // fragment gets cloned into the document.
    out = out
        .replace(/<head\b[^>]*>/gi, "")
        .replace(/<\/head>/gi, "")
        .replace(/<body\b[^>]*>/gi, "")
        .replace(/<\/body>/gi, "");
    return (out.trim() + "\n");
}
const MAX_DEPTH = 16;
function renderOne(absPath, args, resolve, visited, inflight, registry, publicDir, assets) {
    return __awaiter(this, void 0, void 0, function* () {
        if (inflight.has(absPath))
            throw new Error(`[hs090] cycle detected in Hot.include: ${absPath}`);
        if (inflight.size >= MAX_DEPTH)
            throw new Error(`[hs090] Hot.include depth exceeded ${MAX_DEPTH} (runaway recursion?)`);
        inflight.add(absPath);
        visited.push(absPath);
        if (!fs.existsSync(absPath)) {
            inflight.delete(absPath);
            throw new Error(`[hs090] partial file not found: ${absPath}`);
        }
        const source = yield fsp.readFile(absPath, "utf8");
        const parsedJs = HotFile_1.HotFile.parseContent(source, false);
        // The sandbox delegates echoUnsafe/echo to legacy Hot (no code
        // duplication). Hot.Output is the static buffer that echoes
        // accumulate into; we snapshot it before invoking so nested renders
        // get a fresh buffer and the caller's Output isn't polluted. Build-
        // time renders are sequential — no concurrency concern.
        const savedOutput = Hot_1.Hot.Output;
        Hot_1.Hot.Output = "";
        let result = "";
        try {
            yield runCompiledPartial(parsedJs, args, absPath, resolve, visited, inflight, registry, publicDir, assets);
            result = Hot_1.Hot.Output;
        }
        finally {
            Hot_1.Hot.Output = savedOutput;
            inflight.delete(absPath);
        }
        return (result);
    });
}
function runCompiledPartial(parsedJs, args, absFrom, resolve, visited, inflight, registry, publicDir, assets) {
    return __awaiter(this, void 0, void 0, function* () {
        // Build the sandbox Hot shim per invocation so parallel nested
        // renders each get an isolated Output buffer. renderOne's callers
        // see only the final html string.
        const Hot = buildSandboxHot(absFrom, resolve, visited, inflight, registry, publicDir, assets);
        // Declare each arg as a top-level var in the executed scope — matches
        // the legacy HotPage.process() convention of `var ${key} = ${JSON};`
        // so preambles can reference {TITLE, SIDEBAR_ITEMS} by name.
        const argDecls = [];
        for (const key of Object.keys(args))
            argDecls.push(`var ${key} = ${JSON.stringify(args[key])};`);
        // Minimal helpers the legacy compiler expects in scope. We also
        // expose a stub `window` so partials that feature-guard with
        // `typeof window.__X === "undefined"` pass at build time without
        // throwing (admin-panel-style "load deps once" guards).
        const scaffold = `
var window = typeof globalThis.window !== "undefined" ? globalThis.window : { __hott_fake_window: true };
${argDecls.join("\n")}

function echoOutput (content, throwErrors) {
	if (throwErrors === false) {
		try { Hot.echoUnsafe (content); } catch { Hot.echoUnsafe (""); }
		return;
	}
	Hot.echoUnsafe (content);
}

function outputStr (value, possibleValue) {
	let result = \`\\\${\${value}}\`;
	try {
		if (possibleValue != null) result = possibleValue;
		else result = eval (value);
	} catch (ex) {}
	return (result);
}

function createFunction () { /* no-op at build time */ }
function createTestElement () { /* no-op at build time */ }

async function runContent (CurrentHotFile) {
${parsedJs}
}

return runContent (null).then (() => Hot.Output);
`;
        // eslint-disable-next-line no-new-func
        const fn = new Function("Hot", scaffold);
        return (yield fn(Hot));
    });
}
function buildSandboxHot(absFrom, resolve, visited, inflight, registry, publicDir, assets) {
    const runtimeOnly = (name) => () => {
        throw new Error(`[hs090] Hot.${name} called at build time — this partial has runtime dependencies. ` +
            `Refactor to use literal args, or declare the route as staticRender: true.`);
    };
    // The shim delegates output + html helpers directly to the legacy
    // Hot / HotStaq classes (no duplicated echo/sanitize logic). Methods
    // that would reach runtime-only APIs (fetch, cookies, window) stay
    // custom — they throw or no-op so dynamic partials fall back to the
    // raw-template stash behaviour cleanly.
    const Hot = {
        // Runtime / env context fields legacy compiled .hott may read.
        Output: "", // unused; echoUnsafe/echo write to LegacyHot.Output directly (see renderOne).
        Mode: 0,
        Arguments: null,
        CurrentPage: null,
        PublicKeys: {},
        API: null,
        TesterAPI: null,
        Data: {},
        Debugger: null,
        // --- delegated to legacy Hot ---
        echoUnsafe: (s) => {
            if (s == null)
                return;
            Hot_1.Hot.echoUnsafe(String(s));
        },
        echo: (s) => {
            if (s == null)
                return;
            Hot_1.Hot.echo(String(s));
        },
        // --- custom: needs the build-time renderOne path ---
        include(path_1) {
            return __awaiter(this, arguments, void 0, function* (path, subArgs = {}) {
                if (typeof path !== "string")
                    throw new Error(`[hs090] Hot.include at build time requires a string path; got ${typeof path}`);
                const modEntry = resolveViaRegistry(path, registry);
                const nextAbs = modEntry
                    ? ppath.resolve(publicDir, modEntry.path)
                    : resolve(path, absFrom);
                const nested = yield renderOne(nextAbs, subArgs || {}, resolve, visited, inflight, registry, publicDir, assets);
                Hot_1.Hot.echoUnsafe(nested);
            });
        },
        // --- custom: consults preloaded module registry ---
        import(moduleName) {
            return __awaiter(this, void 0, void 0, function* () {
                const cached = registry.get(moduleName);
                if (cached)
                    return (instantiateModuleShim(cached, Hot, assets));
                const modIndex = ppath.resolve(publicDir, "hotstaq_modules", moduleName, "index.js");
                if (fs.existsSync(modIndex)) {
                    const raw = yield evalInstalledModuleIndex(modIndex, moduleName);
                    registry.set(moduleName, raw);
                    return (instantiateModuleShim(raw, Hot, assets));
                }
                try {
                    return (require(moduleName));
                }
                catch (_a) {
                    return ({});
                }
            });
        },
        // --- runtime-only: throw so HS090-15 fallback kicks in ---
        includeJS(_url, _args) {
            return __awaiter(this, void 0, void 0, function* () {
                throw new Error(`[hs090] Hot.includeJS called at build time — not supported during partial expansion.`);
            });
        },
        getJSON(_url) {
            return __awaiter(this, void 0, void 0, function* () { return (runtimeOnly("getJSON")()); });
        },
        jsonRequest(_url) {
            return __awaiter(this, void 0, void 0, function* () { return (runtimeOnly("jsonRequest")()); });
        },
        // --- runtime-only: no-op stubs (no browser cookies/url in Node) ---
        Cookies: {
            get: (_name) => null,
            set: (_name, _value) => { },
            remove: (_name) => { }
        },
        getUrlParam: () => "",
        // --- delegated to legacy HotStaq (pure, no DOM needed) ---
        sanitizeHTML: (s) => HotStaq_1.HotStaq.sanitizeHTML(s),
        // addHtmlUnsafe touches document.querySelector — DOM-only. At
        // build time it's usually referenced from <hot-dom> blocks for
        // runtime DOM manipulation; a passthrough is correct here because
        // the caller's compiled .hott uses the return value as an HTML
        // string that later gets echoed.
        addHtmlUnsafe: (_parent, html) => html
    };
    return (Hot);
}
/**
 * Look up a Hot.include path against a registered module's html map.
 * admin-panel's installed index.js registers entries like:
 *   { name: "@hotstaq/admin-panel/admin-header.hott",
 *     path: "hotstaq_modules/@hotstaq/admin-panel/public/html/admin-header.hott" }
 * so a caller's `Hot.include("@hotstaq/admin-panel/admin-header.hott")` can
 * resolve to the real file without a heuristic.
 */
function resolveViaRegistry(requested, registry) {
    for (const mod of registry.values()) {
        for (const entry of mod.html) {
            if (entry.name === requested)
                return (entry);
        }
    }
    return (null);
}
/**
 * Execute an installed hotstaq_modules/<name>/index.js to produce a
 * SandboxModule. The legacy index.js expects `HotStaqWeb.HotModule` to
 * be globally available — we provide a minimal class that captures the
 * name/css/js/html/components assignments and returns itself.
 */
function evalInstalledModuleIndex(indexPath, moduleName) {
    return __awaiter(this, void 0, void 0, function* () {
        const source = yield fsp.readFile(indexPath, "utf8");
        class HotModuleShim {
            constructor(n) {
                this.js = [];
                this.css = [];
                this.html = [];
                this.componentLibrary = "";
                this.components = [];
                this.name = n;
            }
        }
        const HotStaqWeb = { HotModule: HotModuleShim };
        // The legacy installer emits a script whose top level is
        //   let newModule = new HotStaqWeb.HotModule("<name>");
        //   newModule.xxx = [...];
        //   ...
        //   return (newModule);
        // so wrap it in a function, apply with HotStaqWeb in scope, capture
        // the return value.
        // eslint-disable-next-line no-new-func
        const fn = new Function("HotStaqWeb", source);
        const result = fn(HotStaqWeb);
        if (!result || typeof result !== "object" || !result.name)
            throw new Error(`[hs090] installed module ${moduleName} at ${indexPath} did not return a HotModule.`);
        return (result);
    });
}
/**
 * Wrap a SandboxModule's data with the runtime helpers the legacy
 * admin-panel templates call on the imported object: outputCSS /
 * outputJS / outputComponents. Each emits HTML into the currently
 * active Hot.Output buffer.
 */
function instantiateModuleShim(mod, Hot, assets) {
    // v0.9.0 behaviour: outputCSS/outputJS register the referenced files
    // with the builder's shell-level asset collector (so <link>/<script>
    // tags land in dist/index.html's <head>) instead of inlining them
    // into the template stash. outputComponents behaves similarly —
    // registers with the collector for a single shell-level script.
    //
    // Callers that pass echoOut=false still get the HTML back as a string
    // so they can decide where to place it; we don't touch Hot.Output in
    // that case.
    return ({
        name: mod.name,
        js: mod.js,
        css: mod.css,
        html: mod.html,
        componentLibrary: mod.componentLibrary,
        components: mod.components,
        outputCSS(echoOut = true) {
            let out = "";
            for (const file of mod.css) {
                out += `<link rel="stylesheet" href="${file}">\n`;
                if (assets)
                    assets.addCss(file);
            }
            // Intentionally do NOT echo to Hot.Output by default — the
            // <link> tags belong in the shell <head>, not the template.
            // Returning the HTML keeps the `const css = outputCSS(false)`
            // pattern working for callers that need to place tags manually.
            if (echoOut && !assets)
                Hot.echoUnsafe(out);
            return (out);
        },
        outputJS(echoOut = true) {
            let out = "";
            for (const file of mod.js) {
                out += `<script src="${file}"></script>\n`;
                if (assets)
                    assets.addJs(file);
            }
            if (echoOut && !assets)
                Hot.echoUnsafe(out);
            return (out);
        },
        outputComponents(echoOut = true) {
            if (mod.components.length === 0)
                return ("");
            const lib = mod.componentLibrary ? `${mod.componentLibrary}.` : "";
            if (assets)
                assets.addComponents(mod.componentLibrary, mod.components);
            let out = `<script type="text/javascript">\n`;
            for (const c of mod.components)
                out += `  if (typeof ${lib}${c} !== "undefined" && typeof Hot !== "undefined" && Hot.CurrentPage) Hot.CurrentPage.processor.addComponent(${lib}${c});\n`;
            out += `</script>\n`;
            if (echoOut && !assets)
                Hot.echoUnsafe(out);
            return (out);
        }
    });
}
// ── ts-morph argument helpers ──────────────────────────────────────────
/**
 * Determine whether an AST node is a literal JSON-compatible expression.
 * Strings, numbers, booleans, null, arrays of literals, object literals
 * whose values are literals, NoSubstitutionTemplateLiterals.
 */
function isLiteralAst(node) {
    const kind = node.getKind();
    switch (kind) {
        case ts_morph_1.SyntaxKind.StringLiteral:
        case ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral:
        case ts_morph_1.SyntaxKind.NumericLiteral:
        case ts_morph_1.SyntaxKind.TrueKeyword:
        case ts_morph_1.SyntaxKind.FalseKeyword:
        case ts_morph_1.SyntaxKind.NullKeyword:
            return (true);
        case ts_morph_1.SyntaxKind.PrefixUnaryExpression:
            {
                // Allow negative numbers: -42
                const pu = node.asKindOrThrow(ts_morph_1.SyntaxKind.PrefixUnaryExpression);
                if (pu.getOperatorToken() !== ts_morph_1.SyntaxKind.MinusToken)
                    return (false);
                return (isLiteralAst(pu.getOperand()));
            }
        case ts_morph_1.SyntaxKind.ArrayLiteralExpression:
            {
                const arr = node.asKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
                return (arr.getElements().every(isLiteralAst));
            }
        case ts_morph_1.SyntaxKind.ObjectLiteralExpression:
            {
                // Object literal passes if its SHAPE is valid (all property
                // keys are names / strings / numbers). Values may be non-
                // literal — materialiseLiteral substitutes `undefined` for
                // those, which is what the caller's preamble-local variables
                // would resolve to at build time anyway.
                const obj = node.asKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
                for (const prop of obj.getProperties()) {
                    if (prop.getKind() !== ts_morph_1.SyntaxKind.PropertyAssignment &&
                        prop.getKind() !== ts_morph_1.SyntaxKind.ShorthandPropertyAssignment)
                        return (false);
                    if (prop.getKind() === ts_morph_1.SyntaxKind.PropertyAssignment) {
                        const pa = prop.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAssignment);
                        const nameNode = pa.getNameNode();
                        const nk = nameNode.getKind();
                        if (nk !== ts_morph_1.SyntaxKind.Identifier &&
                            nk !== ts_morph_1.SyntaxKind.StringLiteral &&
                            nk !== ts_morph_1.SyntaxKind.NumericLiteral)
                            return (false);
                    }
                    // ShorthandPropertyAssignment always has an Identifier key.
                }
                return (true);
            }
        default:
            return (false);
    }
}
/**
 * Materialise a literal-AST node as a plain JS value. Undefined
 * behaviour if `isLiteralAst(node)` is false — callers must gate.
 */
function materialiseLiteral(node) {
    const kind = node.getKind();
    switch (kind) {
        case ts_morph_1.SyntaxKind.StringLiteral:
            return (node.asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral).getLiteralValue());
        case ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral:
            return (node.asKindOrThrow(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral).getLiteralValue());
        case ts_morph_1.SyntaxKind.NumericLiteral:
            return (Number(node.getText()));
        case ts_morph_1.SyntaxKind.TrueKeyword:
            return (true);
        case ts_morph_1.SyntaxKind.FalseKeyword:
            return (false);
        case ts_morph_1.SyntaxKind.NullKeyword:
            return (null);
        case ts_morph_1.SyntaxKind.PrefixUnaryExpression:
            {
                const pu = node.asKindOrThrow(ts_morph_1.SyntaxKind.PrefixUnaryExpression);
                return (-materialiseLiteral(pu.getOperand()));
            }
        case ts_morph_1.SyntaxKind.ArrayLiteralExpression:
            {
                const arr = node.asKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
                return (arr.getElements().map(materialiseLiteral));
            }
        case ts_morph_1.SyntaxKind.ObjectLiteralExpression:
            {
                const obj = node.asKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
                const result = {};
                for (const prop of obj.getProperties()) {
                    if (prop.getKind() === ts_morph_1.SyntaxKind.ShorthandPropertyAssignment) {
                        // `{ foo }` — identifier, not a literal; substitute undefined.
                        const sp = prop.asKindOrThrow(ts_morph_1.SyntaxKind.ShorthandPropertyAssignment);
                        result[sp.getName()] = undefined;
                        continue;
                    }
                    const pa = prop.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAssignment);
                    const nameNode = pa.getNameNode();
                    const key = nameNode.getKind() === ts_morph_1.SyntaxKind.StringLiteral
                        ? nameNode.asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral).getLiteralValue()
                        : nameNode.getText();
                    const value = pa.getInitializerOrThrow();
                    result[key] = isLiteralAst(value) ? materialiseLiteral(value) : undefined;
                }
                return (result);
            }
        default:
            throw new Error(`[hs090] materialiseLiteral: unsupported kind ${kind}`);
    }
}
//# sourceMappingURL=build-expand.js.map