"use strict";
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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Project = void 0;
const util_1 = require("./util");
const context_1 = require("./context");
const sdk_build_core_1 = require("@servicenow/sdk-build-core");
const CorePlugins = __importStar(require("@servicenow/sdk-build-plugins"));
const project_file_1 = require("./project-file");
const util_2 = require("./util");
const md5_1 = __importDefault(require("crypto-js/md5"));
const fast_xml_parser_1 = require("fast-xml-parser");
const plugin_registry_1 = require("./plugin-registry");
const picomatch_1 = __importDefault(require("picomatch"));
function getScriptKindFromPath(path) {
    switch (sdk_build_core_1.path.extname(path)) {
        case '.ts':
            return sdk_build_core_1.ts.ScriptKind.TS;
        case '.js':
            return sdk_build_core_1.ts.ScriptKind.JS;
        case '.json':
            return sdk_build_core_1.ts.ScriptKind.JSON;
        default:
            return sdk_build_core_1.ts.ScriptKind.Unknown;
    }
}
function isSysAppXml(filePath, scopeId) {
    const fileName = sdk_build_core_1.path.basename(filePath);
    return fileName === `sys_app_${scopeId}.xml` || fileName === `sys_store_app_${scopeId}.xml`;
}
class Project {
    rootDir;
    files = {};
    filesForDeletion = new Set();
    plugins;
    fileSystem;
    logger;
    compiler;
    config;
    package;
    keys;
    diagnostics;
    transformService;
    factory;
    parser;
    inspector;
    committer;
    targeter;
    context;
    telemetryClient;
    transformMode;
    staticContentInputDirs = new Set();
    constructor({ fileSystem = require('fs'), logger = (0, sdk_build_core_1.createConsoleLogger)(), rootDir = process && 'cwd' in process ? sdk_build_core_1.path.normalize(process.cwd()) : '/', sourceFilePaths = [], compilerOptions = sdk_build_core_1.DEFAULT_COMPILER_OPTIONS, compiler = new sdk_build_core_1.Compiler(fileSystem, {
        rootDir,
        compilerOptions: { ...sdk_build_core_1.DEFAULT_COMPILER_OPTIONS, ...compilerOptions },
        sourceFilePaths,
        typesDir: sdk_build_core_1.path.join('@types', 'servicenow'),
    }), plugins = plugin_registry_1.PLUGINS, config, packageJson = (0, util_1.createPackageJson)({ rootDir, compiler, fileSystem }), keys, diagnostics = new context_1.Diagnostics(), transform, factory, parser, inspector, committer, targeter, telemetryClient, transformMode, } = {}) {
        if (!sdk_build_core_1.path.isAbsolute(rootDir)) {
            throw new Error(`Supplied root directory '${rootDir}' is not an absolute path`);
        }
        this.rootDir = rootDir;
        this.plugins = plugins instanceof sdk_build_core_1.Plugins ? plugins : new sdk_build_core_1.Plugins(plugins);
        this.fileSystem = fileSystem;
        this.logger = logger;
        this.compiler = compiler;
        this.config = config ? sdk_build_core_1.NowConfig.create(config) : sdk_build_core_1.NowConfig.load({ rootDir, compiler, fileSystem, logger });
        this.package = packageJson;
        this.keys = keys ?? new context_1.Keys(this);
        this.diagnostics = diagnostics;
        this.transformService = transform ?? new context_1.MultiProjectTransform(this);
        this.factory = factory ?? new context_1.Factory(this);
        this.inspector = inspector ?? new context_1.Inspector(this);
        this.committer = committer ?? new context_1.Committer(this);
        this.targeter = targeter ?? new context_1.Targeter(this);
        this.parser = parser ?? new context_1.Parser(this);
        this.context = {
            project: this,
            fs: this.fileSystem,
            logger: this.logger,
            compiler: this.compiler,
            diagnostics: this.diagnostics,
            config: this.config,
            packageJson: this.package,
            plugins: this.plugins,
            keys: this.keys,
            transform: this.transformService,
            factory: this.factory,
            parser: this.parser,
            inspect: this.inspector.inspect.bind(this.inspector),
            commit: this.committer.commit.bind(this.committer),
        };
        this.telemetryClient = telemetryClient ?? sdk_build_core_1.TelemetryFactory.create();
        this.transformMode = transformMode ?? 'transform';
        const fluentDir = sdk_build_core_1.path.resolve(rootDir, this.config.fluentDir);
        const configFilePath = sdk_build_core_1.NowConfig.getFilePath(rootDir);
        const packageJsonPath = (0, util_1.getPackageJsonFilePath)(rootDir);
        const metadataDir = sdk_build_core_1.path.resolve(rootDir, this.config.metadataDir);
        if (sdk_build_core_1.FileSystem.existsSync(this.fileSystem, metadataDir)) {
            this.addDirectory(metadataDir);
        }
        for (const pattern of Object.keys(this.config.staticContent.assets)) {
            const { base } = picomatch_1.default.scan(pattern);
            this.registerStaticContentDir(sdk_build_core_1.path.resolve(rootDir, base));
        }
        const aiuxJson = sdk_build_core_1.path.resolve(rootDir, 'aiux.json');
        if (sdk_build_core_1.FileSystem.existsSync(this.fileSystem, aiuxJson)) {
            this.addFile(aiuxJson);
        }
        (0, plugin_registry_1.injectDevelopmentPlugins)(this.plugins, this.resolvePath('package.json'), logger);
        this.registerFiles([
            this.keys.getSourceFile(),
            this.compiler.addSourceFileAtPathIfExists(configFilePath) ??
                this.compiler.createSourceFile(configFilePath, JSON.stringify({ scope: this.config.scope, scopeId: this.config.scopeId }, null, 4), {
                    overwrite: true,
                    scriptKind: sdk_build_core_1.ts.ScriptKind.JSON,
                }),
            this.compiler.addSourceFileAtPathIfExists(packageJsonPath) ??
                this.compiler.createSourceFile(packageJsonPath, JSON.stringify(packageJson, null, 4), {
                    overwrite: true,
                    scriptKind: sdk_build_core_1.ts.ScriptKind.JSON,
                }),
            ...this.compiler.addFluentSourceFilesFromDirectory(fluentDir),
            ...this.compiler.addSourceFilesFromDirectory(this.resolvePath(this.config.serverModulesDir), this.config.serverModulesIncludePatterns, this.config.serverModulesExcludePatterns),
        ]);
        this.registerClaimsFile();
    }
    registerClaimsFile() {
        const claimsFile = this.compiler.addSourceFileAtPathIfExists(this.resolvePath(this.config.generatedDir, CorePlugins.CLAIMS_FILE));
        if (claimsFile) {
            (0, sdk_build_core_1.validateClaimsFile)(claimsFile.getFullText());
            this.registerFiles([claimsFile]);
        }
    }
    registerFiles(files) {
        files.forEach((file) => {
            const path = file instanceof sdk_build_core_1.ts.SourceFile ? file.getFilePath() : file.path;
            const existing = this.files[path];
            if (existing) {
                existing.reload(false);
            }
            else {
                this.files[path] = new project_file_1.ProjectFile(file, this);
            }
        });
    }
    getContext() {
        return this.context;
    }
    getTelemetryClient() {
        return this.telemetryClient;
    }
    getPlugins() {
        return this.plugins;
    }
    getFileSystem() {
        return this.fileSystem;
    }
    getLogger() {
        return this.logger;
    }
    getDiagnosticsService() {
        return this.diagnostics;
    }
    getCompiler() {
        return this.compiler;
    }
    getKeys() {
        return this.keys;
    }
    getConfig() {
        return this.config;
    }
    getPackage() {
        return this.package;
    }
    getTransform() {
        return this.transformService;
    }
    getFactory() {
        return this.factory;
    }
    getFiles() {
        return Object.values(this.files);
    }
    async getEntryPoints() {
        const context = this.getContext();
        const entryPoints = [];
        for (const file of this.getFiles()) {
            if ((file.isInMetadataDir() && !file.isInUiHtmlContentDir()) ||
                (await this.plugins.isEntryPoint(file.getPath(), context))) {
                entryPoints.push(file);
            }
        }
        return entryPoints;
    }
    getFileIfExists(path) {
        const absolutePath = this.resolvePath(path);
        return this.files[absolutePath];
    }
    getFile(path) {
        const file = this.getFileIfExists(path);
        if (file) {
            return file;
        }
        throw new Error(`No file found at path: ${this.resolvePath(path)}`);
    }
    findFile(nameOrPredicate) {
        if (typeof nameOrPredicate === 'string') {
            return Object.entries(this.files).find(([p]) => sdk_build_core_1.path.basename(p) === nameOrPredicate)?.[1];
        }
        else {
            return Object.values(this.files).find(nameOrPredicate);
        }
    }
    reloadFile(path, refreshFromFileSystem = true) {
        const absolutePath = this.resolvePath(path);
        let reloadedFile;
        if (absolutePath in this.files) {
            reloadedFile = this.getFile(path);
            reloadedFile.reload(refreshFromFileSystem);
        }
        else {
            reloadedFile = this.addFile(absolutePath);
        }
        return reloadedFile;
    }
    reloadDirectory(path, refreshFromFileSystem = true) {
        const directoryPath = this.resolvePath(path);
        this.checkDirectoryExists(directoryPath);
        const reloadedFiles = [];
        const removedFilePaths = new Set();
        for (const filePath of Object.keys(this.files)) {
            if (this.resolvePath(filePath).startsWith(directoryPath)) {
                removedFilePaths.add(filePath);
            }
        }
        (0, util_1.traverseDirectory)(this.fileSystem, directoryPath, {
            visitor: ((filePath) => {
                removedFilePaths.delete(filePath);
                reloadedFiles.push(this.reloadFile(filePath, refreshFromFileSystem));
            }).bind(this),
        });
        // Remove files that are no longer present in the directory
        for (const filePath of removedFilePaths) {
            this.removeFile(filePath);
        }
        return reloadedFiles;
    }
    addFile(fileOrPath, options) {
        this.addFiles(typeof fileOrPath === 'string' ? [fileOrPath] : [fileOrPath], options);
        return this.getFile(typeof fileOrPath === 'string' ? fileOrPath : fileOrPath.path);
    }
    addFiles(files, { excludeFromCompiler = false } = {}) {
        const unregisteredFiles = files.reduce((unregistered, file) => {
            const { path, content } = typeof file === 'string' ? { path: file, content: undefined } : file;
            const existingFile = this.getFileIfExists(path);
            if (existingFile) {
                if (content !== undefined) {
                    existingFile.setContent(content);
                }
            }
            else {
                unregistered.push(file);
            }
            return unregistered;
        }, []);
        const filesToRegister = unregisteredFiles.map((unregisteredFile) => {
            const { path, content } = typeof unregisteredFile === 'string' ? { path: unregisteredFile, content: undefined } : unregisteredFile;
            const absolutePath = this.resolvePath(path);
            const kind = getScriptKindFromPath(path);
            let file;
            if (kind === sdk_build_core_1.ts.ScriptKind.Unknown || this.isInStaticContentAssetsDir(path) || excludeFromCompiler) {
                if (content !== undefined) {
                    file = { path: absolutePath, content };
                }
                else if (sdk_build_core_1.FileSystem.existsSync(this.fileSystem, absolutePath)) {
                    if (this.fileSystem.statSync(absolutePath).isDirectory()) {
                        throw new Error(`Cannot add directory as file: ${absolutePath}`);
                    }
                    file = {
                        path: absolutePath,
                        content: this.fileSystem.readFileSync(absolutePath).toString('utf-8'),
                        originalPath: absolutePath,
                    };
                }
                else {
                    throw new Error(`No file found at path: ${absolutePath}`);
                }
            }
            else {
                file =
                    content === undefined
                        ? this.compiler.addSourceFileAtPath(absolutePath)
                        : this.compiler.createSourceFile(absolutePath, content, {
                            overwrite: true,
                            scriptKind: getScriptKindFromPath(path),
                        });
            }
            if (absolutePath === (0, util_1.getPackageJsonFilePath)(this.rootDir)) {
                overwriteObject(this.package, (0, util_1.createPackageJson)({
                    rootDir: this.rootDir,
                    compiler: this.compiler,
                    fileSystem: this.fileSystem,
                }));
            }
            else if (absolutePath === sdk_build_core_1.NowConfig.getFilePath(this.rootDir)) {
                overwriteObject(this.config, sdk_build_core_1.NowConfig.load({
                    rootDir: this.rootDir,
                    compiler: this.compiler,
                    fileSystem: this.fileSystem,
                    logger: this.logger,
                }));
            }
            return file;
        });
        this.registerFiles(filesToRegister);
        return files.map((file) => this.getFile(typeof file === 'string' ? file : file.path));
    }
    checkDirectoryExists(path) {
        if (!sdk_build_core_1.FileSystem.existsSync(this.fileSystem, path)) {
            throw Error(`No directory found at path: ${path}`);
        }
        if (!this.fileSystem.statSync(path).isDirectory()) {
            throw Error(`Expected directory but got file path: ${path}`);
        }
    }
    addDirectory(path) {
        const directoryPath = this.resolvePath(path);
        this.checkDirectoryExists(directoryPath);
        const files = [];
        (0, util_1.traverseDirectory)(this.fileSystem, directoryPath, {
            visitor: ((filePath) => {
                files.push(filePath);
            }).bind(this),
        });
        return this.addFiles(files);
    }
    removeFile(fileOrPath) {
        const file = fileOrPath instanceof project_file_1.ProjectFile ? fileOrPath : this.getFileIfExists(fileOrPath);
        if (file) {
            delete this.files[file.getPath()];
            file.remove();
        }
    }
    async transform(input, { format = true, tables } = {}) {
        const transformStartTime = Date.now();
        // Normalize tables to Set for efficient lookup
        const tablesSet = tables ? (tables instanceof Set ? tables : new Set(tables)) : undefined;
        await this.getRecords(); // Ensure all side effects are finished before transforming
        const inputs = Array.isArray(input) ? input : [input];
        const files = [];
        const shapes = [];
        for (const input of inputs) {
            if (typeof input === 'string') {
                const absolutePath = this.resolvePath(input);
                const subPaths = [];
                if (this.fileSystem.statSync(absolutePath).isDirectory()) {
                    (0, util_1.traverseDirectory)(this.fileSystem, absolutePath, {
                        extensions: ['.xml'],
                        visitor: (path) => subPaths.push(path),
                    });
                }
                else {
                    subPaths.push(absolutePath);
                }
                files.push(...subPaths.map((path) => ({
                    path,
                    content: this.fileSystem.readFileSync(path).toString('utf-8'),
                })));
            }
            else if (input instanceof sdk_build_core_1.Shape) {
                shapes.push(input);
            }
            else {
                files.push({
                    path: this.resolvePath(input.path),
                    content: input.content,
                });
            }
        }
        const existingRecords = new sdk_build_core_1.Database();
        const metadataDirRecords = new sdk_build_core_1.Database();
        const existingRecordsParseStartTime = Date.now();
        for (const file of await this.getEntryPoints()) {
            this.logger.debug(`Parsing records from existing file: ${file.getPath()}`);
            const db = file.isInMetadataDir() ? metadataDirRecords : existingRecords;
            db.insert(...(await file.getRecords(false)));
        }
        this.logger.debug(`Finished parsing existing records in ${Date.now() - existingRecordsParseStartTime}ms`);
        const incoming = new Project({
            compiler: this.compiler,
            fileSystem: this.fileSystem,
            logger: this.logger,
            rootDir: this.rootDir,
            config: this.config,
            packageJson: this.package,
            plugins: this.plugins,
            keys: this.keys,
        });
        const incomingRecords = new sdk_build_core_1.Database();
        const incomingRecordsParseStartTime = Date.now();
        const handledPaths = new Set();
        // Sort conditional directories last
        files.sort((a, b) => Number(this.isInConditionalDir(a.path)) - Number(this.isInConditionalDir(b.path)));
        for (const file of files.map((f) => incoming.addFile(f))) {
            const path = file.getPath();
            if ((0, sdk_build_core_1.matchesGlob)(file.getName(), this.config.excludeFilePatterns)) {
                this.logger.info(`Skipping file ${file.getName()} due to excludeFilePatterns`);
                continue;
            }
            this.logger.debug(`Parsing records from incoming file: ${path}`);
            const isInMetadataDir = this.isInMetadataDir(path);
            let anyRecordInMeta;
            let forceMetadataTransformConditional;
            let forceMetadataTransformAttachment;
            let ignoredRecord;
            let filteredRecord;
            const records = await file.getRecords(false);
            const recordsArray = records.query();
            if (file.isUpdateSetXml()) {
                const metadataCopyRecords = [];
                for (const record of recordsArray) {
                    const metadataDirRecord = metadataDirRecords.resolve(record.getId());
                    if (metadataDirRecord) {
                        metadataCopyRecords.push(record);
                    }
                    else if (this.config.ignoreTransformTableList.includes(record.getTable())) {
                        metadataCopyRecords.push(record);
                    }
                    else if (tablesSet && !tablesSet.has(record.getTable())) {
                        // Record's table not in target tables - leave XML untouched
                        // (don't copy to metadata, don't transform)
                    }
                    else if (!(0, sdk_build_core_1.isGUID)(record.getId().getValue())) {
                        metadataCopyRecords.push(record);
                    }
                    else {
                        let attachmentShapes = {};
                        // Pre-test attachments to make sure they are valid. If not, skip them
                        try {
                            attachmentShapes = await this.getAttachmentsForFields(records, record);
                        }
                        catch (e) {
                            this.logger.warn(`Skipping unreadable attachment ${record.getTable()} record ${record.getId().getValue()}: ${e instanceof Error ? e.message : String(e)}`);
                            metadataCopyRecords.push(record);
                            continue;
                        }
                        // Clear creator to allow any plugin to handle the new record
                        incomingRecords.insert(record.setCreator(undefined, true).merge(attachmentShapes));
                        handledPaths.add(path);
                    }
                }
                const metadataOutput = await (0, util_2.getOutput)(new sdk_build_core_1.Database(metadataCopyRecords), this.context);
                metadataOutput.forEach((metadata) => {
                    this.handleMetadataCopy(new project_file_1.ProjectFile(metadata, this));
                });
            }
            else {
                const sourceConditionalDir = this.isInConditionalDir(path);
                const attachmentShapesByRecordId = new Map();
                for (const record of recordsArray) {
                    const metadataDirRecord = metadataDirRecords.resolve(record.getId());
                    if (metadataDirRecord && !isInMetadataDir) {
                        const destConditionalDir = this.isInConditionalDir(metadataDirRecord.getOriginalFilePath());
                        // A record can exist in both Fluent code and a conditional directory as xml,
                        // or as conditional directory Fluent code and xml in a normal metadata directory
                        // Compare apples to apples here
                        if ((sourceConditionalDir && destConditionalDir) ||
                            (!sourceConditionalDir && !destConditionalDir)) {
                            anyRecordInMeta ??= metadataDirRecord;
                            break;
                        }
                    }
                    if (this.config.ignoreTransformTableList.includes(record.getTable())) {
                        ignoredRecord = { record, reason: 'ignored' };
                    }
                    if (!(0, sdk_build_core_1.isGUID)(record.getId().getValue())) {
                        // Records that don't have a sys_id (Ex. bootstrap table file) will be provided a legit one by the record factory
                        ignoredRecord = { record, reason: 'sys_id' };
                    }
                    if (tablesSet && !tablesSet.has(record.getTable())) {
                        // Any record outside the target tables filter causes the whole file to be
                        // preserved as XML, even if other records in the file do match. Non-update-set
                        // files are handled at file granularity, not per-record, so a mixed-table file
                        // is never partially transformed — that would require splitting one physical
                        // XML file into two, which we don't do (see the update-set path above, which
                        // filters per-record instead). Break immediately, consistent with the other
                        // early exits in this loop (anyRecordInMeta, forceMetadataTransform*) — no
                        // need to keep pre-fetching attachments for records we'll discard.
                        filteredRecord = record;
                        break;
                    }
                    const alreadySeenRecord = incomingRecords.resolve(record.getId()) ?? existingRecords.resolve(record.getId());
                    if (alreadySeenRecord &&
                        ((sourceConditionalDir && !this.isInConditionalDir(alreadySeenRecord.getOriginalFilePath())) ||
                            (!sourceConditionalDir &&
                                this.isInConditionalDir(alreadySeenRecord.getOriginalFilePath())) ||
                            this.inDifferentConditionalDirs(path, alreadySeenRecord.getOriginalFilePath()))) {
                        // We are transforming an xml from a conditional directory (if/hosted)
                        // An iteration of this record has already been transformed by a prior transform operation or a different xml in this transform operation
                        // The previously fluentized record is not in a conditional directory but our xml is, so treat them as separate and copy the metadata
                        forceMetadataTransformConditional = record;
                        break;
                    }
                    try {
                        // Pre-test attachments to make sure we can extract, caching the result so
                        // the processing loop below doesn't re-fetch and re-decompress them.
                        attachmentShapesByRecordId.set(record.getId().getValue(), await this.getAttachmentsForFields(records, record));
                    }
                    catch (e) {
                        forceMetadataTransformAttachment = record;
                        break;
                    }
                }
                if (!isInMetadataDir && anyRecordInMeta) {
                    this.logger.debug(`Updating incoming ${anyRecordInMeta.getTable()} record with ID ${anyRecordInMeta.getId().getValue()} in metadata directory as XML`);
                    this.handleMetadataCopy(file, anyRecordInMeta.getOriginalFilePath());
                }
                else if (ignoredRecord) {
                    const { record: ignored, reason } = ignoredRecord;
                    this.logger.debug(reason === 'ignored'
                        ? `Incoming ${ignored.getTable()} record with ID ${ignored.getId().getValue()} is in "ignoreTransformTableList" configuration and moved to metadata directory as XML`
                        : `Incoming ${ignored.getTable()} record with ID ${ignored.getId().getValue()} has an invalid sys_id. Moved to metadata directory as XML`);
                    this.handleMetadataCopy(file);
                }
                else if (filteredRecord) {
                    // TODO: key registration is a side effect of getRecords() above; keys for records
                    // outside the table filter are already registered here. Without copying to metadata,
                    // the next full build would see these keys in keys.ts but find no record source,
                    // emitting spurious deletes. Copy to metadata as a workaround until key registration
                    // is decoupled from parsing.
                    const fileTables = [...new Set(recordsArray.map((r) => r.getTable()))];
                    // filteredRecord is only ever set inside `if (tablesSet && ...)` above, so
                    // tablesSet is guaranteed defined here.
                    const hasMatchingTables = fileTables.some((t) => tablesSet.has(t));
                    const tables = fileTables.join(', ');
                    this.logger.warn(hasMatchingTables
                        ? `${file.getName()} contains a mix of target and non-target tables (file tables: [${tables}]). The entire file is preserved as XML in the metadata directory rather than partially transformed; any target-table records in this file are also skipped.`
                        : `${file.getName()} contains no records matching the target tables filter (file tables: [${tables}]). The entire file is preserved as XML in the metadata directory.`);
                    this.handleMetadataCopy(file);
                }
                else if (!isInMetadataDir && recordsArray.length === 0) {
                    this.logger.info(`No records parsed from ${file.getName()}, moving to metadata directory`);
                    this.handleMetadataCopy(file);
                }
                else if (forceMetadataTransformConditional) {
                    this.logger.info(`File ${file.getName()} contains records that conflict between conditional/non-conditional contexts. The file will be preserved as XML in metadataDir.`);
                    this.handleMetadataCopy(file);
                }
                else if (forceMetadataTransformAttachment) {
                    this.logger.info(`File ${file.getName()} contains invalid attachment data. The file will be preserved as XML in metadataDir.`);
                    this.handleMetadataCopy(file);
                }
                else {
                    for (const record of records) {
                        // Clear creator to allow any plugin to handle the new record
                        if (record.getAction() === 'DELETE') {
                            const alreadyEncountered = incomingRecords.get(record.getTable(), record.getId().getValue());
                            if (alreadyEncountered && alreadyEncountered.getAction() === 'INSERT_OR_UPDATE') {
                                this.logger.debug(`Encountered a DELETE for record '${record.getId().getValue()}' that has a corresponding INSERT_OR_UPDATE. Skipping DELETE`);
                                continue;
                            }
                        }
                        // The pre-test loop above already fetched and validated attachments for every
                        // record here (it's the only way this loop is reached without a break), so
                        // this is always a cache hit.
                        const attachmentShapes = attachmentShapesByRecordId.get(record.getId().getValue());
                        incomingRecords.insert(record.setCreator(undefined, true).merge(attachmentShapes));
                        handledPaths.add(path);
                    }
                }
            }
        }
        //compare existing and incoming records
        const mergedDatabase = existingRecords.compare(incomingRecords);
        const changeDatabase = new sdk_build_core_1.DiffDatabase(existingRecords.query());
        const visitedRecords = new sdk_build_core_1.Database();
        for (const mode of ['explicit', 'catch-all']) {
            for (const plugin of this.plugins) {
                const changeRecords = await plugin.computeDiff({
                    mode,
                    existing: existingRecords,
                    incoming: incomingRecords,
                    context: this.getContext(),
                    visitedRecords,
                    mergedDatabase,
                });
                changeDatabase.insert(...changeRecords);
            }
        }
        await this.getKeys().commit();
        this.logger.debug(`Finished parsing incoming records in ${Date.now() - incomingRecordsParseStartTime}ms`);
        // This is a separate loop from the one above to make sure all side effects (like
        // registration of keys) are finished before we calculate the MD5 hashes
        const beforeFileHashes = {};
        for (const file of this.getFiles()) {
            beforeFileHashes[file.getPath()] = (0, md5_1.default)(file.getContent()).toString();
        }
        const context = this.getContext();
        const handledRecords = new sdk_build_core_1.Database();
        const shapeTransformStartTime = Date.now();
        for (const step of [
            // First, let plugins handle records they created and explicitly handle
            { mode: 'explicit', creatorOnly: true },
            // Then, let plugins handle records they created but don't explicitly handle
            { mode: 'catch-all', creatorOnly: true },
            // Then, let plugins handle records they didn't create but explicitly handle
            { mode: 'explicit', creatorOnly: false },
            // Finally, let any plugin handle any record
            { mode: 'catch-all', creatorOnly: false },
        ]) {
            for (const plugin of this.plugins) {
                const result = await plugin.recordsToShapes({
                    ...step,
                    database: changeDatabase,
                    existingDatabase: existingRecords,
                    context,
                    handledRecords,
                    changedOnly: true,
                });
                if (result.success === 'partial' && result.unhandledRecords.length > 0) {
                    for (const r of result.unhandledRecords) {
                        // If unhandled, remove the previous entry so we don't merge and get the wrong source
                        changeDatabase.delete(r.getId());
                    }
                    changeDatabase.insert(...result.unhandledRecords);
                }
                if (result.success === true || result.success === 'partial') {
                    shapes.push(...result.value);
                }
            }
        }
        this.logger.debug(`Finished transforming records into shapes in ${Date.now() - shapeTransformStartTime}ms`);
        const finalShapes = [];
        if (this.isTypesGenerationMode()) {
            // convert callexpress into variable statement
            finalShapes.push(...shapes.map((shape) => {
                if (shape.is(sdk_build_core_1.CallExpressionShape)) {
                    const exportName = shape.getExportName();
                    if (!exportName) {
                        return shape;
                    }
                    return new sdk_build_core_1.VariableStatementShape({
                        source: shape.getSource(),
                        isExported: true,
                        variableName: exportName,
                        initializer: shape,
                    });
                }
                return shape;
            }));
        }
        else {
            finalShapes.push(...shapes);
        }
        const shapeCommitStartTime = Date.now();
        // Commit all the shapes, handling undefined first to ensure broken references are cleaned up
        for (const shape of finalShapes.sort((a) => (a.isUndefined() ? -1 : 0))) {
            const target = await this.targeter.getTarget(shape);
            if (target.success) {
                this.logger.debug(`Committing shape: ${shape.getDescription()}`);
                await context.commit(shape, target.value);
            }
        }
        this.logger.debug(`Finished committing shapes in ${Date.now() - shapeCommitStartTime}ms`);
        const affectedFiles = [];
        for (const file of this.getFiles()) {
            if (!beforeFileHashes[file.getPath()]) {
                affectedFiles.push(file);
                continue;
            }
            const beforeHash = beforeFileHashes[file.getPath()];
            const afterHash = (0, md5_1.default)(file.getContent()).toString();
            if (beforeHash !== afterHash) {
                affectedFiles.push(file);
            }
        }
        for (const file of affectedFiles) {
            file.fix();
            if (format) {
                await file.format();
            }
            file.reload(false);
        }
        this.logger.debug(`Transform completed in ${Date.now() - transformStartTime}ms`);
        return {
            changedFiles: affectedFiles,
            handledPaths: [...handledPaths],
        };
    }
    registerExplicitId(table, key, guidOverride) {
        return this.keys.registerExplicitId(table, key, guidOverride);
    }
    registerCoalesceId(table, key, guidOverride) {
        return this.keys.registerCoalesceId(table, key, guidOverride);
    }
    async getOutput(sysIds, errorOnConflict = false) {
        const output = await this.getOutput0(sysIds, errorOnConflict);
        const sysApp = output.find((f) => isSysAppXml(f.path, this.config.scopeId));
        if (sysApp) {
            const packageJsonPath = sdk_build_core_1.NowConfig.moduleResolutionPath(this.config, this.package, false, 'package.json');
            if (updateSysApp(sysApp, packageJsonPath, this.package.version, this.config)) {
                this.logger.warn(`Fixed package and version in sys_app output file: ${this.relativePath(sysApp.path)}\n` +
                    `This is likely due to a static sys_app XML file in the metadata directory. It is recommended to remove this file and allow the build system to generate one.`);
            }
        }
        this.telemetryClient.sendEvent(this.plugins
            .getShapeCountPerPlugin()
            .filter((data) => data.count > 0)
            .map((data) => ({
            name: 'build.plugin',
            appInfo: { scopeId: this.config.scopeId },
            data,
        })));
        this.plugins.clearShapeCounts();
        return output;
    }
    async getOutput0(sysIds, errorOnConflict = false) {
        this.plugins.clearShapeCounts();
        const records = await this.getRecords(sysIds, errorOnConflict);
        const nonMetadataDirRecords = new sdk_build_core_1.Database(records.query().filter((r) => !this.isInMetadataDir(r.getOriginalFilePath())));
        const files = Object.fromEntries((await (0, util_2.getOutput)(nonMetadataDirRecords, this.getContext())).map((f) => [
            f.path,
            { source: f.source, content: f.content },
        ]));
        const filesWithMetadata = this.copyMetadataRecords(files, records, sysIds);
        return Object.entries(filesWithMetadata).map(([path, sourceAndContent]) => ({ path, ...sourceAndContent }));
    }
    copyMetadataRecords(files, records, sysIds) {
        if (!sysIds) {
            for (const file of this.getFiles().filter((f) => f.isInMetadataDir())) {
                const relativePath = this.config.type === 'configuration'
                    ? file.getName()
                    : sdk_build_core_1.path.relative(this.getMetadataDir(), file.getPath());
                files[this.resolvePath(this.config.appOutputDir, relativePath)] = {
                    content: file.getContent(),
                    originalPath: file.getPath(),
                };
            }
            return files;
        }
        const metadataDirRecords = new sdk_build_core_1.Database(records.query().filter((r) => this.isInMetadataDir(r.getOriginalFilePath())));
        const specifiedMetadata = metadataDirRecords.query().filter((record) => sysIds.has(record.getId().getValue()));
        specifiedMetadata.forEach((record) => {
            const relativePath = sdk_build_core_1.path.relative(this.getMetadataDir(), record.getOriginalFilePath());
            files[this.resolvePath(this.config.appOutputDir, relativePath)] = {
                content: record.getSource().content,
                originalPath: record.getSource().path,
            };
        });
        return files;
    }
    async getRecords(sysIds, errorOnConflict = false) {
        this.plugins.clearShapeCounts();
        // Collect all the defined records up front, BEFORE generating deletes
        const definedRecords = [];
        // Sort metadata dir records first so their sys IDs get registered, followed by server modules to ensure we compute their diagnostics first
        const projectFiles = this.config.type === 'package'
            ? (await this.getEntryPoints()).sort((f1, f2) => (f1.isInMetadataDir() ? 0 : f1.isInServerModulesDir() ? 1 : 2) -
                (f2.isInMetadataDir() ? 0 : f2.isInServerModulesDir() ? 1 : 2))
            : (await this.getEntryPoints())
                .filter((f) => !f.isInServerModulesDir())
                .sort((f1, f2) => (f1.isInMetadataDir() ? 0 : 1) - (f2.isInMetadataDir() ? 0 : 1));
        for (const file of projectFiles) {
            const recordsNoDeDupe = await file.getRecordsNoDedupe(false, sysIds);
            // Reversing is a hack to preserve ordering behavior; skip for server modules to avoid stale data
            // For server modules, reversing can cause stale data to be packaged
            definedRecords.push(...(file.isInServerModulesDir() ? recordsNoDeDupe : recordsNoDeDupe.reverse()));
        }
        //FIXME: reiterate over files to recompute the typescript diagnostics now that we have processed all table entities
        const fluentFiles = projectFiles.filter((f) => sdk_build_core_1.path.basename(f.getPath()).endsWith(sdk_build_core_1.NOW_FILE_EXTENSION));
        for (const file of fluentFiles) {
            this.compiler.getDiagnosticsForFluentFile(file.getPath()).forEach((diagnostic) => {
                this.diagnostics.add(diagnostic);
            });
        }
        // Now it's safe to generate deletes since all keys should be registered at this point
        const records = sysIds || this.config.type === 'configuration' ? new sdk_build_core_1.Database() : this.generateRecordsFromDeletedKeys();
        const generatedDeletes = new WeakSet(records.query());
        /**  Self Conflicting Scenarios:
         * 1. Deletes in the metadata dir can conflict with themselves, so we need to check for that scenario.
         * 2. Records with same sources, happens since we parse child nodes first and then the parent node.
         * ex: Consider default exporting record call, we generate a record for the call expression node first and
         * then for export assignment. Since both records originate from same source we shouldn't log any conflict warnings.
         */
        const isSelfConflicting = (record, conflict) => (this.isInMetadataDir(record.getOriginalFilePath()) &&
            record.isDeleted() &&
            generatedDeletes.has(conflict)) ||
            record.getOriginalSource() === conflict.getOriginalSource();
        // Metadata dir records can conflict with other metadata dir records, but this information is not helpful to the user
        const isMetadataDirOnlyConflict = (record, conflict) => this.isInMetadataDir(record.getOriginalFilePath()) && this.isInMetadataDir(conflict.getOriginalFilePath());
        const isModuleConflict = (record, conflict) => record.getTable() === 'sys_module' && conflict.getTable() === 'sys_module';
        // there are legitimate reasons to keep values in sys_app.xml; if a sys_app XML file has the same
        // sys_id as the one defined in now.config file, the conflict is expected and harmless — suppress it.
        const isNowConfigSysAppConflict = (record, conflict) => {
            if (record.getTable() !== 'sys_app' || conflict.getTable() !== 'sys_app') {
                return false;
            }
            const isNowConfigFile = (p) => p.endsWith(sdk_build_core_1.NowConfig.FILE_NAME);
            return ((isNowConfigFile(record.getOriginalFilePath()) &&
                this.isInMetadataDir(conflict.getOriginalFilePath())) ||
                (isNowConfigFile(conflict.getOriginalFilePath()) && this.isInMetadataDir(record.getOriginalFilePath())));
        };
        // Records in different conditional contexts are not real conflicts — one conditional and
        // one not, or both conditional but under different plugins (if/<plugin>). Only records in
        // the same conditional context (or both non-conditional) should be merged as a conflict.
        const isConditionalDirConflict = (record, conflict) => this.isInConditionalDir(record.getOriginalFilePath()) !==
            this.isInConditionalDir(conflict.getOriginalFilePath()) ||
            this.inDifferentConditionalDirs(record.getOriginalFilePath(), conflict.getOriginalFilePath());
        // Insert the defined records in reverse order so that metadata dir records come last and take precedence
        const fluentConflicts = {};
        const xmlFluentConflicts = {};
        for (const record of definedRecords.reverse()) {
            const conflict = records.resolve(record.getId());
            if (conflict &&
                !isSelfConflicting(record, conflict) &&
                !isMetadataDirOnlyConflict(record, conflict) &&
                !isModuleConflict(record, conflict) &&
                !isNowConfigSysAppConflict(record, conflict) &&
                !isConditionalDirConflict(record, conflict)) {
                const key = `${conflict.getTable()}.${conflict.getId().getValue()}`;
                const bothFluentFiles = record.getOriginalFilePath().endsWith(sdk_build_core_1.NOW_FILE_EXTENSION) &&
                    conflict.getOriginalFilePath().endsWith(sdk_build_core_1.NOW_FILE_EXTENSION);
                ((bothFluentFiles ? fluentConflicts : xmlFluentConflicts)[key] ??= [conflict]).push(record);
            }
            if (conflict && isModuleConflict(record, conflict)) {
                // For modules, only the last module should be added. Keep only the first occurrence during reverse traversal
                continue;
            }
            if (conflict && isConditionalDirConflict(record, conflict)) {
                continue;
            }
            records.insert(record);
        }
        // Run plugin inspect handlers before throwing conflict errors so plugins can surface
        // domain-specific diagnostics (e.g., "use inline choices instead of ChoiceSet")
        // instead of the generic "defined N times" conflict message.
        for (const plugin of this.plugins) {
            await plugin.inspectRecords(records, this.getContext());
        }
        const warnings = this.diagnostics.getWarnings();
        warnings.forEach((w) => this.logger.warn(w.getFormattedText()));
        const errors = this.diagnostics.getErrors();
        if (errors.length > 0) {
            errors.forEach((e) => this.logger.error(e.getFormattedText()));
            throw new Error(`Found ${errors.length} diagnostic error(s) while building the project.`);
        }
        const formatConflict = (updateName, conflictingRecords) => `Record "${updateName}" is defined ${conflictingRecords.length} times in the project:\n    ${conflictingRecords
            .map((r, i) => {
            const path = this.relativePath(r.getOriginalFilePath());
            const source = r.getOriginalSource();
            const deleted = r.isDeleted() ? ' (delete)' : '';
            const line = sdk_build_core_1.ts.Node.isNode(source) ? `:${source.getStartLineNumber(false)}` : '';
            return `${i + 1}. ${path}${line}${deleted}`;
        })
            .join('\n    ')}`;
        // Check if this might be a chunked transform duplicate by checking which plugin created each record.
        // If one record was created by RecordPlugin and another by a parent plugin (e.g., TablePlugin, FlowPlugin),
        // it's likely a chunked transform duplicate where RecordPlugin created a standalone file that
        // a parent plugin also inlines.
        const isChunkedTransformDuplicate = (records) => {
            const hasRecordPluginRecord = records.some((r) => r.getCreator()?.getName() === 'RecordPlugin');
            const hasParentPluginRecord = records.some((r) => r.getCreator()?.getName() !== 'RecordPlugin');
            return hasRecordPluginRecord && hasParentPluginRecord;
        };
        // Fluent-only conflicts always error regardless of --error-on-conflict
        let chunkedTransformDuplicateCount = 0;
        for (const [updateName, conflictingRecords] of Object.entries(fluentConflicts)) {
            if (isChunkedTransformDuplicate(conflictingRecords)) {
                chunkedTransformDuplicateCount++;
            }
            this.logger.error(formatConflict(updateName, conflictingRecords));
        }
        const fluentConflictCount = Object.keys(fluentConflicts).length;
        if (fluentConflictCount > 0) {
            let errorMessage = `Total of ${fluentConflictCount} conflict(s) found between .now.ts files.`;
            if (chunkedTransformDuplicateCount > 0 && chunkedTransformDuplicateCount === fluentConflictCount) {
                // All conflicts appear to be from chunked transform
                errorMessage +=
                    '\n\nThis may be caused by running separate transforms with --table. ' +
                        'Some records are automatically included in their parent definitions, ' +
                        'but a separate transform may have also created standalone Record() files for them.\n\n' +
                        'To fix:\n' +
                        '  1. Delete the standalone Record() files shown above\n' +
                        '  2. Re-run transform with all related tables together';
            }
            else if (chunkedTransformDuplicateCount > 0) {
                // Some conflicts appear to be from chunked transform, others are different
                errorMessage +=
                    ` ${chunkedTransformDuplicateCount} of these may be caused by running separate ` +
                        'transforms with --table (delete the standalone Record() files and re-run transform ' +
                        'with related tables together). Remove the other duplicate definitions manually.';
            }
            else {
                errorMessage += ' Remove duplicate definitions to fix.';
            }
            throw new Error(errorMessage);
        }
        // now.ts ↔ XML conflicts respect the --errorOnConflict flag
        const log = errorOnConflict ? this.logger.error.bind(this.logger) : this.logger.warn.bind(this.logger);
        for (const [updateName, conflictingRecords] of Object.entries(xmlFluentConflicts)) {
            log(formatConflict(updateName, conflictingRecords));
        }
        const mixedConflictCount = Object.keys(xmlFluentConflicts).length;
        if (mixedConflictCount > 0) {
            const summary = `Total of ${mixedConflictCount} conflict(s) found.`;
            if (errorOnConflict) {
                throw new Error(`${summary} Remove duplicate definitions or omit --errorOnConflict to demote these to warnings.`);
            }
            this.logger.warn(`${summary} The last entry from each conflict will take precedence.`);
        }
        // Group records by their keys to find duplicates
        const recordsByKey = new Map();
        records.query().forEach((record) => {
            const key = record.getId().getNowIdKey();
            if (key !== undefined && record.getTable() !== 'sys_module') {
                recordsByKey.set(key, [...(recordsByKey.get(key) ?? []), record]);
            }
        });
        // Find and log records with duplicate keys
        const duplicateKeys = Array.from(recordsByKey.entries()).filter(([_, records]) => records.length > 1);
        if (duplicateKeys.length > 0) {
            duplicateKeys.forEach(([key, records]) => {
                this.logger.error(`Explicit key "${key}" is defined multiple times in the project:\n    ${records
                    .map((r, i) => {
                    const path = this.relativePath(r.getOriginalFilePath());
                    const source = r.getOriginalSource();
                    const line = sdk_build_core_1.ts.Node.isNode(source) ? `:${source.getStartLineNumber(false)}` : '';
                    const table = r.getTable();
                    return `${i + 1}. [${table}] ${path}${line}`;
                })
                    .join('\n    ')}`);
            });
            throw new Error(`Found ${duplicateKeys.length} duplicate explicit key(s) in the project.`);
        }
        await this.getKeys().commit();
        return records;
    }
    async getAttachmentsForFields(database, record) {
        const updateName = await this.getTransform().getUpdateName(record);
        const attachmentEntries = await Promise.all(database
            .query('sys_attachment', {
            table_name: `ZZ_YY${record.getTable()}`,
            table_sys_id: record.getId().getValue(),
        })
            .map(async (attachment) => {
            const fileName = attachment.get('file_name').toString().getValue();
            const shape = CorePlugins.NowAttachShape.fromAttachmentRecord(updateName, attachment, database.query('sys_attachment_doc', {
                sys_attachment: attachment.getId().getValue(),
            }));
            await shape.getBufferData();
            return [fileName, shape];
        }));
        return Object.fromEntries(attachmentEntries.filter((entry) => entry !== undefined));
    }
    generateRecordsFromDeletedKeys() {
        const database = new sdk_build_core_1.Database();
        const source = this.getKeys().getSourceFile();
        for (const { table, id, key: rawKey } of this.getKeys().getEffectiveDeletedKeys()) {
            if (table === 'sys_ui_view') {
                continue; // TODO: When the installer is fixed to prevent out-of-scope deletion, we can stop being so cautious here
            }
            else if (table === 'sys_user_role') {
                const scope = this.getConfig().scope;
                const name = rawKey?.['name'];
                if (name && typeof name === 'string' && !name.startsWith(scope)) {
                    continue;
                }
            }
            const key = Object.fromEntries(Object.entries(rawKey ?? {}).map(([k, v]) => [
                k,
                typeof v === 'string'
                    ? v === 'NULL'
                        ? undefined
                        : v
                    : new sdk_build_core_1.RecordId({
                        source,
                        guid: v.id,
                        table: k,
                        keys: Object.fromEntries(Object.entries(v.key).map(([k, v]) => [k, typeof v === 'string' ? v : v.id])),
                    }),
            ]));
            database.insert(new sdk_build_core_1.Record({
                source,
                action: 'DELETE',
                installCategory: 'author_elective_update',
                properties: key,
                id: new sdk_build_core_1.RecordId({
                    source,
                    table,
                    guid: id,
                }),
            }));
        }
        return database;
    }
    async getDiagnostics() {
        this.plugins.clearShapeCounts();
        const diagnostics = new context_1.Diagnostics();
        const entryPoints = await this.getEntryPoints();
        entryPoints.sort((a, _b) => {
            return this.isInServerDir(a.getPath()) ? -1 : 1;
        });
        for (const file of entryPoints) {
            if (file.isInMetadataDir()) {
                continue;
            }
            for (const diagnostic of await file.getDiagnostics()) {
                diagnostics.add(diagnostic);
            }
        }
        //FIXME: reiterate over files to recompute the typescript diagnostics now that we have processed all table entities
        const fluentFiles = entryPoints.filter((f) => sdk_build_core_1.path.basename(f.getPath()).endsWith(sdk_build_core_1.NOW_FILE_EXTENSION));
        for (const file of fluentFiles) {
            this.compiler.getDiagnosticsForFluentFile(file.getPath()).forEach((diagnostic) => {
                diagnostics.add(diagnostic);
            });
        }
        return diagnostics.getAll();
    }
    registerStaticContentDir(dir) {
        if (this.staticContentInputDirs.has(dir)) {
            return;
        }
        this.staticContentInputDirs.add(dir);
        if (sdk_build_core_1.FileSystem.existsSync(this.fileSystem, dir)) {
            this.addDirectory(dir);
        }
    }
    isInStaticContentAssetsDir(path) {
        for (const dir of this.staticContentInputDirs) {
            if (path.startsWith(dir)) {
                return true;
            }
        }
        return false;
    }
    isInDir(dir, path) {
        const relativePath = sdk_build_core_1.path.relative(this.resolvePath(dir), this.resolvePath(path));
        return !relativePath.startsWith('..');
    }
    getRootDir() {
        return this.rootDir;
    }
    isInRootDir(path) {
        return this.isInDir(this.getRootDir(), path);
    }
    getMetadataDir() {
        return this.resolvePath(this.config.metadataDir);
    }
    isInMetadataDir(path) {
        return this.isInDir(this.getMetadataDir(), path);
    }
    isInUiHtmlContentDir(path) {
        return this.isInDir(sdk_build_core_1.path.join(this.getMetadataDir(), 'ui.html'), path);
    }
    getSnDirName(path) {
        if ((0, sdk_build_core_1.isSNScope)(this.config.scope)) {
            const dir = sdk_build_core_1.path.dirname(path);
            const parts = dir.split(sdk_build_core_1.path.sep);
            const ifDirectoryIdx = parts.lastIndexOf('if');
            if (ifDirectoryIdx !== -1) {
                return parts.slice(ifDirectoryIdx).join(sdk_build_core_1.path.sep);
            }
            const hostedPluginIdx = parts.lastIndexOf('hosted_plugins');
            if (hostedPluginIdx !== -1) {
                return parts.slice(hostedPluginIdx).join(sdk_build_core_1.path.sep);
            }
        }
        const dirName = sdk_build_core_1.path.basename(sdk_build_core_1.path.dirname(path));
        const snDirNames = [
            'update',
            'author_elective_update',
            'dictionary',
            'scope',
            'demo',
            'unload',
            'unload.demo',
            'apply_once',
        ];
        return snDirNames.includes(dirName) ? dirName : '';
    }
    getFluentDir() {
        return this.resolvePath(this.config.fluentDir);
    }
    isInFluentDir(path) {
        return this.isInDir(this.getFluentDir(), path);
    }
    getServerDir() {
        return this.resolvePath(this.config.serverModulesDir);
    }
    isInServerDir(path) {
        return this.isInDir(this.getServerDir(), path);
    }
    isInConditionalDir(path) {
        // Only ServiceNow apps can have conditional content
        if (!(0, sdk_build_core_1.isSNScope)(this.config.scope)) {
            return false;
        }
        const parts = this.resolvePath(path).split(sdk_build_core_1.path.sep);
        // Require at least one segment for the plugin name AND one for the file after the marker
        const hasMarker = (marker) => {
            const idx = parts.lastIndexOf(marker);
            return idx !== -1 && idx < parts.length - 2;
        };
        return hasMarker('if') || hasMarker('hosted_plugins');
    }
    /**
     * True when both paths live under conditional directories but belong to different
     * conditional plugins (if/<plugin> or hosted_plugins/<plugin>). These records deliver to
     * separate plugins and must be treated as distinct even when they share a sys_id, so the
     * incoming one is preserved as metadata XML rather than merged into the already-seen record.
     */
    inDifferentConditionalDirs(pathA, pathB) {
        const dirA = (0, sdk_build_core_1.getConditionalPluginDir)(this.resolvePath(pathA));
        const dirB = (0, sdk_build_core_1.getConditionalPluginDir)(this.resolvePath(pathB));
        return dirA !== undefined && dirB !== undefined && dirA !== dirB;
    }
    isInFluentHostedPluginDir(path, pluginId) {
        if (pluginId) {
            return this.isInDir(sdk_build_core_1.path.join(this.getFluentDir(), 'hosted_plugins', pluginId), path);
        }
        return this.isInDir(sdk_build_core_1.path.join(this.getFluentDir(), 'hosted_plugins'), path);
    }
    resolvePath(...paths) {
        const path = sdk_build_core_1.path.join(...paths);
        return sdk_build_core_1.path.isAbsolute(path) ? sdk_build_core_1.path.normalize(path) : sdk_build_core_1.path.resolve(this.rootDir, path);
    }
    relativePath(...paths) {
        return sdk_build_core_1.path.relative(this.rootDir, this.resolvePath(...paths));
    }
    dispose() {
        this.compiler.getLanguageService().compilerObject.dispose();
    }
    async fix() {
        return Promise.all(this.getFiles().map((file) => file.fix()));
    }
    async format() {
        const formatter = new sdk_build_core_1.Formatter(this.fileSystem);
        return Promise.all(this.getFiles().map((file) => file.format(formatter)));
    }
    save() {
        this.getFiles().forEach((file) => file.save());
        this.filesForDeletion.forEach((path) => this.fileSystem.rmSync(path, { force: true }));
    }
    async saveOutput(sysIds, errorOnConflict = false) {
        const output = await this.getOutput(sysIds, errorOnConflict);
        output.forEach((file) => (0, util_1.writeFileWithParentDirs)(this.fileSystem, file));
        return output;
    }
    deleteFile(path) {
        this.removeFile(path);
        this.filesForDeletion.add(path);
    }
    handleMetadataCopy(file, metadataDirRecordPath) {
        let destinationPath;
        if (metadataDirRecordPath && file.getName() === sdk_build_core_1.path.basename(metadataDirRecordPath)) {
            // Update an existing record in metadata folder
            destinationPath = metadataDirRecordPath;
        }
        else {
            // Preserve directory from incoming metadata record if there is one
            destinationPath = sdk_build_core_1.path.join(this.getMetadataDir(), this.getSnDirName(file.getPath()), sdk_build_core_1.path.basename(file.getPath()));
        }
        return this.addFile({
            content: file.getContent(),
            path: destinationPath,
        });
    }
    isTypesGenerationMode() {
        return this.transformMode === 'type-generation';
    }
}
exports.Project = Project;
function updateSysApp(sysAppFile, packageJsonPath, sysAppVersion, config) {
    const parser = new fast_xml_parser_1.XMLParser({
        ignoreAttributes: false,
        ignoreDeclaration: false,
    });
    const parsedSysAppXml = parser.parse(sysAppFile.content);
    const appElement = parsedSysAppXml['record_update']
        ? parsedSysAppXml['record_update']['sys_app'] || parsedSysAppXml['record_update']['sys_store_app']
        : parsedSysAppXml['unload']
            ? parsedSysAppXml['unload']['sys_app'] || parsedSysAppXml['unload']['sys_store_app']
            : undefined;
    if (!appElement) {
        throw new Error(`Could not parse sys_app file`);
    }
    const builder = new fast_xml_parser_1.XMLBuilder({
        format: true,
        indentBy: '    ',
        ignoreAttributes: false,
        suppressEmptyNode: true,
    });
    const updatedPackageJsonPath = updateSysAppWithPackageJsonPath(appElement, packageJsonPath, sysAppVersion);
    const updatedPackageResolver = updateSysAppWithPackageResolverVersion(appElement, config);
    const updatedSysCode = updateSysAppWithSysCode(appElement, config.sysCode);
    if (updatedPackageJsonPath || updatedPackageResolver || updatedSysCode) {
        //fxp adds new line after xml declaration while instance doesn't
        sysAppFile.content = builder.build(parsedSysAppXml).replace(/(<\?xml[^>]*\?>)\s*/, '$1');
        return true;
    }
    return false;
}
function updateSysAppWithPackageResolverVersion(appElement, config) {
    const legacyResolver = sdk_build_core_1.NowConfig.legacyPackageResolution(config);
    if (appElement.package_resolver_version) {
        return false;
    }
    appElement.package_resolver_version = legacyResolver ? sdk_build_core_1.MODULE_RESOLUTION.V1 : sdk_build_core_1.MODULE_RESOLUTION.V2;
    return true;
}
function updateSysAppWithPackageJsonPath(appElement, packageJsonPath, sysAppVersion) {
    if (appElement.version && appElement.version === sysAppVersion && appElement.package_json === packageJsonPath) {
        return false;
    }
    appElement.package_json = packageJsonPath;
    if (sysAppVersion) {
        appElement.version = sysAppVersion;
    }
    return true;
}
function updateSysAppWithSysCode(appElement, sysCode) {
    if (appElement.sys_code) {
        return false;
    }
    if (!sysCode) {
        return false;
    }
    appElement.sys_code = sysCode;
    return true;
}
function overwriteObject(target, source) {
    for (const key in target) {
        delete target[key];
    }
    Object.assign(target, source);
}
//# sourceMappingURL=project.js.map