lincd-cli
Version:
Command line tools for the lincd.js library
542 lines • 22.3 kB
JavaScript
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 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$/, '')
.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 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, 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