"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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.ProjectConfigService = void 0;
const constants = require("../constants");
const constants_1 = require("../constants");
const path = require("path");
const _ = require("lodash");
const ts = require("typescript");
const config_transformer_1 = require("../tools/config-manipulation/config-transformer");
const yok_1 = require("../common/yok");
const os_1 = require("os");
const prettier_1 = require("prettier");
const decorators_1 = require("../common/decorators");
const semver = require("semver/preload");
class ProjectConfigService {
    constructor($fs, $logger, $injector, $options, $cleanupService) {
        this.$fs = $fs;
        this.$logger = $logger;
        this.$injector = $injector;
        this.$options = $options;
        this.$cleanupService = $cleanupService;
        this.forceUsingNewConfig = false;
        this.forceUsingLegacyConfig = false;
    }
    setForceUsingNewConfig(force) {
        return (this.forceUsingNewConfig = force);
    }
    setForceUsingLegacyConfig(force) {
        return (this.forceUsingLegacyConfig = force);
    }
    requireFromString(src, filename) {
        // @ts-ignore
        const m = new module.constructor();
        m.paths = module.paths;
        m._compile(src, filename);
        return m.exports;
    }
    get projectHelper() {
        return this.$injector.resolve("projectHelper");
    }
    getDefaultTSConfig(appId = "org.nativescript.app", appPath = "app") {
        return `import { NativeScriptConfig } from '@nativescript/core';

export default {
  id: '${appId}',
  appPath: '${appPath}',
  appResourcesPath: 'App_Resources',
  android: {
    v8Flags: '--expose_gc',
    markingMode: 'none'
  }
} as NativeScriptConfig;`.trim();
    }
    warnUsingLegacyNSConfig() {
        // todo: remove hack
        const isMigrate = _.get(this.$options, "argv._[0]") === "migrate";
        if (isMigrate) {
            return;
        }
        this.$logger.warn(`You are using the deprecated ${constants_1.CONFIG_NS_FILE_NAME} file. Just be aware that NativeScript now has an improved ${constants_1.CONFIG_FILE_NAME_DISPLAY} file for when you're ready to upgrade this project.`);
    }
    detectProjectConfigs(projectDir) {
        var _a;
        // allow overriding config name with env variable or --config (or -c)
        let configName = (_a = process.env.NATIVESCRIPT_CONFIG_NAME) !== null && _a !== void 0 ? _a : this.$options.config;
        if (configName === "false") {
            configName = false;
        }
        const possibleConfigPaths = [
            configName &&
                ((configName === null || configName === void 0 ? void 0 : configName.endsWith(".ts")) ? configName : `${configName}.ts`),
            configName &&
                ((configName === null || configName === void 0 ? void 0 : configName.endsWith(".js")) ? configName : `${configName}.js`),
            configName &&
                ((configName === null || configName === void 0 ? void 0 : configName.endsWith(".json")) ? configName : `${configName}.json`),
            constants_1.CONFIG_FILE_NAME_TS,
            constants_1.CONFIG_FILE_NAME_JS,
            constants_1.CONFIG_NS_FILE_NAME,
        ]
            .filter(Boolean)
            .map((c) => {
            if (this.$fs.isRelativePath(c)) {
                const dir = projectDir || this.projectHelper.projectDir;
                if (!dir) {
                    return c;
                }
                return path.join(dir, c);
            }
            return c;
        });
        const existingConfigs = possibleConfigPaths.filter((path) => {
            return this.$fs.exists(path);
        });
        // push the first possible config into the "existing" list
        const hasExistingConfig = !!existingConfigs.length;
        if (!hasExistingConfig) {
            this.$logger.trace(`No config file found - falling back to ${possibleConfigPaths[0]}.`);
            existingConfigs.push(possibleConfigPaths[0]);
        }
        const TSConfigPath = existingConfigs.find((config) => config.endsWith(".ts"));
        const JSConfigPath = existingConfigs.find((config) => config.endsWith(".js"));
        const NSConfigPath = existingConfigs.find((config) => config.endsWith(".json"));
        const hasTSConfig = !!TSConfigPath && hasExistingConfig;
        const hasJSConfig = !!JSConfigPath && hasExistingConfig;
        const hasNSConfig = !!NSConfigPath && hasExistingConfig;
        const usingNSConfig = !(hasTSConfig || hasJSConfig);
        if (hasTSConfig && hasJSConfig) {
            this.$logger.warn(`You have both a ${constants_1.CONFIG_FILE_NAME_JS} and ${constants_1.CONFIG_FILE_NAME_TS} file. Defaulting to ${constants_1.CONFIG_FILE_NAME_TS}.`);
        }
        return {
            hasTSConfig,
            hasJSConfig,
            hasNSConfig,
            usingNSConfig,
            TSConfigPath,
            JSConfigPath,
            NSConfigPath,
        };
    }
    readConfig(projectDir) {
        const info = this.detectProjectConfigs(projectDir);
        if (this.forceUsingLegacyConfig ||
            (info.usingNSConfig && !this.forceUsingNewConfig)) {
            this.$logger.trace("Project Config Service using legacy configuration...");
            if (!this.forceUsingLegacyConfig && info.hasNSConfig) {
                this.warnUsingLegacyNSConfig();
            }
            return this.fallbackToLegacyNSConfig(info);
        }
        let config;
        if (info.hasTSConfig) {
            const rawSource = this.$fs.readText(info.TSConfigPath);
            const transpiledSource = ts.transpileModule(rawSource, {
                compilerOptions: { module: ts.ModuleKind.CommonJS },
            });
            const result = this.requireFromString(transpiledSource.outputText, info.TSConfigPath);
            config = result["default"] ? result["default"] : result;
        }
        else if (info.hasJSConfig) {
            const rawSource = this.$fs.readText(info.JSConfigPath);
            config = this.requireFromString(rawSource, info.JSConfigPath);
        }
        return config;
    }
    getValue(key, defaultValue) {
        return _.get(this.readConfig(), key, defaultValue);
    }
    setValue(key, value) {
        return __awaiter(this, void 0, void 0, function* () {
            const { hasTSConfig, hasNSConfig, TSConfigPath, JSConfigPath, usingNSConfig, NSConfigPath, } = this.detectProjectConfigs();
            const configFilePath = TSConfigPath || JSConfigPath;
            if (this.forceUsingLegacyConfig ||
                (usingNSConfig && !this.forceUsingNewConfig)) {
                try {
                    this.$logger.trace("Project Config Service -> setValue writing to legacy config.");
                    const NSConfig = hasNSConfig ? this.$fs.readJson(NSConfigPath) : {};
                    _.set(NSConfig, key, value);
                    this.$fs.writeJson(NSConfigPath, NSConfig);
                    return true;
                }
                catch (error) {
                    this.$logger.trace(`Failed to setValue on legacy config. Error is ${error.message}`, error);
                    return false;
                }
            }
            if (!this.$fs.exists(configFilePath)) {
                this.writeDefaultConfig(this.projectHelper.projectDir);
            }
            if (typeof value === "object") {
                let allSuccessful = true;
                for (const prop of this.flattenObjectToPaths(value)) {
                    if (!(yield this.setValue(prop.key, prop.value))) {
                        allSuccessful = false;
                    }
                }
                return allSuccessful;
            }
            const configContent = this.$fs.readText(configFilePath);
            try {
                const transformer = new config_transformer_1.ConfigTransformer(configContent);
                const newContent = transformer.setValue(key, value);
                const prettierOptions = (yield (0, prettier_1.resolveConfig)(this.projectHelper.projectDir, { editorconfig: true })) || {
                    semi: false,
                    singleQuote: true,
                };
                this.$logger.trace("updating config, prettier options: ", prettierOptions);
                this.$fs.writeFile(configFilePath, (0, prettier_1.format)(newContent, Object.assign(Object.assign({}, prettierOptions), { parser: "typescript", 
                    // note: we don't use plugins here, since we are only formatting ts files, and they are supported by default
                    // and this also causes issues with certain plugins, like prettier-plugin-tailwindcss.
                    plugins: [] })));
            }
            catch (error) {
                this.$logger.error(`Failed to update config.` + error);
            }
            finally {
                // verify config is updated correctly
                if (this.getValue(key) !== value) {
                    this.$logger.error(`${os_1.EOL}Failed to update ${hasTSConfig ? constants_1.CONFIG_FILE_NAME_TS : constants_1.CONFIG_FILE_NAME_JS}.${os_1.EOL}`);
                    this.$logger.printMarkdown(`Please manually update \`${hasTSConfig ? constants_1.CONFIG_FILE_NAME_TS : constants_1.CONFIG_FILE_NAME_JS}\` and set \`${key}\` to \`${value}\`.${os_1.EOL}`);
                    // restore original content
                    this.$fs.writeFile(configFilePath, configContent);
                    return false;
                }
                return true;
            }
        });
    }
    writeDefaultConfig(projectDir, appId) {
        const TSConfigPath = path.resolve(projectDir, constants_1.CONFIG_FILE_NAME_TS);
        if (this.$fs.exists(TSConfigPath)) {
            return false;
        }
        const possibleAppPaths = [
            path.resolve(projectDir, constants.SRC_DIR),
            path.resolve(projectDir, constants.APP_FOLDER_NAME),
        ];
        let appPath = possibleAppPaths.find((possiblePath) => this.$fs.exists(possiblePath));
        if (appPath) {
            appPath = path.relative(projectDir, appPath).replace(path.sep, "/");
        }
        this.$fs.writeFile(TSConfigPath, this.getDefaultTSConfig(appId, appPath));
        return TSConfigPath;
    }
    fallbackToLegacyNSConfig(info) {
        const additionalData = [];
        const NSConfig = info.hasNSConfig
            ? this.$fs.readJson(info.NSConfigPath)
            : {};
        try {
            // injecting here to avoid circular dependency
            const projectData = this.$injector.resolve("projectData");
            const embeddedPackageJsonPath = path.resolve(this.projectHelper.projectDir, projectData.getAppDirectoryRelativePath(), constants.PACKAGE_JSON_FILE_NAME);
            const embeddedPackageJson = this.$fs.readJson(embeddedPackageJsonPath);
            // filter only the supported keys
            additionalData.push(_.pick(embeddedPackageJson, [
                "android",
                "ios",
                "profiling",
                "cssParser",
                "discardUncaughtJsExceptions",
                "main",
            ]));
        }
        catch (err) {
            this.$logger.trace("failed to add embedded package.json data to config", err);
            // ignore if the file doesn't exist
        }
        try {
            const packageJson = this.$fs.readJson(path.join(this.projectHelper.projectDir, "package.json"));
            // add app id to additionalData for backwards compatibility
            if (!NSConfig.id &&
                packageJson &&
                packageJson.nativescript &&
                packageJson.nativescript.id) {
                const ids = packageJson.nativescript.id;
                if (typeof ids === "string") {
                    additionalData.push({
                        id: packageJson.nativescript.id,
                    });
                }
                else if (typeof ids === "object") {
                    for (const platform of Object.keys(ids)) {
                        additionalData.push({
                            [platform]: {
                                id: packageJson.nativescript.id[platform],
                            },
                        });
                    }
                }
            }
        }
        catch (err) {
            this.$logger.trace("failed to read package.json data for config", err);
            // ignore if the file doesn't exist
        }
        return _.defaultsDeep({}, ...additionalData, NSConfig);
        // return Object.assign({}, ...additionalData, NSConfig);
    }
    writeLegacyNSConfigIfNeeded(projectDir, runtimePackage) {
        return __awaiter(this, void 0, void 0, function* () {
            const { usingNSConfig } = this.detectProjectConfigs(projectDir);
            if (usingNSConfig) {
                return;
            }
            if (runtimePackage.version &&
                semver.gte(semver.coerce(runtimePackage.version), "7.0.0-rc.5")) {
                // runtimes >= 7.0.0-rc.5 support passing appPath and appResourcesPath through gradle project flags
                // so writing an nsconfig is not necessary.
                return;
            }
            const runtimePackageDisplay = `${runtimePackage.name}${runtimePackage.version ? " v" + runtimePackage.version : ""}`;
            this.$logger.info();
            this.$logger.printMarkdown(`
Using __${runtimePackageDisplay}__ which requires \`nsconfig.json\` to be present.
Writing \`nsconfig.json\` based on the values set in \`${constants_1.CONFIG_FILE_NAME_DISPLAY}\`.
You may add \`nsconfig.json\` to \`.gitignore\` as the CLI will regenerate it as necessary.`);
            const nsConfigPath = path.join(projectDir || this.projectHelper.projectDir, "nsconfig.json");
            this.$fs.writeJson(nsConfigPath, {
                _info1: `Auto Generated for backwards compatibility with the currently used runtime.`,
                _info2: `Do not edit this file manually, as any changes will be ignored.`,
                _info3: `Config changes should be done in ${constants_1.CONFIG_FILE_NAME_DISPLAY} instead.`,
                appPath: this.getValue("appPath"),
                appResourcesPath: this.getValue("appResourcesPath"),
            });
            // mark the file for cleanup after the CLI exits
            yield this.$cleanupService.addCleanupDeleteAction(nsConfigPath);
        });
    }
    // todo: move into config manipulation
    flattenObjectToPaths(obj, basePath) {
        const toPath = (key) => [basePath, key].filter(Boolean).join(".");
        return Object.keys(obj).reduce((all, key) => {
            if (typeof obj[key] === "object") {
                return [...all, ...this.flattenObjectToPaths(obj[key], toPath(key))];
            }
            return [
                ...all,
                {
                    key: toPath(key),
                    value: obj[key],
                },
            ];
        }, []);
    }
}
__decorate([
    (0, decorators_1.cache)() // @cache should prevent the message being printed multiple times
], ProjectConfigService.prototype, "warnUsingLegacyNSConfig", null);
__decorate([
    (0, decorators_1.exported)("projectConfigService")
], ProjectConfigService.prototype, "readConfig", null);
__decorate([
    (0, decorators_1.exported)("projectConfigService")
], ProjectConfigService.prototype, "getValue", null);
__decorate([
    (0, decorators_1.exported)("projectConfigService")
], ProjectConfigService.prototype, "setValue", null);
exports.ProjectConfigService = ProjectConfigService;
yok_1.injector.register("projectConfigService", ProjectConfigService);
//# sourceMappingURL=project-config-service.js.map