"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLACEHOLDER_KEYS = exports.FORMATTED_PLACEHOLDER = exports.DETAIL_PLACEHOLDER = exports.COMPONENTS_PLACEHOLDER = exports.GLOBAL_SEARCH_PLACEHOLDER = exports.EXAMPLES_PLACEHOLDER = exports.OBJECT_SEARCH_PLACEHOLDER = exports.FEATURES_PLACEHOLDER = exports.A4D_SKILL_AGENTFORCE_PLACEHOLDER = exports.A4DRULES_PLACEHOLDER = exports.APP_SUFFIX_PLACEHOLDER = exports.SITE_PLACEHOLDER = exports.DIGITAL_EXPERIENCES_PLACEHOLDER = exports.DIGITAL_EXPERIENCE_CONFIGS_PLACEHOLDER = exports.APP_PLACEHOLDER = exports.UI_BUNDLES_PLACEHOLDER = exports.MAIN_DEFAULT_PLACEHOLDER = exports.PACKAGE_DIR_PLACEHOLDER = exports.WINDOWS_MAX_ALLOWABLE_PATH_LENGTH = exports.FULL_TEMPLATE_SKIP_DIRS = exports.FULL_TEMPLATE_DEFAULT_NAMES = exports.BUILT_IN_FULL_TEMPLATES = exports.EJS_EXTENSIONS = void 0;
exports.copyTreeSanitized = copyTreeSanitized;
exports.toAlphanumericForPath = toAlphanumericForPath;
exports.ensureLowercaseUrlName = ensureLowercaseUrlName;
exports.isLikelyText = isLikelyText;
exports.renderEjsFile = renderEjsFile;
exports.generateBuiltInFullTemplate = generateBuiltInFullTemplate;
exports.generateFromProjectTemplateDir = generateFromProjectTemplateDir;
const tslib_1 = require("tslib");
/*
 * Copyright (c) 2019, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
 */
const fs = require("fs");
const promises_1 = require("node:fs/promises");
const path = require("path");
const template_placeholders_1 = require("./template-placeholders");
/**
 * Trim trailing whitespace from a filename segment and warn when it differs.
 * Upstream packages occasionally ship files whose names end with a space
 * (e.g. "lds-guide-graphql.md "), which produces invalid OPC Part URIs
 * when vsce builds the .vsix.
 */
function sanitizeSegment(name) {
    const trimmed = name.trimEnd();
    if (trimmed !== name) {
        console.warn(`[uiBundleTemplateUtils] Sanitised filename: "${name}" → "${trimmed}"`);
    }
    return trimmed;
}
/**
 * Recursively copy a directory tree from src to dest, trimming trailing
 * whitespace from every path segment so that upstream filenames with stray
 * spaces never leak into the output.
 *
 * This is a drop-in replacement for `fs.cpSync(src, dest, { recursive: true })`
 * that is resilient to the "trailing-space filename" issue seen in
 * `@salesforce/templates`.
 */
function copyTreeSanitized(src, dest) {
    const stat = fs.statSync(src);
    if (stat.isFile()) {
        fs.mkdirSync(path.dirname(dest), { recursive: true });
        fs.copyFileSync(src, dest);
        return;
    }
    if (!stat.isDirectory()) {
        return;
    }
    fs.mkdirSync(dest, { recursive: true });
    for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
        const sanitized = sanitizeSegment(entry.name);
        const srcChild = path.join(src, entry.name);
        const destChild = path.join(dest, sanitized);
        if (entry.isDirectory()) {
            copyTreeSanitized(srcChild, destChild);
        }
        else if (entry.isFile()) {
            fs.copyFileSync(srcChild, destChild);
        }
    }
}
/** File extensions that should be processed as EJS templates */
exports.EJS_EXTENSIONS = new Set([
    '.json',
    '.js',
    '.ts',
    '.jsx',
    '.tsx',
    '.md',
    '.txt',
    '.xml',
    '.html',
    '.yml',
    '.yaml',
    '.env',
    '.cfg',
    '.config.js',
    '.config.ts',
]);
/** Templates that have a full folder under src/templates/project/ (populated at build time from npm) */
exports.BUILT_IN_FULL_TEMPLATES = new Set([
    'reactinternalapp',
    'reactexternalapp',
]);
/**
 * Default app/site names embedded in each full template; all are renamed to the project name.
 * Order matters: replace longer (suffix) first to avoid partial replacements.
 */
