@patchstack/connect 0.2.7 → 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 +136 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +133 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +126 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,9 +17,11 @@ npm install --save-dev @patchstack/connect
|
|
|
17
17
|
npx @patchstack/connect scan
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
> **Use your project's own package manager.** On bun-managed projects (Lovable, Bolt, most vibe-coding platforms) install with `bun add -d @patchstack/connect` instead — running `npm install` there plants a `package-lock.json` that the platform's native dependency flow never updates again, leaving a stale lockfile next to the live one. The connector detects and works around that (see *Stale lockfiles* below), but not creating the fossil is better.
|
|
21
|
+
|
|
20
22
|
That's it. The first `scan`:
|
|
21
23
|
|
|
22
|
-
1. Reads your
|
|
24
|
+
1. Reads your lockfile (see *Supported lockfiles*).
|
|
23
25
|
2. POSTs the package list to Patchstack with **no** UUID.
|
|
24
26
|
3. Patchstack provisions a fresh site and returns its UUID.
|
|
25
27
|
4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
|
|
@@ -123,6 +125,10 @@ That's the entire payload. No source code, no environment variables, no file pat
|
|
|
123
125
|
|
|
124
126
|
If both a Bun lockfile and `node_modules/` are present, the connector walks `node_modules/` to enumerate the installed packages. Run `bun install` (or `npm install`) before scanning so the directory is populated.
|
|
125
127
|
|
|
128
|
+
### Stale lockfiles
|
|
129
|
+
|
|
130
|
+
Every scanned source is validated against `package.json`: if the chosen lockfile is missing dependencies that `package.json` declares, it is treated as a fossil (e.g. a `package-lock.json` created by a one-off `npm install` in a bun-managed project) and the connector falls through to the next source — ultimately walking `node_modules/`, the installed truth — and prints a warning naming the stale file. Delete the stale lockfile to silence the warning. Without this, the manifest and the build fingerprint would silently freeze while the real dependency set drifts.
|
|
131
|
+
|
|
126
132
|
## Development
|
|
127
133
|
|
|
128
134
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync, writeFileSync } from "fs";
|
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
8
|
-
import
|
|
8
|
+
import path5 from "path";
|
|
9
9
|
|
|
10
10
|
// src/types.ts
|
|
11
11
|
var PatchstackError = class extends Error {
|
|
@@ -19,13 +19,52 @@ var PatchstackError = class extends Error {
|
|
|
19
19
|
cause;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// src/parsers/
|
|
22
|
+
// src/parsers/consistency.ts
|
|
23
23
|
import { readFile } from "fs/promises";
|
|
24
24
|
import path from "path";
|
|
25
|
+
async function readDeclaredDependencyNames(cwd) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(path.join(cwd, "package.json"), "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const names = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const group of ["dependencies", "devDependencies"]) {
|
|
43
|
+
const deps = parsed[group];
|
|
44
|
+
if (typeof deps !== "object" || deps === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
48
|
+
if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
names.add(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...names];
|
|
55
|
+
}
|
|
56
|
+
function missingDependencies(declared, packages) {
|
|
57
|
+
const present = new Set(packages.map((entry) => entry.name));
|
|
58
|
+
return declared.filter((name) => !present.has(name));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/parsers/npm.ts
|
|
62
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
63
|
+
import path2 from "path";
|
|
25
64
|
async function parseNpmLockfile(lockfilePath) {
|
|
26
65
|
let raw;
|
|
27
66
|
try {
|
|
28
|
-
raw = await
|
|
67
|
+
raw = await readFile2(lockfilePath, "utf8");
|
|
29
68
|
} catch (cause) {
|
|
30
69
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
31
70
|
}
|
|
@@ -83,7 +122,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
|
|
|
83
122
|
return acc;
|
|
84
123
|
}
|
|
85
124
|
function extractNameFromPath(pkgPath) {
|
|
86
|
-
const segments = pkgPath.split("node_modules" +
|
|
125
|
+
const segments = pkgPath.split("node_modules" + path2.sep === pkgPath ? path2.sep : "/");
|
|
87
126
|
const parts = pkgPath.split("/");
|
|
88
127
|
const nmIndex = parts.lastIndexOf("node_modules");
|
|
89
128
|
if (nmIndex === -1 || nmIndex >= parts.length - 1) {
|
|
@@ -106,10 +145,10 @@ function isDirectV2(pkgPath) {
|
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
// src/parsers/node_modules.ts
|
|
109
|
-
import { lstat, readFile as
|
|
110
|
-
import
|
|
148
|
+
import { lstat, readFile as readFile3, readdir, stat } from "fs/promises";
|
|
149
|
+
import path3 from "path";
|
|
111
150
|
async function walkNodeModules(cwd) {
|
|
112
|
-
const root =
|
|
151
|
+
const root = path3.join(cwd, "node_modules");
|
|
113
152
|
try {
|
|
114
153
|
const info = await stat(root);
|
|
115
154
|
if (!info.isDirectory()) {
|
|
@@ -143,7 +182,7 @@ async function walk(dir, acc, depth) {
|
|
|
143
182
|
if (name.startsWith(".")) {
|
|
144
183
|
continue;
|
|
145
184
|
}
|
|
146
|
-
const fullPath =
|
|
185
|
+
const fullPath = path3.join(dir, name);
|
|
147
186
|
if (!await isPlainDirectory(fullPath)) {
|
|
148
187
|
continue;
|
|
149
188
|
}
|
|
@@ -158,7 +197,7 @@ async function walk(dir, acc, depth) {
|
|
|
158
197
|
if (sub.startsWith(".")) {
|
|
159
198
|
continue;
|
|
160
199
|
}
|
|
161
|
-
const scopedDir =
|
|
200
|
+
const scopedDir = path3.join(fullPath, sub);
|
|
162
201
|
if (!await isPlainDirectory(scopedDir)) {
|
|
163
202
|
continue;
|
|
164
203
|
}
|
|
@@ -174,7 +213,7 @@ async function walk(dir, acc, depth) {
|
|
|
174
213
|
async function readPackage(pkgDir, depth, acc) {
|
|
175
214
|
let raw;
|
|
176
215
|
try {
|
|
177
|
-
raw = await
|
|
216
|
+
raw = await readFile3(path3.join(pkgDir, "package.json"), "utf8");
|
|
178
217
|
} catch {
|
|
179
218
|
return;
|
|
180
219
|
}
|
|
@@ -197,7 +236,7 @@ async function readPackage(pkgDir, depth, acc) {
|
|
|
197
236
|
});
|
|
198
237
|
}
|
|
199
238
|
async function walkNested(pkgDir, acc, depth) {
|
|
200
|
-
const nested =
|
|
239
|
+
const nested = path3.join(pkgDir, "node_modules");
|
|
201
240
|
if (!await isPlainDirectory(nested)) {
|
|
202
241
|
return;
|
|
203
242
|
}
|
|
@@ -213,11 +252,11 @@ async function isPlainDirectory(dir) {
|
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
// src/parsers/pnpm.ts
|
|
216
|
-
import { readFile as
|
|
255
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
217
256
|
async function parsePnpmLockfile(lockfilePath) {
|
|
218
257
|
let raw;
|
|
219
258
|
try {
|
|
220
|
-
raw = await
|
|
259
|
+
raw = await readFile4(lockfilePath, "utf8");
|
|
221
260
|
} catch (cause) {
|
|
222
261
|
throw new PatchstackError(
|
|
223
262
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -465,12 +504,12 @@ function extractLeafName(trimmed) {
|
|
|
465
504
|
}
|
|
466
505
|
|
|
467
506
|
// src/parsers/yarn.ts
|
|
468
|
-
import { readFile as
|
|
469
|
-
import
|
|
507
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
508
|
+
import path4 from "path";
|
|
470
509
|
async function parseYarnLockfile(lockfilePath) {
|
|
471
510
|
let raw;
|
|
472
511
|
try {
|
|
473
|
-
raw = await
|
|
512
|
+
raw = await readFile5(lockfilePath, "utf8");
|
|
474
513
|
} catch (cause) {
|
|
475
514
|
throw new PatchstackError(
|
|
476
515
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -485,7 +524,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
485
524
|
"LOCKFILE_PARSE_ERROR"
|
|
486
525
|
);
|
|
487
526
|
}
|
|
488
|
-
const directNames = await readDirectDepNames(
|
|
527
|
+
const directNames = await readDirectDepNames(path4.dirname(lockfilePath));
|
|
489
528
|
const entries = [];
|
|
490
529
|
const seen = /* @__PURE__ */ new Set();
|
|
491
530
|
for (const block of blocks) {
|
|
@@ -629,7 +668,7 @@ async function readDirectDepNames(cwd) {
|
|
|
629
668
|
const names = /* @__PURE__ */ new Set();
|
|
630
669
|
let raw;
|
|
631
670
|
try {
|
|
632
|
-
raw = await
|
|
671
|
+
raw = await readFile5(path4.join(cwd, "package.json"), "utf8");
|
|
633
672
|
} catch {
|
|
634
673
|
return names;
|
|
635
674
|
}
|
|
@@ -666,7 +705,7 @@ var LOCKFILE_CANDIDATES = [
|
|
|
666
705
|
async function detectLockfile(cwd) {
|
|
667
706
|
const probed = await Promise.all(
|
|
668
707
|
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
669
|
-
const filePath =
|
|
708
|
+
const filePath = path5.join(cwd, candidate.filename);
|
|
670
709
|
return { ...candidate, filePath, present: await exists(filePath) };
|
|
671
710
|
})
|
|
672
711
|
);
|
|
@@ -686,9 +725,69 @@ async function detectLockfile(cwd) {
|
|
|
686
725
|
);
|
|
687
726
|
}
|
|
688
727
|
async function scanLockfile(cwd) {
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
728
|
+
const candidates = await presentLockfiles(cwd);
|
|
729
|
+
if (candidates.length === 0) {
|
|
730
|
+
await detectLockfile(cwd);
|
|
731
|
+
}
|
|
732
|
+
const declared = await readDeclaredDependencyNames(cwd);
|
|
733
|
+
const warnings = [];
|
|
734
|
+
let firstParsed = null;
|
|
735
|
+
let walkTried = false;
|
|
736
|
+
for (const candidate of candidates) {
|
|
737
|
+
let packages;
|
|
738
|
+
try {
|
|
739
|
+
packages = await runStrategy(candidate, cwd);
|
|
740
|
+
} catch {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
walkTried ||= candidate.strategy === "node-modules-walk";
|
|
744
|
+
firstParsed ??= { packages, filename: candidate.filename };
|
|
745
|
+
const missing = missingDependencies(declared, packages);
|
|
746
|
+
if (missing.length === 0) {
|
|
747
|
+
return manifestWith(packages, warnings);
|
|
748
|
+
}
|
|
749
|
+
warnings.push(staleWarning(candidate.filename, missing));
|
|
750
|
+
}
|
|
751
|
+
if (!walkTried) {
|
|
752
|
+
try {
|
|
753
|
+
const packages = await walkNodeModules(cwd);
|
|
754
|
+
const missing = missingDependencies(declared, packages);
|
|
755
|
+
if (missing.length > 0) {
|
|
756
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
757
|
+
}
|
|
758
|
+
warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
|
|
759
|
+
return manifestWith(packages, warnings);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (firstParsed === null) {
|
|
764
|
+
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
765
|
+
throw new PatchstackError(
|
|
766
|
+
`No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
|
|
767
|
+
"LOCKFILE_NOT_FOUND"
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
warnings.push(
|
|
771
|
+
`No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
|
|
772
|
+
);
|
|
773
|
+
return manifestWith(firstParsed.packages, warnings);
|
|
774
|
+
}
|
|
775
|
+
function manifestWith(packages, warnings) {
|
|
776
|
+
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
777
|
+
}
|
|
778
|
+
function staleWarning(source, missing) {
|
|
779
|
+
const sample = missing.slice(0, 3).join(", ");
|
|
780
|
+
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
781
|
+
return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
|
|
782
|
+
}
|
|
783
|
+
async function presentLockfiles(cwd) {
|
|
784
|
+
const probed = await Promise.all(
|
|
785
|
+
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
786
|
+
const filePath = path5.join(cwd, candidate.filename);
|
|
787
|
+
return { ...candidate, filePath, present: await exists(filePath) };
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
692
791
|
}
|
|
693
792
|
async function runStrategy(detected, cwd) {
|
|
694
793
|
switch (detected.strategy) {
|
|
@@ -906,8 +1005,8 @@ function isTimeoutError(cause) {
|
|
|
906
1005
|
}
|
|
907
1006
|
|
|
908
1007
|
// src/config.ts
|
|
909
|
-
import { readFile as
|
|
910
|
-
import
|
|
1008
|
+
import { readFile as readFile6, writeFile } from "fs/promises";
|
|
1009
|
+
import path6 from "path";
|
|
911
1010
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
912
1011
|
var DEFAULT_ENVIRONMENT = "production";
|
|
913
1012
|
async function resolveConfig(options) {
|
|
@@ -944,7 +1043,7 @@ async function resolveConfig(options) {
|
|
|
944
1043
|
};
|
|
945
1044
|
}
|
|
946
1045
|
async function writeConfigFile(cwd, config) {
|
|
947
|
-
const target =
|
|
1046
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
948
1047
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
949
1048
|
await writeFile(target, content, "utf8");
|
|
950
1049
|
return target;
|
|
@@ -954,10 +1053,10 @@ async function persistSiteUuid(cwd, siteUuid) {
|
|
|
954
1053
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
955
1054
|
}
|
|
956
1055
|
async function readConfigFile(cwd) {
|
|
957
|
-
const target =
|
|
1056
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
958
1057
|
let raw;
|
|
959
1058
|
try {
|
|
960
|
-
raw = await
|
|
1059
|
+
raw = await readFile6(target, "utf8");
|
|
961
1060
|
} catch (err) {
|
|
962
1061
|
if (err.code === "ENOENT") {
|
|
963
1062
|
return {};
|
|
@@ -1008,16 +1107,16 @@ function isEnvironment(value) {
|
|
|
1008
1107
|
|
|
1009
1108
|
// src/mark-build.ts
|
|
1010
1109
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1011
|
-
import
|
|
1110
|
+
import path7 from "path";
|
|
1012
1111
|
var MARKER_ATTR = "data-patchstack-build";
|
|
1013
1112
|
var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
|
|
1014
1113
|
function resolveBuildDir(cwd, override) {
|
|
1015
1114
|
if (override !== void 0 && override !== "") {
|
|
1016
|
-
const abs =
|
|
1115
|
+
const abs = path7.resolve(cwd, override);
|
|
1017
1116
|
return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
|
|
1018
1117
|
}
|
|
1019
1118
|
for (const candidate of BUILD_DIR_CANDIDATES) {
|
|
1020
|
-
const abs =
|
|
1119
|
+
const abs = path7.resolve(cwd, candidate);
|
|
1021
1120
|
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
1022
1121
|
return abs;
|
|
1023
1122
|
}
|
|
@@ -1028,7 +1127,7 @@ function findHtmlFiles(dir) {
|
|
|
1028
1127
|
const out = [];
|
|
1029
1128
|
const walk2 = (current) => {
|
|
1030
1129
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1031
|
-
const full =
|
|
1130
|
+
const full = path7.join(current, entry.name);
|
|
1032
1131
|
if (entry.isDirectory()) {
|
|
1033
1132
|
walk2(full);
|
|
1034
1133
|
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
@@ -1157,6 +1256,9 @@ async function runScan(args) {
|
|
|
1157
1256
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
1158
1257
|
});
|
|
1159
1258
|
const manifest = await scanLockfile(process.cwd());
|
|
1259
|
+
for (const warning of manifest.warnings ?? []) {
|
|
1260
|
+
console.warn(`patchstack: ${warning}`);
|
|
1261
|
+
}
|
|
1160
1262
|
const { payload, stats } = buildWirePayload(manifest);
|
|
1161
1263
|
console.log(
|
|
1162
1264
|
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
|
|
@@ -1226,6 +1328,9 @@ async function runMarkBuild(args) {
|
|
|
1226
1328
|
let checksum = null;
|
|
1227
1329
|
try {
|
|
1228
1330
|
const manifest = await scanLockfile(cwd);
|
|
1331
|
+
for (const warning of manifest.warnings ?? []) {
|
|
1332
|
+
console.warn(`mark-build: ${warning}`);
|
|
1333
|
+
}
|
|
1229
1334
|
const { payload } = buildWirePayload(manifest);
|
|
1230
1335
|
checksum = computeManifestChecksum(payload.packages);
|
|
1231
1336
|
} catch (err) {
|