"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseNodeModules = void 0;
const lib_1 = require("tnp-core/lib");
const lib_2 = require("tnp-core/lib");
const lib_3 = require("tnp-core/lib");
const index_1 = require("../../index");
class BaseNodeModules {
    cwd;
    npmHelpers;
    _npmHelpersType;
    //#region constructor
    constructor(
    /**
     * my-path/node_modules
     */
    cwd, npmHelpers) {
        this.cwd = cwd;
        this.npmHelpers = npmHelpers;
        this.npmHelpers = npmHelpers;
    }
    //#endregion
    //#region path
    /**
     * cwd + node_modules
     */
    get path() {
        return (0, lib_2.crossPlatformPath)([this.cwd, lib_1.config.folder.node_modules]);
    }
    //#endregion
    //#region path for package
    /**
     * Path to package inside node_modules folder
     * @example <project-path>/node_modules/<package-name>
     */
    pathFor(packageName) {
        //#region @backendFunc
        packageName = (0, lib_2.crossPlatformPath)(packageName);
        return (0, lib_2.crossPlatformPath)([this.realPath, packageName]);
        //#endregion
    }
    //#endregion
    //#region real path of node_modules - if node_modules is a link
    /**
     * real path of node_modules
     * if node_modules is a link
     * it will return real path.
     * if node_modules is folder = path
     */
    get realPath() {
        try {
            const realPath = (0, lib_2.crossPlatformPath)(lib_2.fse.realpathSync(this.path));
            return realPath;
        }
        catch (error) {
            return this.path;
        }
    }
    //#endregion
    //#region prevent wrong link destination
    preventWrongLinkDestination(dest) {
        if (!lib_2.path.isAbsolute(dest)) {
            lib_1.Helpers.error(`[linkTo..] target destination path is not absolute "${dest}"`);
        }
        if (dest.endsWith(`/${lib_1.config.folder.node_modules}`)) {
            return dest;
        }
        return (0, lib_2.crossPlatformPath)([dest, lib_1.config.folder.node_modules]);
    }
    //#endregion
    //#region link node_modules to other project
    linkToProject(project) {
        //#region @backendFunc
        const source = this.realPath;
        let dest = project.pathFor(lib_1.config.folder.node_modules);
        dest = this.preventWrongLinkDestination(dest);
        lib_1.Helpers.createSymLink(source, dest, {
            continueWhenExistedFolderDoesntExists: true,
        });
        //#endregion
    }
    //#endregion
    //#region link node_modules to other project
    copyToProject(project) {
        //#region @backendFunc
        lib_1.Helpers.taskStarted(`Copying node_modules folder to project ${project.name}/node_modules`);
        const source = this.realPath;
        let dest = project.pathFor(lib_1.config.folder.node_modules);
        dest = this.preventWrongLinkDestination(dest);
        lib_1.Helpers.removeSymlinks(dest);
        lib_1.Helpers.remove(dest, true);
        index_1.HelpersTaon.copy(source, dest, {
            recursive: true,
            overwrite: true,
            copySymlinksAsFiles: false,
        });
        lib_1.Helpers.taskDone(`Done copying node_modules folder to project ${project.name}/node_modules`);
        //#endregion
    }
    //#endregion
    //#region link to project or location
    linkToLocation(location) {
        //#region @backendFunc
        let dest = (0, lib_2.crossPlatformPath)(location);
        dest = this.preventWrongLinkDestination(dest);
        lib_1.Helpers.remove(dest, true);
        lib_1.Helpers.createSymLink(this.realPath, dest, {
            continueWhenExistedFolderDoesntExists: true,
        });
        //#endregion
    }
    //#endregion
    //#region make sure node modules installed
    async makeSureInstalled(options) {
        if (this.isEmpty()) {
            await this.reinstall(options);
        }
    }
    //#endregion
    //#region delete node_modules
    async remove(options) {
        //#region @backendFunc
        options = options || {};
        if (options.skipQuestion ||
            (await lib_1.Helpers.questionYesNo(`You are about delete ${lib_1.config.folder.node_modules} (Yes -> continue, No -> skip action) ?`))) {
            lib_1.Helpers.removeSymlinks(this.path);
            lib_1.Helpers.remove(this.path, true);
        }
        //#endregion
    }
    //#endregion
    //#region remove package inside
    removePackage(packageInside) {
        //#region @backendFunc
        lib_1.Helpers.log(`Removing node_modules from ${this.path}`);
        lib_1.Helpers.removeIfExists([this.path, packageInside]);
        //#endregion
    }
    //#endregion
    //#region reinstall node modules
    async reinstall(options) {
        //#region @backendFunc
        options = lib_2._.cloneDeep(options || {});
        if (lib_2._.isUndefined(options.useYarn)) {
            options.useYarn = this.npmHelpers.preferYarnOverNpm();
        }
        if (lib_2._.isUndefined(options.removeYarnOrPackageJsonLock)) {
            options.removeYarnOrPackageJsonLock = true;
        }
        lib_1.Helpers.taskStarted(`
      [${(0, lib_2.dateformat)(new Date(), 'dd-mm-yyyy HH:MM:ss')}]
      Reinstalling node modules in:
${this.cwd}

with ${options.useYarn ? 'yarn' : 'npm'}

      `);
        while (true) {
            if (!options.skipRemovingNodeModules) {
                await this.remove({ skipQuestion: true });
            }
            // TODO @LAST - use method makeSureNodeModulesInstalled()
            try {
                lib_1.Helpers.run(await this.prepareCommand(options, this.cwd), {
                    output: true,
                    silence: false,
                    cwd: this.cwd,
                }).sync();
                break;
            }
            catch (error) {
                console.log(error);
                const nodeModulesInstallFailOptions = {
                    again: {
                        name: 'Try again normal installation',
                    },
                    againForce: {
                        name: 'Try again force installation',
                    },
                    skip: {
                        name: 'Skip this error and continue',
                    },
                    exit: {
                        name: 'Exit process',
                    },
                };
                const res = await index_1.HelpersTaon.consoleGui.select('What to do?', nodeModulesInstallFailOptions);
                if (res === 'again') {
                    options.force = false;
                    continue;
                }
                if (res === 'againForce') {
                    options.force = true;
                    continue;
                }
                if (res === 'exit') {
                    process.exit(0);
                }
                if (res === 'skip') {
                    break;
                }
            }
        }
        lib_1.Helpers.taskDone(`
      [${(0, lib_2.dateformat)(new Date(), 'dd-mm-yyyy HH:MM:ss')}]
      Reinstalled node modules for ${this.cwd}
      `);
        this.npmHelpers.packageJson.reloadPackageJsonInMemory();
        //#endregion
    }
    //#endregion
    //#region is link
    get isLink() {
        return lib_1.Helpers.isSymlinkFileExitedOrUnexisted(this.path);
    }
    //#endregion
    unlinkNodeModulesWhenLinked() {
        //#region @backendFunc
        if (this.isLink) {
            try {
                lib_1.Helpers.info(`Unlinking incorrect node_modules link from ${lib_2.path.basename(this.cwd)}`);
                lib_2.fse.unlinkSync(this.path);
            }
            catch (error) { }
        }
        //#endregion
    }
    //#region empty
    get empty() {
        return this.isEmpty();
    }
    //#endregion
    //#region fields & getters / empty node_modules
    /**
     * @returns true if node_modules folder is empty
     * DEEP CHECK - also if node_modules is a link
     * and is not existent
     */
    isEmpty() {
        //#region @backendFunc
        let node_modules_path = this.path;
        if (lib_1.Helpers.isUnexistedLink(node_modules_path)) {
            try {
                lib_1.Helpers.logWarn(`[npm-helpers][emptyNodeModules] removing unexisted node_modules` +
                    ` link form ${this.cwd}`);
                lib_2.fse.unlinkSync(node_modules_path);
            }
            catch (error) { }
            return true;
        }
        if (lib_1.Helpers.exists(node_modules_path)) {
            try {
                const real_node_modules_path = (0, lib_2.crossPlatformPath)(lib_2.fse.realpathSync((0, lib_2.crossPlatformPath)([this.cwd, lib_1.config.folder.node_modules])));
                node_modules_path = real_node_modules_path;
            }
            catch (error) { }
        }
        if (!lib_1.Helpers.exists(node_modules_path)) {
            return true;
        }
        if (!lib_1.Helpers.exists((0, lib_2.crossPlatformPath)([node_modules_path, '.bin']))) {
            return true;
        }
        const childrenInNodeModules = lib_3.UtilsFilesFoldersSync.getFoldersFrom(node_modules_path, {
            followSymlinks: false,
            recursive: false,
        });
        if (childrenInNodeModules.length === 0) {
            return true;
        }
        if (childrenInNodeModules.length === 1) {
            lib_1.Helpers.logWarn(`[npm-helpers] Only ` +
                `${lib_2.path.basename(lib_2._.first(childrenInNodeModules))} folder in node_modules in ${this.cwd}`);
            return true;
        }
        const minDepsLength = Object.keys(this.npmHelpers.packageJson.allDependencies).length;
        const notFullyInstalled = childrenInNodeModules.length < minDepsLength + 1;
        if (notFullyInstalled) {
            lib_1.Helpers.logWarn(`[npm-helpers] Not all deps are installed in ${this.cwd}`);
        }
        return notFullyInstalled;
        //#endregion
    }
    //#endregion
    //#region prepare command
    /**
     *  Prepare command for npm install
     * (or yarn install)
     */
    async prepareCommand(options, cwd = this.cwd) {
        //#region @backendFunc
        let { pkg, silent, useYarn, force, removeYarnOrPackageJsonLock, generateYarnOrPackageJsonLock, } = options || {};
        force = true; // TODO QUICK_FIX @UNCOMMENT
        let command = '';
        const commonOptions = `--ignore-engines`;
        if (useYarn) {
            //#region yarn
            const argsForFasterInstall = `${force ? '--force' : ''} ${commonOptions} `;
            if (removeYarnOrPackageJsonLock) {
                const yarnLock = (0, lib_2.crossPlatformPath)([cwd, lib_1.config.file.yarn_lock]);
                try {
                    lib_2.fse.unlinkSync(yarnLock);
                }
                catch (error) { }
                lib_2.fse.writeFileSync(yarnLock, ''); // simulate touch
            }
            command =
                `yarn ${pkg ? (pkg?.installType === 'remove' ? 'remove' : 'add') : 'install'} ${pkg ? pkg.name : ''} ` +
                    ` ${generateYarnOrPackageJsonLock ? '' : '--no-lockfile'} ` +
                    ` ${argsForFasterInstall} ` +
                    ` ${pkg && pkg.installType && pkg.installType === '--save-dev' ? '-dev' : ''} `;
            //#endregion
        }
        else {
            //#region npm
            const argsForFasterInstall = `${force ? '--force' : ''} ${commonOptions} --no-audit ` +
                `${silent ? '--silent --no-progress' : ''}   `;
            if (removeYarnOrPackageJsonLock) {
                const packageLock = (0, lib_2.crossPlatformPath)([
                    cwd,
                    lib_1.config.file.package_lock_json,
                ]);
                try {
                    lib_2.fse.unlinkSync(packageLock);
                }
                catch (error) { }
                lib_2.fse.writeFileSync(packageLock, ''); // simulate touch
            }
            command =
                `npx --node-options=--max-old-space-size=8000 npm ` +
                    `${pkg?.installType === 'remove' ? 'uninstall' : 'install'} ${pkg ? pkg.name : ''} ` +
                    ` ${generateYarnOrPackageJsonLock ? '' : '--no-package-lock'} ` +
                    ` ${pkg && pkg.installType ? pkg.installType : ''} ` +
                    ` ${argsForFasterInstall} `;
            //#endregion
        }
        lib_1.Helpers.info(`Command for npm install:

      ${command}

      `);
        return command;
        //#endregion
    }
    //#endregion
    //#region find package dirs
    /**
     *
     * @param rootDir
     * @param packageName
     * @returns absolute paths to package dirs
     */
    findPackageDirs(rootDir, packageName, options = {}) {
        //#region @backendFunc
        const results = [];
        const visited = new Set();
        const maxDepth = options.maxDepth ?? 10;
        const targetParts = packageName.split('/'); // handle @scope/pkg
        const walk = (dir, depth = 0) => {
            if (options.signal?.aborted)
                return;
            if (depth > maxDepth)
                return;
            if (visited.has(dir))
                return;
            visited.add(dir);
            let entries;
            try {
                entries = lib_2.fse.readdirSync(dir, { withFileTypes: true });
            }
            catch {
                return; // permission or broken symlink
            }
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                const subdir = (0, lib_2.crossPlatformPath)([dir, entry.name]);
                // we only care about node_modules structure
                if (entry.name === 'node_modules' || dir.endsWith('node_modules')) {
                    // try to match direct package or @scope/pkg structure
                    if (entry.name === targetParts[0] && targetParts.length === 1) {
                        results.push(subdir);
                        continue;
                    }
                    if (entry.name === targetParts[0] && targetParts.length > 1) {
                        const scoped = (0, lib_2.crossPlatformPath)([subdir, targetParts[1]]);
                        try {
                            const stat = lib_2.fse.statSync(scoped);
                            if (stat.isDirectory())
                                results.push(scoped);
                        }
                        catch { }
                        continue;
                    }
                    // otherwise keep descending only within node_modules
                    walk(subdir, depth + 1);
                }
            }
        };
        walk(rootDir);
        return results;
        //#endregion
    }
    //#endregion
    //#region dedupe packages action
    dedupePackages(packagesConfig, countOnly = false, fake = false) {
        //#region @backendFunc
        // packagesConfig = ['@angular/cdk', 'tnp-models'];
        lib_1.Helpers.taskStarted(`${countOnly ? 'Counting' : 'Removing'} duplicates in node_modules ${fake ? '(fake process)' : ''}...`);
        for (const entry of packagesConfig) {
            let packageNameForDuplicationRemoval;
            let excludeFrom = [];
            let includeOnlyIn = [];
            if (typeof entry === 'string') {
                packageNameForDuplicationRemoval = entry;
            }
            else {
                packageNameForDuplicationRemoval = entry.package;
                excludeFrom = entry.excludeFrom || [];
                includeOnlyIn = entry.includeOnlyIn || [];
            }
            packageNameForDuplicationRemoval = (0, lib_2.crossPlatformPath)(packageNameForDuplicationRemoval);
            lib_1.Helpers.info(`[${lib_1.config.frameworkName}] Checking npm duplicates of ${packageNameForDuplicationRemoval}`);
            const nodeModulesRoot = (0, lib_2.crossPlatformPath)([
                this.cwd,
                lib_1.config.folder.node_modules,
            ]);
            lib_1.Helpers.taskStarted(`Looking for directories ${packageNameForDuplicationRemoval}`);
            const foundAbsPaths = this.findPackageDirs(nodeModulesRoot, packageNameForDuplicationRemoval, { maxDepth: 15 });
            lib_1.Helpers.log(foundAbsPaths);
            // console.log({foundAbsPaths})
            lib_1.Helpers.taskDone(`Looking for directories done. Found: ${foundAbsPaths.length}`);
            const duplicates = foundAbsPaths.filter(foundedAbsPath => {
                // console.log({ foundedRelativePath });
                const relative = (0, lib_2.crossPlatformPath)([
                    foundedAbsPath.replace(nodeModulesRoot + '/', ''),
                ]);
                // console.log({ relative });
                if (relative?.startsWith('..')) {
                    return false;
                }
                let root = lib_2._.first(relative.split('/'));
                if (root?.startsWith('@')) {
                    root = root + '/' + relative.split('/')[1];
                }
                root = root || '';
                const packageJsonAbsPath = (0, lib_2.crossPlatformPath)([
                    this.cwd,
                    lib_1.config.folder.node_modules,
                    relative,
                    'package.json',
                ]);
                const packageJsonExtractedName = lib_1.Helpers.readJsonC(packageJsonAbsPath)?.name;
                return (packageJsonExtractedName === packageNameForDuplicationRemoval &&
                    root !== packageNameForDuplicationRemoval &&
                    lib_2._.first(root.split('/')) !== lib_1.config.folder._bin &&
                    lib_2.fse.existsSync(packageJsonAbsPath));
            });
            // console.log({ duplicates });
            duplicates.forEach(duplicatePathAbs => {
                const pathParts = duplicatePathAbs.split('/').filter(Boolean);
                const nodeModulesIndex = pathParts.lastIndexOf('node_modules');
                let parentName;
                if (pathParts[nodeModulesIndex - 2]?.startsWith('@')) {
                    parentName = pathParts[nodeModulesIndex - 2];
                }
                else {
                    parentName = pathParts[nodeModulesIndex - 1];
                }
                if (!lib_1.Helpers.exists(duplicatePathAbs)) {
                    lib_1.Helpers.warn(`Skipping non-existing path: ${duplicatePathAbs}`);
                    return;
                }
                if (excludeFrom.some(rule => parentName.includes(rule.replace('!', '')))) {
                    lib_1.Helpers.warn(`Skipping removal of ${packageNameForDuplicationRemoval} from excluded parent: ${parentName}`);
                    return;
                }
                if (includeOnlyIn.length > 0 &&
                    !includeOnlyIn.some(rule => parentName.includes(rule.replace('*', '')))) {
                    lib_1.Helpers.warn(`Skipping removal of ${packageNameForDuplicationRemoval} from non-included parent: ${parentName}`);
                    return;
                }
                if (countOnly) {
                    lib_1.Helpers.info(`Found duplicate ${packageNameForDuplicationRemoval}`);
                }
                else {
                    lib_1.Helpers.info(`Removing path ${duplicatePathAbs}`);
                    if (!fake) {
                        lib_1.Helpers.removeSymlinks(duplicatePathAbs);
                        lib_1.Helpers.remove(duplicatePathAbs);
                    }
                    // Helpers.info(
                    //   `Removed duplicate ${packageNameForDuplicationRemoval} `,
                    // );
                }
            });
        }
        lib_1.Helpers.taskDone(`${countOnly ? 'Counting' : 'Removing'} duplicates done.`);
        //#endregion
    }
}
exports.BaseNodeModules = BaseNodeModules;
//# sourceMappingURL=base-node-modules.js.map