/
gendeps.ts
179 lines (168 loc) · 6.17 KB
/
gendeps.ts
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
import flat from "array.prototype.flat";
import fs from "fs";
import glob from "glob";
import { depFile, depGraph, parser } from "google-closure-deps";
import path from "path";
import { promisify } from "util";
import { DependencyParserWithWorkers } from "./dependency-parser-wrapper";
import { EntryConfig } from "./entryconfig";
import { googBaseUrlPath, inputsUrlPath } from "./urls";
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const pathToDependencyCache: Map<string, Promise<depGraph.Dependency>> = new Map();
const globPromise = promisify(glob);
/**
* Generate deps.js source text for RAW mode.
* The result excludes deps of Closure Library.
*/
export async function generateDepFileText(
entryConfig: Pick<EntryConfig, "paths" | "test-excludes">,
inputsRoot: string,
ignoreDirs: readonly string[] = [],
workers?: number
): Promise<string> {
const dependencies = await getDependencies(entryConfig, ignoreDirs, workers);
const googBaseDirVirtualPath = path.dirname(
path.resolve(inputsRoot, path.relative(inputsUrlPath, googBaseUrlPath))
);
return generateDepFileTextFromDeps(dependencies, googBaseDirVirtualPath);
}
export function generateDepFileTextFromDeps(
dependencies: depGraph.Dependency[],
googBaseDir: string
): string {
// `getDepFileText()` doesn't generate addDependency() for SCRIPT,
// so change the type to CLOSURE_PROVIDE temporally.
// TODO: fix upstream google-closure-deps and remove this
const scriptDeps = dependencies.filter(dep => dep.type === depGraph.DependencyType.SCRIPT);
scriptDeps.forEach(dep => {
dep.type = depGraph.DependencyType.CLOSURE_PROVIDE;
});
const depFileText = depFile.getDepFileText(googBaseDir, dependencies);
// restore the type
scriptDeps.forEach(dep => {
dep.type = depGraph.DependencyType.SCRIPT;
});
return depFileText;
}
/**
* NOTE: This doesn't support ES Modules, because a bug of google-closure-deps.
*/
export async function writeCachedDepsOnDisk(depsJsPath: string, closureLibraryDir: string) {
const closureBaseDir = path.join(closureLibraryDir, "closure", "goog");
const deps = await Promise.all(Array.from(pathToDependencyCache.values()));
const content = generateDepFileTextFromDeps(deps, closureBaseDir);
return writeFile(depsJsPath, content);
}
/**
* Load and cache deps.js.
* Call this before getDependencies().
*
* @throws if deps.js doesn't exist.
*/
export async function restoreDepsJs(depsJsPath: string, closureLibraryDir: string): Promise<void> {
let depsText = "";
try {
depsText = await readFile(depsJsPath, "utf8");
} catch (e) {
throw new Error(`${depsJsPath} doesn't exist. Run \`duck build:deps\`. ${e}`);
}
const result = parser.parseDependencyFile(depsText, depsJsPath);
if (result.hasFatalError) {
throw new Error(`Fatal parse error in ${depsJsPath}: ${result.errors}`);
}
appendGoogImport(result.dependencies, path.join(closureLibraryDir, "closure", "goog"));
result.dependencies.forEach(dep => {
pathToDependencyCache.set(dep.path, Promise.resolve(dep));
});
}
/**
* Get Dependencies from the paths of the entry config.
* This ignores filename `deps.js`.
*/
export async function getDependencies(
entryConfig: Pick<EntryConfig, "paths" | "test-excludes">,
ignoreDirs: readonly string[] = [],
numOfWorkers?: number
): Promise<depGraph.Dependency[]> {
const ignoreDirPatterns = ignoreDirs.map(dir => path.join(dir, "**/*"));
const parser = new DependencyParserWithWorkers(numOfWorkers);
try {
// TODO: uniq
const parseResultPromises = entryConfig.paths.map(async p => {
let testExcludes: readonly string[] = [];
if (entryConfig["test-excludes"]) {
testExcludes = entryConfig["test-excludes"];
}
const files = await globPromise(path.join(p, "**/*.js"), {
ignore: ignoreDirPatterns,
follow: true,
});
return Promise.all(
files
// TODO: load deps.js path from config
.filter(file => !/\bdeps\.js$/.test(file))
.filter(file => {
if (testExcludes.some(exclude => file.startsWith(exclude))) {
return !/_test\.js$/.test(file);
}
return true;
})
.map(async file => {
if (pathToDependencyCache.has(file)) {
return pathToDependencyCache.get(file)!;
} else {
const promise = parser.parse(file);
pathToDependencyCache.set(file, promise);
return promise;
}
})
);
});
return flat(await Promise.all(parseResultPromises));
} finally {
await parser.terminate();
}
}
/**
* Get dependencies of Closure Library by loading deps.js
*/
export async function getClosureLibraryDependencies(
closureLibraryDir: string
): Promise<depGraph.Dependency[]> {
const googDepsPath = path.join(closureLibraryDir, "closure", "goog", "deps.js");
const depsContent = await readFile(googDepsPath, "utf8");
const result = parser.parseDependencyFile(depsContent, googDepsPath);
if (result.errors.length > 0) {
throw new Error(`Fail to parse deps.js of Closure Library: ${result.errors.join(", ")}`);
}
appendGoogImport(result.dependencies, path.dirname(googDepsPath));
return result.dependencies;
}
/**
* deps.js generated by google-closure-deps doesn't include "goog" in the imports.
* https://github.com/google/closure-library/blob/v20190415/closure-deps/lib/depfile.js#L43-L47
* To require "base.js", "goog" needs to be added to the imports.
*
* @param dependencies
* @param googBaseDir A path to the directory including base.js
*/
function appendGoogImport(dependencies: readonly depGraph.Dependency[], googBaseDir: string) {
dependencies.forEach(dep => {
dep.setClosurePath(googBaseDir);
if (dep.closureSymbols.length > 0 || dep.imports.find(i => i.isGoogRequire())) {
const goog = new depGraph.GoogRequire("goog");
goog.from = dep;
dep.imports.push(goog);
}
});
}
export function countDepCache(): number {
return pathToDependencyCache.size;
}
export function removeDepCacheByPath(filepath: string): boolean {
return pathToDependencyCache.delete(filepath);
}
export function clearDepCache(): void {
pathToDependencyCache.clear();
}