@patchstack/connect 0.3.30 → 0.3.31

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/dist/index.d.cts CHANGED
@@ -87,10 +87,45 @@ declare function scanLockfile(cwd: string): Promise<Manifest>;
87
87
  interface WirePackage {
88
88
  name: string;
89
89
  version: string;
90
+ /**
91
+ * Where this exact name+version is installed, repo-relative and sorted.
92
+ *
93
+ * The same package can be installed more than once at different versions — a workspace pinning an old
94
+ * copy under `apps/api/node_modules`, a transitive dependency getting its own nested install. Without
95
+ * the locations, `lodash@4.17.11` and `lodash@4.17.21` arrive as two bare pairs and nothing can say
96
+ * WHICH one the app's own import resolves to. A consumer then has to treat every installed version as
97
+ * if the code used it: it warns on a copy nothing reaches, or pins a rule to a route running the safe
98
+ * one — a rule that never fires while reporting as protection.
99
+ *
100
+ * Node resolves an import by walking up from the importing file, so the map's import sites plus these
101
+ * paths together answer the question. Neither half answers it alone.
102
+ *
103
+ * OPT-IN. Absent unless the caller asks for locations (`scan --install-paths`), and absent even then
104
+ * when the scan source does not record them — see `installPathsComplete`, which is what separates
105
+ * "not installed there" from "we were not told".
106
+ *
107
+ * Off by default because it changes what leaves the machine. The package's standing promise is that
108
+ * `scan` sends names and versions and no paths of any kind; locations are a real widening of that, and
109
+ * an upload that widens it should be an explicit choice rather than a consequence of upgrading.
110
+ */
111
+ paths?: string[];
90
112
  }
91
113
  interface WirePayload {
92
114
  ecosystem: Manifest['ecosystem'];
93
115
  packages: WirePackage[];
116
+ /**
117
+ * Whether every entry's `paths` is the complete set of locations for it.
118
+ *
119
+ * False when locations were not requested at all, when the scan source cannot supply them (a yarn.lock
120
+ * is flat — hoisting is decided at install time and the file does not record it; a v1 npm lockfile
121
+ * describes the dependency graph, not the tree), or when only some entries had one. A consumer MUST NOT
122
+ * read a missing or short `paths` as "installed nowhere else" while this is false; absence is then "not
123
+ * recorded", which is not an answer.
124
+ *
125
+ * The default is therefore `false`, and that is the safe direction: the field withholds a negative
126
+ * rather than granting one.
127
+ */
128
+ installPathsComplete: boolean;
94
129
  }
