@patchstack/connect 0.2.6 → 0.2.8

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.cjs CHANGED
@@ -47,8 +47,8 @@ __export(src_exports, {
47
47
  module.exports = __toCommonJS(src_exports);
48
48
 
49
49
  // src/parsers/index.ts
50
- var import_promises5 = require("fs/promises");
51
- var import_node_path4 = __toESM(require("path"), 1);
50
+ var import_promises6 = require("fs/promises");
51
+ var import_node_path5 = __toESM(require("path"), 1);
52
52
 
53
53
  // src/types.ts
54
54
  var PatchstackError = class extends Error {
@@ -62,13 +62,52 @@ var PatchstackError = class extends Error {
62
62
  cause;
63
63
  };
64
64
 
65
- // src/parsers/npm.ts
65
+ // src/parsers/consistency.ts
66
66
  var import_promises = require("fs/promises");
67
67
  var import_node_path = __toESM(require("path"), 1);
68
+ async function readDeclaredDependencyNames(cwd) {
69
+ let raw;
70
+ try {
71
+ raw = await (0, import_promises.readFile)(import_node_path.default.join(cwd, "package.json"), "utf8");
72
+ } catch {
73
+ return [];
74
+ }
75
+ let parsed;
76
+ try {
77
+ parsed = JSON.parse(raw);
78
+ } catch {
79
+ return [];
80
+ }
81
+ if (typeof parsed !== "object" || parsed === null) {
82
+ return [];
83
+ }
84
+ const names = /* @__PURE__ */ new Set();
85
+ for (const group of ["dependencies", "devDependencies"]) {
86
+ const deps = parsed[group];
87
+ if (typeof deps !== "object" || deps === null) {
88
+ continue;
89
+ }
90
+ for (const [name, spec] of Object.entries(deps)) {
91
+ if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
92
+ continue;
93
+ }
94
+ names.add(name);
95
+ }
96
+ }
97
+ return [...names];
98
+ }
99
+ function missingDependencies(declared, packages) {
100
+ const present = new Set(packages.map((entry) => entry.name));
101
+ return declared.filter((name) => !present.has(name));
102
+ }
103
+
104
+ // src/parsers/npm.ts
105
+ var import_promises2 = require("fs/promises");
106
+ var import_node_path2 = __toESM(require("path"), 1);
68
107
  async function parseNpmLockfile(lockfilePath) {
69
108
  let raw;
70
109
  try {
71
- raw = await (0, import_promises.readFile)(lockfilePath, "utf8");
110
+ raw = await (0, import_promises2.readFile)(lockfilePath, "utf8");
72
111
  } catch (cause) {
73
112
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
74
113
  }
@@ -126,7 +165,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
126
165
  return acc;
127
166
  }
128
167
  function extractNameFromPath(pkgPath) {
129
- const segments = pkgPath.split("node_modules" + import_node_path.default.sep === pkgPath ? import_node_path.default.sep : "/");
168
+ const segments = pkgPath.split("node_modules" + import_node_path2.default.sep === pkgPath ? import_node_path2.default.sep : "/");
130
169
  const parts = pkgPath.split("/");
131
170
  const nmIndex = parts.lastIndexOf("node_modules");
132
171
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -149,12 +188,12 @@ function isDirectV2(pkgPath) {
149
188
  }
150
189
 
151
190
  // src/parsers/node_modules.ts
152
- var import_promises2 = require("fs/promises");
153
- var import_node_path2 = __toESM(require("path"), 1);
191
+ var import_promises3 = require("fs/promises");
192
+ var import_node_path3 = __toESM(require("path"), 1);
154
193
  async function walkNodeModules(cwd) {
155
- const root = import_node_path2.default.join(cwd, "node_modules");
194
+ const root = import_node_path3.default.join(cwd, "node_modules");
156
195
  try {
157
- const info = await (0, import_promises2.stat)(root);
196
+ const info = await (0, import_promises3.stat)(root);
158
197
  if (!info.isDirectory()) {
159
198
  throw new PatchstackError(
160
199
  `${root} exists but is not a directory.`,
@@ -178,7 +217,7 @@ async function walkNodeModules(cwd) {
178
217
  async function walk(dir, acc, depth) {
179
218
  let names;
180
219
  try {
181
- names = await (0, import_promises2.readdir)(dir);
220
+ names = await (0, import_promises3.readdir)(dir);
182
221
  } catch {
183
222
  return;
184
223
  }
@@ -186,14 +225,14 @@ async function walk(dir, acc, depth) {
186
225
  if (name.startsWith(".")) {
187
226
  continue;
188
227
  }
189
- const fullPath = import_node_path2.default.join(dir, name);
228
+ const fullPath = import_node_path3.default.join(dir, name);
190
229
  if (!await isPlainDirectory(fullPath)) {
191
230
  continue;
192
231
  }
193
232
  if (name.startsWith("@")) {
194
233
  let subNames;
195
234
  try {
196
- subNames = await (0, import_promises2.readdir)(fullPath);
235
+ subNames = await (0, import_promises3.readdir)(fullPath);
197
236
  } catch {
198
237
  continue;
199
238
  }
@@ -201,7 +240,7 @@ async function walk(dir, acc, depth) {
201
240
  if (sub.startsWith(".")) {
202
241
  continue;
203
242
  }
204
- const scopedDir = import_node_path2.default.join(fullPath, sub);
243
+ const scopedDir = import_node_path3.default.join(fullPath, sub);
205
244
  if (!await isPlainDirectory(scopedDir)) {
206
245
  continue;
207
246
  }
@@ -217,7 +256,7 @@ async function walk(dir, acc, depth) {
217
256
  async function readPackage(pkgDir, depth, acc) {
218
257
  let raw;
219
258
  try {
220
- raw = await (0, import_promises2.readFile)(import_node_path2.default.join(pkgDir, "package.json"), "utf8");
259
+ raw = await (0, import_promises3.readFile)(import_node_path3.default.join(pkgDir, "package.json"), "utf8");
221
260
  } catch {
222
261
  return;
223
262
  }
@@ -240,7 +279,7 @@ async function readPackage(pkgDir, depth, acc) {
240
279
  });
241
280
  }
242
281
  async function walkNested(pkgDir, acc, depth) {
243
- const nested = import_node_path2.default.join(pkgDir, "node_modules");
282
+ const nested = import_node_path3.default.join(pkgDir, "node_modules");
244
283
  if (!await isPlainDirectory(nested)) {
245
284
  return;
246
285
  }
@@ -248,7 +287,7 @@ async function walkNested(pkgDir, acc, depth) {
248
287
  }
249
288
  async function isPlainDirectory(dir) {
250
289
  try {
251
- const info = await (0, import_promises2.lstat)(dir);
290
+ const info = await (0, import_promises3.lstat)(dir);
252
291
  return info.isDirectory() && !info.isSymbolicLink();
253
292
  } catch {
254
293
  return false;
@@ -256,11 +295,11 @@ async function isPlainDirectory(dir) {
256
295
  }
257
296
 
258
297
  // src/parsers/pnpm.ts
259
- var import_promises3 = require("fs/promises");
298
+ var import_promises4 = require("fs/promises");
260
299
  async function parsePnpmLockfile(lockfilePath) {
261
300
  let raw;
262
301
  try {
263
- raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
302
+ raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
264
303
  } catch (cause) {
265
304
  throw new PatchstackError(
266
305
  `Could not read lockfile at ${lockfilePath}`,
@@ -508,12 +547,12 @@ function extractLeafName(trimmed) {
508
547
  }
509
548
 
510
549
  // src/parsers/yarn.ts
511
- var import_promises4 = require("fs/promises");
512
- var import_node_path3 = __toESM(require("path"), 1);
550
+ var import_promises5 = require("fs/promises");
551
+ var import_node_path4 = __toESM(require("path"), 1);
513
552
  async function parseYarnLockfile(lockfilePath) {
514
553
  let raw;
515
554
  try {
516
- raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
555
+ raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
517
556
  } catch (cause) {
518
557
  throw new PatchstackError(
519
558
  `Could not read lockfile at ${lockfilePath}`,
@@ -528,7 +567,7 @@ async function parseYarnLockfile(lockfilePath) {
528
567
  "LOCKFILE_PARSE_ERROR"
529
568
  );
530
569
  }
531
- const directNames = await readDirectDepNames(import_node_path3.default.dirname(lockfilePath));
570
+ const directNames = await readDirectDepNames(import_node_path4.default.dirname(lockfilePath));
532
571
  const entries = [];
533
572
  const seen = /* @__PURE__ */ new Set();
534
573
  for (const block of blocks) {
@@ -672,7 +711,7 @@ async function readDirectDepNames(cwd) {
672
711
  const names = /* @__PURE__ */ new Set();
673
712
  let raw;
674
713
  try {
675
- raw = await (0, import_promises4.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
714
+ raw = await (0, import_promises5.readFile)(import_node_path4.default.join(cwd, "package.json"), "utf8");
676
715
  } catch {
677
716
  return names;
678
717
  }
@@ -699,61 +738,99 @@ async function readDirectDepNames(cwd) {
699
738
  }
700
739
 
701
740
  // src/parsers/index.ts
741
+ var LOCKFILE_CANDIDATES = [
742
+ { filename: "package-lock.json", strategy: "npm-lockfile" },
743
+ { filename: "bun.lock", strategy: "node-modules-walk" },
744
+ { filename: "bun.lockb", strategy: "node-modules-walk" },
745
+ { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
746
+ { filename: "yarn.lock", strategy: "yarn-lockfile" }
747
+ ];
702
748
  async function detectLockfile(cwd) {
703
- const npmLock = import_node_path4.default.join(cwd, "package-lock.json");
704
- if (await exists(npmLock)) {
705
- return {
706
- ecosystem: "npm",
707
- filePath: npmLock,
708
- filename: "package-lock.json",
709
- strategy: "npm-lockfile"
710
- };
711
- }
712
- const bunLock = import_node_path4.default.join(cwd, "bun.lock");
713
- if (await exists(bunLock)) {
714
- return {
715
- ecosystem: "npm",
716
- filePath: bunLock,
717
- filename: "bun.lock",
718
- strategy: "node-modules-walk"
719
- };
720
- }
721
- const bunLockB = import_node_path4.default.join(cwd, "bun.lockb");
722
- if (await exists(bunLockB)) {
723
- return {
724
- ecosystem: "npm",
725
- filePath: bunLockB,
726
- filename: "bun.lockb",
727
- strategy: "node-modules-walk"
728
- };
729
- }
730
- const pnpmLock = import_node_path4.default.join(cwd, "pnpm-lock.yaml");
731
- if (await exists(pnpmLock)) {
732
- return {
733
- ecosystem: "npm",
734
- filePath: pnpmLock,
735
- filename: "pnpm-lock.yaml",
736
- strategy: "pnpm-lockfile"
737
- };
738
- }
739
- const yarnLock = import_node_path4.default.join(cwd, "yarn.lock");
740
- if (await exists(yarnLock)) {
749
+ const probed = await Promise.all(
750
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
751
+ const filePath = import_node_path5.default.join(cwd, candidate.filename);
752
+ return { ...candidate, filePath, present: await exists(filePath) };
753
+ })
754
+ );
755
+ const match = probed.find((candidate) => candidate.present);
756
+ if (match) {
741
757
  return {
742
758
  ecosystem: "npm",
743
- filePath: yarnLock,
744
- filename: "yarn.lock",
745
- strategy: "yarn-lockfile"
759
+ filePath: match.filePath,
760
+ filename: match.filename,
761
+ strategy: match.strategy
746
762
  };
747
763
  }
764
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
748
765
  throw new PatchstackError(
749
- `No lockfile found in ${cwd}. Expected one of: package-lock.json, bun.lock, bun.lockb, yarn.lock, pnpm-lock.yaml.`,
766
+ `No lockfile found in ${cwd}. Expected one of: ${expected}.`,
750
767
  "LOCKFILE_NOT_FOUND"
751
768
  );
752
769
  }
753
770
  async function scanLockfile(cwd) {
754
- const detected = await detectLockfile(cwd);
755
- const packages = await runStrategy(detected, cwd);
756
- return { ecosystem: detected.ecosystem, packages };
771
+ const candidates = await presentLockfiles(cwd);
772
+ if (candidates.length === 0) {
773
+ await detectLockfile(cwd);
774
+ }
775
+ const declared = await readDeclaredDependencyNames(cwd);
776
+ const warnings = [];
777
+ let firstParsed = null;
778
+ let walkTried = false;
779
+ for (const candidate of candidates) {
780
+ let packages;
781
+ try {
782
+ packages = await runStrategy(candidate, cwd);
783
+ } catch {
784
+ continue;
785
+ }
786
+ walkTried ||= candidate.strategy === "node-modules-walk";
787
+ firstParsed ??= { packages, filename: candidate.filename };
788
+ const missing = missingDependencies(declared, packages);
789
+ if (missing.length === 0) {
790
+ return manifestWith(packages, warnings);
791
+ }
792
+ warnings.push(staleWarning(candidate.filename, missing));
793
+ }
794
+ if (!walkTried) {
795
+ try {
796
+ const packages = await walkNodeModules(cwd);
797
+ const missing = missingDependencies(declared, packages);
798
+ if (missing.length > 0) {
799
+ warnings.push(staleWarning("node_modules/", missing));
800
+ }
801
+ warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
802
+ return manifestWith(packages, warnings);
803
+ } catch {
804
+ }
805
+ }
806
+ if (firstParsed === null) {
807
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
808
+ throw new PatchstackError(
809
+ `No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
810
+ "LOCKFILE_NOT_FOUND"
811
+ );
812
+ }
813
+ warnings.push(
814
+ `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
815
+ );
816
+ return manifestWith(firstParsed.packages, warnings);
817
+ }
818
+ function manifestWith(packages, warnings) {
819
+ return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
820
+ }
821
+ function staleWarning(source, missing) {
822
+ const sample = missing.slice(0, 3).join(", ");
823
+ const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
824
+ return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
825
+ }
826
+ async function presentLockfiles(cwd) {
827
+ const probed = await Promise.all(
828
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
829
+ const filePath = import_node_path5.default.join(cwd, candidate.filename);
830
+ return { ...candidate, filePath, present: await exists(filePath) };
831
+ })
832
+ );
833
+ return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
757
834
  }
758
835
  async function runStrategy(detected, cwd) {
759
836
  switch (detected.strategy) {
@@ -769,7 +846,7 @@ async function runStrategy(detected, cwd) {
769
846
  }
770
847
  async function exists(filePath) {
771
848
  try {
772
- await (0, import_promises5.access)(filePath);
849
+ await (0, import_promises6.access)(filePath);
773
850
  return true;
774
851
  } catch {
775
852
  return false;
@@ -901,7 +978,7 @@ async function postManifest(config, payload) {
901
978
  Accept: "application/json",
902
979
  "User-Agent": "@patchstack/connect"
903
980
  },
904
- body: JSON.stringify(payload),
981
+ body: JSON.stringify({ ...payload, environment: config.environment }),
905
982
  signal: AbortSignal.timeout(timeoutMs)
906
983
  });
907
984
  } catch (cause) {
@@ -956,15 +1033,24 @@ function isTimeoutError(cause) {
956
1033
  }
957
1034
 
958
1035
  // src/config.ts
959
- var import_promises6 = require("fs/promises");
960
- var import_node_path5 = __toESM(require("path"), 1);
1036
+ var import_promises7 = require("fs/promises");
1037
+ var import_node_path6 = __toESM(require("path"), 1);
961
1038
  var CONFIG_FILENAME = ".patchstackrc.json";
1039
+ var DEFAULT_ENVIRONMENT = "production";
962
1040
  async function resolveConfig(options) {
963
1041
  const fromFile = await readConfigFile(options.cwd);
964
1042
  const fromEnv = readEnv();
965
1043
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
966
1044
  const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
967
1045
  const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1046
+ const environmentRaw = fromEnv.environment ?? fromFile.environment;
1047
+ if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
1048
+ throw new PatchstackError(
1049
+ `Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
1050
+ "CONFIG_INVALID"
1051
+ );
1052
+ }
1053
+ const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
968
1054
  if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
969
1055
  throw new PatchstackError(
970
1056
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -980,13 +1066,14 @@ async function resolveConfig(options) {
980
1066
  return {
981
1067
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
982
1068
  endpoint,
983
- timeoutMs
1069
+ timeoutMs,
1070
+ environment
984
1071
  };
985
1072
  }
986
1073
  async function writeConfigFile(cwd, config) {
987
- const target = import_node_path5.default.join(cwd, CONFIG_FILENAME);
1074
+ const target = import_node_path6.default.join(cwd, CONFIG_FILENAME);
988
1075
  const content = JSON.stringify(config, null, 2) + "\n";
989
- await (0, import_promises6.writeFile)(target, content, "utf8");
1076
+ await (0, import_promises7.writeFile)(target, content, "utf8");
990
1077
  return target;
991
1078
  }
992
1079
  async function persistSiteUuid(cwd, siteUuid) {
@@ -994,10 +1081,10 @@ async function persistSiteUuid(cwd, siteUuid) {
994
1081
  return writeConfigFile(cwd, { ...existing, siteUuid });
995
1082
  }
996
1083
  async function readConfigFile(cwd) {
997
- const target = import_node_path5.default.join(cwd, CONFIG_FILENAME);
1084
+ const target = import_node_path6.default.join(cwd, CONFIG_FILENAME);
998
1085
  let raw;
999
1086
  try {
1000
- raw = await (0, import_promises6.readFile)(target, "utf8");
1087
+ raw = await (0, import_promises7.readFile)(target, "utf8");
1001
1088
  } catch (err) {
1002
1089
  if (err.code === "ENOENT") {
1003
1090
  return {};
@@ -1031,15 +1118,20 @@ function readEnv() {
1031
1118
  }
1032
1119
  timeoutMs = parsed;
1033
1120
  }
1121
+ const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
1034
1122
  return {
1035
1123
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
1036
1124
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
1037
- timeoutMs
1125
+ timeoutMs,
1126
+ environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
1038
1127
  };
1039
1128
  }
1040
1129
  function isUuid(value) {
1041
1130
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1042
1131
  }
1132
+ function isEnvironment(value) {
1133
+ return value === "production" || value === "sandbox";
1134
+ }
1043
1135
 
1044
1136
  // src/index.ts
1045
1137
  async function scanAndReport(options = {}) {