@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/parsers/index.ts
2
2
  import { access } from "fs/promises";
3
- import path4 from "path";
3
+ import path5 from "path";
4
4
 
5
5
  // src/types.ts
6
6
  var PatchstackError = class extends Error {
@@ -14,13 +14,52 @@ var PatchstackError = class extends Error {
14
14
  cause;
15
15
  };
16
16
 
17
- // src/parsers/npm.ts
17
+ // src/parsers/consistency.ts
18
18
  import { readFile } from "fs/promises";
19
19
  import path from "path";
20
+ async function readDeclaredDependencyNames(cwd) {
21
+ let raw;
22
+ try {
23
+ raw = await readFile(path.join(cwd, "package.json"), "utf8");
24
+ } catch {
25
+ return [];
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ } catch {
31
+ return [];
32
+ }
33
+ if (typeof parsed !== "object" || parsed === null) {
34
+ return [];
35
+ }
36
+ const names = /* @__PURE__ */ new Set();
37
+ for (const group of ["dependencies", "devDependencies"]) {
38
+ const deps = parsed[group];
39
+ if (typeof deps !== "object" || deps === null) {
40
+ continue;
41
+ }
42
+ for (const [name, spec] of Object.entries(deps)) {
43
+ if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
44
+ continue;
45
+ }
46
+ names.add(name);
47
+ }
48
+ }
49
+ return [...names];
50
+ }
51
+ function missingDependencies(declared, packages) {
52
+ const present = new Set(packages.map((entry) => entry.name));
53
+ return declared.filter((name) => !present.has(name));
54
+ }
55
+
56
+ // src/parsers/npm.ts
57
+ import { readFile as readFile2 } from "fs/promises";
58
+ import path2 from "path";
20
59
  async function parseNpmLockfile(lockfilePath) {
21
60
  let raw;
22
61
  try {
23
- raw = await readFile(lockfilePath, "utf8");
62
+ raw = await readFile2(lockfilePath, "utf8");
24
63
  } catch (cause) {
25
64
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
26
65
  }
@@ -78,7 +117,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
78
117
  return acc;
79
118
  }