exports.FULL_TEMPLATE_DEFAULT_NAMES = {
    reactinternalapp: {
        base: 'appreacttemplateb2e',
        withSuffix: 'appreacttemplateb2e1',
    },
    reactexternalapp: {
        base: 'appreacttemplateb2x',
        withSuffix: 'appreacttemplateb2x1',
    },
};
/** Directories to skip when walking a full template dir (e.g. node_modules) */
exports.FULL_TEMPLATE_SKIP_DIRS = new Set(['node_modules', '.git']);
/**
 * Max path length for package paths allowed by `sf pack:verify` on Windows.
 * Matches the "supported allowable path length" from ensureWindowsPathLengths in
 * salesforcecli/cli (plugin-release-management): 259 - supportedBaseWindowsPath.length
 * with --windows-username-buffer 34 (e.g. supported username length 34 → base path 102 → 157).
 * Paths with length >= this value fail pack:verify.
 */
exports.WINDOWS_MAX_ALLOWABLE_PATH_LENGTH = 157;
/**
 * Path segment placeholders used in template dirs; replaced only during project generation.
 * Short names keep lib/templates paths short.
 */
exports.PACKAGE_DIR_PLACEHOLDER = '_p_';
/** Replaced with defaultpackagedir (e.g. force-app). */
exports.MAIN_DEFAULT_PLACEHOLDER = '_m_';
/** Replaced with literal "main/default". */
exports.UI_BUNDLES_PLACEHOLDER = '_w_';
/** Replaced with the app folder name. */
exports.APP_PLACEHOLDER = '_a_';
/** Replaced with project name (alphanumeric) for the UI bundle folder. */
exports.DIGITAL_EXPERIENCE_CONFIGS_PLACEHOLDER = '_dc_';
/** Replaced with literal "digitalExperienceConfigs". */
exports.DIGITAL_EXPERIENCES_PLACEHOLDER = '_d_';
/** Replaced with literal "digitalExperiences". */
exports.SITE_PLACEHOLDER = '_s_';
/** Replaced with literal "site". */
exports.APP_SUFFIX_PLACEHOLDER = '_a1_';
/** Replaced with project name + "1" (e.g. digital experience site folder). */
exports.A4DRULES_PLACEHOLDER = '_r_';
/** Replaced with literal ".a4drules". */
exports.A4D_SKILL_AGENTFORCE_PLACEHOLDER = '_k_';
/** Replaced with literal "feature-react-agentforce-conversation-client-embedded-agent". */
/** Replaced with literal "features" (under app src; short for Windows path length). */
exports.FEATURES_PLACEHOLDER = '_f_';
/** Replaced with literal "object-search". */
exports.OBJECT_SEARCH_PLACEHOLDER = '_os_';
/** Replaced with literal "__examples__". */
exports.EXAMPLES_PLACEHOLDER = '_ex_';
/** Replaced with literal "global-search". */
exports.GLOBAL_SEARCH_PLACEHOLDER = '_gs_';
/** Replaced with literal "components". */
exports.COMPONENTS_PLACEHOLDER = '_c_';
/** Replaced with literal "detail". */
exports.DETAIL_PLACEHOLDER = '_det_';
/** Replaced with literal "formatted". */
exports.FORMATTED_PLACEHOLDER = '_fmt_';
/** All placeholder keys; used by tests to assert sync with copy-templates.js */
exports.PLACEHOLDER_KEYS = [
    'PACKAGE_DIR_PLACEHOLDER',
    'MAIN_DEFAULT_PLACEHOLDER',
    'UI_BUNDLES_PLACEHOLDER',
    'APP_PLACEHOLDER',
    'DIGITAL_EXPERIENCE_CONFIGS_PLACEHOLDER',
    'DIGITAL_EXPERIENCES_PLACEHOLDER',
    'SITE_PLACEHOLDER',
    'APP_SUFFIX_PLACEHOLDER',
    'A4DRULES_PLACEHOLDER',
    'A4D_SKILL_AGENTFORCE_PLACEHOLDER',
    'FEATURES_PLACEHOLDER',
    'OBJECT_SEARCH_PLACEHOLDER',
    'EXAMPLES_PLACEHOLDER',
    'GLOBAL_SEARCH_PLACEHOLDER',
    'COMPONENTS_PLACEHOLDER',
    'DETAIL_PLACEHOLDER',
    'FORMATTED_PLACEHOLDER',
];
const TEMPLATE_PLACEHOLDERS_SPEC = template_placeholders_1.default;
function resolveReplacement(replacement, ctx) {
    switch (replacement) {
        case 'defaultpackagedir':
            return ctx.defaultpackagedir;
        case 'projectname':
            return ctx.projectnameAlphanumeric;
        case 'projectname1':
            return ctx.projectnameAlphanumeric + '1';
        default:
            return replacement;
    }
}
/**
 * Returns a string containing only alphanumeric characters [A-Za-z0-9].
 * Used for folder and file names under uiBundles, which must be alphanumeric.
 */
function toAlphanumericForPath(name) {
    return name.replace(/[^A-Za-z0-9]/g, '');
}
/**
 * For sfdc_cms__site content.json files, the urlName must be lowercase.
 * Name replacements preserve casing from the project name, so we need to
 * post-process the JSON to enforce this constraint.
 */
function ensureLowercaseUrlName(content, destPath) {
    if (path.basename(destPath) !== 'content.json' ||
        !destPath.includes(`sfdc_cms__site${path.sep}`)) {
        return content;
    }
    try {
        const parsed = JSON.parse(content);
        if (typeof parsed.urlName === 'string') {
            const lower = parsed.urlName.toLowerCase();
            if (lower !== parsed.urlName) {
                parsed.urlName = lower;
                return JSON.stringify(parsed, null, 2) + '\n';
            }
        }
    }
    catch (_a) {
        // not valid JSON, return as-is
    }
    return content;
}
/** Heuristic: treat as text if no null byte in the first chunk and decodable as UTF-8 */
function isLikelyText(filename, buffer) {
    const ext = path.extname(filename).toLowerCase();
    const binaryExts = new Set([
        '.png',
        '.jpg',
        '.jpeg',
        '.gif',
        '.ico',
        '.svg',
        '.woff',
        '.woff2',
        '.ttf',
        '.eot',
        '.pdf',
        '.zip',
    ]);
    if (binaryExts.has(ext)) {
        return false;
    }
    const chunk = buffer.slice(0, 1024);
    if (chunk.includes(0)) {
        return false;
    }
    try {
        chunk.toString('utf8');
        return true;
    }
    catch (_a) {
        return false;
    }
}
/** Renders an EJS template file with the given data (stateless, no generator dependency). */
function renderEjsFile(sourcePath, data) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const { renderFile } = yield Promise.resolve().then(() => require('ejs'));
        return new Promise((resolve, reject) => {
            renderFile(sourcePath, data, (err, str) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(str !== null && str !== void 0 ? str : '');
                }
            });
        });
    });
}
/**
 * Generate project files from a built-in full template (e.g. reactinternalapp, reactexternalapp).
 * Builds template vars and name replacements and delegates to generateFromProjectTemplateDir.
 */
