socket-function 1.2.35 → 1.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.35",
3
+ "version": "1.2.37",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
package/src/dnsCache.ts CHANGED
@@ -158,19 +158,21 @@ export async function resolveHost(hostname: string, family = 0): Promise<DNSReco
158
158
  // the socket use our cache instead of getaddrinfo. Always resolves via our cache; happy eyeballs
159
159
  // (autoSelectFamily) then races the returned addresses.
160
160
  export const dnsCacheLookup: net.LookupFunction = function dnsCacheLookup(hostname, options, callback) {
161
- let rawFamily = typeof options === "number" ? options : options.family ?? 0;
162
- let family = rawFamily === "IPv4" ? 4 : rawFamily === "IPv6" ? 6 : rawFamily;
161
+ // Node's own callers pass the family as a number, but the option is documented as accepting "IPv4"/"IPv6"
162
+ // too, so handle both even though the typings only admit the number.
163
+ let rawFamily: number | string = typeof options === "number" ? options : options.family ?? 0;
164
+ let family = rawFamily === "IPv4" ? 4 : rawFamily === "IPv6" ? 6 : Number(rawFamily) || 0;
163
165
  let all = typeof options === "object" && options.all;
164
166
  resolveHost(hostname, family).then(
165
167
  records => {
166
168
  if (all) {
167
- callback(null, records.map(r => ({ address: r.address, family: r.family })));
169
+ callback(null, records.map(r => ({ address: r.address, family: r.family })), family);
168
170
  } else {
169
171
  let record = records[0];
170
172
  callback(null, record.address, record.family);
171
173
  }
172
174
  },
173
- (err: NodeJS.ErrnoException) => callback(err, "", undefined)
175
+ (err: NodeJS.ErrnoException) => callback(err, "", family)
174
176
  );
175
177
  };
176
178
 
package/src/upreal.ts CHANGED
@@ -11,10 +11,23 @@ const DEP_SECTIONS = [
11
11
  "peerDependencies",
12
12
  ] as const;
13
13
 
