github.com/ernestokarim/closurer@v0.0.0-20130119214741-f245d086c750/scan/deps.go (about)

     1  package scan
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"path"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/ernestokarim/closurer/app"
    11  	"github.com/ernestokarim/closurer/config"
    12  	"github.com/ernestokarim/closurer/domain"
    13  )
    14  
    15  // Store the info of a dependencies tree
    16  type DepsTree struct {
    17  	MustCompile bool
    18  
    19  	sources  map[string]*domain.Source
    20  	provides map[string]*domain.Source
    21  	base     *domain.Source
    22  	basePath string
    23  	dest     string
    24  }
    25  
    26  // Build a dependency tree that allows the client to know the order of
    27  // compilation
    28  // Dest will be "compile" or "input" depending on the use.
    29  func NewDepsTree(dest string) (*DepsTree, error) {
    30  	conf := config.Current()
    31  
    32  	// Initialize the tree
    33  	depstree := &DepsTree{
    34  		sources:  map[string]*domain.Source{},
    35  		provides: map[string]*domain.Source{},
    36  		dest:     dest,
    37  	}
    38  
    39  	if conf.Library != nil {
    40  		depstree.basePath = path.Join(conf.Library.Root, "closure", "goog", "base.js")
    41  	}
    42  
    43  	// Build the deps tree scanning each root directory recursively
    44  	roots := BaseJSPaths()
    45  	for _, root := range roots {
    46  		// Scan the sources
    47  		src, err := Do(root, ".js")
    48  		if err != nil {
    49  			return nil, err
    50  		}
    51  
    52  		// Add them to the tree
    53  		for _, s := range src {
    54  			if err := depstree.AddSource(s); err != nil {
    55  				return nil, err
    56  			}
    57  		}
    58  	}
    59  
    60  	// Check the integrity of the tree
    61  	if err := depstree.Check(); err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	return depstree, nil
    66  }
    67  
    68  // Adds a new JS source file to the tree
    69  func (tree *DepsTree) AddSource(filename string) error {
    70  	// Build the source
    71  	src, cached, err := domain.NewSource(tree.dest, filename, tree.basePath)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	// If it's the base file, save it
    77  	if src.Base {
    78  		tree.base = src
    79  	}
    80  
    81  	conf := config.Current()
    82  
    83  	// Scan all the previous sources searching for repeated
    84  	// namespaces. We ignore closure library files because they're
    85  	// supposed to be correct and tested by other methods
    86  	if conf.Library == nil || !strings.HasPrefix(filename, conf.Library.Root) {
    87  		for k, source := range tree.sources {
    88  			for _, provide := range source.Provides {
    89  				if In(src.Provides, provide) {
    90  					return app.Errorf("multiple provide %s: %s and %s", provide, k, filename)
    91  				}
    92  			}
    93  		}
    94  	}
    95  
    96  	// Files without the goog.provide directive
    97  	// use a trick to provide its own name. It fullfills the need
    98  	// to compile things apart from the Closure style (Angular, ...).
    99  	if len(src.Provides) == 0 {
   100  		src.Provides = []string{filename}
   101  	}
   102  
   103  	// Add all the provides to the map
   104  	for _, provide := range src.Provides {
   105  		tree.provides[provide] = src
   106  	}
   107  
   108  	// Save the source
   109  	tree.sources[filename] = src
   110  
   111  	// Update the MustCompile flag
   112  	tree.MustCompile = tree.MustCompile || !cached
   113  
   114  	return nil
   115  }
   116  
   117  // Check if all required namespaces are provided by the 
   118  // scanned files
   119  func (tree *DepsTree) Check() error {
   120  	for k, source := range tree.sources {
   121  		for _, require := range source.Requires {
   122  			_, ok := tree.provides[require]
   123  			if !ok {
   124  				return app.Errorf("namespace not found %s: %s", require, k)
   125  			}
   126  		}
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // Returns the provides list of a source file, or an error if it hasn't been
   133  // scanned previously into the tree
   134  func (tree *DepsTree) GetProvides(filename string) ([]string, error) {
   135  	src, ok := tree.sources[filename]
   136  	if !ok {
   137  		return nil, app.Errorf("input not present in the sources: %s", filename)
   138  	}
   139  
   140  	return src.Provides, nil
   141  }
   142  
   143  // Return the list of namespaces need to include the test files too
   144  func (tree *DepsTree) GetTestingNamespaces() []string {
   145  	ns := make([]string, 0)
   146  	for _, src := range tree.sources {
   147  		if strings.Contains(src.Filename, "_test.js") {
   148  			ns = append(ns, src.Provides...)
   149  		}
   150  	}
   151  	return ns
   152  }
   153  
   154  // Struct to store the info of a dependencies tree traversal
   155  type TraversalInfo struct {
   156  	deps      []*domain.Source
   157  	traversal []string
   158  }
   159  
   160  // Returns the list of files (in order) that must be compiled to finally
   161  // obtain all namespaces, including the base one.
   162  func (tree *DepsTree) GetDependencies(namespaces []string) ([]*domain.Source, error) {
   163  	// Prepare the info
   164  	info := &TraversalInfo{
   165  		deps:      []*domain.Source{},
   166  		traversal: []string{},
   167  	}
   168  
   169  	for _, ns := range namespaces {
   170  		// Resolve all the needed dependencies
   171  		if err := tree.ResolveDependencies(ns, info); err != nil {
   172  			return nil, err
   173  		}
   174  	}
   175  
   176  	return info.deps, nil
   177  }
   178  
   179  // Adds to the traversal info the list of dependencies recursively.
   180  func (tree *DepsTree) ResolveDependencies(ns string, info *TraversalInfo) error {
   181  	// Check that the namespace is correct
   182  	src, ok := tree.provides[ns]
   183  	if !ok {
   184  		return app.Errorf("namespace not found: %s", ns)
   185  	}
   186  
   187  	// Detects circular deps
   188  	if In(info.traversal, ns) {
   189  		info.traversal = append(info.traversal, ns)
   190  		return app.Errorf("circular dependency detected: %v", info.traversal)
   191  	}
   192  
   193  	// Memoize results, don't recalculate old depencies
   194  	if !InSource(info.deps, src) {
   195  		// Add a new namespace to the traversal
   196  		info.traversal = append(info.traversal, ns)
   197  
   198  		// Compile first all dependencies
   199  		for _, require := range src.Requires {
   200  			tree.ResolveDependencies(require, info)
   201  		}
   202  
   203  		// Add ourselves to the list of files
   204  		info.deps = append(info.deps, src)
   205  
   206  		// Remove the namespace from the traversal
   207  		info.traversal = info.traversal[:len(info.traversal)-1]
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  func WriteDeps(f io.Writer, deps []*domain.Source) error {
   214  	paths := BaseJSPaths()
   215  	for _, src := range deps {
   216  		// Accumulates the provides & requires of the source
   217  		provides := "'" + strings.Join(src.Provides, "', '") + "'"
   218  		requires := "'" + strings.Join(src.Requires, "', '") + "'"
   219  
   220  		// Search the base path to the file, and put the path
   221  		// relative to it
   222  		var n string
   223  		for _, p := range paths {
   224  			tn, err := filepath.Rel(p, src.Filename)
   225  			if err == nil && !strings.Contains(tn, "..") {
   226  				n = tn
   227  				break
   228  			}
   229  		}
   230  		if n == "" {
   231  			return app.Errorf("cannot generate the relative filename for %s", src.Filename)
   232  		}
   233  
   234  		// Write the line to the output of the deps.js file request
   235  		fmt.Fprintf(f, "goog.addDependency('%s', [%s], [%s]);\n", n, provides, requires)
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // Base paths, all routes to a JS must start from one
   242  // of these ones.
   243  // The order is important, the paths will be scanned as
   244  // they've been written.
   245  func BaseJSPaths() []string {
   246  	conf := config.Current()
   247  
   248  	p := []string{}
   249  
   250  	if conf.Library != nil {
   251  		p = append(p, path.Join(conf.Library.Root, "closure", "goog"))
   252  		p = append(p, conf.Library.Root)
   253  	}
   254  
   255  	if conf.Js != nil {
   256  		p = append(p, conf.Js.Root)
   257  	}
   258  
   259  	if conf.Soy != nil {
   260  		path.Join(conf.Soy.Compiler, "javascript")
   261  		if conf.Soy.Root != "" {
   262  			p = append(p, path.Join(conf.Build, "templates"))
   263  		}
   264  	}
   265  
   266  	return p
   267  }