UNPKG

lincd-cli

Version:

Command line tools for the lincd.js library

521 lines 21.6 kB
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 * as fs from 'fs'; import * as path from 'path'; import chalk from 'chalk'; import { exec } from 'child_process'; import ts from 'typescript'; import { builtinModules } from 'module'; import { findNearestPackageJsonSync } from 'find-nearest-package-json'; import * as glob from 'glob'; import * as crypto from 'crypto'; 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) { 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(['lincd']); let lincdItself = lincdPackagePaths.find(([packageName]) => { return packageName === 'lincd'; }); if (lincdItself) { sortedPackagePaths.push(lincdItself); } 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|scss|modules)/**/*', { ignore: [ packagePath + '/**/*.scss.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 var getGruntConfig = function (root = process.cwd(), error = true) { let gruntFile = path.join(root, 'Gruntfile.js'); if (fs.existsSync(gruntFile)) { return require(gruntFile)(); } else if (root === process.cwd()) { if (error) { console.warn('Could not find Gruntfile.js. Make sure you run this command from the root of a lincd module or a lincd yarn workspace'); process.exit(); } } }; 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|scss)/, ''); 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 lincd.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) { return __awaiter(this, void 0, void 0, function* () { const entries = yield fs.promises.readdir(dir, { withFileTypes: true }); const files = yield Promise.all(entries.map((entry) => { const res = path.resolve(dir, entry.name); return entry.isDirectory() ? getFiles(res) : res; })); return Array.prototype.concat(...files); }); } //# sourceMappingURL=utils.js.map