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());
    });
};
import chalk from 'chalk';
import { exec } from 'child_process';
import * as fs from 'fs';
import { builtinModules } from 'module';
import * as path from 'path';
import ts from 'typescript';
import * as crypto from 'crypto';
import { findNearestPackageJsonSync } from 'find-nearest-package-json';
import * as glob from 'glob';
var gruntConfig;
// Credit: https://gist.github.com/tinovyatkin/727ddbf7e7e10831a1eca9e4ff2fc32e
const tsHost = ts.createCompilerHost({
    allowJs: true,
    noEmit: true,
    isolatedModules: true,
    resolveJsonModule: false,
    moduleResolution: ts.ModuleResolutionKind.Classic, // we don't want node_modules
    incremental: true,
    noLib: true,
    noResolve: true,
}, true);
export var getFileImports = function (filePath) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            const importing = [];
            const delintNode = (node) => {
                if (ts.isImportDeclaration(node)) {
                    const moduleName = node.moduleSpecifier.getText().replace(/['"]/g, '');
                    if (!moduleName.startsWith('node:') &&
                        !builtinModules.includes(moduleName))
                        importing.push(moduleName);
                }
                else
                    ts.forEachChild(node, delintNode);
            };
            const sourceFile = tsHost.getSourceFile(filePath, ts.ScriptTarget.Latest, (msg) => {
                throw new Error(`Failed to parse ${filePath}: ${msg}`);
            });
            //check if its a directory, then we wanr that we can't parse it
            let stat = fs.lstatSync(filePath);
            if (stat.isDirectory()) {
                return importing;
            }
            if (!sourceFile) {
                console.warn(`Failed to find file ${filePath}`);
                return importing;
            }
            delintNode(sourceFile);
            return importing;
        }
        catch (err) {
            console.warn(`Error parsing file ${filePath}: ${err}`);
            return [];
        }
    });
};
/**
 *
 * @param importPath The import path to check
 * @param curFileDepth How many folders deep the current file is (0 = src, 1 = src/foo, etc.)
 * @returns
 */
export var isInvalidLINCDImport = function (importPath, curFileDepth) {
    return (importPath.includes('lincd') &&
        (importPath.includes('/src/') || importPath.includes('/lib/')));
};
export var isImportOutsideOfPackage = function (importPath, curFileDepth) {
    if (importPath.includes('..')) {
        //the number of '..' in the import path should be less than or equal to the current file depth
        //if its bigger then the import is outside of the package
        return importPath.split('..').length - 1 > curFileDepth;
    }
    return false;
};
/**
 *
 * @param importPath The import path to check
 * @param curFileDepth How many folders deep the current file is (0 = src, 1 = src/foo, etc.)
 * @returns
 */
export var isValidLINCDImport = function (importPath, curFileDepth) {
    const validLincdPath = importPath.includes('lincd') && !importPath.includes('/src/');
    let validRelativePath = false;
    if (importPath.includes('..')) {
        // '../bad/path' from 'src/file.ts' should be invalid:
        //    ^ should get split into ['', '/bad/path'], and the file depth is 0
        //    meaning that it'll be invalid.
        // And this should be true for all relative imports containing '..'
        validRelativePath = importPath.split('..').length - 1 <= curFileDepth;
    }
    return validLincdPath || validRelativePath;
};
export var isImportWithMissingExtension = function (importPath) {
    //if  a relative import then it needs an extension
    if (importPath.startsWith('../') || importPath.startsWith('./')) {
        //check if the last part of the import after the last slash has a file extension
        if (importPath.split('/').pop().split('.').length < 2) {
            //if it doesn't have an extension, then it's missing, so return true
            return true;
        }
    }
    return false;
};
export var getPackageJSON = function (root = process.cwd(), error = true) {
    // console.log('Getting package.json from ' + chalk.cyan(root));
    //log stack trace
    let packagePath = path.join(root, 'package.json');
    if (fs.existsSync(packagePath)) {
        return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
    }
    else if (root === process.cwd()) {
        if (error) {
            console.warn('Could not find package.json. Make sure you run this command from the root of a lincd module or a lincd yarn workspace');
            process.exit();
        }
    }
};
/**
 * Scans package.json for dependencies that are LINCD packages
 * Also looks into dependencies of dependencies
 * If no packageJson is given, it will attempt to obtain it from the current working directory
 * Returns an array of lincd packages, with each entry containing an array with the package name and the local path to the package
 * @param packageJson
 */
