├── .gitignore ├── History.md ├── LICENSE ├── Readme.md ├── go.mod ├── go.sum ├── internal └── prune │ └── prune.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.2.0 / 2020-05-12 3 | =================== 4 | 5 | * add `--include` and `--exclude` flags 6 | * remove goreleaser.yml 7 | 8 | v1.1.0 / 2020-02-10 9 | =================== 10 | 11 | * move `./cmd/node-prune/main.go` to root 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What? 4 | 5 | node-prune is a small tool to prune unnecessary files from ./node_modules, such as markdown, typescript source files, and so on. Primarily built for [Up](https://github.com/apex/up) which lets you deploy serverless web applications in seconds. 6 | 7 | ## Installation 8 | 9 | From [gobinaries.com](https://gobinaries.com): 10 | 11 | ```sh 12 | $ curl -sf https://gobinaries.com/tj/node-prune | sh 13 | ``` 14 | 15 | From source: 16 | 17 | ``` 18 | $ go get github.com/tj/node-prune 19 | ``` 20 | 21 | ## Usage 22 | 23 | In your app directory: 24 | 25 | ``` 26 | $ node-prune 27 | 28 | files total 27,330 29 | files removed 3,990 30 | size removed 13 MB 31 | duration 200ms 32 | ``` 33 | 34 | Somewhere else: 35 | 36 | ``` 37 | $ node-prune path/to/node_modules 38 | 39 | files total 27,330 40 | files removed 3,990 41 | size removed 13 MB 42 | duration 200ms 43 | ``` 44 | 45 | Or add to the ``package.json`` scripts field 46 | 47 | ``` 48 | "scripts": { 49 | "postinstall": "node-prune" 50 | } 51 | ``` 52 | 53 | ## Why? 54 | 55 | ![huge](https://pbs.twimg.com/media/DEIV_1XWsAAlY29.jpg) 56 | 57 | --- 58 | 59 | [![GoDoc](https://godoc.org/github.com/tj/node-prune?status.svg)](https://godoc.org/github.com/tj/node-prune) 60 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 61 | ![](https://img.shields.io/badge/status-stable-green.svg) 62 | 63 | 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tj/node-prune 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/apex/log v1.1.1 7 | github.com/dustin/go-humanize v1.0.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA= 2 | github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA= 3 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 4 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 5 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 6 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 10 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 11 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 12 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 13 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 14 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 15 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 18 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 19 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 20 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 21 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 22 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 23 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 24 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 25 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 26 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 27 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 28 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 29 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 30 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 31 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 35 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 36 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 37 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 38 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 41 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 42 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 43 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 44 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 45 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 46 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 47 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 48 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 49 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 51 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 56 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 59 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 60 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 61 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 62 | -------------------------------------------------------------------------------- /internal/prune/prune.go: -------------------------------------------------------------------------------- 1 | // Package prune provides node_modules pruning of unnecessary files. 2 | package prune 3 | 4 | import ( 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "sync" 9 | "sync/atomic" 10 | 11 | "github.com/apex/log" 12 | ) 13 | 14 | // DefaultFiles pruned. 15 | // 16 | // Copied from yarn (mostly). 17 | var DefaultFiles = []string{ 18 | "Jenkinsfile", 19 | "Makefile", 20 | "Gulpfile.js", 21 | "Gruntfile.js", 22 | "gulpfile.js", 23 | ".DS_Store", 24 | ".tern-project", 25 | ".gitattributes", 26 | ".editorconfig", 27 | ".eslintrc", 28 | "eslint", 29 | ".eslintrc.js", 30 | ".eslintrc.json", 31 | ".eslintrc.yml", 32 | ".eslintignore", 33 | ".stylelintrc", 34 | "stylelint.config.js", 35 | ".stylelintrc.json", 36 | ".stylelintrc.yaml", 37 | ".stylelintrc.yml", 38 | ".stylelintrc.js", 39 | ".htmllintrc", 40 | "htmllint.js", 41 | ".lint", 42 | ".npmrc", 43 | ".npmignore", 44 | ".jshintrc", 45 | ".flowconfig", 46 | ".documentup.json", 47 | ".yarn-metadata.json", 48 | ".travis.yml", 49 | "appveyor.yml", 50 | ".gitlab-ci.yml", 51 | "circle.yml", 52 | ".coveralls.yml", 53 | "CHANGES", 54 | "changelog", 55 | "LICENSE.txt", 56 | "LICENSE", 57 | "LICENSE-MIT", 58 | "LICENSE.BSD", 59 | "license", 60 | "LICENCE.txt", 61 | "LICENCE", 62 | "LICENCE-MIT", 63 | "LICENCE.BSD", 64 | "licence", 65 | "AUTHORS", 66 | "CONTRIBUTORS", 67 | ".yarn-integrity", 68 | ".yarnclean", 69 | "_config.yml", 70 | ".babelrc", 71 | ".yo-rc.json", 72 | "jest.config.js", 73 | "karma.conf.js", 74 | "wallaby.js", 75 | "wallaby.conf.js", 76 | ".prettierrc", 77 | ".prettierrc.yml", 78 | ".prettierrc.toml", 79 | ".prettierrc.js", 80 | ".prettierrc.json", 81 | "prettier.config.js", 82 | ".appveyor.yml", 83 | "tsconfig.json", 84 | "tslint.json", 85 | } 86 | 87 | // DefaultDirectories pruned. 88 | // 89 | // Copied from yarn (mostly). 90 | var DefaultDirectories = []string{ 91 | "__tests__", 92 | "test", 93 | "tests", 94 | "powered-test", 95 | "docs", 96 | "doc", 97 | ".idea", 98 | ".vscode", 99 | "website", 100 | "images", 101 | "assets", 102 | "example", 103 | "examples", 104 | "coverage", 105 | ".nyc_output", 106 | ".circleci", 107 | ".github", 108 | } 109 | 110 | // DefaultExtensions pruned. 111 | var DefaultExtensions = []string{ 112 | ".markdown", 113 | ".md", 114 | ".mkd", 115 | ".ts", 116 | ".jst", 117 | ".coffee", 118 | ".tgz", 119 | ".swp", 120 | } 121 | 122 | // Stats for a prune. 123 | type Stats struct { 124 | FilesTotal int64 125 | FilesRemoved int64 126 | SizeRemoved int64 127 | } 128 | 129 | // Pruner is a module pruner. 130 | type Pruner struct { 131 | dir string 132 | log log.Interface 133 | dirs map[string]struct{} 134 | exts map[string]struct{} 135 | excepts []string 136 | globs []string 137 | files map[string]struct{} 138 | ch chan func() 139 | wg sync.WaitGroup 140 | } 141 | 142 | // Option function. 143 | type Option func(*Pruner) 144 | 145 | // New with the given options. 146 | func New(options ...Option) *Pruner { 147 | v := &Pruner{ 148 | dir: "node_modules", 149 | log: log.Log, 150 | exts: toMap(DefaultExtensions), 151 | excepts: []string{}, 152 | globs: []string{}, 153 | dirs: toMap(DefaultDirectories), 154 | files: toMap(DefaultFiles), 155 | ch: make(chan func()), 156 | } 157 | 158 | for _, o := range options { 159 | o(v) 160 | } 161 | 162 | return v 163 | } 164 | 165 | // WithDir option. 166 | func WithDir(s string) Option { 167 | return func(v *Pruner) { 168 | v.dir = s 169 | } 170 | } 171 | 172 | // WithGlobs option. 173 | func WithGlobs(s []string) Option { 174 | return func(v *Pruner) { 175 | v.globs = s 176 | } 177 | } 178 | 179 | // WithExceptions option. 180 | func WithExceptions(s []string) Option { 181 | return func(v *Pruner) { 182 | v.excepts = s 183 | } 184 | } 185 | 186 | // WithExtensions option. 187 | func WithExtensions(s []string) Option { 188 | return func(v *Pruner) { 189 | v.exts = toMap(s) 190 | } 191 | } 192 | 193 | // WithDirectories option. 194 | func WithDirectories(s []string) Option { 195 | return func(v *Pruner) { 196 | v.dirs = toMap(s) 197 | } 198 | } 199 | 200 | // WithFiles option. 201 | func WithFiles(s []string) Option { 202 | return func(v *Pruner) { 203 | v.files = toMap(s) 204 | } 205 | } 206 | 207 | // Prune performs the pruning. 208 | func (p *Pruner) Prune() (*Stats, error) { 209 | var stats Stats 210 | 211 | p.startN(runtime.NumCPU()) 212 | defer p.stop() 213 | 214 | err := filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error { 215 | if err != nil { 216 | return err 217 | } 218 | 219 | stats.FilesTotal++ 220 | 221 | ctx := p.log.WithFields(log.Fields{ 222 | "path": path, 223 | "size": info.Size(), 224 | "dir": info.IsDir(), 225 | }) 226 | 227 | // keep 228 | if !p.prune(path, info) { 229 | ctx.Debug("keep") 230 | return nil 231 | } 232 | 233 | // prune 234 | ctx.Info("prune") 235 | atomic.AddInt64(&stats.FilesRemoved, 1) 236 | atomic.AddInt64(&stats.SizeRemoved, info.Size()) 237 | 238 | // remove and skip dir 239 | if info.IsDir() { 240 | p.ch <- func() { 241 | s, _ := dirStats(path) 242 | 243 | atomic.AddInt64(&stats.FilesTotal, s.FilesTotal) 244 | atomic.AddInt64(&stats.FilesRemoved, s.FilesRemoved) 245 | atomic.AddInt64(&stats.SizeRemoved, s.SizeRemoved) 246 | 247 | if err := os.RemoveAll(path); err != nil { 248 | ctx.WithError(err).Error("removing directory") 249 | } 250 | } 251 | return filepath.SkipDir 252 | } 253 | 254 | // remove file 255 | p.ch <- func() { 256 | if err := os.Remove(path); err != nil { 257 | ctx.WithError(err).Error("removing file") 258 | } 259 | } 260 | 261 | return nil 262 | }) 263 | 264 | return &stats, err 265 | } 266 | 267 | // prune returns true if the file or dir should be pruned. 268 | func (p *Pruner) prune(path string, info os.FileInfo) bool { 269 | // exceptions 270 | for _, glob := range p.excepts { 271 | matched, _ := filepath.Match(glob, info.Name()) 272 | if matched { 273 | return false 274 | } 275 | } 276 | 277 | // globs 278 | for _, glob := range p.globs { 279 | matched, _ := filepath.Match(glob, info.Name()) 280 | if matched { 281 | return true 282 | } 283 | } 284 | 285 | // directories 286 | if info.IsDir() { 287 | _, ok := p.dirs[info.Name()] 288 | return ok 289 | } 290 | 291 | // files 292 | if _, ok := p.files[info.Name()]; ok { 293 | return true 294 | } 295 | 296 | // files exact match 297 | if _, ok := p.files[path]; ok { 298 | return true 299 | } 300 | 301 | // extensions 302 | ext := filepath.Ext(path) 303 | _, ok := p.exts[ext] 304 | return ok 305 | } 306 | 307 | // startN starts n loops. 308 | func (p *Pruner) startN(n int) { 309 | for i := 0; i < n; i++ { 310 | p.wg.Add(1) 311 | go p.start() 312 | } 313 | } 314 | 315 | // start loop. 316 | func (p *Pruner) start() { 317 | defer p.wg.Done() 318 | for fn := range p.ch { 319 | fn() 320 | } 321 | } 322 | 323 | // stop loop. 324 | func (p *Pruner) stop() { 325 | close(p.ch) 326 | p.wg.Wait() 327 | } 328 | 329 | // dirStats returns stats for files in dir. 330 | func dirStats(dir string) (*Stats, error) { 331 | var stats Stats 332 | 333 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 334 | stats.FilesTotal++ 335 | stats.FilesRemoved++ 336 | stats.SizeRemoved += info.Size() 337 | return err 338 | }) 339 | 340 | return &stats, err 341 | } 342 | 343 | // toMap returns a map from slice. 344 | func toMap(s []string) map[string]struct{} { 345 | m := make(map[string]struct{}) 346 | for _, v := range s { 347 | m[v] = struct{}{} 348 | } 349 | return m 350 | } 351 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/apex/log" 10 | "github.com/apex/log/handlers/cli" 11 | "github.com/dustin/go-humanize" 12 | 13 | "github.com/tj/node-prune/internal/prune" 14 | ) 15 | 16 | func init() { 17 | log.SetHandler(cli.Default) 18 | log.SetLevel(log.WarnLevel) 19 | } 20 | 21 | type arrayFlags []string 22 | 23 | func (i *arrayFlags) String() string { 24 | return strings.Join(*i, ", ") 25 | } 26 | 27 | func (i *arrayFlags) Set(value string) error { 28 | *i = append(*i, value) 29 | return nil 30 | } 31 | 32 | // Globs of files that should not be pruned 33 | var exclusionGlobs arrayFlags 34 | 35 | // Globs of files that should always be pruned in addition to the defaults 36 | var inclusionGlobs arrayFlags 37 | 38 | func main() { 39 | debug := flag.Bool("verbose", false, "Verbose log output.") 40 | flag.Var(&exclusionGlobs, "exclude", "Glob of files that should not be pruned. Can be specified multiple times.") 41 | flag.Var(&inclusionGlobs, "include", "Globs of files that should always be pruned in addition to the defaults. Can be specified multiple times.") 42 | flag.Parse() 43 | dir := flag.Arg(0) 44 | 45 | start := time.Now() 46 | 47 | if *debug { 48 | log.SetLevel(log.DebugLevel) 49 | } 50 | 51 | var options []prune.Option 52 | 53 | if dir != "" { 54 | options = append(options, prune.WithDir(dir)) 55 | } 56 | 57 | if len(exclusionGlobs) > 0 { 58 | options = append(options, prune.WithExceptions(exclusionGlobs)) 59 | } 60 | 61 | if len(inclusionGlobs) > 0 { 62 | options = append(options, prune.WithGlobs(inclusionGlobs)) 63 | } 64 | 65 | p := prune.New(options...) 66 | 67 | stats, err := p.Prune() 68 | if err != nil { 69 | log.Fatalf("error: %s", err) 70 | } 71 | 72 | println() 73 | defer println() 74 | 75 | output("files total", humanize.Comma(stats.FilesTotal)) 76 | output("files removed", humanize.Comma(stats.FilesRemoved)) 77 | output("size removed", humanize.Bytes(uint64(stats.SizeRemoved))) 78 | output("duration", time.Since(start).Round(time.Millisecond).String()) 79 | } 80 | 81 | func output(name, val string) { 82 | fmt.Printf("\x1b[1m%20s\x1b[0m %s\n", name, val) 83 | } 84 | --------------------------------------------------------------------------------