80
119
  function extractNameFromPath(pkgPath) {
81
- const segments = pkgPath.split("node_modules" + path.sep === pkgPath ? path.sep : "/");
120
+ const segments = pkgPath.split("node_modules" + path2.sep === pkgPath ? path2.sep : "/");
82
121
  const parts = pkgPath.split("/");
83
122
  const nmIndex = parts.lastIndexOf("node_modules");
84
123
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -101,10 +140,10 @@ function isDirectV2(pkgPath) {
101
140
  }
102
141
 
103
142
  // src/parsers/node_modules.ts
104
- import { lstat, readFile as readFile2, readdir, stat } from "fs/promises";
105
- import path2 from "path";
143
+ import { lstat, readFile as readFile3, readdir, stat } from "fs/promises";
144
+ import path3 from "path";
106
145
  async function walkNodeModules(cwd) {
107
- const root = path2.join(cwd, "node_modules");
146
+ const root = path3.join(cwd, "node_modules");
108
147
  try {
109
148
  const info = await stat(root);
110
149
  if (!info.isDirectory()) {
@@ -138,7 +177,7 @@ async function walk(dir, acc, depth) {
138
177
  if (name.startsWith(".")) {
139
178
  continue;
140
179
  }
141
- const fullPath = path2.join(dir, name);
180
+ const fullPath = path3.join(dir, name);
142
181
  if (!await isPlainDirectory(fullPath)) {
143
182
  continue;
144
183
  }
@@ -153,7 +192,7 @@ async function walk(dir, acc, depth) {
153
192
  if (sub.startsWith(".")) {
154
193
  continue;
155
194
  }
156
- const scopedDir = path2.join(fullPath, sub);
195
+ const scopedDir = path3.join(fullPath, sub);
157
196
  if (!await isPlainDirectory(scopedDir)) {
158
197
  continue;
159
198
  }
@@ -169,7 +208,7 @@ async function walk(dir, acc, depth) {
169
208
  async function readPackage(pkgDir, depth, acc) {
170
209
  let raw;
171
210
  try {
172
- raw = await readFile2(path2.join(pkgDir, "package.json"), "utf8");
211
+ raw = await readFile3(path3.join(pkgDir, "package.json"), "utf8");
173
212
  } catch {
174
213
  return;
175
214
  }
@@ -192,7 +231,7 @@ async function readPackage(pkgDir, depth, acc) {
192
231
  });
193
232
  }
194
233
  async function walkNested(pkgDir, acc, depth) {
195
- const nested = path2.join(pkgDir, "node_modules");
234
+ const nested = path3.join(pkgDir, "node_modules");
196
235
  if (!await isPlainDirectory(nested)) {
197
236
  return;
198
237
  }
@@ -208,11 +247,11 @@ async function isPlainDirectory(dir) {
208
247
  }
209
248
 
210
249
  // src/parsers/pnpm.ts
211
- import { readFile as readFile3 } from "fs/promises";
250
+ import { readFile as readFile4 } from "fs/promises";
212
251
  async function parsePnpmLockfile(lockfilePath) {
213
252
  let raw;
214
253
  try {
215
- raw = await readFile3(lockfilePath, "utf8");
254
+ raw = await readFile4(lockfilePath, "utf8");
216
255
  } catch (cause) {
217
256
  throw new PatchstackError(
218
257
  `Could not read lockfile at ${lockfilePath}`,
@@ -460,12 +499,12 @@ function extractLeafName(trimmed) {
460
499
  }
461
500
 
462
501
  // src/parsers/yarn.ts
463
- import { readFile as readFile4 } from "fs/promises";
464
- import path3 from "path";
502
+ import { readFile as readFile5 } from "fs/promises";
503
+ import path4 from "path";
465
504
  async function parseYarnLockfile(lockfilePath) {
466
505
  let raw;
467
506
  try {
468
- raw = await readFile4(lockfilePath, "utf8");
507
+ raw = await readFile5(lockfilePath, "utf8");
469
508
  } catch (cause) {
470
509
  throw new PatchstackError(
471
510
  `Could not read lockfile at ${lockfilePath}`,
@@ -480,7 +519,7 @@ async function parseYarnLockfile(lockfilePath) {
480
519
  "LOCKFILE_PARSE_ERROR"
481
520
  );
482
521
  }
483
- const directNames = await readDirectDepNames(path3.dirname(lockfilePath));
522
+ const directNames = await readDirectDepNames(path4.dirname(lockfilePath));
484
523
  const entries = [];
485
524
  const seen = /* @__PURE__ */ new Set();
486
525
  for (const block of blocks) {
@@ -624,7 +663,7 @@ async function readDirectDepNames(cwd) {
624
663
  const names = /* @__PURE__ */ new Set();
625
664
  let raw;
626
665
  try {
627
- raw = await readFile4(path3.join(cwd, "package.json"), "utf8");
666
+ raw = await readFile5(path4.join(cwd, "package.json"), "utf8");
628
667
  } catch {
629
668
  return names;
630
669
  }
@@ -651,61 +690,99 @@ async function readDirectDepNames(cwd) {
651
690
  }
652
691
 
653
692
  // src/parsers/index.ts
693
+ var LOCKFILE_CANDIDATES = [
694
+ { filename: "package-lock.json", strategy: "npm-lockfile" },
695
+ { filename: "bun.lock", strategy: "node-modules-walk" },
696
+ { filename: "bun.lockb", strategy: "node-modules-walk" },
697
+ { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
698
+ { filename: "yarn.lock", strategy: "yarn-lockfile" }
699
+ ];
654
700
  async function detectLockfile(cwd) {
655
- const npmLock = path4.join(cwd, "package-lock.json");
656
- if (await exists(npmLock)) {
657
- return {
658
- ecosystem: "npm",
659
- filePath: npmLock,
660
- filename: "package-lock.json",
661
- strategy: "npm-lockfile"
662
- };
663
- }
664
- const bunLock = path4.join(cwd, "bun.lock");
665
- if (await exists(bunLock)) {
666
- return {
667
- ecosystem: "npm",
668
- filePath: bunLock,
669
- filename: "bun.lock",
670
- strategy: "node-modules-walk"
671
- };
672
- }
673
- const bunLockB = path4.join(cwd, "bun.lockb");
674
- if (await exists(bunLockB)) {
675
- return {
676
- ecosystem: "npm",
677
- filePath: bunLockB,
678
- filename: "bun.lockb",
679
- strategy: "node-modules-walk"
680
- };
681
- }
682
- const pnpmLock = path4.join(cwd, "pnpm-lock.yaml");
683
- if (await exists(pnpmLock)) {
684
- return {
685
- ecosystem: "npm",
686
- filePath: pnpmLock,
687
- filename: "pnpm-lock.yaml",
688
- strategy: "pnpm-lockfile"
689
- };
690
- }
691
- const yarnLock = path4.join(cwd, "yarn.lock");
692
- if (await exists(yarnLock)) {
701
+ const probed = await Promise.all(
702
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
703
+ const filePath = path5.join(cwd, candidate.filename);
704
+ return { ...candidate, filePath, present: await exists(filePath) };
705
+ })
706
+ );
707
+ const match = probed.find((candidate) => candidate.present);
708
+ if (match) {
693
709
  return {
694
710
  ecosystem: "npm",
695
- filePath: yarnLock,
696
- filename: "yarn.lock",
697
- strategy: "yarn-lockfile"
711
+ filePath: match.filePath,
712
+ filename: match.filename,
713
+ strategy: match.strategy
698
714
  };
699
715
  }
716
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
700
717
  throw new PatchstackError(
701
- `No lockfile found in ${cwd}. Expected one of: package-lock.json, bun.lock, bun.lockb, yarn.lock, pnpm-lock.yaml.`,
718
+ `No lockfile found in ${cwd}. Expected one of: ${expected}.`,
702
719
  "LOCKFILE_NOT_FOUND"
703
720
  );
704
721
  }
705
722
  async function scanLockfile(cwd) {
706
- const detected = await detectLockfile(cwd);
707
- const packages = await runStrategy(detected, cwd);
708
- return { ecosystem: detected.ecosystem, packages };
723
+ const candidates = await presentLockfiles(cwd);
724
+ if (candidates.length === 0) {
725
+ await detectLockfile(cwd);
726
+ }
727
+ const declared = await readDeclaredDependencyNames(cwd);
728
+ const warnings = [];
729
+ let firstParsed = null;
730
+ let walkTried = false;
731
+ for (const candidate of candidates) {
732
+ let packages;
733
+ try {
734
+ packages = await runStrategy(candidate, cwd);
735
+ } catch {
736
+ continue;
737
+ }
738
+ walkTried ||= candidate.strategy === "node-modules-walk";
739
+ firstParsed ??= { packages, filename: candidate.filename };
740
+ const missing = missingDependencies(declared, packages);
741
+ if (missing.length === 0) {
742
+ return manifestWith(packages, warnings);
743
+ }
744
+ warnings.push(staleWarning(candidate.filename, missing));
745
+ }
746
+ if (!walkTried) {
747
+ try {
748
+ const packages = await walkNodeModules(cwd);
749
+ const missing = missingDependencies(declared, packages);
750
+ if (missing.length > 0) {
751
+ warnings.push(staleWarning("node_modules/", missing));
752
+ }
753
+ warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
754
+ return manifestWith(packages, warnings);
755
+ } catch {
756
+ }
757
+ }
758
+ if (firstParsed === null) {
759
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
760
+ throw new PatchstackError(
761
+ `No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
762
+ "LOCKFILE_NOT_FOUND"
763
+ );
764
+ }
765
+ warnings.push(
766
+ `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
767
+ );
768
+ return manifestWith(firstParsed.packages, warnings);
769
+ }
770
+ function manifestWith(packages, warnings) {
771
+ return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
772
+ }
773
+ function staleWarning(source, missing) {
774
+ const sample = missing.slice(0, 3).join(", ");
775
+ const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
776
+ return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
777
+ }
778
+ async function presentLockfiles(cwd) {
779
+ const probed = await Promise.all(
780
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
781
+ const filePath = path5.join(cwd, candidate.filename);
782
+ return { ...candidate, filePath, present: await exists(filePath) };
783
+ })
784
+ );
785
+ return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
709
786
  }
710
787
  async function runStrategy(detected, cwd) {
711
788
  switch (detected.strategy) {
@@ -853,7 +930,7 @@ async function postManifest(config, payload) {
853
930
  Accept: "application/json",
854
931
  "User-Agent": "@patchstack/connect"
855
932
  },
856
- body: JSON.stringify(payload),
933
+ body: JSON.stringify({ ...payload, environment: config.environment }),
857
934
  signal: AbortSignal.timeout(timeoutMs)
858
935
  });
859
936
  } catch (cause) {
@@ -908,15 +985,24 @@ function isTimeoutError(cause) {
908
985
  }
909
986
 
910
987
  // src/config.ts
911
- import { readFile as readFile5, writeFile } from "fs/promises";
912
- import path5 from "path";
988
+ import { readFile as readFile6, writeFile } from "fs/promises";
989
+ import path6 from "path";
913
990
  var CONFIG_FILENAME = ".patchstackrc.json";
991
+ var DEFAULT_ENVIRONMENT = "production";
914
992
  async function resolveConfig(options) {
915
993
  const fromFile = await readConfigFile(options.cwd);
916
994
  const fromEnv = readEnv();
917
995
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
918
996
  const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
919
997
  const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
998
+ const environmentRaw = fromEnv.environment ?? fromFile.environment;
999
+ if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
1000
+ throw new PatchstackError(
1001
+ `Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
1002
+ "CONFIG_INVALID"
1003
+ );
1004
+ }
1005
+ const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
920
1006
  if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
921
1007
  throw new PatchstackError(
922
1008
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -932,11 +1018,12 @@ async function resolveConfig(options) {
932
1018
  return {
933
1019
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
934
1020
  endpoint,
935
- timeoutMs
1021
+ timeoutMs,
1022
+ environment
936
1023
  };
937
1024
  }
938
1025
  async function writeConfigFile(cwd, config) {
939
- const target = path5.join(cwd, CONFIG_FILENAME);
1026
+ const target = path6.join(cwd, CONFIG_FILENAME);
940
1027
  const content = JSON.stringify(config, null, 2) + "\n";
941
1028
  await writeFile(target, content, "utf8");
942
1029
  return target;
@@ -946,10 +1033,10 @@ async function persistSiteUuid(cwd, siteUuid) {
946
1033
  return writeConfigFile(cwd, { ...existing, siteUuid });
947
1034
  }
948
1035
  async function readConfigFile(cwd) {
949
- const target = path5.join(cwd, CONFIG_FILENAME);
1036
+ const target = path6.join(cwd, CONFIG_FILENAME);
950
1037
  let raw;
951
1038
  try {
952
- raw = await readFile5(target, "utf8");
1039
+ raw = await readFile6(target, "utf8");
953
1040
  } catch (err) {
954
1041
  if (err.code === "ENOENT") {
955
1042
  return {};
@@ -983,15 +1070,20 @@ function readEnv() {
983
1070
  }
984
1071
  timeoutMs = parsed;
985
1072
  }
1073
+ const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
986
1074
  return {
987
1075
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
988
1076
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
989
- timeoutMs
1077
+ timeoutMs,
1078
+ environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
990
1079
  };
991
1080
  }
992
1081
  function isUuid(value) {
993
1082
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
994
1083
  }
1084
+ function isEnvironment(value) {
1085
+ return value === "production" || value === "sandbox";
1086
+ }
995
1087
 
996
1088
  // src/index.ts
997
1089
  async function scanAndReport(options = {}) {