socket-function 1.2.36 → 1.2.38

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.
@@ -1,5 +1,6 @@
1
1
  /// <reference path="require/RequireController.d.ts" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, SocketRegisterType } from "./SocketFunctionTypes";
4
5
  import { SocketServerConfig } from "./src/webSocketServer";
5
6
  import { Args, MaybePromise } from "./src/types";
package/index.d.ts CHANGED
@@ -9,6 +9,7 @@ declare module "socket-function/SetProcessVariables" {
9
9
  declare module "socket-function/SocketFunction" {
10
10
  /// <reference path="require/RequireController.d.ts" />
11
11
  /// <reference types="node" />
12
+ /// <reference types="node" />
12
13
  import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, SocketRegisterType } from "socket-function/SocketFunctionTypes";
13
14
  import { SocketServerConfig } from "socket-function/src/webSocketServer";
14
15
  import { Args, MaybePromise } from "socket-function/src/types";
@@ -320,6 +321,7 @@ declare module "socket-function/require/CSSShim" {
320
321
  declare module "socket-function/require/RequireController" {
321
322
  /// <reference path="../../typenode/index.d.ts" />
322
323
  /// <reference types="node" />
324
+ /// <reference types="node" />
323
325
  declare global {
324
326
  namespace NodeJS {
325
327
  interface Module {
@@ -449,6 +451,7 @@ declare module "socket-function/require/compileFlags" {
449
451
  }
450
452
 
451
453
  declare module "socket-function/require/extMapper" {
454
+ /// <reference types="node" />
452
455
  /// <reference types="node" />
453
456
  export declare function getExtContentType(ext: string): string;
454
457
  export declare function getContentTypeFromBuffer(buffer: Buffer): string | undefined;
@@ -474,6 +477,7 @@ declare module "socket-function/require/require" {
474
477
  }
475
478
 
476
479
  declare module "socket-function/src/CallFactory" {
480
+ /// <reference types="node" />
477
481
  /// <reference types="node" />
478
482
  /// <reference types="node" />
479
483
  import { CallType } from "socket-function/SocketFunctionTypes";
@@ -532,6 +536,7 @@ declare module "socket-function/src/CallFactory" {
532
536
  }
533
537
 
534
538
  declare module "socket-function/src/JSONLACKS/JSONLACKS" {
539
+ /// <reference types="node" />
535
540
  /// <reference types="node" />
536
541
  export interface JSONLACKS_ParseConfig {
537
542
  extended?: boolean;
@@ -567,6 +572,7 @@ declare module "socket-function/src/JSONLACKS/JSONLACKS.generated.js" {
567
572
  }
568
573
 
569
574
  declare module "socket-function/src/Zip" {
575
+ /// <reference types="node" />
570
576
  /// <reference types="node" />
571
577
  import { MaybePromise } from "socket-function/src/types";
572
578
  export declare class Zip {
@@ -654,6 +660,7 @@ declare module "socket-function/src/batching" {
654
660
  }
655
661
 
656
662
  declare module "socket-function/src/bits" {
663
+ /// <reference types="node" />
657
664
  /// <reference types="node" />
658
665
  /** Subtracts the smallest possible value from a number (a double). This makes it possible to convert an exclusive range end
659
666
  * to an inclusive range end, which is sometimes required (as in, < x is the same as <= minusEpsilon(x)).
@@ -680,6 +687,7 @@ declare module "socket-function/src/bits" {
680
687
  }
681
688
 
682
689
  declare module "socket-function/src/buffers" {
690
+ /// <reference types="node" />
683
691
  /// <reference types="node" />
684
692
  export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
685
693
  export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
@@ -757,6 +765,7 @@ declare module "socket-function/src/caching" {
757
765
  }
758
766
 
759
767
  declare module "socket-function/src/callHTTPHandler" {
768
+ /// <reference types="node" />
760
769
  /// <reference types="node" />
761
770
  /// <reference types="node" />
762
771
  import http from "http";
@@ -811,6 +820,7 @@ declare module "socket-function/src/callManager" {
811
820
  }
812
821
 
813
822
  declare module "socket-function/src/certStore" {
823
+ /// <reference types="node" />
814
824
  /// <reference types="node" />
815
825
  /** Must be populated before the server starts */
816
826
  export declare function trustCertificate(cert: string | Buffer): void;
@@ -1046,6 +1056,7 @@ declare module "socket-function/src/getUniqueTime" {
1046
1056
  }
1047
1057
 
1048
1058
  declare module "socket-function/src/https" {
1059
+ /// <reference types="node" />
1049
1060
  /// <reference types="node" />
1050
1061
  export interface HttpsResponseInfo {
1051
1062
  statusCode?: number;
@@ -1065,6 +1076,7 @@ declare module "socket-function/src/https" {
1065
1076
  }
1066
1077
 
1067
1078
  declare module "socket-function/src/lz4/LZ4" {
1079
+ /// <reference types="node" />
1068
1080
  /// <reference types="node" />
1069
1081
  export declare class LZ4 {
1070
1082
  static compress(data: Buffer): Buffer;
@@ -1132,6 +1144,7 @@ declare module "socket-function/src/lz4/lz4_wasm_nodejs_bg.wasm" {
1132
1144
  }
1133
1145
 
1134
1146
  declare module "socket-function/src/misc" {
1147
+ /// <reference types="node" />
1135
1148
  /// <reference types="node" />
1136
1149
  import { MaybePromise } from "socket-function/src/types";
1137
1150
  export declare const timeInSecond = 1000;
@@ -1563,6 +1576,7 @@ declare module "socket-function/src/storagePath" {
1563
1576
  }
1564
1577
 
1565
1578
  declare module "socket-function/src/tlsParsing" {
1579
+ /// <reference types="node" />
1566
1580
  /// <reference types="node" />
1567
1581
  export declare function parseTLSHello(buffer: Buffer): {
1568
1582
  extensions: {
@@ -1592,6 +1606,7 @@ declare module "socket-function/src/upreal" {
1592
1606
  }
1593
1607
 
1594
1608
  declare module "socket-function/src/webSocketServer" {
1609
+ /// <reference types="node" />
1595
1610
  /// <reference types="node" />
1596
1611
  /// <reference types="node" />
1597
1612
  import https from "https";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -1,5 +1,6 @@
1
1
  /// <reference path="../../typenode/index.d.ts" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  declare global {
4
5
  namespace NodeJS {
5
6
  interface Module {
@@ -1,3 +1,4 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export declare function getExtContentType(ext: string): string;
3
4
  export declare function getContentTypeFromBuffer(buffer: Buffer): string | undefined;
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import { CallType } from "../SocketFunctionTypes";
4
5
  import * as ws from "ws";
5
6
  import * as tls from "tls";
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export interface JSONLACKS_ParseConfig {
3
4
  extended?: boolean;
4
5
  discardMissingReferences?: boolean;
package/src/Zip.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { MaybePromise } from "./types";
3
4
  export declare class Zip {
4
5
  static gzip(buffer: Buffer, level?: number): Promise<Buffer>;
package/src/bits.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  /** Subtracts the smallest possible value from a number (a double). This makes it possible to convert an exclusive range end
3
4
  * to an inclusive range end, which is sometimes required (as in, < x is the same as <= minusEpsilon(x)).
4
5
  */
package/src/buffers.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
3
4
  export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
4
5
  export declare function cloneBuffer(data: Buffer): Buffer;
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import http from "http";
4
5
  import { CallType } from "../SocketFunctionTypes";
5
6
  export declare function setDefaultHTTPCall(call: CallType): void;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  /** Must be populated before the server starts */
3
4
  export declare function trustCertificate(cert: string | Buffer): void;
4
5
  export declare function getTrustedCertificates(): string[];
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
 
@@ -89,10 +89,11 @@ export async function listPortMappings(): Promise<PortMapping[]> {
89
89
  // live IGD; see test.ts.)
90
90
  const PERMANENT_LEASE = 0;
91
91
 
92
- // A permanent mapping never expires, so we don't renew. We only watch, on this interval, for
93
- // another application having superseded our forward (or a router reboot having wiped it), and log
94
- // loudly if so. We do NOT re-take it last writer wins, so fighting back would just be a war.
95
- const SUPERSEDE_CHECK_INTERVAL = timeInMinute * 30;
92
+ // A permanent mapping never expires, so we don't renew. We only watch, on this interval, for our
93
+ // forward having disappeared (routers do drop them, e.g. on reboot) or another application having
94
+ // taken it. A mapping that is simply gone is free, so we re-take it; one that someone else holds we
95
+ // leave alone and only log — fighting over it would just be a takeover war neither side wins.
96
+ const MAPPING_CHECK_INTERVAL = timeInMinute * 30;
96
97
 
97
98
  /** Outcome of forwardPort. `owned` is true once we hold the router mapping for the port. When
98
99
  * false, `reason` says why: "declined" = noPortStealing and another host holds the port (the
@@ -137,7 +138,9 @@ export async function forwardPort(config: {
137
138
  return (await listPortMappings()).find(m => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP");
138
139
  }
139
140
 
140
- async function takeOwnership(): Promise<ForwardPortResult> {
141
+ // allowSteal is false for the periodic re-take, so a mapping that changed hands between our check
142
+ // and this call is still left alone.
143
+ async function takeOwnership(options: { allowSteal: boolean }): Promise<ForwardPortResult> {
141
144
  try {
142
145
  const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
143
146
 
@@ -145,8 +148,8 @@ export async function forwardPort(config: {
145
148
  // whether a mapping exists at all (so we only delete when there's something to delete).
146
149
  let existing = await readMapping();
147
150
 
148
- if (existing && existing.internalClient !== internalIP && config.noPortStealing) {
149
- console.log(`Port ${externalPort} is already forwarded to ${existing.internalClient} (not us); not stealing it (noPortStealing).`);
151
+ if (existing && existing.internalClient !== internalIP && !options.allowSteal) {
152
+ console.log(`Port ${externalPort} is already forwarded to ${existing.internalClient} (not us); not stealing it.`);
150
153
  return { owned: false, reason: "declined" };
151
154
  }
152
155
 
@@ -195,32 +198,39 @@ export async function forwardPort(config: {
195
198
  return { owned: false, reason: "error" };
196
199
  }
197
200
 
198
- // Re-read the mapping and warn if we no longer own it. Compares against our CURRENT LAN IP, so
199
- // this also catches the case where our own IP changed out from under the old mapping.
200
- async function warnIfSuperseded() {
201
+ // Re-read the mapping and act on having lost it. Compares against our CURRENT LAN IP, so a mapping
202
+ // left over from an IP change of ours reads as someone else's which is the safe way around, as
203
+ // we can't tell it apart from a genuine takeover and would rather not steal.
204
+ async function checkMapping() {
201
205
  try {
202
206
  let currentIP = await getLocalInternalIP();
203
- let ours = (await listPortMappings()).find(m => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP");
204
- if (!ours) {
205
- console.error(`Port forward ${externalPort} is gone — the mapping was removed (router reboot, or another host deleted it).`);
206
- } else if (currentIP && ours.internalClient !== currentIP) {
207
- console.error(`Port forward ${externalPort} was superseded — it now forwards to ${ours.internalClient} instead of us (${currentIP}). Another application has taken the port.`);
207
+ let mapping = await readMapping();
208
+ if (!mapping) {
209
+ console.error(`Port forward ${externalPort} is gone — the mapping was removed (router reboot, or another host deleted it). Nothing holds the port, so re-taking it.`);
210
+ let retaken = await takeOwnership({ allowSteal: false });
211
+ if (!retaken.owned) {
212
+ console.error(`Failed to re-take port forward ${externalPort} (${retaken.reason}).`);
213
+ }
214
+ return;
215
+ }
216
+ if (currentIP && mapping.internalClient !== currentIP) {
217
+ console.error(`Port forward ${externalPort} was taken — it now forwards to ${mapping.internalClient} instead of us (${currentIP}). Leaving it alone rather than fighting for it.`);
208
218
  }
209
219
  } catch (e) {
210
- console.error(`Failed to check port forward ${externalPort} for supersession`, (e as Error).stack ?? e);
220
+ console.error(`Failed to check port forward ${externalPort}`, (e as Error).stack ?? e);
211
221
  }
212
222
  }
213
223
 
214
- let result = await takeOwnership();
224
+ let result = await takeOwnership({ allowSteal: !config.noPortStealing });
215
225
 
216
226
  // Only monitor permanent mappings we actually own. A finite lease is expected to expire, so its
217
- // disappearance isn't a supersession worth warning about; a declined/failed claim owns nothing.
227
+ // disappearance isn't a loss worth reporting or reclaiming; a declined/failed claim owns nothing.
218
228
  if (result.owned && permanent) {
219
229
  // unref so a script that only wants a one-off forward (and then exits) isn't held open by
220
230
  // the monitor timer; a running server stays alive on its own and the checks keep firing.
221
231
  let timer = setInterval(() => {
222
- void warnIfSuperseded();
223
- }, SUPERSEDE_CHECK_INTERVAL);
232
+ void checkMapping();
233
+ }, MAPPING_CHECK_INTERVAL);
224
234
  timer.unref?.();
225
235
  }
226
236
 
package/src/https.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export interface HttpsResponseInfo {
3
4
  statusCode?: number;
4
5
  statusMessage?: string;
package/src/lz4/LZ4.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export declare class LZ4 {
3
4
  static compress(data: Buffer): Buffer;
4
5
  static compressUntracked(data: Buffer): Buffer;
package/src/misc.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { MaybePromise } from "./types";
3
4
  export declare const timeInSecond = 1000;
4
5
  export declare const timeInMinute: number;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export declare function parseTLSHello(buffer: Buffer): {
3
4
  extensions: {
4
5
  type: number;
package/src/upreal.ts CHANGED
@@ -11,17 +11,17 @@ const DEP_SECTIONS = [
11
11
  "peerDependencies",
12
12
  ] as const;
13
13
 
14
- // yarn can re-fetch a requesting package's real manifest (instead of trusting our rewritten lockfile entry),
15
- // which re-introduces the old pinned range. Re-running the rewrite converges then, so allow a few passes.
16
- const MAX_DEDUPE_PASSES = 3;
17
-
18
- // Depth at which yarn.lock lists a block's dependencies. Block fields ("version", "resolved") sit shallower,
19
- // so this is what keeps us from mistaking a field for a dependency entry.
20
- const LOCK_DEPENDENCY_INDENT = 4;
21
-
22
14
  // Yarn itself spells this "--dry-run", so accept that plus every spelling anyone reasonably reaches for.
23
15
  const DRY_RUN_FLAGS = ["--dry", "-dry", "--dryrun", "-dryrun", "--dry-run", "-dry-run", "--dry_run", "-dry_run"];
24
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
+
25
25
  async function main() {
26
26
  let args = process.argv.slice(2);
27
27
  let packageName = args.find(arg => !arg.startsWith("-"));
@@ -36,18 +36,20 @@ async function main() {
36
36
  let packageJsonPath = path.join(projectRoot, "package.json");
37
37
  let lockPath = path.join(projectRoot, "yarn.lock");
38
38
 
39
- let latest = await getLatestVersion(packageName);
40
- console.log(`Latest version of ${packageName}: ${latest}`);
39
+ let manifest = await getLatestManifest(packageName);
40
+ console.log(`Latest version of ${packageName}: ${manifest.version}`);
41
41
 
42
- let newRange = updatePackageJson(packageJsonPath, packageName, latest);
43
- console.log(`Forcing every reference to ${packageName} onto ${newRange}`);
42
+ let newRange = updatePackageJson(packageJsonPath, packageName, manifest.version);
44
43
 
45
- for (let pass = 1; pass <= MAX_DEDUPE_PASSES; pass++) {
44
+ for (let pass = 1; pass <= MAX_MERGE_PASSES; pass++) {
46
45
  if (fs.existsSync(lockPath)) {
47
- let result = forceLockRange(lockPath, packageName, newRange);
48
- console.log(`Lockfile pass ${pass}: dropped ${result.removedBlocks} entr${result.removedBlocks === 1 ? "y" : "ies"} for ${packageName}, repointed ${result.rewrittenRequests} request${result.rewrittenRequests === 1 ? "" : "s"} for it to ${newRange}.`);
49
- for (let skipped of result.skippedRequests) {
50
- console.log(`Left ${skipped.requestedBy} asking for ${packageName}@${skipped.range} (not a registry range, so it can't be moved).`);
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)`);
51
53
  }
52
54
  }
53
55
 
@@ -61,19 +63,19 @@ async function main() {
61
63
 
62
64
  let versions = getLockedVersions(lockPath, packageName);
63
65
  if (versions.length <= 1) {
64
- console.log(`Done. ${packageName} resolves to a single version: ${versions[0] ?? latest}`);
66
+ console.log(`Done. ${packageName} resolves to a single version: ${versions[0] ?? manifest.version}`);
65
67
  return;
66
68
  }
67
- console.log(`${packageName} still resolves to ${versions.length} versions (${versions.join(", ")}); rewriting again.`);
69
+ console.log(`${packageName} still resolves to ${versions.length} versions (${versions.join(", ")}); merging again.`);
68
70
  }
69
71
 
70
72
  let versions = getLockedVersions(lockPath, packageName);
71
- console.error(`Failed to collapse ${packageName} onto one version after ${MAX_DEDUPE_PASSES} passes; still at ${versions.join(", ")}`);
73
+ console.error(`Failed to collapse ${packageName} onto one version after ${MAX_MERGE_PASSES} passes; still at ${versions.join(", ")}`);
72
74
  process.exit(1);
73
75
  }
74
76
 
75
77
  // Point every package.json section that mentions the package at the latest version, keeping each section's
76
- // existing range operator, and return the range we are forcing everything else onto.
78
+ // existing range operator, and return the range the lockfile entry has to satisfy.
77
79
  function updatePackageJson(packageJsonPath: string, packageName: string, latest: string): string {
78
80
  let raw = fs.readFileSync(packageJsonPath, "utf8");
79
81
  let packageJson = JSON.parse(raw) as {
@@ -99,28 +101,35 @@ function updatePackageJson(packageJsonPath: string, packageName: string, latest:
99
101
  }
100
102
 
101
103
  if (newRange === undefined) {
102
- console.log(`${packageName} is not a direct dependency; only deduplicating transitive references.`);
104
+ console.log(`${packageName} is not a direct dependency; only merging the transitive references.`);
103
105
  return "^" + latest;
104
106
  }
105
107
  return newRange;
106
108
  }
107
109
 
108
- async function getLatestVersion(packageName: string): Promise<string> {
110
+ interface Manifest {
111
+ version: string;
112
+ dist?: { tarball?: string; shasum?: string; integrity?: string };
113
+ dependencies?: { [name: string]: string };
114
+ optionalDependencies?: { [name: string]: string };
115
+ }
116
+
117
+ async function getLatestManifest(packageName: string): Promise<Manifest> {
109
118
  // Scoped names ("@scope/name") must have their slash encoded in the registry path.
110
119
  let encodedName = packageName.startsWith("@")
111
120
  ? "@" + encodeURIComponent(packageName.slice(1))
112
121
  : encodeURIComponent(packageName);
113
- let url = `https://registry.npmjs.org/${encodedName}/latest`;
122
+ let url = `https://${NPM_REGISTRY_HOST}/${encodedName}/latest`;
114
123
 
115
124
  // Via httpsRequest so registry lookups go through the DNS cache (and its re-resolve/retry), rather
116
125
  // than getaddrinfo, which caches certain failures forever.
117
126
  let body = (await httpsRequest(url)).toString("utf8");
118
127
 
119
- let parsed = JSON.parse(body) as { version?: string };
128
+ let parsed = JSON.parse(body) as Manifest;
120
129
  if (!parsed.version) {
121
130
  throw new Error(`Registry response for ${packageName} had no version field`);
122
131
  }
123
- return parsed.version;
132
+ return parsed;
124
133
  }
125
134
 
126
135
  // Keep whatever range operator the user already chose (^, ~, exact, or *) and point it at the new version.
@@ -167,14 +176,14 @@ function readLock(lockPath: string): LockFile {
167
176
  let blocks: LockBlock[] = [];
168
177
  let current: LockBlock | undefined;
169
178
  for (let i = 0; i < lines.length; i++) {
170
- let line = lines[i];
171
- if (isBlockHeader(line)) {
172
- if (current) {
173
- current.end = i;
174
- }
175
- current = { start: i, end: lines.length, specs: parseHeaderSpecs(line) };
176
- blocks.push(current);
179
+ if (!isBlockHeader(lines[i])) {
180
+ continue;
177
181
  }
182
+ if (current) {
183
+ current.end = i;
184
+ }
185
+ current = { start: i, end: lines.length, specs: parseHeaderSpecs(lines[i]) };
186
+ blocks.push(current);
178
187
  }
179
188
  return { lines, eol, blocks };
180
189
  }
@@ -202,67 +211,143 @@ function specNamePackage(spec: string): string {
202
211
  return spec.slice(0, atIndex);
203
212
  }
204
213
 
214
+ function specRange(spec: string): string {
215
+ return spec.slice(specNamePackage(spec).length + 1);
216
+ }
217
+
205
218
  // Ranges that don't come from the registry (git urls, file:, link:, npm: aliases) can't be pointed at a
206
- // version number, so they have to stay as they are.
219
+ // version number, so their entry has to stay separate.
207
220
  function isRegistryRange(range: string): boolean {
208
221
  return !range.includes(":") && !range.includes("/");
209
222
  }
210
223
 
211
- // Force the whole tree onto one version: drop every block for the package (so yarn re-resolves it), and
212
- // repoint every *other* block's request for it at the new range, so nothing is left asking for an old
213
- // pinned version that yarn would then have to resolve into a second entry.
214
- function forceLockRange(lockPath: string, packageName: string, newRange: string): {
215
- removedBlocks: number;
216
- rewrittenRequests: number;
217
- skippedRequests: { requestedBy: string; range: string }[];
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[];
218
242
  } {
219
243
  let lock = readLock(lockPath);
244
+ let specs = new Set<string>([`${packageName}@${newRange}`]);
245
+ let movedFrom: { spec: string; version: string }[] = [];
246
+ let leftAlone: string[] = [];
220
247
  let removeLines = new Set<number>();
221
- let removedBlocks = 0;
222
- let rewrittenRequests = 0;
223
- let skippedRequests: { requestedBy: string; range: string }[] = [];
248
+ let insertAt: number | undefined;
249
+ let body: string[] | undefined;
224
250
 
225
251
  for (let block of lock.blocks) {
226
- if (block.specs.some(spec => specNamePackage(spec) === packageName)) {
227
- removedBlocks++;
228
- for (let i = block.start; i < block.end; i++) {
229
- removeLines.add(i);
230
- }
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);
231
259
  continue;
232
260
  }
233
261
 
234
- for (let i = block.start + 1; i < block.end; i++) {
235
- let parsed = parseDependencyLine(lock.lines[i]);
236
- if (!parsed || parsed.name !== packageName) {
237
- continue;
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 ?? "?" });
238
266
  }
239
- if (!isRegistryRange(parsed.range)) {
240
- skippedRequests.push({ requestedBy: block.specs[0], range: parsed.range });
241
- continue;
242
- }
243
- if (parsed.range === newRange) {
244
- continue;
245
- }
246
- lock.lines[i] = parsed.prefix + `"${newRange}"`;
247
- rewrittenRequests++;
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));
248
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--;
249
311
  }
312
+ return end;
313
+ }
250
314
 
251
- let kept = lock.lines.filter((_, i) => !removeLines.has(i));
252
- fs.writeFileSync(lockPath, kept.join(lock.eol));
253
- return { removedBlocks, rewrittenRequests, skippedRequests };
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;
254
323
  }
255
324
 
256
- function parseDependencyLine(line: string): { prefix: string; name: string; range: string } | undefined {
257
- let match = /^(\s+)("?)([^"\s]+)\2 "([^"]*)"$/.exec(line);
258
- if (!match) {
259
- return undefined;
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)}`);
260
339
  }
261
- let [, indent, quote, name, range] = match;
262
- if (indent.length < LOCK_DEPENDENCY_INDENT) {
263
- return undefined;
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
+ }
264
349
  }
265
- return { prefix: `${indent}${quote}${name}${quote} `, name, range };
350
+ return lines;
266
351
  }
267
352
 
268
353
  function getLockedVersions(lockPath: string, packageName: string): string[] {
@@ -275,11 +360,9 @@ function getLockedVersions(lockPath: string, packageName: string): string[] {
275
360
  if (!block.specs.some(spec => specNamePackage(spec) === packageName)) {
276
361
  continue;
277
362
  }
278
- for (let i = block.start + 1; i < block.end; i++) {
279
- let match = /^\s+version "(.*)"$/.exec(lock.lines[i]);
280
- if (match) {
281
- versions.add(match[1]);
282
- }
363
+ let version = getBlockVersion(lock, block);
364
+ if (version) {
365
+ versions.add(version);
283
366
  }
284
367
  }
285
368
  return Array.from(versions);
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import https from "https";
4
5
  import { Watchable } from "./misc";
5
6
  export type SocketServerConfig = (https.ServerOptions & {