14
+ // Yarn itself spells this "--dry-run", so accept that plus every spelling anyone reasonably reaches for.
15
+ const DRY_RUN_FLAGS = ["--dry", "-dry", "--dryrun", "-dryrun", "--dry-run", "-dry-run", "--dry_run", "-dry_run"];
16
+
17
+ // yarn install can still decide to re-resolve and re-split; re-running the merge converges in that case.
18
+ const MAX_MERGE_PASSES = 3;
19
+
20
+ // yarn v1 rewrites npm registry urls to its own mirror, so entries we synthesize have to match or yarn
21
+ // churns the lockfile back on the next install.
22
+ const NPM_REGISTRY_HOST = "registry.npmjs.org";
23
+ const YARN_REGISTRY_HOST = "registry.yarnpkg.com";
24
+
14
25
  async function main() {
15
- let packageName = process.argv.slice(2).find(arg => !arg.startsWith("-"));
26
+ let args = process.argv.slice(2);
27
+ let packageName = args.find(arg => !arg.startsWith("-"));
28
+ let dryRun = args.some(arg => DRY_RUN_FLAGS.includes(arg.toLowerCase()));
16
29
  if (!packageName) {
17
- console.error("Usage: yarn upreal <package-name>");
30
+ console.error("Usage: yarn upreal <package-name> [--dry-run]");
18
31
  process.exit(1);
19
32
  return;
20
33
  }
@@ -23,78 +36,100 @@ async function main() {
23
36
  let packageJsonPath = path.join(projectRoot, "package.json");
24
37
  let lockPath = path.join(projectRoot, "yarn.lock");
25
38
 
26
- let packageJsonRaw = fs.readFileSync(packageJsonPath, "utf8");
27
- let packageJson = JSON.parse(packageJsonRaw) as {
28
- [section: string]: { [name: string]: string } | undefined;
29
- };
39
+ let manifest = await getLatestManifest(packageName);
40
+ console.log(`Latest version of ${packageName}: ${manifest.version}`);
30
41
 
31
- let currentRange = findCurrentRange(packageJson, packageName);
32
- if (currentRange === undefined) {
33
- console.error(`Package "${packageName}" is not listed in ${DEP_SECTIONS.join(", ")} of package.json`);
34
- process.exit(1);
35
- return;
36
- }
42
+ let newRange = updatePackageJson(packageJsonPath, packageName, manifest.version);
37
43
 
38
- console.log(`Current range for ${packageName}: ${currentRange}`);
44
+ for (let pass = 1; pass <= MAX_MERGE_PASSES; pass++) {
45
+ if (fs.existsSync(lockPath)) {
46
+ let result = mergeLockEntries(lockPath, packageName, manifest, newRange);
47
+ console.log(`Lockfile pass ${pass}: ${result.specs.length} spec${result.specs.length === 1 ? "" : "s"} for ${packageName} now share one entry at ${manifest.version}: ${result.specs.join(", ")}`);
48
+ for (let moved of result.movedFrom) {
49
+ console.log(` ${moved.spec} was resolving to ${moved.version}`);
50
+ }
51
+ for (let left of result.leftAlone) {
52
+ console.log(` left ${left} alone (not a registry range, so it can't point at a version)`);
53
+ }
54
+ }
39
55
 
40
- let latest = await getLatestVersion(packageName);
41
- console.log(`Latest version of ${packageName}: ${latest}`);
56
+ if (dryRun) {
57
+ console.log(`Dry run: package.json and yarn.lock are rewritten, skipping yarn install.`);
58
+ return;
59
+ }
42
60
 
43
- let newRange = applyPrefix(currentRange, latest);
44
- if (newRange === currentRange) {
45
- console.log(`package.json range unchanged (${currentRange}); will still re-resolve the lockfile.`);
46
- } else {
47
- packageJsonRaw = replaceRange(packageJsonRaw, packageName, currentRange, newRange);
48
- fs.writeFileSync(packageJsonPath, packageJsonRaw);
49
- console.log(`Updated package.json: ${packageName} ${currentRange} -> ${newRange}`);
50
- }
61
+ console.log(`Running yarn install...`);
62
+ await runYarnInstall(projectRoot);
51
63
 
52
- // Drop every lockfile entry for this package so `yarn install` re-resolves each of its ranges (ours and any
53
- // transitive ones) to the newest version they can reach. This is what makes "everything that could be updated"
54
- // update in one shot, rather than yarn pinning the previously-locked versions.
55
- if (fs.existsSync(lockPath)) {
56
- let lockRaw = fs.readFileSync(lockPath, "utf8");
57
- let { lock, removed } = removeLockEntries(lockRaw, packageName);
58
- if (removed > 0) {
59
- fs.writeFileSync(lockPath, lock);
60
- console.log(`Removed ${removed} lockfile entr${removed === 1 ? "y" : "ies"} for ${packageName} so they re-resolve.`);
64
+ let versions = getLockedVersions(lockPath, packageName);
65
+ if (versions.length <= 1) {
66
+ console.log(`Done. ${packageName} resolves to a single version: ${versions[0] ?? manifest.version}`);
67
+ return;
61
68
  }
69
+ console.log(`${packageName} still resolves to ${versions.length} versions (${versions.join(", ")}); merging again.`);
62
70
  }
63
71
 
64
- console.log(`Running yarn install...`);
65
- await runYarnInstall(projectRoot);
66
- console.log(`Done. ${packageName} is now at ${newRange}.`);
72
+ let versions = getLockedVersions(lockPath, packageName);
73
+ console.error(`Failed to collapse ${packageName} onto one version after ${MAX_MERGE_PASSES} passes; still at ${versions.join(", ")}`);
74
+ process.exit(1);
67
75
  }
68
76
 
69
- function findCurrentRange(
70
- packageJson: { [section: string]: { [name: string]: string } | undefined },
71
- packageName: string
72
- ): string | undefined {
77
+ // Point every package.json section that mentions the package at the latest version, keeping each section's
78
+ // existing range operator, and return the range the lockfile entry has to satisfy.
79
+ function updatePackageJson(packageJsonPath: string, packageName: string, latest: string): string {
80
+ let raw = fs.readFileSync(packageJsonPath, "utf8");
81
+ let packageJson = JSON.parse(raw) as {
82
+ [section: string]: { [name: string]: string } | undefined;
83
+ };
84
+
85
+ let newRange: string | undefined;
73
86
  for (let section of DEP_SECTIONS) {
74
87
  let deps = packageJson[section];
75
- if (deps && packageName in deps) {
76
- return deps[packageName];
88
+ if (!deps || !(packageName in deps)) {
89
+ continue;
77
90
  }
91
+ let currentRange = deps[packageName];
92
+ let sectionRange = applyPrefix(currentRange, latest);
93
+ newRange = newRange ?? sectionRange;
94
+ if (sectionRange === currentRange) {
95
+ console.log(`package.json ${section}.${packageName} already ${currentRange}`);
96
+ continue;
97
+ }
98
+ raw = replaceRange(raw, packageName, currentRange, sectionRange);
99
+ fs.writeFileSync(packageJsonPath, raw);
100
+ console.log(`Updated package.json ${section}.${packageName}: ${currentRange} -> ${sectionRange}`);
78
101
  }
79
- return undefined;
102
+
103
+ if (newRange === undefined) {
104
+ console.log(`${packageName} is not a direct dependency; only merging the transitive references.`);
105
+ return "^" + latest;
106
+ }
107
+ return newRange;
108
+ }
109
+
110
+ interface Manifest {
111
+ version: string;
112
+ dist?: { tarball?: string; shasum?: string; integrity?: string };
113
+ dependencies?: { [name: string]: string };
114
+ optionalDependencies?: { [name: string]: string };
80
115
  }
81
116
 
82
- async function getLatestVersion(packageName: string): Promise<string> {
117
+ async function getLatestManifest(packageName: string): Promise<Manifest> {
83
118
  // Scoped names ("@scope/name") must have their slash encoded in the registry path.
84
119
  let encodedName = packageName.startsWith("@")
85
120
  ? "@" + encodeURIComponent(packageName.slice(1))
86
121
  : encodeURIComponent(packageName);
87
- let url = `https://registry.npmjs.org/${encodedName}/latest`;
122
+ let url = `https://${NPM_REGISTRY_HOST}/${encodedName}/latest`;
88
123
 
89
124
  // Via httpsRequest so registry lookups go through the DNS cache (and its re-resolve/retry), rather
90
125
  // than getaddrinfo, which caches certain failures forever.
91
126
  let body = (await httpsRequest(url)).toString("utf8");
92
127
 
93
- let parsed = JSON.parse(body) as { version?: string };
128
+ let parsed = JSON.parse(body) as Manifest;
94
129
  if (!parsed.version) {
95
130
  throw new Error(`Registry response for ${packageName} had no version field`);
96
131
  }
97
- return parsed.version;
132
+ return parsed;
98
133
  }
99
134
 
100
135
  // Keep whatever range operator the user already chose (^, ~, exact, or *) and point it at the new version.
@@ -120,37 +155,50 @@ function replaceRange(packageJsonRaw: string, packageName: string, oldRange: str
120
155
  return packageJsonRaw.replace(keyPattern, `$1${newRange}$2`);
121
156
  }
122
157
 
123
- // Remove all yarn.lock v1 blocks that resolve the given package (across every range variant of it).
124
- function removeLockEntries(lockRaw: string, packageName: string): { lock: string; removed: number } {
125
- // yarn.lock v1 blocks are separated by blank lines; the first line of each block is the comma-separated
126
- // list of specs it satisfies, e.g. `"pkg@^1.0.0", "pkg@~1.2.0":`.
127
- let blocks = lockRaw.split(/\r?\n\r?\n/);
128
- let removed = 0;
129
- let kept: string[] = [];
130
- for (let block of blocks) {
131
- if (blockResolvesPackage(block, packageName)) {
132
- removed++;
158
+ interface LockBlock {
159
+ // Index of the header line, and the exclusive end of the block (trailing blank lines included).
160
+ start: number;
161
+ end: number;
162
+ specs: string[];
163
+ }
164
+
165
+ interface LockFile {
166
+ lines: string[];
167
+ eol: string;
168
+ blocks: LockBlock[];
169
+ }
170
+
171
+ function readLock(lockPath: string): LockFile {
172
+ let raw = fs.readFileSync(lockPath, "utf8");
173
+ let eol = raw.includes("\r\n") ? "\r\n" : "\n";
174
+ let lines = raw.split(/\r?\n/);
175
+
176
+ let blocks: LockBlock[] = [];
177
+ let current: LockBlock | undefined;
178
+ for (let i = 0; i < lines.length; i++) {
179
+ if (!isBlockHeader(lines[i])) {
133
180
  continue;
134
181
  }
135
- kept.push(block);
182
+ if (current) {
183
+ current.end = i;
184
+ }
185
+ current = { start: i, end: lines.length, specs: parseHeaderSpecs(lines[i]) };
186
+ blocks.push(current);
136
187
  }
137
- return { lock: kept.join("\n\n"), removed };
188
+ return { lines, eol, blocks };
138
189
  }
139
190
 
140
- function blockResolvesPackage(block: string, packageName: string): boolean {
141
- let headerLine = block.split(/\r?\n/).find(line => line.trim().length > 0 && !line.startsWith("#"));
142
- if (!headerLine || !headerLine.trimEnd().endsWith(":")) {
191
+ function isBlockHeader(line: string): boolean {
192
+ if (!line || line.startsWith("#") || /^\s/.test(line)) {
143
193
  return false;
144
194
  }
145
- // Strip the trailing colon, then split on commas into individual quoted-or-bare specs.
195
+ return line.trimEnd().endsWith(":");
196
+ }
197
+
198
+ function parseHeaderSpecs(headerLine: string): string[] {
199
+ // e.g. `"pkg@^1.0.0", "pkg@~1.2.0":` -> ["pkg@^1.0.0", "pkg@~1.2.0"]
146
200
  let specsPart = headerLine.trimEnd().replace(/:$/, "");
147
- let specs = specsPart.split(",").map(spec => spec.trim().replace(/^"|"$/g, ""));
148
- for (let spec of specs) {
149
- if (specNamePackage(spec) === packageName) {
150
- return true;
151
- }
152
- }
153
- return false;
201
+ return specsPart.split(",").map(spec => spec.trim().replace(/^"|"$/g, ""));
154
202
  }
155
203
 
156
204
  // Extract the package name from a lock spec like `@scope/name@^1.0.0` or `name@~1.2.0`.
@@ -163,6 +211,163 @@ function specNamePackage(spec: string): string {
163
211
  return spec.slice(0, atIndex);
164
212
  }
165
213
 
214
+ function specRange(spec: string): string {
215
+ return spec.slice(specNamePackage(spec).length + 1);
216
+ }
217
+
218
+ // Ranges that don't come from the registry (git urls, file:, link:, npm: aliases) can't be pointed at a
219
+ // version number, so their entry has to stay separate.
220
+ function isRegistryRange(range: string): boolean {
221
+ return !range.includes(":") && !range.includes("/");
222
+ }
223
+
224
+ // yarn.lock quotes a token only when it would otherwise be ambiguous, and rewriting a token with different
225
+ // quoting than yarn would use makes every later install churn the file.
226
+ function quoteToken(value: string): string {
227
+ let needsQuotes = /[:\s\n\\",[\]]/.test(value) || !/^[a-zA-Z]/.test(value);
228
+ return needsQuotes ? `"${value}"` : value;
229
+ }
230
+
231
+ function formatHeader(specs: string[]): string {
232
+ return specs.map(quoteToken).join(", ") + ":";
233
+ }
234
+
235
+ // Put every registry spec for the package onto ONE entry resolving to the latest version. Nothing is deleted:
236
+ // a spec that asked for an old version (`pkg@1.2.3`) stays in the header and now points at the new version,
237
+ // which is what stops yarn from re-creating a second entry the next time something re-resolves.
238
+ function mergeLockEntries(lockPath: string, packageName: string, manifest: Manifest, newRange: string): {
239
+ specs: string[];
240
+ movedFrom: { spec: string; version: string }[];
241
+ leftAlone: string[];
242
+ } {
243
+ let lock = readLock(lockPath);
244
+ let specs = new Set<string>([`${packageName}@${newRange}`]);
245
+ let movedFrom: { spec: string; version: string }[] = [];
246
+ let leftAlone: string[] = [];
247
+ let removeLines = new Set<number>();
248
+ let insertAt: number | undefined;
249
+ let body: string[] | undefined;
250
+
251
+ for (let block of lock.blocks) {
252
+ if (!block.specs.some(spec => specNamePackage(spec) === packageName)) {
253
+ continue;
254
+ }
255
+ let mergeable = block.specs.filter(spec => specNamePackage(spec) === packageName && isRegistryRange(specRange(spec)));
256
+ let keep = block.specs.filter(spec => !mergeable.includes(spec));
257
+ if (mergeable.length === 0) {
258
+ leftAlone.push(...block.specs);
259
+ continue;
260
+ }
261
+
262
+ let version = getBlockVersion(lock, block);
263
+ for (let spec of mergeable) {
264
+ if (!specs.has(spec) && version !== manifest.version) {
265
+ movedFrom.push({ spec, version: version ?? "?" });
266
+ }
267
+ specs.add(spec);
268
+ }
269
+ // Reuse the real entry when the lockfile already holds the latest version, so we keep yarn's own
270
+ // resolved url and integrity hash rather than synthesizing them.
271
+ if (version === manifest.version && !body) {
272
+ body = lock.lines.slice(block.start + 1, blockBodyEnd(lock, block));
273
+ }
274
+ if (keep.length > 0) {
275
+ lock.lines[block.start] = formatHeader(keep);
276
+ leftAlone.push(...keep);
277
+ continue;
278
+ }
279
+ insertAt = insertAt ?? block.start;
280
+ for (let i = block.start; i < block.end; i++) {
281
+ removeLines.add(i);
282
+ }
283
+ }
284
+
285
+ let merged = [formatHeader(Array.from(specs).sort()), ...(body ?? synthesizeBody(manifest)), ""];
286
+
287
+ let output: string[] = [];
288
+ for (let i = 0; i < lock.lines.length; i++) {
289
+ if (i === insertAt) {
290
+ output.push(...merged);
291
+ }
292
+ if (removeLines.has(i)) {
293
+ continue;
294
+ }
295
+ output.push(lock.lines[i]);
296
+ }
297
+ if (insertAt === undefined) {
298
+ // The package wasn't in the lockfile at all; append and let yarn sort it.
299
+ output.push(...merged);
300
+ }
301
+
302
+ fs.writeFileSync(lockPath, output.join(lock.eol));
303
+ return { specs: Array.from(specs).sort(), movedFrom, leftAlone };
304
+ }
305
+
306
+ // The lines a block owns, excluding the blank line(s) separating it from the next block.
307
+ function blockBodyEnd(lock: LockFile, block: LockBlock): number {
308
+ let end = block.end;
309
+ while (end > block.start + 1 && lock.lines[end - 1].trim() === "") {
310
+ end--;
311
+ }
312
+ return end;
313
+ }
314
+
315
+ function getBlockVersion(lock: LockFile, block: LockBlock): string | undefined {
316
+ for (let i = block.start + 1; i < block.end; i++) {
317
+ let match = /^\s+version "(.*)"$/.exec(lock.lines[i]);
318
+ if (match) {
319
+ return match[1];
320
+ }
321
+ }
322
+ return undefined;
323
+ }
324
+
325
+ // Build a lockfile entry for a version the lockfile has never seen, from the registry's own metadata.
326
+ function synthesizeBody(manifest: Manifest): string[] {
327
+ let tarball = manifest.dist?.tarball;
328
+ if (!tarball) {
329
+ throw new Error(`Registry response for ${manifest.version} had no dist.tarball, so no lockfile entry can be written`);
330
+ }
331
+ let resolved = tarball.replace(NPM_REGISTRY_HOST, YARN_REGISTRY_HOST);
332
+ if (manifest.dist?.shasum) {
333
+ resolved += "#" + manifest.dist.shasum;
334
+ }
335
+
336
+ let lines = [` version ${quoteToken(manifest.version)}`, ` resolved ${quoteToken(resolved)}`];
337
+ if (manifest.dist?.integrity) {
338
+ lines.push(` integrity ${quoteToken(manifest.dist.integrity)}`);
339
+ }
340
+ for (let section of ["dependencies", "optionalDependencies"] as const) {
341
+ let deps = manifest[section];
342
+ if (!deps || Object.keys(deps).length === 0) {
343
+ continue;
344
+ }
345
+ lines.push(` ${section}:`);
346
+ for (let name of Object.keys(deps).sort()) {
347
+ lines.push(` ${quoteToken(name)} ${quoteToken(deps[name])}`);
348
+ }
349
+ }
350
+ return lines;
351
+ }
352
+
353
+ function getLockedVersions(lockPath: string, packageName: string): string[] {
354
+ if (!fs.existsSync(lockPath)) {
355
+ return [];
356
+ }
357
+ let lock = readLock(lockPath);
358
+ let versions = new Set<string>();
359
+ for (let block of lock.blocks) {
360
+ if (!block.specs.some(spec => specNamePackage(spec) === packageName)) {
361
+ continue;
362
+ }
363
+ let version = getBlockVersion(lock, block);
364
+ if (version) {
365
+ versions.add(version);
366
+ }
367
+ }
368
+ return Array.from(versions);
369
+ }
370
+
166
371
  function escapeRegExp(value: string): string {
167
372
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
168
373
  }