-
Notifications
You must be signed in to change notification settings - Fork 264
Expand file tree
/
Copy pathsnippet-dependencies.ts
More file actions
249 lines (217 loc) · 7.91 KB
/
snippet-dependencies.ts
File metadata and controls
249 lines (217 loc) · 7.91 KB
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import * as cp from 'child_process';
import * as fastGlob from 'fast-glob';
import { promises as fsPromises } from 'fs';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as semver from 'semver';
import { intersect } from 'semver-intersect';
import { findDependencyDirectory, findUp } from './find-utils';
import * as logging from './logging';
import { TypeScriptSnippet, CompilationDependency } from './snippet';
import { mkDict, formatList, pathExists } from './util';
/**
* Collect the dependencies of a bunch of snippets together in one declaration
*
* We assume here the dependencies will not conflict.
*/
export function collectDependencies(snippets: TypeScriptSnippet[]) {
const ret: Record<string, CompilationDependency> = {};
for (const snippet of snippets) {
for (const [name, source] of Object.entries(snippet.compilationDependencies ?? {})) {
ret[name] = resolveConflict(name, source, ret[name]);
}
}
return ret;
}
function resolveConflict(
name: string,
a: CompilationDependency,
b: CompilationDependency | undefined,
): CompilationDependency {
if (!b) {
return a;
}
if (a.type === 'concrete' && b.type === 'concrete') {
if (b.resolvedDirectory !== a.resolvedDirectory) {
throw new Error(`Dependency conflict: ${name} can be either ${a.resolvedDirectory} or ${b.resolvedDirectory}`);
}
return a;
}
if (a.type === 'symbolic' && b.type === 'symbolic') {
// Intersect the ranges
return {
type: 'symbolic',
versionRange: intersect(a.versionRange, b.versionRange),
};
}
if (a.type === 'concrete' && b.type === 'symbolic') {
const concreteVersion: string = JSON.parse(
fs.readFileSync(path.join(a.resolvedDirectory, 'package.json'), 'utf-8'),
).version;
if (!semver.satisfies(concreteVersion, b.versionRange)) {
throw new Error(
`Dependency conflict: ${name} expected to match ${b.versionRange} but found ${concreteVersion} at ${a.resolvedDirectory}`,
);
}
return a;
}
if (a.type === 'symbolic' && b.type === 'concrete') {
// Reverse roles so we fall into the previous case
return resolveConflict(name, b, a);
}
throw new Error('Cases should have been exhaustive');
}
/**
* Check that the directory we were given has all the necessary dependencies in it
*
* It's a warning if this is not true, not an error.
*/
export async function validateAvailableDependencies(directory: string, deps: Record<string, CompilationDependency>) {
const failures = await Promise.all(
Object.entries(deps).flatMap(async ([name, _dep]) => {
try {
await findDependencyDirectory(name, directory);
return [];
} catch {
return [name];
}
}),
);
if (failures.length > 0) {
logging.warn(
`${directory}: packages necessary to compile examples missing from supplied directory: ${failures.join(', ')}`,
);
}
}
/**
* Prepare a temporary directory with symlinks to all the dependencies we need.
*
* - Symlinks the concrete dependencies
* - Tries to first find the symbolic dependencies in a potential monorepo that might be present
* (try both `lerna` and `yarn` monorepos).
* - Installs the remaining symbolic dependencies using 'npm'.
*/
export async function prepareDependencyDirectory(deps: Record<string, CompilationDependency>): Promise<string> {
const concreteDirs = Object.values(deps)
.filter(isConcrete)
.map((x) => x.resolvedDirectory);
const monorepoPackages = await scanMonoRepos(concreteDirs);
const tmpDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'rosetta'));
logging.info(`Preparing dependency closure at ${tmpDir}`);
// Resolved symbolic packages against monorepo
const resolvedDeps = mkDict(
Object.entries(deps).map(([name, dep]) => [
name,
dep.type === 'concrete'
? dep
: ((monorepoPackages[name]
? { type: 'concrete', resolvedDirectory: monorepoPackages[name] }
: dep) as CompilationDependency),
]),
);
// Use 'npm install' only for the symbolic packages. For the concrete packages,
// npm is going to try and find transitive dependencies as well and it won't know
// about monorepos.
const symbolicInstalls = Object.entries(resolvedDeps).flatMap(([name, dep]) =>
isSymbolic(dep) ? [`${name}@${dep.versionRange}`] : [],
);
const linkedInstalls = mkDict(
Object.entries(resolvedDeps).flatMap(([name, dep]) =>
isConcrete(dep) ? [[name, dep.resolvedDirectory] as const] : [],
),
);
// Run 'npm install' on it
if (symbolicInstalls.length > 0) {
logging.debug(`Installing example dependencies: ${symbolicInstalls.join(' ')}`);
cp.execSync(`npm install ${symbolicInstalls.join(' ')}`, { cwd: tmpDir, encoding: 'utf-8' });
}
// Symlink the rest
if (Object.keys(linkedInstalls).length > 0) {
logging.debug(`Symlinking example dependencies: ${Object.values(linkedInstalls).join(' ')}`);
const modDir = path.join(tmpDir, 'node_modules');
await Promise.all(
Object.entries(linkedInstalls).map(async ([name, source]) => {
const target = path.join(modDir, name);
if (!(await pathExists(target))) {
// Package could be namespaced, so ensure the namespace dir exists
await fsPromises.mkdir(path.dirname(target), { recursive: true });
await fsPromises.symlink(source, target, 'dir');
}
}),
);
}
return tmpDir;
}
/**
* Map package name to directory
*/
async function scanMonoRepos(startingDirs: readonly string[]): Promise<Record<string, string>> {
const globs = new Set<string>();
for (const dir of startingDirs) {
// eslint-disable-next-line no-await-in-loop
setExtend(globs, await findMonoRepoGlobs(dir));
}
if (globs.size === 0) {
return {};
}
logging.debug(`Monorepo package sources: ${Array.from(globs).join(', ')}`);
const packageDirectories = await fastGlob(Array.from(globs).map(windowsToUnix), { onlyDirectories: true });
const results = mkDict(
(
await Promise.all(
packageDirectories.map(async (directory) => {
const pjLocation = path.join(directory, 'package.json');
return (await pathExists(pjLocation))
? [[JSON.parse(await fsPromises.readFile(pjLocation, 'utf-8')).name as string, directory] as const]
: [];
}),
)
).flat(),
);
logging.debug(`Found ${Object.keys(results).length} packages in monorepo: ${formatList(Object.keys(results))}`);
return results;
}
async function findMonoRepoGlobs(startingDir: string): Promise<Set<string>> {
const ret = new Set<string>();
// Lerna monorepo
const lernaJsonDir = await findUp(startingDir, async (dir) => pathExists(path.join(dir, 'lerna.json')));
if (lernaJsonDir) {
const lernaJson = JSON.parse(await fsPromises.readFile(path.join(lernaJsonDir, 'lerna.json'), 'utf-8'));
for (const glob of lernaJson?.packages ?? []) {
ret.add(path.join(lernaJsonDir, glob));
}
}
// Yarn monorepo
const yarnWsDir = await findUp(
startingDir,
async (dir) =>
(await pathExists(path.join(dir, 'package.json'))) &&
JSON.parse(await fsPromises.readFile(path.join(dir, 'package.json'), 'utf-8'))?.workspaces !== undefined,
);
if (yarnWsDir) {
const yarnWs = JSON.parse(await fsPromises.readFile(path.join(yarnWsDir, 'package.json'), 'utf-8'));
for (const glob of yarnWs.workspaces?.packages ?? []) {
ret.add(path.join(yarnWsDir, glob));
}
}
return ret;
}
function isSymbolic(x: CompilationDependency): x is Extract<CompilationDependency, { type: 'symbolic' }> {
return x.type === 'symbolic';
}
function isConcrete(x: CompilationDependency): x is Extract<CompilationDependency, { type: 'concrete' }> {
return x.type === 'concrete';
}
function setExtend<A>(xs: Set<A>, ys: Set<A>) {
for (const y of ys) {
xs.add(y);
}
return xs;
}
/**
* Necessary for fastGlob
*/
function windowsToUnix(x: string) {
return x.replace(/\\/g, '/');
}