github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/misc/closure/gendeps.go (about)

     1  /*
     2  Copyright 2013 The Camlistore 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 closure provides tools to help with the use of the
    18  // closure library.
    19  //
    20  // See https://code.google.com/p/closure-library/
    21  package closure
    22  
    23  import (
    24  	"bufio"
    25  	"bytes"
    26  	"fmt"
    27  	"io"
    28  	"net/http"
    29  	"os"
    30  	"regexp"
    31  	"strings"
    32  	"sync"
    33  	"time"
    34  )
    35  
    36  // GenDeps returns the namespace dependencies between the closure javascript files in root. It does not descend in directories.
    37  // Each of the files listed in the output is prepended with the path "../../", which is assumed to be the location where these files can be found, relative to Closure's base.js.
    38  //
    39  // The format for each relevant javascript file is:
    40  // goog.addDependency("filepath", ["namespace provided"], ["required namespace 1", "required namespace 2", ...]);
    41  func GenDeps(root http.FileSystem) ([]byte, error) {
    42  	// In the typical configuration, Closure is served at 'closure/goog/...''
    43  	return GenDepsWithPath("../../", root)
    44  }
    45  
    46  // GenDepsWithPath is like GenDeps, but you can specify a path where the files are to be found at runtime relative to Closure's base.js.
    47  func GenDepsWithPath(pathPrefix string, root http.FileSystem) ([]byte, error) {
    48  	d, err := root.Open("/")
    49  	if err != nil {
    50  		return nil, fmt.Errorf("Failed to open root of %v: %v", root, err)
    51  	}
    52  	fi, err := d.Stat()
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	if !fi.IsDir() {
    57  		return nil, fmt.Errorf("root of %v is not a dir", root)
    58  	}
    59  	ent, err := d.Readdir(-1)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("Could not read dir entries of root: %v", err)
    62  	}
    63  	var buf bytes.Buffer
    64  	for _, info := range ent {
    65  		name := info.Name()
    66  		if !strings.HasSuffix(name, ".js") {
    67  			continue
    68  		}
    69  		f, err := root.Open(name)
    70  		if err != nil {
    71  			return nil, fmt.Errorf("Could not open %v: %v", name, err)
    72  		}
    73  		prov, req, err := parseProvidesRequires(info, name, f)
    74  		f.Close()
    75  		if err != nil {
    76  			return nil, fmt.Errorf("Could not parse deps for %v: %v", name, err)
    77  		}
    78  		if len(prov) > 0 {
    79  			fmt.Fprintf(&buf, "goog.addDependency(%q, %v, %v);\n", pathPrefix+name, jsList(prov), jsList(req))
    80  		}
    81  	}
    82  	return buf.Bytes(), nil
    83  }
    84  
    85  var provReqRx = regexp.MustCompile(`^goog\.(provide|require)\(['"]([\w\.]+)['"]\)`)
    86  
    87  type depCacheItem struct {
    88  	modTime            time.Time
    89  	provides, requires []string
    90  }
    91  
    92  var (
    93  	depCacheMu sync.Mutex
    94  	depCache   = map[string]depCacheItem{}
    95  )
    96  
    97  func parseProvidesRequires(fi os.FileInfo, path string, f io.Reader) (provides, requires []string, err error) {
    98  	mt := fi.ModTime()
    99  	depCacheMu.Lock()
   100  	defer depCacheMu.Unlock()
   101  	if ci := depCache[path]; ci.modTime.Equal(mt) {
   102  		return ci.provides, ci.requires, nil
   103  	}
   104  
   105  	scanner := bufio.NewScanner(f)
   106  	for scanner.Scan() {
   107  		l := scanner.Text()
   108  		if !strings.HasPrefix(l, "goog.") {
   109  			continue
   110  		}
   111  		m := provReqRx.FindStringSubmatch(l)
   112  		if m != nil {
   113  			if m[1] == "provide" {
   114  				provides = append(provides, m[2])
   115  			} else {
   116  				requires = append(requires, m[2])
   117  			}
   118  		}
   119  	}
   120  	if err := scanner.Err(); err != nil {
   121  		return nil, nil, err
   122  	}
   123  	depCache[path] = depCacheItem{provides: provides, requires: requires, modTime: mt}
   124  	return provides, requires, nil
   125  }
   126  
   127  // jsList prints a list of strings as JavaScript list.
   128  type jsList []string
   129  
   130  func (s jsList) String() string {
   131  	var buf bytes.Buffer
   132  	buf.WriteByte('[')
   133  	for i, v := range s {
   134  		if i > 0 {
   135  			buf.WriteString(", ")
   136  		}
   137  		fmt.Fprintf(&buf, "%q", v)
   138  	}
   139  	buf.WriteByte(']')
   140  	return buf.String()
   141  }
   142  
   143  // Example of a match:
   144  // goog.addDependency('asserts/asserts.js', ['goog.asserts', 'goog.asserts.AssertionError'], ['goog.debug.Error', 'goog.string']);
   145  // So with m := depsRx.FindStringSubmatch,
   146  // the provider: m[1] == "asserts/asserts.js"
   147  // the provided namespaces: m[2] == "'goog.asserts', 'goog.asserts.AssertionError'"
   148  // the required namespaces: m[5] == "'goog.debug.Error', 'goog.string'"
   149  var depsRx = regexp.MustCompile(`^goog.addDependency\(['"]([^/]+[a-zA-Z0-9\-\_/\.]*\.js)['"], \[((['"][\w\.]+['"])+(, ['"][\w\.]+['"])*)\], \[((['"][\w\.]+['"])+(, ['"][\w\.]+['"])*)?\]\);`)
   150  
   151  // ParseDeps reads closure namespace dependency lines and
   152  // returns a map giving the js file provider for each namespace,
   153  // and a map giving the namespace dependencies for each namespace.
   154  func ParseDeps(r io.Reader) (providedBy map[string]string, requires map[string][]string, err error) {
   155  	providedBy = make(map[string]string)
   156  	requires = make(map[string][]string)
   157  	scanner := bufio.NewScanner(r)
   158  	for scanner.Scan() {
   159  		l := scanner.Text()
   160  		if strings.HasPrefix(l, "//") {
   161  			continue
   162  		}
   163  		if l == "" {
   164  			continue
   165  		}
   166  		m := depsRx.FindStringSubmatch(l)
   167  		if m == nil {
   168  			return nil, nil, fmt.Errorf("Invalid line in deps: %q", l)
   169  		}
   170  		jsfile := m[1]
   171  		provides := strings.Split(m[2], ", ")
   172  		var required []string
   173  		if m[5] != "" {
   174  			required = strings.Split(
   175  				strings.Replace(strings.Replace(m[5], "'", "", -1), `"`, "", -1), ", ")
   176  		}
   177  		for _, v := range provides {
   178  			namespace := strings.Trim(v, `'"`)
   179  			if otherjs, ok := providedBy[namespace]; ok {
   180  				return nil, nil, fmt.Errorf("Name %v is provided by both %v and %v", namespace, jsfile, otherjs)
   181  			}
   182  			providedBy[namespace] = jsfile
   183  			if _, ok := requires[namespace]; ok {
   184  				return nil, nil, fmt.Errorf("Name %v has two sets of dependencies")
   185  			}
   186  			if required != nil {
   187  				requires[namespace] = required
   188  			}
   189  		}
   190  	}
   191  	if err := scanner.Err(); err != nil {
   192  		return nil, nil, err
   193  	}
   194  	return providedBy, requires, nil
   195  }
   196  
   197  // DeepParseDeps reads closure namespace dependency lines and
   198  // returns a map giving all the required js files for each namespace.
   199  func DeepParseDeps(r io.Reader) (map[string][]string, error) {
   200  	providedBy, requires, err := ParseDeps(r)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	filesDeps := make(map[string][]string)
   205  	var deeperDeps func(namespace string) []string
   206  	deeperDeps = func(namespace string) []string {
   207  		if jsdeps, ok := filesDeps[namespace]; ok {
   208  			return jsdeps
   209  		}
   210  		jsfiles := []string{providedBy[namespace]}
   211  		for _, dep := range requires[namespace] {
   212  			jsfiles = append(jsfiles, deeperDeps(dep)...)
   213  		}
   214  		return jsfiles
   215  	}
   216  	for namespace, _ := range providedBy {
   217  		filesDeps[namespace] = deeperDeps(namespace)
   218  	}
   219  	return filesDeps, nil
   220  }