Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ $ dependency-report './client/**/*.js' --packages=evergreen-ui --exports=SideShe
## Usage

```javascript
const DependencyReport = require('@segment/dependency-report')
import DependencyReport from '@segment/dependency-report'

const report = new DependencyReport({
files: '**/*.js'
Expand Down
28 changes: 13 additions & 15 deletions lib/ast-runner.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
'use strict'
const recast = require('recast')
import recast from 'recast'

const visit = recast.types.visit
const types = recast.types.namedTypes
const { visit, namedTypes } = recast.types

function getExportNames(specifiers) {
const exportNames = []

for (const specifier of specifiers) {
const exportName = specifier.local.name

if (types.ImportDefaultSpecifier.check(specifier)) {
if (namedTypes.ImportDefaultSpecifier.check(specifier)) {
exportNames.push(exportName)
} else {
// Only non-default specifiers have `imported`
Expand All @@ -23,29 +21,27 @@ function getExportNames(specifiers) {
return exportNames
}

module.exports = (contents, parserName) => {
const parser =
parserName === 'typescript'
? require('./typescript-tsx-parser') // Custom parse to set `isTSX: true` and provide `jsx` plugin
: require(`recast/parsers/${parserName || 'babel'}`)
async function runAst(contents, parserName = 'babel') {
const parserImport = parserName === 'typescript'
? './typescript-tsx-parser.js' // Custom parse to set `isTSX: true` and provide `jsx` plugin
: `recast/parsers/${parserName}.js`

const parser = await import(parserImport)
const ast = recast.parse(contents, { isTSX: true, parser })
const packages = []
let exportNames = []

// Loop over all the import statements
visit(ast, {
visitImportDeclaration(path) {
const specifiers = path.node.specifiers
const packageName = path.node.source.value
this.traverse(path)

const results = getExportNames(specifiers)
const results = getExportNames(path.node.specifiers)

exportNames = exportNames.concat(results)
exportNames = [...exportNames, ...results]

const packageObject = {
name: packageName,
name: path.node.source.value,
exportNames: results
}

Expand All @@ -58,3 +54,5 @@ module.exports = (contents, parserName) => {
exportNames
}
}

export default runAst
32 changes: 17 additions & 15 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/env node
'use strict'
const meow = require('meow')
const ora = require('ora')
const chalk = require('chalk')
const DependencyReport = require('./dependency-report')
import process from 'node:process'
import meow from 'meow'
import ora from 'ora'
import DependencyReport from './dependency-report.js'

// Since we're CPU bound, loading lots of files at the same time just hurts performance
let spinner
Expand Down Expand Up @@ -40,14 +39,14 @@ Examples
}
)

function toJSON(obj) {
return JSON.stringify(obj, null, 2)
function toJSON(object) {
return JSON.stringify(object, null, 2)
}

async function main() {
const inputPackages = cli.flags.packages ? cli.flags.packages.split(',') : []
const inputExports = cli.flags.exports ? cli.flags.exports.split(',') : []
const parser = cli.flags.parser
const { parser } = cli.flags

// A glob is required
if (cli.input.length === 0) {
Expand Down Expand Up @@ -75,9 +74,9 @@ async function main() {
})

await report.run()
} catch (err) {
console.error(err)
spinner.fail(err)
} catch (error) {
console.error(error)
spinner.fail(error)
process.exitCode = 2
return
}
Expand All @@ -97,7 +96,7 @@ async function main() {
console.log(toJSON(result))
}
} else {
chalk.red(`no packages found for: ${cli.flags.packages}`)
console.log(`no packages found for: ${cli.flags.packages}`)
}
} else if (inputExports.length > 0) {
const exportsUsage = report.getByExportNames(inputExports)
Expand All @@ -107,11 +106,14 @@ async function main() {
}
}

main().catch(err => {
try {
await main()
} catch(error) {
// Handle uncaught errors gracefully
if (spinner) {
spinner.fail()
}
console.error(err)

console.error(error)
process.exitCode = 1
})
}
125 changes: 63 additions & 62 deletions lib/dependency-report.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use strict'
const path = require('path')
const globby = require('globby')
const fs = require('fs-extra')
const pLimit = require('p-limit')
const minimatch = require('minimatch')
const arrify = require('arrify')
const astRunner = require('./ast-runner')
const Package = require('./package')
import path from 'node:path'
import fs from 'node:fs/promises'
import { globby } from 'globby'
import pLimit from 'p-limit'
import { minimatch } from 'minimatch'
import astRunner from './ast-runner.js'
import Package from './package.js'

// Since we're CPU bound, loading lots of files at the same time just hurts performance
const limit = pLimit(5)
Expand All @@ -16,7 +14,7 @@ function DependencyReport(options) {
throw new Error('No options object being passed to DependencyReport.')
}

if (!Object.hasOwnProperty.call(options, 'files')) {
if (!Object.hasOwn(options, 'files')) {
throw new Error('No files being passed to DependencyReport.')
}

Expand All @@ -33,8 +31,8 @@ function DependencyReport(options) {
*/
DependencyReport.prototype.run = async function() {
const filepaths = await globby([...this.files, this.excludeGlob])
const packages = {}
const exportNames = {}
const packages = new Map()
const exportNames = new Map()

if (filepaths.length === 0) {
throw new Error('No matching files found.')
Expand All @@ -45,53 +43,56 @@ DependencyReport.prototype.run = async function() {
.map(filepath =>
limit(async () => {
// Limit concurrency
const contents = await fs.readFile(filepath, { encoding: 'utf-8' })

const contents = await fs.readFile(filepath, { encoding: 'utf8' })
let result
try {
result = astRunner(contents, this.parser)
} catch (err) {
console.error(`AST Runner failed on filepath: ${filepath}: ${err}`)
result = await astRunner(contents, this.parser)
} catch (error) {
console.error(`AST Runner failed on filepath: ${filepath}: ${error}`)
return null
}

result.packages.forEach(packageObject => {
for (const packageObject of result.packages) {
/**
* If the packageObject.name is a path, join it to the filepath.
*/
const packageKey =
packageObject.name.indexOf('./') > -1
packageObject.name.includes('./')
? path.join(filepath, packageObject.name)
: packageObject.name

if (!packages[packageKey]) {
packages[packageKey] = new Package(packageKey)
if (!packages.has(packageKey)) {
packages.set(packageKey, new Package(packageKey))
}
packages[packageKey].addExports(packageObject.exportNames, filepath)

packages
.get(packageKey)
.addExports(packageObject.exportNames, filepath)

packages[packageKey].addFilepath(filepath)
packages.get(packageKey).addFilepath(filepath)

// By export
packageObject.exportNames.forEach(exportName => {
if (!exportNames[exportName]) {
exportNames[exportName] = {
packages: {}
}
for (const exportName of packageObject.exportNames) {
if (!exportNames.has(exportName)) {
exportNames.set(exportName, {
packages: new Map()
})
}

if (!exportNames[exportName].packages[packageKey]) {
exportNames[exportName].packages[packageKey] = {
if (!exportNames.get(exportName).packages.has(packageKey)) {
exportNames.get(exportName).packages.set(packageKey, {
usage: 0,
filepaths: []
}
})
}

++exportNames[exportName].packages[packageKey].usage
exportNames[exportName].packages[packageKey].filepaths.push(
filepath
)
})
})
++exportNames.get(exportName).packages.get(packageKey).usage
exportNames
.get(exportName)
.packages.get(packageKey)
.filepaths.push(filepath)
}
}

return {
filepath,
Expand All @@ -112,29 +113,28 @@ DependencyReport.prototype.run = async function() {
* @param {Array|String} packages — a list of minimatch globs.
*/
DependencyReport.prototype.getPackages = function(packages) {
return Object.keys(this.packages)
.filter(packageKey => {
return (
arrify(packages).filter(packageGlob => {
return minimatch(packageKey, packageGlob)
}).length > 0
)
})
.map(packageKey => {
return this.packages[packageKey]
})
const packagesArray = Array.isArray(packages) ? packages : [packages]

return [...this.packages.keys()]
.filter(packageKey => packagesArray.some(packageGlob => minimatch(packageKey, packageGlob)))
.map(packageKey => this.packages.get(packageKey))
}

DependencyReport.prototype.toPlainObject = function() {
const packages = {}
const exportNames = {}

Object.keys(this.packages).forEach(key => {
packages[key] = this.packages[key].toPlainObject()
})
for (const [key, value] of this.packages) {
packages[key] = value.toPlainObject()
}

for (const [key, value] of this.exportNames) {
exportNames[key] = value
}

return {
files: this.files,
exportNames: this.exportNames,
exportNames,
excludeGlob: this.excludeGlob,
packages
}
Expand All @@ -144,20 +144,21 @@ DependencyReport.prototype.toPlainObject = function() {
* @param {Array|String} exportNames — a list of names.
*/
DependencyReport.prototype.getByExportNames = function(exportNames) {
return Object.keys(this.exportNames)
.filter(exportName => {
return (
arrify(exportNames).filter(innerExportName => {
return innerExportName === exportName
}).length > 0
)
})
const exportNamesArray = Array.isArray(exportNames) ? exportNames : [exportNames]

return [...this.exportNames.keys()]
.filter(exportName => exportNamesArray.includes(exportName))
.map(exportName => {
const packages = {}
for (const [key, value] of this.exportNames.get(exportName).packages) {
packages[key] = value
}

return {
name: exportName,
...this.exportNames[exportName]
packages
}
})
}

module.exports = DependencyReport
export default DependencyReport
Loading