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  }