@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/README.md +7 -1
- package/dist/cli.js +272 -162
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +170 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +163 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/parsers/index.ts
|
|
2
2
|
import { access } from "fs/promises";
|
|
3
|
-
import
|
|
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/
|
|
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
|
|
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" +
|
|
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
|
|
105
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
250
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
212
251
|
async function parsePnpmLockfile(lockfilePath) {
|
|
213
252
|
let raw;
|
|
214
253
|
try {
|
|
215
|
-
raw = await
|
|
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
|
|
464
|
-
import
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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:
|
|
696
|
-
filename:
|
|
697
|
-
strategy:
|
|
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:
|
|
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
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|
912
|
-
import
|
|
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 =
|
|
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 =
|
|
1036
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
950
1037
|
let raw;
|
|
951
1038
|
try {
|
|
952
|
-
raw = await
|
|
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 = {}) {
|