95
130
  interface NormalizeStats {
96
131
  uniqueNames: number;
@@ -101,7 +136,14 @@ interface NormalizeResult {
101
136
  payload: WirePayload;
102
137
  stats: NormalizeStats;
103
138
  }
104
- declare function buildWirePayload(manifest: Manifest): NormalizeResult;
139
+ interface WireOptions {
140
+ /**
141
+ * Include each package's install location. Off by default — see `WirePackage.paths`: it widens what
142
+ * `scan` transmits, so it is the caller's explicit choice, not a side effect of upgrading.
143
+ */
144
+ installPaths?: boolean;
145
+ }
146
+ declare function buildWirePayload(manifest: Manifest, options?: WireOptions): NormalizeResult;
105
147
  declare function compareVersions(a: string, b: string): number;
106
148
 
107
149
  declare const DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
@@ -270,6 +312,11 @@ declare function ensureSourceWidget(cwd: string, siteUuid: string): SourceWidget
270
312
  interface ScanAndReportOptions {
271
313
  cwd?: string;
272
314
  config?: Config;
315
+ /**
316
+ * Include each package's install location in the uploaded payload. Off by default: it widens what
317
+ * leaves the machine, so it is an explicit choice rather than something an upgrade turns on.
318
+ */
319
+ installPaths?: boolean;
273
320
  }
274
321
  interface ScanAndReportResult {
275
322
  manifest: Manifest;
package/dist/index.d.ts CHANGED
@@ -87,10 +87,45 @@ declare function scanLockfile(cwd: string): Promise<Manifest>;
87
87
  interface WirePackage {
88
88
  name: string;
89
89
  version: string;
90
+ /**
91
+ * Where this exact name+version is installed, repo-relative and sorted.
92
+ *
93
+ * The same package can be installed more than once at different versions — a workspace pinning an old
94
+ * copy under `apps/api/node_modules`, a transitive dependency getting its own nested install. Without
95
+ * the locations, `lodash@4.17.11` and `lodash@4.17.21` arrive as two bare pairs and nothing can say
96
+ * WHICH one the app's own import resolves to. A consumer then has to treat every installed version as
97
+ * if the code used it: it warns on a copy nothing reaches, or pins a rule to a route running the safe
98
+ * one — a rule that never fires while reporting as protection.
99
+ *
100
+ * Node resolves an import by walking up from the importing file, so the map's import sites plus these
101
+ * paths together answer the question. Neither half answers it alone.
102
+ *
103
+ * OPT-IN. Absent unless the caller asks for locations (`scan --install-paths`), and absent even then
104
+ * when the scan source does not record them — see `installPathsComplete`, which is what separates
105
+ * "not installed there" from "we were not told".
106
+ *
107
+ * Off by default because it changes what leaves the machine. The package's standing promise is that
108
+ * `scan` sends names and versions and no paths of any kind; locations are a real widening of that, and
109
+ * an upload that widens it should be an explicit choice rather than a consequence of upgrading.
110
+ */
111
+ paths?: string[];
90
112
  }
91
113
  interface WirePayload {
92
114
  ecosystem: Manifest['ecosystem'];
93
115
  packages: WirePackage[];
116
+ /**
117
+ * Whether every entry's `paths` is the complete set of locations for it.
118
+ *
119
+ * False when locations were not requested at all, when the scan source cannot supply them (a yarn.lock
120
+ * is flat — hoisting is decided at install time and the file does not record it; a v1 npm lockfile
121
+ * describes the dependency graph, not the tree), or when only some entries had one. A consumer MUST NOT
122
+ * read a missing or short `paths` as "installed nowhere else" while this is false; absence is then "not
123
+ * recorded", which is not an answer.
124
+ *
125
+ * The default is therefore `false`, and that is the safe direction: the field withholds a negative
126
+ * rather than granting one.
127
+ */
128
+ installPathsComplete: boolean;
94
129
  }
95
130
  interface NormalizeStats {
96
131
  uniqueNames: number;
@@ -101,7 +136,14 @@ interface NormalizeResult {
101
136
  payload: WirePayload;
102
137
  stats: NormalizeStats;
103
138
  }
104
- declare function buildWirePayload(manifest: Manifest): NormalizeResult;
139
+ interface WireOptions {
140
+ /**
141
+ * Include each package's install location. Off by default — see `WirePackage.paths`: it widens what
142
+ * `scan` transmits, so it is the caller's explicit choice, not a side effect of upgrading.
143
+ */
144
+ installPaths?: boolean;
145
+ }
146
+ declare function buildWirePayload(manifest: Manifest, options?: WireOptions): NormalizeResult;
105
147
  declare function compareVersions(a: string, b: string): number;
106
148
 
107
149
  declare const DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
@@ -270,6 +312,11 @@ declare function ensureSourceWidget(cwd: string, siteUuid: string): SourceWidget
270
312
  interface ScanAndReportOptions {
271
313
  cwd?: string;
272
314
  config?: Config;
315
+ /**
316
+ * Include each package's install location in the uploaded payload. Off by default: it widens what
317
+ * leaves the machine, so it is an explicit choice rather than something an upgrade turns on.
318
+ */
319
+ installPaths?: boolean;
273
320
  }
274
321
  interface ScanAndReportResult {
275
322
  manifest: Manifest;
package/dist/index.js CHANGED
@@ -325,7 +325,7 @@ async function walkNodeModules(cwd) {
325
325
  seen.set(await realpath(cwd), -1);
326
326
  } catch {
327
327
  }
328
- return dedupeByShallowest(await walk(root, 0, seen));
328
+ return dedupeByShallowest(await walk(root, 0, seen, cwd));
329
329
  }
330
330
  function dedupeByShallowest(found) {
331
331
  const best = /* @__PURE__ */ new Map();
@@ -336,7 +336,7 @@ function dedupeByShallowest(found) {
336
336
  return [...best.values()].map(({ entry }) => entry);
337
337
  }
338
338
  var CONCURRENCY = 32;
339
- async function walk(dir, depth, seen) {
339
+ async function walk(dir, depth, seen, root) {
340
340
  let names;
341
341
  try {
342
342
  names = await readdir(dir);
@@ -348,7 +348,7 @@ async function walk(dir, depth, seen) {
348
348
  const fullPath = path3.join(dir, name);
349
349
  const resolved = await claim(fullPath, seen, depth);
350
350
  if (resolved === null) return [];
351
- if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
351
+ if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
352
352
  if (name.startsWith("@")) {
353
353
  let subNames;
354
354
  try {
@@ -361,16 +361,16 @@ async function walk(dir, depth, seen) {
361
361
  const scopedDir = path3.join(fullPath, sub);
362
362
  const scopedReal = await claim(scopedDir, seen, depth);
363
363
  if (scopedReal === null) return [];
364
- return packageAndNested(scopedDir, scopedReal, depth, seen);
364
+ return packageAndNested(scopedDir, scopedReal, depth, seen, root);
365
365
  });
366
366
  return scoped.flat();
367
367
  }
368
- return packageAndNested(fullPath, resolved, depth, seen);
368
+ return packageAndNested(fullPath, resolved, depth, seen, root);
369
369
  });
370
370
  return perEntry.flat();
371
371
  }
372
- async function packageAndNested(pkgDir, resolved, depth, seen) {
373
- const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
372
+ async function packageAndNested(pkgDir, resolved, depth, seen, root) {
373
+ const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
374
374
  return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
375
375
  }
376
376
  async function mapLimit(items, fn) {
@@ -385,7 +385,10 @@ async function mapLimit(items, fn) {
385
385
  await Promise.all(workers);
386
386
  return results;
387
387
  }
388
- async function readPackage(pkgDir, depth) {
388
+ function installLocation(root, pkgDir, impl = path3) {
389
+ return impl.relative(root, pkgDir).split(impl.sep).join("/");
390
+ }
391
+ async function readPackage(pkgDir, depth, root) {
389
392
  let raw;
390
393
  try {
391
394
  raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
@@ -404,14 +407,14 @@ async function readPackage(pkgDir, depth) {
404
407
  if (typeof parsed.version !== "string" || parsed.version.length === 0) {
405
408
  return null;
406
409
  }
407
- return { name: parsed.name, version: parsed.version, direct: depth === 0 };
410
+ return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
408
411
  }
409
- async function walkNested(pkgDir, depth, seen) {
412
+ async function walkNested(pkgDir, depth, seen, root) {
410
413
  const nested = path3.join(pkgDir, "node_modules");
411
414
  if (await claim(nested, seen, depth + 1) === null) {
412
415
  return [];
413
416
  }
414
- return walk(nested, depth + 1, seen);
417
+ return walk(nested, depth + 1, seen, root);
415
418
  }
416
419
  async function claim(dir, seen, depth) {
417
420
  let resolved;
@@ -1054,21 +1057,33 @@ async function exists(filePath) {
1054
1057
  }
1055
1058
 
1056
1059
  // src/normalize.ts
1057
- function buildWirePayload(manifest) {
1060
+ function buildWirePayload(manifest, options = {}) {
1061
+ const withPaths = options.installPaths === true;
1058
1062
  const seen = /* @__PURE__ */ new Map();
1059
1063
  const wirePackages = [];
1064
+ const byIdentity = /* @__PURE__ */ new Map();
1065
+ let entriesWithoutPath = 0;
1060
1066
  for (const entry of manifest.packages) {
1067
+ const identity = `${entry.name}@${entry.version}`;
1068
+ const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
1069
+ if (location === void 0) entriesWithoutPath++;
1070
+ const existing = byIdentity.get(identity);
1071
+ if (existing) {
1072
+ if (location !== void 0) addPath(existing, location);
1073
+ continue;
1074
+ }
1061
1075
  const versions = seen.get(entry.name);
1062
1076
  if (versions) {
1063
- if (versions.has(entry.version)) {
1064
- continue;
1065
- }
1066
1077
  versions.add(entry.version);
1067
1078
  } else {
1068
1079
  seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
1069
1080
  }
1070
- wirePackages.push({ name: entry.name, version: entry.version });
1081
+ const wire = { name: entry.name, version: entry.version };
1082
+ if (location !== void 0) addPath(wire, location);
1083
+ byIdentity.set(identity, wire);
1084
+ wirePackages.push(wire);
1071
1085
  }
1086
+ for (const wire of wirePackages) wire.paths?.sort();
1072
1087
  wirePackages.sort((a, b) => {
1073
1088
  if (a.name === b.name) {
1074
1089
  return compareVersions(a.version, b.version);
@@ -1082,7 +1097,18 @@ function buildWirePayload(manifest) {
1082
1097
  }
1083
1098
  }
1084
1099
  return {
1085
- payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
1100
+ payload: {
1101
+ ecosystem: manifest.ecosystem,
1102
+ packages: wirePackages,
1103
+ // Every entry had a location, so a missing path anywhere means the package is not installed there.
1104
+ // One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
1105
+ // no way to tell which entry was the incomplete one.
1106
+ //
1107
+ // No separate check for "locations were not requested": with them off every entry counts as without
1108
+ // one, so this is already false — which is the default, and the safe direction. An explicit
1109
+ // `withPaths &&` here was a guard no test could distinguish from its absence.
1110
+ installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
1111
+ },
1086
1112
  stats: {
1087
1113
  uniqueNames: seen.size,
1088
1114
  duplicateNames,
@@ -1090,6 +1116,10 @@ function buildWirePayload(manifest) {
1090
1116
  }
1091
1117
  };
1092
1118
  }
1119
+ function addPath(wire, installPath) {
1120
+ if (wire.paths === void 0) wire.paths = [installPath];
1121
+ else if (!wire.paths.includes(installPath)) wire.paths.push(installPath);
1122
+ }
1093
1123
  function compareVersions(a, b) {
1094
1124
  if (a === b) {
1095
1125
  return 0;
@@ -1665,7 +1695,7 @@ async function scanAndReport(options = {}) {
1665
1695
  const cwd = options.cwd ?? process.cwd();
1666
1696
  const config = options.config ?? await resolveConfig({ cwd });
1667
1697
  const manifest = await scanLockfile(cwd);
1668
- const { payload, stats } = buildWirePayload(manifest);
1698
+ const { payload, stats } = buildWirePayload(manifest, { installPaths: options.installPaths === true });
1669
1699
  const response = await postManifest(config, payload);
1670
1700
  if (config.siteUuid === null && response.uuid !== void 0 && response.uuid.length > 0) {
1671
1701
  await persistSiteUuid(cwd, response.uuid);