Skip to content

Instantly share code, notes, and snippets.

@sgardn
Created October 12, 2020 19:28
Show Gist options
  • Save sgardn/a8506ee958ba874cbbfefe5551277218 to your computer and use it in GitHub Desktop.
Save sgardn/a8506ee958ba874cbbfefe5551277218 to your computer and use it in GitHub Desktop.
package main
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
)
type manifest struct {
Name string `json:"name"`
// we don't care about anything else!
}
type component struct {
path string
depth int
deps []component
}
var folder = regexp.MustCompile(`^([a-zA-Z]*)/.*$`)
func containsFolder(path string) string {
matches := folder.FindSubmatch([]byte(path))
// if it matches, we have an array of 2 ["some/folder/filepath", "some"]
if len(matches) > 0 {
return string(matches[1])
}
return ""
}
func scanFileForImports(path, pName string) ([]string, error) {
var targets []string
re := regexp.MustCompile(`^import .* from [\"']` +
pName + `/(.*)[\"']$`)
file, err := os.Open(path)
defer file.Close()
if err != nil {
fmt.Printf("failed opening file: %s\n", err)
return targets, err
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
matches := re.FindSubmatch([]byte(line))
if len(matches) != 0 {
targets = append(targets, string(matches[1]))
}
}
return targets, nil
}
// take a filepath and depth, and return the component of that filepath with
// child components filled out
func recurse(path, pName string, depth int) (component, error) {
c := component{depth: depth, path: path}
imps, e := scanFileForImports(path, pName)
if e != nil {
fmt.Printf("failed to recurse on file: %s\n", path)
return c, e
}
if len(imps) != 0 {
var cs []component
for i := 0; i < len(imps); i++ {
jsPath := imps[i] + ".js"
comp, err := recurse(jsPath, pName, depth+1)
if err != nil {
return c, err
}
cs = append(cs, comp)
}
c.deps = cs
}
return c, nil
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
// flatten takes a component, and flattens it + deps into a map
// from pathname to the minimum depth (and makes them unique)
func flatten(c component) (map[string]int, error) {
m := make(map[string]int)
m[c.path] = c.depth
for i := 0; i < len(c.deps); i++ {
toAdd, e := flatten(c.deps[i])
if e != nil {
return m, e
}
for k, v := range toAdd {
if val, ok := m[k]; ok {
m[k] = min(val, v)
} else {
m[k] = v
}
}
}
return m, nil
}
// returns a list of folder prefixes from our hash
func getFolderList(m map[string]int) []string {
var stringList []string
structList := make(map[string]struct{})
for k := range m {
target := containsFolder(k)
// if there is a directory target, try to add it
if target != "" {
if _, ok := structList[target]; !ok {
structList[target] = struct{}{}
}
}
}
for k := range structList {
stringList = append(stringList, k)
}
return stringList
}
// returns a list of files (not directories) inside a directory
func listFilesInFolderRecursively(folder string) ([]string, error) {
paths := make([]string, 0)
e := filepath.Walk(folder,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// we don't care about directories
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
return paths, e
}
func printUnused(fs []string) {
fmt.Printf("Found %d unused files:\n", len(fs))
for _, f := range fs {
fmt.Printf("- %s\n", f)
}
}
func generateUsageMap(folderList []string) (map[string]bool, error) {
fileUsageMap := make(map[string]bool)
for _, v := range folderList {
children, err := listFilesInFolderRecursively(v)
if err != nil {
fmt.Printf("failed to list files: %v \n", err)
return fileUsageMap, err
}
for _, child := range children {
fileUsageMap[child] = false
}
}
return fileUsageMap, nil
}
func removeFiles(unused []string) error {
fmt.Println("Deleting...")
for i := 0; i < len(unused); i++ {
if removeErr := os.Remove(unused[i]); removeErr != nil {
return removeErr
}
fmt.Printf("- %s\n", unused[i])
}
fmt.Println("Done!")
return nil
}
func main() {
fmt.Println("Detecting package namespacing from package.json...")
jsonFile, err := ioutil.ReadFile("package.json")
if err != nil {
fmt.Println("Failed to open users.json", err)
return
}
var m manifest
json.Unmarshal([]byte(jsonFile), &m)
if m.Name == "" {
fmt.Println("No package name found, exiting!")
return
}
fmt.Printf("- Package named '%s'\n", m.Name)
fmt.Println("Recursing on App.js to map dependencies...")
c, e := recurse("App.js", m.Name, 0)
if e != nil {
fmt.Printf("Failed to recurse: %v\n", e)
return
}
uniqueHash, err := flatten(c)
if err != nil {
fmt.Printf("Failed flattening component tree: %v\n", err)
return
}
fmt.Printf("%d files included\n", len(uniqueHash))
list := getFolderList(uniqueHash)
fmt.Printf("Top level folders referenced: %v\n", list)
fmt.Println("Shaking those directories...")
fileUsageMap, err := generateUsageMap(list)
if err != nil {
fmt.Printf("Failed to create map of possible files %v\n", err)
return
}
for k := range uniqueHash {
fileUsageMap[k] = true
}
unusedFiles := make([]string, 0)
for k, v := range fileUsageMap {
if !v {
unusedFiles = append(unusedFiles, k)
}
}
printUnused(unusedFiles)
if len(unusedFiles) > 0 {
fmt.Println("\n> Do you want to remove these files?")
fmt.Println(`Type "y" + enter to remove, anything else + enter to exit:`)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
t := scanner.Text()
if t == "y" {
if err = removeFiles(unusedFiles); err != nil {
fmt.Printf("Failed to remove files: %v\n", err)
}
} else {
fmt.Println("Exiting...")
}
return
}
} else {
fmt.Println("No files to remove! Exiting...")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment