github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/hack/ts-rollup/main.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bufio" 21 "context" 22 "flag" 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "path" 28 "strings" 29 "sync" 30 31 "github.com/sirupsen/logrus" 32 "gopkg.in/yaml.v3" 33 ) 34 35 const ( 36 defaultOutputDir = "_output/js" 37 38 rimraf = "node_modules/rimraf/bin.js" 39 tsc = "node_modules/typescript/bin/tsc" 40 rollup = "node_modules/rollup/dist/bin/rollup" 41 terser = "node_modules/terser/bin/terser" 42 43 defaultRollupConfig = "rollup.config.js" 44 defaultTerserConfig = "hack/ts.rollup_bundle.min.minify_options.json" 45 46 defaultWorkersCount = 5 47 ) 48 49 var ( 50 rootDir string 51 ) 52 53 func rootDirWithGit() { 54 // Best effort 55 out, err := runCmd(nil, "git", "rev-parse", "--show-toplevel") 56 if err != nil { 57 logrus.WithError(err).Warn("Failed getting git root dir") 58 } 59 rootDir = out 60 } 61 62 type options struct { 63 packages string 64 workers int 65 cleanupOnly bool 66 } 67 68 type packagesInfo struct { 69 Packages []packageInfo `yaml:"packages"` 70 } 71 72 type packageInfo struct { 73 Dir string `yaml:"dir"` 74 Entrypoint string `yaml:"entrypoint"` 75 Dst string `yaml:"dst"` 76 } 77 78 func loadPackagesInfo(f string) (*packagesInfo, error) { 79 b, err := os.ReadFile(f) 80 if err != nil { 81 return nil, fmt.Errorf("reading file %q: %w", f, err) 82 } 83 var res packagesInfo 84 return &res, yaml.Unmarshal(b, &res) 85 } 86 87 // Mock for unit testing purpose 88 var runCmdInDirFunc = runCmdInDir 89 90 func runCmdInDir(dir string, additionalEnv []string, cmd string, args ...string) (string, error) { 91 log := logrus.WithFields(logrus.Fields{"cmd": cmd, "args": args}) 92 command := exec.Command(cmd, args...) 93 if dir != "" { 94 command.Dir = dir 95 } 96 command.Env = append(os.Environ(), additionalEnv...) 97 stdOut, err := command.StdoutPipe() 98 if err != nil { 99 return "", err 100 } 101 stdErr, err := command.StderrPipe() 102 if err != nil { 103 return "", err 104 } 105 if err := command.Start(); err != nil { 106 return "", err 107 } 108 scanner := bufio.NewScanner(stdOut) 109 var allOut string 110 for scanner.Scan() { 111 out := scanner.Text() 112 allOut = allOut + out 113 log.Info(out) 114 } 115 allErr, _ := io.ReadAll(stdErr) 116 err = command.Wait() 117 if len(allErr) > 0 { 118 if err != nil { 119 log.Error(string(allErr)) 120 } else { 121 log.Warn(string(allErr)) 122 } 123 } 124 return strings.TrimSpace(allOut), err 125 } 126 127 func runCmd(additionalEnv []string, cmd string, args ...string) (string, error) { 128 return runCmdInDirFunc(rootDir, additionalEnv, cmd, args...) 129 } 130 131 func rollupOne(pi *packageInfo, cleanupOnly bool) error { 132 entrypointFileBasename := strings.TrimSuffix(path.Base(pi.Entrypoint), ".ts") 133 // Intermediate output files, stored under `_output` dir 134 jsOutputFile := path.Join(defaultOutputDir, pi.Dir, entrypointFileBasename+".js") 135 bundleOutputDir := path.Join(defaultOutputDir, pi.Dir) 136 rollupOutputFile := path.Join(bundleOutputDir, fmt.Sprintf("%s_bundle.js", entrypointFileBasename)) 137 // terserOutputFile is the minified bundle, which is placed next to all 138 // other static files in the source tree 139 terserOutputFile := path.Join(pi.Dir, pi.Dst) 140 if cleanupOnly { 141 return os.Remove(terserOutputFile) 142 } 143 if _, err := runCmd(nil, rimraf, "dist"); err != nil { 144 return fmt.Errorf("running rimraf: %w", err) 145 } 146 if _, err := runCmd(nil, tsc, "-p", path.Join(pi.Dir, "tsconfig.json"), "--outDir", defaultOutputDir); err != nil { 147 return fmt.Errorf("running tsc: %w", err) 148 } 149 if _, err := runCmd(nil, rollup, "--environment", fmt.Sprintf("ROLLUP_OUT_FILE:%s,ROLLUP_ENTRYPOINT:%s", rollupOutputFile, jsOutputFile), "-c", defaultRollupConfig, "--preserveSymlinks"); err != nil { 150 return fmt.Errorf("running rollup: %w", err) 151 } 152 if _, err := runCmd(nil, terser, rollupOutputFile, "--output", terserOutputFile, "--config-file", defaultTerserConfig); err != nil { 153 return fmt.Errorf("running terser: %w", err) 154 } 155 return nil 156 } 157 158 func main() { 159 var o options 160 flag.StringVar(&o.packages, "packages", "", "Yaml file contains list of packages to be rolled up.") 161 flag.IntVar(&o.workers, "workers", defaultWorkersCount, "Number of workers in parallel.") 162 flag.BoolVar(&o.cleanupOnly, "cleanup-only", false, "Indicate cleanup only.") 163 flag.StringVar(&rootDir, "root-dir", "", "Root dir of this repo, where everything happens.") 164 flag.Parse() 165 166 if rootDir == "" { 167 rootDirWithGit() 168 } 169 if rootDir == "" { 170 logrus.Error("Unable to determine root dir, please pass in --root-dir.") 171 os.Exit(1) 172 } 173 174 pis, err := loadPackagesInfo(o.packages) 175 if err != nil { 176 logrus.WithError(err).WithField("packages", o.packages).Error("Failed loading") 177 os.Exit(1) 178 } 179 180 var wg sync.WaitGroup 181 packageChan := make(chan packageInfo, 10) 182 errChan := make(chan error, len(pis.Packages)) 183 doneChan := make(chan packageInfo, len(pis.Packages)) 184 // Start workers 185 ctx, cancel := context.WithCancel(context.Background()) 186 defer cancel() 187 for i := 0; i < o.workers; i++ { 188 go func(ctx context.Context, packageChan chan packageInfo, errChan chan error, doneChan chan packageInfo) { 189 for { 190 select { 191 case pi := <-packageChan: 192 err := rollupOne(&pi, o.cleanupOnly) 193 if err != nil { 194 errChan <- fmt.Errorf("rollup package %q failed: %v", pi.Entrypoint, err) 195 } 196 doneChan <- pi 197 case <-ctx.Done(): 198 return 199 } 200 } 201 }(ctx, packageChan, errChan, doneChan) 202 } 203 204 for _, pi := range pis.Packages { 205 pi := pi 206 wg.Add(1) 207 packageChan <- pi 208 } 209 210 go func(ctx context.Context, wg *sync.WaitGroup, doneChan chan packageInfo) { 211 var done int 212 for { 213 select { 214 case pi := <-doneChan: 215 done++ 216 logrus.WithFields(logrus.Fields{"entrypoint": pi.Entrypoint, "done": done, "total": len(pis.Packages)}).Info("Done with package.") 217 wg.Done() 218 case <-ctx.Done(): 219 return 220 } 221 } 222 }(ctx, &wg, doneChan) 223 224 wg.Wait() 225 for { 226 select { 227 case err := <-errChan: 228 logrus.WithError(err).Error("Failed.") 229 os.Exit(1) 230 default: 231 return 232 } 233 } 234 }