-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathindex.js
267 lines (187 loc) · 9.28 KB
/
index.js
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/////////////////////
// --- Chapter 1 ---
import fs from 'fs-extra';
import fetch from 'node-fetch';
import semver from 'semver';
async function fetchPackage({name, reference}) {
// In a pure JS fashion, if it looks like a path, it must be a path.
if ([`/`, `./`, `../`].some(prefix => reference.startsWith(prefix)))
return await fs.readFile(reference);
if (semver.valid(reference))
return await fetchPackage({name, reference: `https://registry.yarnpkg.com/${name}/-/${name}-${reference}.tgz`});
let response = await fetch(reference);
if (!response.ok)
throw new Error(`Couldn't fetch package "${reference}"`);
return await response.buffer();
}
/////////////////////
// --- Chapter 2 ---
export async function getPinnedReference({name, reference}) {
// 1.0.0 is a valid range per semver syntax, but since it's also a pinned
// reference, we don't actually need to process it. Less work, yay!~
if (semver.validRange(reference) && !semver.valid(reference)) {
let response = await fetch(`https://registry.yarnpkg.com/${name}`);
let info = await response.json();
let versions = Object.keys(info.versions);
let maxSatisfying = semver.maxSatisfying(versions, reference);
if (maxSatisfying === null)
throw new Error(`Couldn't find a version matching "${reference}" for package "${name}"`);
reference = maxSatisfying;
}
return {name, reference};
}
/////////////////////
// --- Chapter 3 ---
import {readPackageJsonFromArchive} from './utilities';
export async function getPackageDependencies({name, reference}) {
let packageBuffer = await fetchPackage({name, reference});
let packageJson = JSON.parse(await readPackageJsonFromArchive(packageBuffer));
// Some packages have no dependency field
let dependencies = packageJson.dependencies || {};
// It's much easier for us to just keep using the same {name, reference}
// data structure across all of our code, so we convert it there.
return Object.keys(dependencies).map(name => {
return { name, reference: dependencies[name] };
});
}
/////////////////////
// --- Chapter 4 ---
async function getPackageDependencyTree(pace, {name, reference, dependencies}, available = new Map()) {
return {name, reference, dependencies: await Promise.all(dependencies.filter(volatileDependency => {
let availableReference = available.get(volatileDependency.name);
// If the volatile reference exactly matches the available reference (for
// example in the case of two URLs, or two file paths), it means that it
// is already satisfied by the package provided by its parent. In such a
// case, we can safely ignore this dependency!
if (volatileDependency.reference === availableReference)
return false;
// If the volatile dependency is a semver range, and if the package
// provided by its parent satisfies it, we can also safely ignore the
// dependency.
if (semver.validRange(volatileDependency.reference)
&& semver.satisfies(availableReference, volatileDependency.reference))
return false;
return true;
}).map(async (volatileDependency) => {
pace.total += 1;
let staticDependency = await getPinnedReference(volatileDependency);
let subDependencies = await getPackageDependencies(staticDependency);
let subAvailable = new Map(available);
subAvailable.set(staticDependency.name, staticDependency.reference);
pace.tick();
return await getPackageDependencyTree(pace, Object.assign({}, staticDependency, {dependencies: subDependencies}), subAvailable);
}))};
}
/////////////////////
// --- Chapter 5 ---
import cp from 'child_process';
import {resolve, relative} from 'path';
import util from 'util';
// This function extracts an npm-compatible archive somewhere on the disk
import {extractNpmArchiveTo} from './utilities';
const exec = util.promisify(cp.exec);
async function linkPackages(pace, {name, reference, dependencies}, cwd) {
pace.total += 1;
// As we previously seen, the root package will be the only one containing
// no reference. We can simply skip its linking, since by definition it already
// contains the entirety of its own code :)
if (reference) {
let packageBuffer = await fetchPackage({name, reference});
await extractNpmArchiveTo(packageBuffer, cwd);
}
await Promise.all(dependencies.map(async ({name, reference, dependencies}) => {
let target = `${cwd}/node_modules/${name}`;
let binTarget = `${cwd}/node_modules/.bin`;
await linkPackages(pace, {name, reference, dependencies}, target);
let dependencyPackageJson = require(`${target}/package.json`);
let bin = dependencyPackageJson.bin || {};
if (typeof bin === `string`)
bin = {[name]: bin};
for (let binName of Object.keys(bin)) {
let source = resolve(target, bin[binName]);
let dest = resolve(binTarget, binName);
await fs.mkdirp(binTarget);
await fs.symlink(relative(binTarget, source), dest);
}
if (dependencyPackageJson.scripts) {
for (let scriptName of [`preinstall`, `install`, `postinstall`]) {
let script = dependencyPackageJson.scripts[scriptName];
if (!script)
continue;
await exec(script, {cwd: target, env: Object.assign({}, process.env, {
PATH: `${target}/node_modules/.bin:${process.env.PATH}`
})});
}
}
}));
pace.tick();
}
/////////////////////
// --- Chapter 6 ---
function optimizePackageTree({name, reference, dependencies}) {
// This is a Divide & Conquer algorithm - we split the large problem into
// subproblems that we solve on their own, then we combine their results
// to find the final solution.
//
// In this particular case, we will say that our optimized tree is the result
// of optimizing a single depth of already-optimized dependencies (ie we first
// optimize each one of our dependencies independently, then we aggregate their
// results and optimize them all a last time).
dependencies = dependencies.map(dependency => {
return optimizePackageTree(dependency);
});
// Now that our dependencies have been optimized, we can start working on
// doing the second pass to combine their results together. We'll iterate on
// each one of those "hard" dependencies (called as such because they are
// strictly required by the package itself rather than one of its dependencies),
// and check if they contain any sub-dependency that we could "adopt" as our own.
for (let hardDependency of dependencies.slice()) {
for (let subDependency of hardDependency.dependencies.slice()) {
// First we look for a dependency we own that is called
// just like the sub-dependency we're iterating on.
let availableDependency = dependencies.find(dependency => {
return dependency.name === subDependency.name;
});
// If there's none, great! It means that there won't be any collision
// if we decide to adopt this one, so we can just go ahead.
if (!availableDependency)
dependencies.push(subDependency);
// If we've adopted the sub-dependency, or if the already existing
// dependency has the exact same reference than the sub-dependency,
// then it becomes useless and we can simply delete it.
if (!availableDependency || availableDependency.name === subDependency.name) {
hardDependency.dependencies.splice(hardDependency.dependencies.findIndex(dependency => {
return dependency.name === subDependency.name;
}));
}
}
}
return { name, reference, dependencies };
}
//////////////////////
// --- Conclusion ---
import { trackProgress } from './utilities';
// We'll use the first command line argument (argv[2]) as working directory,
// but if there's none we'll just use the directory from which we've executed
// the script
let cwd = resolve(process.argv[2] || process.cwd());
let packageJson = require(resolve(cwd, `package.json`));
// And as destination, we'll use the second command line argument (argv[3]),
// or the cwd if there's none. We do this because for such a minipkg, it would
// be nice not to override the 'true' node_modules :)
let dest = resolve(process.argv[3] || cwd);
// Remember that because we use a different format for our dependencies than
// a simple dictionary, we also need to convert it when reading this file
packageJson.dependencies = Object.keys(packageJson.dependencies || {}).map(name => {
return { name, reference: packageJson.dependencies[name] };
});
Promise.resolve().then(() => {
console.log(`Resolving the package tree...`);
return trackProgress(pace => getPackageDependencyTree(pace, packageJson));
}).then(packageTree => {
console.log(`Linking the packages on the filesystem...`);
return trackProgress(pace => linkPackages(pace, optimizePackageTree(packageTree), dest));
}).catch(error => {
console.log(error.stack);
process.exit(1);
});