dynamic-doctor
Advanced tools
Comparing version 0.0.5 to 0.1.0-beta.0
@@ -5,3 +5,3 @@ #! /usr/bin/env node | ||
var commander = require('commander'); | ||
var enquirer = require('enquirer'); | ||
var pkg = require('enquirer'); | ||
var fs = require('fs'); | ||
@@ -14,2 +14,4 @@ var child_process = require('child_process'); | ||
var promises = require('fs/promises'); | ||
var satisfies = require('semver/functions/satisfies'); | ||
var archy = require('archy'); | ||
@@ -111,15 +113,11 @@ const getPackageManagerVersion = (packageManager) => { | ||
console.log(...args); | ||
this.newLine(); | ||
} | ||
static error(...args) { | ||
console.log(chalk.red(...args)); | ||
this.newLine(); | ||
} | ||
static success(...args) { | ||
console.log(chalk.green(...args)); | ||
this.newLine(); | ||
} | ||
static warning(...args) { | ||
console.log(chalk.yellow(...args)); | ||
this.newLine(); | ||
} | ||
@@ -132,5 +130,3 @@ static dashedLine() { | ||
} | ||
this.newLine(); | ||
console.log('─'.repeat(columns)); | ||
this.newLine(); | ||
} | ||
@@ -563,4 +559,5 @@ static newLine() { | ||
const { prompt } = pkg; | ||
const startDynamicDoctor = async () => { | ||
const { confirm } = await enquirer.prompt({ | ||
const { confirm } = await prompt({ | ||
type: 'confirm', | ||
@@ -592,3 +589,3 @@ name: 'confirm', | ||
DoctorLogger.error('An error occurred while running dynamic doctor.'); | ||
const { confirm: seeLogsConfirm } = await enquirer.prompt({ | ||
const { confirm: seeLogsConfirm } = await prompt({ | ||
type: 'confirm', | ||
@@ -616,2 +613,423 @@ name: 'confirm', | ||
const ROOT_REF = '.'; | ||
const REF_SEPARATOR = ':'; | ||
const DYNAMIC_PACKAGES = [/@dynamic-labs\/.*/]; | ||
const IGNORE_PACKAGES = [ | ||
'@dynamic-labs/sdk-api', | ||
'@dynamic-labs/iconic', | ||
]; | ||
const getModuleReferenceRecursive = (moduleTree, ref, originalRef = ref) => { | ||
// Return the root module tree | ||
if (ref === ROOT_REF) { | ||
return moduleTree; | ||
} | ||
// Split the ref into module parts | ||
const refParts = ref.split(REF_SEPARATOR); | ||
// Grab the left most module | ||
let nextModule = refParts.shift(); | ||
if (nextModule === ROOT_REF) { | ||
nextModule = refParts.shift(); | ||
} | ||
if (nextModule) { | ||
// As we are descending the tree, we need to make sure the next module exists | ||
if (!moduleTree.modules[nextModule]) { | ||
throw new Error(`Invalid ref: ${originalRef}, could not find next module: ${nextModule}`); | ||
} | ||
// If there are no more parts, we have found the module | ||
if (refParts.length === 0) { | ||
return moduleTree.modules[nextModule]; | ||
} | ||
else { | ||
// Otherwise, recurse down the tree | ||
return getModuleReferenceRecursive(moduleTree.modules[nextModule], refParts.join(REF_SEPARATOR), originalRef); | ||
} | ||
} | ||
// If we have not returned by now, the ref is invalid | ||
throw new Error(`Invalid ref: ${originalRef}`); | ||
}; | ||
/** | ||
* Get the module reference in the tree given a ref string | ||
* | ||
* This does not resolve for missing modules, the module MUST exist in the tree | ||
* @example getModuleReference('module1:module2:module3', moduleTree) | ||
* @param ref A colon separated string of module names to traverse | ||
* @param moduleTree The root module tree | ||
* @returns The module tree at the ref | ||
*/ | ||
const getModuleReference = (moduleTree, ref) => getModuleReferenceRecursive(moduleTree, ref); | ||
const getNextRefStep = (ref) => { | ||
const modules = ref.split(REF_SEPARATOR); | ||
modules.pop(); // Drop last element | ||
const nextRef = modules.join(REF_SEPARATOR); | ||
return nextRef; | ||
}; | ||
const resolveModuleRecursive = (moduleTree, dependencyRef, moduleName, rootModuleTree = moduleTree) => { | ||
const currentModuleTree = getModuleReference(rootModuleTree, dependencyRef); | ||
if (currentModuleTree.modules[moduleName]) { | ||
return `${dependencyRef}${REF_SEPARATOR}${moduleName}`; | ||
} | ||
else if (dependencyRef === '.') { | ||
return null; | ||
} | ||
const nextRef = getNextRefStep(dependencyRef); | ||
const nextModuleTree = getModuleReference(rootModuleTree, nextRef); | ||
return resolveModuleRecursive(nextModuleTree, nextRef, moduleName, rootModuleTree); | ||
}; | ||
const resolveModule = (moduleTree, startingRef, moduleName) => resolveModuleRecursive(moduleTree, startingRef, moduleName); | ||
const resolveModuleTreeModule = (type, moduleName, module, moduleTree) => { | ||
const depType = type === 'dependency' ? 'dependencies' : 'peerDependencies'; | ||
const deps = moduleTree[depType]; | ||
if (deps && deps[moduleName]) { | ||
moduleTree.resolved[moduleName] = { | ||
type, | ||
ref: `${moduleTree.ref}${REF_SEPARATOR}${moduleName}`, | ||
optional: false, | ||
satisfied: satisfies(module.version, deps[moduleName]), | ||
requiredVersion: deps[moduleName], | ||
installedVersion: module.version, | ||
}; | ||
} | ||
}; | ||
const resolveModuleTreeModules = (moduleTree, rootModuleTree = moduleTree) => { | ||
Object.entries(moduleTree.modules).forEach(([moduleName, module]) => { | ||
resolveModuleTreeModule('dependency', moduleName, module, moduleTree); | ||
resolveModuleTreeModule('peerDependency', moduleName, module, moduleTree); | ||
resolveModuleTreeRecursive(module, rootModuleTree); | ||
}); | ||
}; | ||
const resolveModuleTreeDependenciesModule = (type, moduleTree, rootModuleTree) => { | ||
const depType = type === 'dependency' ? 'dependencies' : 'peerDependencies'; | ||
const deps = moduleTree[depType]; | ||
if (deps) { | ||
Object.entries(deps).forEach(([moduleName, version]) => { | ||
if (!moduleTree.resolved[moduleName]) { | ||
const resolvedRef = resolveModule(rootModuleTree, moduleTree.ref, moduleName); | ||
const optional = moduleTree.peerDependenciesMeta?.[moduleName]?.optional ?? false; | ||
moduleTree.resolved[moduleName] = { | ||
type, | ||
ref: resolvedRef, | ||
optional, | ||
satisfied: false, | ||
requiredVersion: version, | ||
installedVersion: null, | ||
}; | ||
if (resolvedRef) { | ||
const module = getModuleReference(rootModuleTree, resolvedRef); | ||
moduleTree.resolved[moduleName].satisfied = satisfies(module.version, version); | ||
moduleTree.resolved[moduleName].installedVersion = module.version; | ||
module.isDynamic = moduleTree.isDynamic; | ||
resolveModuleTreeRecursive(module, rootModuleTree); | ||
} | ||
} | ||
}); | ||
} | ||
}; | ||
const resolveModuleTreeDependencies = (moduleTree, rootModuleTree = moduleTree) => { | ||
resolveModuleTreeDependenciesModule('dependency', moduleTree, rootModuleTree); | ||
resolveModuleTreeDependenciesModule('peerDependency', moduleTree, rootModuleTree); | ||
}; | ||
const resolveModuleTreeRecursive = (moduleTree, rootModuleTree = moduleTree) => { | ||
resolveModuleTreeModules(moduleTree, rootModuleTree); | ||
resolveModuleTreeDependencies(moduleTree, rootModuleTree); | ||
return moduleTree; | ||
}; | ||
const resolveModuleTree = (moduleTree) => { | ||
moduleTree = structuredClone(moduleTree); | ||
return resolveModuleTreeRecursive(moduleTree); | ||
}; | ||
const filterDynamicModules = (moduleTree) => { | ||
return Object.fromEntries(Object.entries(moduleTree.modules).filter(([, module]) => module.isDynamic)); | ||
}; | ||
const auditResultsRecursive = (moduleTree, auditResultsStore = {}) => { | ||
const dynamicModuleTree = filterDynamicModules(moduleTree); | ||
Object.values(dynamicModuleTree).forEach((module) => { | ||
Object.entries(module.resolved).forEach(([resolvedModuleName, resolvedModule]) => { | ||
if (!resolvedModule.satisfied && !resolvedModule.optional) { | ||
const key = `${module.ref}${REF_SEPARATOR}${resolvedModuleName}`; | ||
auditResultsStore[key] = { | ||
...resolvedModule, | ||
name: resolvedModuleName, | ||
}; | ||
if (resolvedModule.ref) { | ||
const referencedModule = getModuleReference(moduleTree, resolvedModule.ref); | ||
auditResultsStore[key].dependency = referencedModule; | ||
auditResultsRecursive(referencedModule, auditResultsStore); | ||
} | ||
} | ||
}); | ||
}); | ||
return auditResultsStore; | ||
}; | ||
const auditResults = (moduleTree) => { | ||
return auditResultsRecursive(moduleTree); | ||
}; | ||
const isDynamicPackage = (...dynamicRefs) => { | ||
const refs = dynamicRefs.flat().map((ref) => ref.split(':')); | ||
return refs.some((ref) => { | ||
return ref.some((refPart) => { | ||
return DYNAMIC_PACKAGES.some((p) => p.test(refPart) && !isIgnoredPackage(refPart)); | ||
}); | ||
}); | ||
}; | ||
const isIgnoredPackage = (packageName) => { | ||
return IGNORE_PACKAGES.includes(packageName); | ||
}; | ||
////////////////////////////// | ||
// Helper Methods | ||
////////////////////////////// | ||
/** | ||
* Reads a json file and returns the parsed json. | ||
* @param filePath | ||
* @returns | ||
*/ | ||
const readJsonFile = (filePath) => { | ||
return JSON.parse(fs.readFileSync(filePath, 'utf8').trim()); | ||
}; | ||
/** | ||
* Merges the scoped packages into the packages array. | ||
* @param basePath | ||
* @param packages | ||
* @param dir | ||
* @returns | ||
*/ | ||
const mergeScopes = (basePath, packages, dir) => { | ||
if (/^@/.test(dir)) { | ||
const scopedSuffixPackages = fs | ||
.readdirSync(path.join(basePath, dir)) | ||
.filter((packageName) => !/^\./.test(packageName)); | ||
return packages.concat(scopedSuffixPackages.map((p) => path.join(dir, p))); | ||
} | ||
else { | ||
return packages.concat(dir); | ||
} | ||
}; | ||
////////////////////////////// | ||
// END Helper Methods | ||
////////////////////////////// | ||
const initialBranch = () => structuredClone({ | ||
name: '', | ||
version: '', | ||
dependencies: {}, | ||
isDynamic: false, | ||
peerDependencies: {}, | ||
peerDependenciesMeta: {}, | ||
resolved: {}, | ||
ref: '', | ||
moduleLineage: [], | ||
modules: {}, | ||
modulePath: '', | ||
}); | ||
// This variable will be mutated by the buildModuleTree function | ||
const moduleTreeStore = initialBranch(); | ||
const resolveLineage = (modulePath) => { | ||
const lineage = modulePath | ||
.split(/\/?node_modules\//) | ||
.filter((p) => p && p !== ROOT_REF); | ||
lineage.unshift(ROOT_REF); | ||
return lineage; | ||
}; | ||
const buildModuleTreeRecursive = (basePath, rootBasePath, currentModuleBranch = moduleTreeStore, isDynamicFlag = false) => { | ||
const packageJsonPath = path.join(basePath, 'package.json'); | ||
const nodeModulesPath = path.join(basePath, 'node_modules'); | ||
const isRootPkg = basePath === rootBasePath; | ||
if (fs.existsSync(packageJsonPath)) { | ||
const modulePath = isRootPkg ? '.' : path.relative(rootBasePath, basePath); | ||
const packageJson = readJsonFile(packageJsonPath); | ||
const ref = resolveLineage(modulePath).join(REF_SEPARATOR); | ||
isDynamicFlag = isDynamicFlag || isDynamicPackage(ref); | ||
// If we are a nested module, set the moduleTree branch to the current module. | ||
currentModuleBranch = isRootPkg | ||
? currentModuleBranch | ||
: (currentModuleBranch['modules'][packageJson.name] = initialBranch()); | ||
Object.assign(currentModuleBranch, { | ||
name: packageJson.name, | ||
version: packageJson.version, | ||
isDynamic: isDynamicFlag, | ||
dependencies: packageJson.dependencies, | ||
peerDependencies: packageJson.peerDependencies, | ||
peerDependenciesMeta: packageJson.peerDependenciesMeta, | ||
ref, | ||
moduleLineage: resolveLineage(modulePath), | ||
modules: {}, | ||
modulePath, | ||
}); | ||
if (fs.existsSync(nodeModulesPath)) { | ||
const nodeModulesPackages = fs.readdirSync(nodeModulesPath); | ||
// Filter out hidden directories (eg. .bin) | ||
// Get fully scoped package names (eg. @dynamic-labs/sdk-api) | ||
const nodules = nodeModulesPackages | ||
.filter((dir) => !/^\./.test(dir)) // no hidden directories (eg. .bin) | ||
.reduce((packages, dir) => mergeScopes(nodeModulesPath, packages, dir), []); | ||
// Recurse through modules | ||
nodules.forEach((nmPackage) => buildModuleTreeRecursive(path.join(nodeModulesPath, nmPackage), rootBasePath, currentModuleBranch, isDynamicFlag)); | ||
} | ||
} | ||
return currentModuleBranch; | ||
}; | ||
const buildModuleTree = (basePath = process.cwd(), rootBasePath = process.cwd()) => buildModuleTreeRecursive(basePath, rootBasePath); | ||
// const isOk = () => { | ||
// return ( | ||
// !result.invalidDepVersion.length && | ||
// !result.invalidPeerVersion.length && | ||
// !result.missingDep.length && | ||
// !result.missingPeer.length | ||
// ); | ||
// }; | ||
// export const reporter = () => { | ||
// // Emoki check | ||
// const status = chalk.bold( | ||
// isOk() ? chalk.green('✅ OK') : chalk.red('❌ FAIL'), | ||
// ); | ||
// console.log( | ||
// `Sanity Checking ${chalk.bold('Dynamic SDK')} Dependencies... ${status}`, | ||
// ); | ||
// if (isOk()) { | ||
// return; | ||
// } | ||
// report('dependencies', false, result.invalidDepVersion); | ||
// report('peer dependencies', false, result.invalidPeerVersion); | ||
// report('dependencies', true, result.missingDep); | ||
// report('peer dependencies', true, result.missingPeer); | ||
// console.log( | ||
// chalk.magentaBright( | ||
// '🛠 Ensure all dependencies are installed and meet version requirements.', | ||
// ), | ||
// ); | ||
// DoctorLogger.newLine(); | ||
// }; | ||
// const report = ( | ||
// depType: 'peer dependencies' | 'dependencies', | ||
// missing: boolean, | ||
// resultObj: | ||
// | Result['invalidPeerVersion'] | ||
// | Result['invalidDepVersion'] | ||
// | Result['missingDep'] | ||
// | Result['missingPeer'], | ||
// ) => { | ||
// if (!resultObj.length) { | ||
// return; | ||
// } | ||
// DoctorLogger.dashedLine(); | ||
// console.log(chalk.bold(`${missing ? 'Missing' : 'Incorrect'} ${depType}:`)); | ||
// resultObj.forEach(({ name, lineage, ...rest }) => { | ||
// const { requiredVersion, version } = rest as | ||
// | Result['invalidDepVersion'][number] | ||
// | Result['invalidPeerVersion'][number]; | ||
// lineage.push(name); | ||
// const hierarchy: Data = lineage.reduceRight((acc, curr) => { | ||
// if (isEmpty(acc)) { | ||
// const label = `${curr}${version ? `@${version}` : ''}`; | ||
// acc['label'] = chalk.yellow( | ||
// `${label} ${ | ||
// requiredVersion | ||
// ? chalk.red(`Incorrect version Required: ${requiredVersion}`) | ||
// : '' | ||
// }`, | ||
// ); | ||
// return acc; | ||
// } | ||
// return { | ||
// label: curr, | ||
// nodes: [acc], | ||
// }; | ||
// }, {} as Data); | ||
// console.log(archy(hierarchy)); | ||
// }); | ||
// process.exitCode = 1; | ||
// }; | ||
const isDataObj = (obj) => { | ||
if (typeof obj === 'string' || !obj) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
const getNodeByLabel = (label, data) => { | ||
const existingNode = data.nodes?.find((node) => isDataObj(node) && node.label === label); | ||
if (isDataObj(existingNode)) { | ||
return existingNode; | ||
} | ||
const newNode = { | ||
label, | ||
nodes: [], | ||
}; | ||
data.nodes?.push(newNode); | ||
return newNode; | ||
}; | ||
const capitalizeFirstLetter = (str) => { | ||
return str.charAt(0).toUpperCase() + str.slice(1); | ||
}; | ||
const getLabel = (ref, auditResult) => { | ||
const target = ref.split(REF_SEPARATOR).pop(); | ||
if (ref !== target) { | ||
return ref; | ||
} | ||
if (!auditResult.dependency) { | ||
return chalk.red(`${chalk.yellow(target)} ${capitalizeFirstLetter(auditResult.type)} not found. Required version: ${auditResult.requiredVersion}`); | ||
} | ||
return chalk.red(`${chalk.yellow(`${target}@${auditResult.installedVersion}`)} ${capitalizeFirstLetter(auditResult.type)} is not the required version: ${auditResult.requiredVersion}`); | ||
}; | ||
// Converts AuditResults to archy.Data using the key of AuditResults seperated by REF_SEPARATOR as nodes | ||
const convertToArchy = (auditResults, archyData = { | ||
label: '', | ||
nodes: [], | ||
}) => { | ||
Object.entries(auditResults).forEach(([key, value]) => { | ||
const refParts = key.split(REF_SEPARATOR); | ||
let data = archyData; | ||
while (refParts.length > 0) { | ||
const [parent, child] = refParts; | ||
if (parent === ROOT_REF) { | ||
data = getNodeByLabel(child, archyData); | ||
} | ||
else if (child) { | ||
data.nodes?.push({ | ||
label: getLabel(child, value), | ||
nodes: [], | ||
}); | ||
} | ||
refParts.shift(); | ||
} | ||
}); | ||
return archyData; | ||
}; | ||
const reporter = (auditResults) => { | ||
return archy(convertToArchy(auditResults)); | ||
}; | ||
const checkDependencies = (path) => { | ||
try { | ||
const moduleTree = buildModuleTree(path, path); | ||
const resolvedModuleTree = resolveModuleTree(moduleTree); | ||
const audit = auditResults(resolvedModuleTree); | ||
//console.log(JSON.stringify(resolveModuleTree(rawTree), null, 2)); | ||
// const tree = annotateTree(rawTree); | ||
// const filteredTree = Object.fromEntries( | ||
// Object.entries(tree).filter(([, value]) => value?.meta?.isDynamic), | ||
// ); | ||
// recurseVersionCheck(filteredTree); | ||
// reporter(); | ||
//console.log(reporter(audit)); | ||
console.log(reporter(audit)); | ||
} | ||
catch (e) { | ||
DoctorLogger.error(e); | ||
throw e; | ||
} | ||
}; | ||
const sanityCommand = () => { | ||
const command = new commander.Command('sanity'); | ||
command.argument('[path]', 'Path of the project using Dynamic SDK (defaults to current directory)'); | ||
command.action(checkDependencies); | ||
command.enablePositionalOptions(); | ||
return command; | ||
}; | ||
class DynamicCLI { | ||
@@ -624,2 +1042,3 @@ constructor() { | ||
}); | ||
this.program.addCommand(sanityCommand()); | ||
} | ||
@@ -626,0 +1045,0 @@ run() { |
@@ -12,3 +12,3 @@ { | ||
}, | ||
"version": "0.0.5", | ||
"version": "0.1.0-beta.0", | ||
"license": "MIT", | ||
@@ -42,2 +42,3 @@ "bin": { | ||
"dependencies": { | ||
"archy": "^1.0.0", | ||
"chalk": "4.1.2", | ||
@@ -55,2 +56,3 @@ "commander": "^10.0.1", | ||
"@rollup/plugin-typescript": "^11.1.2", | ||
"@types/archy": "^0.0.36", | ||
"@types/chalk": "^2.2.0", | ||
@@ -57,0 +59,0 @@ "@types/commander": "^2.12.2", |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1761190
95
45178
6
32
3
+ Addedarchy@^1.0.0
+ Addedarchy@1.0.0(transitive)