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