Created
October 12, 2020 19:28
-
-
Save sgardn/a8506ee958ba874cbbfefe5551277218 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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