function generateBuiltInFullTemplate(template, projectname, options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const { templateDir, projectDir, defaultpackagedir, ns, loginurl, apiversion, renderEjs, onFileCreated, } = options;
        const templateVars = {
            projectname,
            defaultpackagedir,
            namespace: ns,
            ns,
            loginurl,
            apiversion,
            name: projectname,
            company: (process.env.USER || 'Demo') + ' company',
        };
        const nameReplacementsEntry = exports.FULL_TEMPLATE_DEFAULT_NAMES[template];
        const projectnameAlphanumeric = toAlphanumericForPath(projectname);
        const replacementCtx = {
            defaultpackagedir,
            projectnameAlphanumeric,
        };
        const nameReplacements = TEMPLATE_PLACEHOLDERS_SPEC.map((entry) => [
            entry.placeholder,
            resolveReplacement(entry.replacement, replacementCtx),
        ]);
        nameReplacements.push([exports.APP_PLACEHOLDER, projectnameAlphanumeric], [exports.APP_SUFFIX_PLACEHOLDER, projectnameAlphanumeric + '1']);
        if (nameReplacementsEntry) {
            nameReplacements.push([nameReplacementsEntry.withSuffix, projectnameAlphanumeric + '1'], [nameReplacementsEntry.base, projectnameAlphanumeric]);
        }
        yield generateFromProjectTemplateDir(templateDir, projectDir, templateVars, {
            nameReplacements,
            renderEjs,
            onFileCreated,
        });
    });
}
/**
 * Recursively walk a full project template directory (e.g. reactinternalapp, reactexternalapp),
 * rendering EJS for text files and copying the rest. Renames template default app/site
 * names (e.g. reactinternalapp) to the project name in paths and file contents.
 */
function generateFromProjectTemplateDir(sourceDir, destDir, templateVars, options) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const { nameReplacements, renderEjs, onFileCreated } = options;
        if (!fs.existsSync(sourceDir)) {
            return;
        }
        const applyReplacements = (s) => {
            if (!nameReplacements) {
                return s;
            }
            let out = s;
            for (const [from, to] of nameReplacements) {
                out = out.split(from).join(to);
            }
            return out;
        };
        const entries = yield (0, promises_1.readdir)(sourceDir, { withFileTypes: true });
        for (const entry of entries) {
            const rawName = entry.name;
            const safeName = sanitizeSegment(rawName);
            const sourcePath = path.join(sourceDir, rawName);
            if (entry.isDirectory()) {
                if (exports.FULL_TEMPLATE_SKIP_DIRS.has(safeName)) {
                    continue;
                }
                const destName = applyReplacements(safeName);
                const destPath = path.join(destDir, destName);
                yield (0, promises_1.mkdir)(destPath, { recursive: true });
                yield generateFromProjectTemplateDir(sourcePath, destPath, templateVars, options);
                continue;
            }
            if (!entry.isFile()) {
                continue;
            }
            const destName = applyReplacements(safeName);
            const destPath = path.join(destDir, destName);
            const ext = path.extname(safeName);
            const isEjsTemplate = exports.EJS_EXTENSIONS.has(ext) ||
                safeName.endsWith('.config.js') ||
                safeName.endsWith('.config.ts');
            if (isEjsTemplate) {
                try {
                    const rendered = yield renderEjs(sourcePath, templateVars);
                    const content = ensureLowercaseUrlName(applyReplacements(rendered), destPath);
                    yield (0, promises_1.mkdir)(path.dirname(destPath), { recursive: true });
                    yield (0, promises_1.writeFile)(destPath, content, 'utf8');
                    onFileCreated(destPath);
                }
                catch (_a) {
                    const raw = yield (0, promises_1.readFile)(sourcePath);
                    const str = raw.toString('utf8');
                    const content = ensureLowercaseUrlName(applyReplacements(str), destPath);
                    yield (0, promises_1.mkdir)(path.dirname(destPath), { recursive: true });
                    yield (0, promises_1.writeFile)(destPath, content, 'utf8');
                    onFileCreated(destPath);
                }
            }
            else {
                yield (0, promises_1.mkdir)(path.dirname(destPath), { recursive: true });
                const content = yield (0, promises_1.readFile)(sourcePath);
                const isText = isLikelyText(entry.name, content);
                if (isText && (nameReplacements === null || nameReplacements === void 0 ? void 0 : nameReplacements.length)) {
                    const str = content.toString('utf8');
                    const replaced = ensureLowercaseUrlName(applyReplacements(str), destPath);
                    yield (0, promises_1.writeFile)(destPath, replaced, 'utf8');
                }
                else {
                    yield (0, promises_1.writeFile)(destPath, new Uint8Array(content));
                }
                onFileCreated(destPath);
            }
        }
    });
}
//# sourceMappingURL=uiBundleTemplateUtils.js.map