export var getLINCDDependencies = function (packageJson, checkedPackages = new Set()) {
    if (!packageJson) {
        packageJson = getPackageJSON();
    }
    let dependencies = Object.assign(Object.assign({}, packageJson.dependencies), packageJson.devDependencies);
    let lincdPackagePaths = [];
    let firstTime = checkedPackages.size === 0;
    for (var dependency of Object.keys(dependencies)) {
        try {
            if (!checkedPackages.has(dependency)) {
                let [modulePackageJson, modulePath] = getModulePackageJSON(dependency);
                checkedPackages.add(dependency);
                if (modulePackageJson === null || modulePackageJson === void 0 ? void 0 : modulePackageJson.lincd) {
                    lincdPackagePaths.push([
                        modulePackageJson.name,
                        modulePath,
                        [
                            ...Object.keys(Object.assign(Object.assign({}, modulePackageJson.dependencies), modulePackageJson.devDependencies)),
                        ],
                    ]);
                    //also check if this package has any dependencies that are lincd packages
                    lincdPackagePaths = lincdPackagePaths.concat(getLINCDDependencies(modulePackageJson, checkedPackages));
                }
                if (!modulePackageJson) {
                    //this seems to only happen with yarn workspaces for some grunt related dependencies of lincd-cli
                    // console.log(`could not find package.json of ${dependency}`);
                }
            }
        }
        catch (err) {
            console.log(`could not check if ${dependency} is a lincd package: ${err}`);
        }
    }
    if (firstTime) {
        // let dependencyMap:Map<string,Set<string>> = new Map();
        let lincdPackageNames = new Set(lincdPackagePaths.map(([packageName, modulePath, pkgDependencies]) => packageName));
        //remove lincd-cli from the list of lincd packages
        lincdPackageNames.delete('lincd-cli');
        lincdPackagePaths.forEach(([packageName, modulePath, pkgDependencies], key) => {
            let lincdDependencies = pkgDependencies.filter((dependency) => lincdPackageNames.has(dependency));
            if (packageName === 'lincd-cli') {
                //remove lincd-modules from the dependencies of lincd-cli (it's not a hard dependency, and it messes things up)
                lincdDependencies.splice(lincdDependencies.indexOf('lincd-modules'), 1);
            }
            // dependencyMap.set(packageName, new Set(lincdDependencies));
            //update dependencies to be the actual lincd package objects
            lincdPackagePaths[key][2] = lincdDependencies;
        });
        // //add the nested dependencies for each lincd package
        // for (let [packageName,pkgDependencies] of dependencyMap) {
        //   pkgDependencies.forEach((dependency) => {
        //     if (dependencyMap.has(dependency)) {
        //       dependencyMap.get(dependency).forEach((nestedDependency) => {
        //         pkgDependencies.add(nestedDependency);
        //       });
        //     }
        //   });
        // }
        //
        // dependencyMap.forEach((dependencies,packageName) => {
        //   //check for circular dependencies
        //   if([...dependencies].some(dependency => {
        //     return dependencyMap.get(dependency).has(packageName);
        //   }))
        //   {
        //     console.warn(`Circular dependency detected between ${packageName} and ${dependency}`);
        //   }
        //
        // });
        // a simple sort with dependencyMap doesn't seem to work,so we start with LINCD (least dependencies) and from there add packages that have all their dependencies already added
        let sortedPackagePaths = [];
        let addedPackages = new Set();
        let lincdItself = lincdPackagePaths.find(([packageName]) => {
            return packageName === 'lincd';
        });
        if (lincdItself) {
            sortedPackagePaths.push(lincdItself);
            addedPackages = new Set(['lincd']);
        }
        while (addedPackages.size !== lincdPackagePaths.length) {
            let startSize = addedPackages.size;
            lincdPackagePaths.forEach(([packageName, modulePath, pkgDependencies]) => {
                if (!addedPackages.has(packageName) &&
                    pkgDependencies.every((dependency) => addedPackages.has(dependency))) {
                    sortedPackagePaths.push([packageName, modulePath, pkgDependencies]);
                    addedPackages.add(packageName);
                }
            });
            if (startSize === addedPackages.size) {
                console.warn('Could not sort lincd packages, circular dependencies?');
                break;
            }
        }
        //sort the lincd packages by least dependent first
        // lincdPackagePaths = lincdPackagePaths.sort(([packageNameA],[packageNameB]) => {
        //   //if package A depends on package B, then package B should come first
        //   if (dependencyMap.get(packageNameA).has(packageNameB)) {
        //     console.log(packageNameA+' depends on '+packageNameB+ ' (below)')
        //       return 1;
        //   }
        //   console.log(packageNameA+' above '+packageNameB)
        //   return -1;
        // });
        return sortedPackagePaths;
    }
    return lincdPackagePaths;
};
export const getLastBuildTime = (packagePath) => {
    return getLastModifiedFile(packagePath + '/@(builds|lib|dist)/**/*.js');
};
export const getLastModifiedSourceTime = (packagePath) => {
    return getLastModifiedFile(packagePath + '/@(src|data|css|modules)/**/*', {
        ignore: [
            packagePath + '/**/*.css.json',
            packagePath + '/**/*.d.ts',
            packagePath + '/**/node_modules/**/*',
            packagePath + '/**/lib/**/*',
            packagePath + '/**/dist/**/*',
        ],
    });
};
export const getLastCommitTime = (packagePath) => {
    // console.log(`git log -1 --format=%ci -- ${packagePath}`);
    // process.exit();
    return execPromise(`cd ${packagePath} && git log -1 --format="%h %ci" -- .`)
        .then((result) => __awaiter(void 0, void 0, void 0, function* () {
        let commitId = result.substring(0, result.indexOf(' '));
        let date = result.substring(commitId.length + 1);
        let lastCommitDate = new Date(date);
        let changes = yield execPromise(`cd ${packagePath} && git show --stat --oneline ${commitId} -- .`);
        // log(packagePath, result, lastCommitDate);
        // log(changes);
        return { date: lastCommitDate, changes, commitId };
    }))
        .catch(({ error, stdout, stderr }) => {
        debugInfo(chalk.red('Git error: ') + error.message.toString());
        return null;
    });
};
export const getLastModifiedFile = (filePath, config = {}) => {
    var files = glob.sync(filePath, config);
    // console.log(files.join(" - "));
    var lastModifiedName;
    var lastModified;
    var lastModifiedTime = 0;
    files.forEach((fileName) => {
        if (fs.lstatSync(fileName).isDirectory()) {
            // console.log("skipping directory "+fileName);
            return;
        }
        let mtime = fs.statSync(path.join(fileName)).mtime;
        let modifiedTime = mtime.getTime();
        if (modifiedTime > lastModifiedTime) {
            // console.log(fileName,mtime);
            lastModifiedName = fileName;
            lastModified = mtime;
            lastModifiedTime = modifiedTime;
        }
    });
    return { lastModified, lastModifiedName, lastModifiedTime };
};
//from https://github.com/haalcala/node-packagejson/blob/master/index.js
export var getModulePackageJSON = function (module_name, work_dir) {
    if (!work_dir) {
        work_dir = process.cwd();
    }
    else {
        work_dir = path.resolve(work_dir);
    }
    var package_json;
    if (fs.existsSync(path.resolve(work_dir, './node_modules'))) {
        var module_dir = path.resolve(work_dir, './node_modules/' + module_name);
        if (fs.existsSync(module_dir) &&
            fs.existsSync(module_dir + '/package.json')) {
            package_json = JSON.parse(fs.readFileSync(module_dir + '/package.json', 'utf-8'));
        }
    }
    if (!package_json && work_dir != '/') {
        return getModulePackageJSON(module_name, path.resolve(work_dir, '..'));
    }
    return [package_json, module_dir];
};
export function execp(cmd, log = false, allowError = false, options = {}) {
    // opts || (opts = {});
    if (log)
        console.log(chalk.cyan(cmd));
    return new Promise((resolve, reject) => {
        var child = exec(cmd, options);
        child.stdout.pipe(process.stdout);
        child.stderr.pipe(process.stderr);
        // process.stdin.pipe(child.stdin);
        child.on('close', function (code) {
            if (code === 0) {
                resolve(null);
            }
            else {
                reject();
            }
            // console.log('killing child');
            // child.kill('SIGHUP');
            // resolve(code);
        });
        // child.on('data', function (result) {
        // 	// if (log)
        // 	// {
        // 	// 	console.log(result);
        // 	// }
        // 	resolve(result);
        // 	console.log('resolve data');
        //
        // });
        child.on('error', function (err) {
            if (!allowError) {
                // console.log('reject err');
                reject(err);
                return;
            }
            else if (log) {
                console.warn(err);
            }
            // console.log('resolve err');
            resolve(null);
        });
        child.on('exit', function (code, signal) {
            if (code !== 0) {
                reject('Child process exited with error code ' + code);
                return;
            }
            // console.log('resolve exit');
            resolve(null);
        });
    });
}
export function execPromise(command, log = false, allowError = false, options, pipeOutput = false) {
    return new Promise(function (resolve, reject) {
        if (log)
            console.log(chalk.cyan(command));
        let child = exec(command, options, (error, stdout, stderr) => {
            if (error) {
                if (!allowError) {
                    reject({ error, stdout, stderr });
                    return;
                }
                else if (log) {
                    console.warn(error);
                }
            }
            //TODO: getting a typescript error for 'trim()', this worked before, is it still used? do we log anywhere?
            let result = stdout['trim']();
            if (log) {
                // console.log(chalk"RESOLVING "+command);
                console.log(result);
                // console.log('ERRORS:'+(result.indexOf('Aborted due to warnings') !== -1));
                // console.log('stderr:'+stderr);
            }
            resolve(result);
        });
        if (pipeOutput) {
            child.stdout.pipe(process.stdout);
            child.stderr.pipe(process.stderr);
        }
    });
}
export function generateScopedNameProduction(cssClassName, filepath, css) {
    //for app development we can use short unique hashes
    //but for webpack bundles of lincd modules, we need to ensure unique class names across bundles of many packages
    //generate a short unique hash based on cssClassName and filepath
    let hash = crypto
        .createHash('md5')
        .update(cssClassName + filepath)
        .digest('hex')
        .substring(0, 6);
    return hash;
}
export function generateScopedName(cssClassName, filepath, css) {
    // return cssClassName;
    var filename = path
        .basename(filepath)
        .replace(/\.module\.css$/, '')
        .replace(/\.css$/, '');
    let resolved = path.resolve(filepath).replace(/[\w\-_\/]+\/file\:/, '');
    let nearestPackageJson = findNearestPackageJsonSync(resolved);
    let packageName = nearestPackageJson
        ? nearestPackageJson.data.name
        : 'unknown';
    return (packageName.replace(/[^a-zA-Z0-9_]+/g, '_') +
        '_' +
        filename +
        '_' +
        cssClassName);
}
export const needsRebuilding = function (pkg_1, useGitForLastModified_1) {
    return __awaiter(this, arguments, void 0, function* (pkg, useGitForLastModified, log = false) {
        let lastModifiedSourceDate;
        let lastModifiedSourceName;
        if (useGitForLastModified) {
            const { changes, commitId, date } = yield getLastCommitTime(pkg.path);
            lastModifiedSourceDate = date;
            lastModifiedSourceName = commitId;
        }
        else {
            const { lastModified, lastModifiedName, lastModifiedTime } = getLastModifiedSourceTime(pkg.path);
            lastModifiedSourceName = lastModifiedName;
            lastModifiedSourceDate = lastModified;
        }
        let lastModifiedBundle = getLastBuildTime(pkg.path);
        let result = lastModifiedSourceDate &&
            lastModifiedSourceDate.getTime() > lastModifiedBundle.lastModifiedTime;
        if (log) {
            console.log(chalk.cyan('Last modified source: ' +
                lastModifiedSourceName +
                ' on ' +
                lastModifiedSourceDate.toString()));
            console.log(chalk.cyan('Last build: ' +
                (lastModifiedBundle &&
                    typeof lastModifiedBundle.lastModified !== 'undefined'
                    ? lastModifiedBundle.lastModified.toString()
                    : 'never')));
        }
        return result;
    });
};
export function log(...messages) {
    messages.forEach((message) => {
        console.log(chalk.cyan(message));
    });
}
export function debug(config, ...messages) {
    if (config.debug) {
        log(...messages);
    }
}
export function debugInfo(...messages) {
    // messages.forEach((message) => {
    //   console.log(chalk.cyan('Info: ') + message);
    // });
    //@TODO: let packages also use linked.config.json? instead of gruntfile...
    // that way we can read "analyse" here and see if we need to log debug info
    // if(!gruntConfig)
    // {
    //   gruntConfig = getGruntConfig();
    //   console.log(gruntConfig);
    //   process.exit();
    // }
    if (gruntConfig && gruntConfig.analyse === true) {
        messages.forEach((message) => {
            console.log(chalk.cyan('Info: ') + message);
        });
    }
}
export function warn(...messages) {
    messages.forEach((message) => {
        console.log(chalk.red(message));
    });
}
export function flatten(arr) {
    return arr.reduce(function (a, b) {
        return b ? a.concat(b) : a;
    }, []);
}
export function getLinkedTailwindColors() {
    return {
        'primary-color': 'var(--primary-color)',
        'font-color': 'var(--font-color)',
    };
}
/**
 * Recursively get all files in a directory
 * https://stackoverflow.com/a/45130990/831465
 * @param dir The directory to get files from
 * @returns A promise that resolves to an array of file paths
 */
export function getFiles(dir, filenameFilter) {
    return __awaiter(this, void 0, void 0, function* () {
        const entries = yield fs.promises.readdir(dir, { withFileTypes: true });
        let files = yield Promise.all(entries.map((entry) => {
            const res = path.resolve(dir, entry.name);
            return entry.isDirectory() ? getFiles(res, filenameFilter) : res;
        }));
        //flatten the array of arrays into a single array
        let flatFiles = flatten(files);
        if (filenameFilter) {
            //filter the files to only include those that match the filenameFilter
            flatFiles = flatFiles.filter((file) => file.includes(filenameFilter));
        }
        return flatFiles;
    });
}
/**
 * Strip ANSI escape codes from a string
 * @param str The string to strip ANSI codes from
 * @returns The string with ANSI codes removed
 */
export function stripAnsi(str) {
    return str.replace(/\x1b\[[0-9;]*m/g, '');
}
//# sourceMappingURL=utils.js.map