@patchstack/connect 0.3.28 → 0.3.30
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/AGENT-INSTALL.md +37 -21
- package/README.md +24 -10
- package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
- package/dist/chunk-LLKP5EJS.js.map +1 -0
- package/dist/cli.js +1267 -224
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +419 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -9
- package/dist/index.d.ts +28 -9
- package/dist/index.js +412 -97
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +33 -13
- package/dist/protect/templates/demo-rules.json +2 -3
- package/dist/protect/templates/express-guard.cjs +26 -15
- package/dist/protect/templates/express-guard.js +26 -15
- package/dist/protect/templates/express-guard.ts +33 -16
- package/dist/protect/templates/fastify-plugin.cjs +33 -11
- package/dist/protect/templates/fastify-plugin.js +33 -11
- package/dist/protect/templates/fastify-plugin.ts +40 -12
- package/dist/protect/templates/generic-guard.cjs +28 -12
- package/dist/protect/templates/generic-guard.js +28 -13
- package/dist/protect/templates/generic-guard.ts +41 -20
- package/dist/protect/templates/guard.ts +47 -27
- package/dist/protect/templates/next-middleware.ts +29 -12
- package/dist/protect/templates/nuxt-middleware.ts +29 -12
- package/dist/protect/templates/rules.json +2 -2
- package/dist/protect/templates/sveltekit-hooks.ts +33 -13
- package/dist/protect.cjs +1039 -235
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +39 -11
- package/dist/protect.edge.js +663 -132
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +665 -134
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
- package/dist/refresh-manifest-ZSP76JWQ.js.map +1 -0
- package/package.json +5 -2
- package/dist/chunk-MJOTFUDE.js.map +0 -1
- package/dist/refresh-manifest-VRBE6RH6.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -58,7 +58,7 @@ __export(src_exports, {
|
|
|
58
58
|
module.exports = __toCommonJS(src_exports);
|
|
59
59
|
|
|
60
60
|
// src/parsers/index.ts
|
|
61
|
-
var
|
|
61
|
+
var import_promises7 = require("fs/promises");
|
|
62
62
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
63
63
|
|
|
64
64
|
// src/types.ts
|
|
@@ -93,7 +93,7 @@ async function readDeclaredDependencyNames(cwd) {
|
|
|
93
93
|
return [];
|
|
94
94
|
}
|
|
95
95
|
const names = /* @__PURE__ */ new Set();
|
|
96
|
-
for (const group of ["dependencies", "devDependencies"]) {
|
|
96
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
97
97
|
const deps = parsed[group];
|
|
98
98
|
if (typeof deps !== "object" || deps === null) {
|
|
99
99
|
continue;
|
|
@@ -111,14 +111,171 @@ function missingDependencies(declared, packages) {
|
|
|
111
111
|
const present = new Set(packages.map((entry) => entry.name));
|
|
112
112
|
return declared.filter((name) => !present.has(name));
|
|
113
113
|
}
|
|
114
|
+
function disagreements(packages, other) {
|
|
115
|
+
const otherVersions = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const entry of other) {
|
|
117
|
+
if (!otherVersions.has(entry.name)) otherVersions.set(entry.name, entry.version);
|
|
118
|
+
}
|
|
119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
120
|
+
const conflicts = [];
|
|
121
|
+
for (const entry of packages) {
|
|
122
|
+
if (seen.has(entry.name)) continue;
|
|
123
|
+
seen.add(entry.name);
|
|
124
|
+
const otherVersion = otherVersions.get(entry.name);
|
|
125
|
+
if (otherVersion !== void 0 && otherVersion !== entry.version) {
|
|
126
|
+
conflicts.push({ name: entry.name, version: entry.version, otherVersion });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return conflicts;
|
|
130
|
+
}
|
|
114
131
|
|
|
115
|
-
// src/parsers/
|
|
132
|
+
// src/parsers/bun.ts
|
|
116
133
|
var import_promises2 = require("fs/promises");
|
|
134
|
+
|
|
135
|
+
// src/parsers/report.ts
|
|
136
|
+
var MAX_SAMPLES = 3;
|
|
137
|
+
function newReport() {
|
|
138
|
+
return { unreadable: 0, samples: [] };
|
|
139
|
+
}
|
|
140
|
+
function recordUnreadable(report, sample) {
|
|
141
|
+
if (!report) return;
|
|
142
|
+
report.unreadable++;
|
|
143
|
+
if (report.samples.length < MAX_SAMPLES) report.samples.push(sample);
|
|
144
|
+
}
|
|
145
|
+
function unreadableWarning(filename, report) {
|
|
146
|
+
if (report.unreadable === 0) return null;
|
|
147
|
+
const shown = report.samples.join(", ");
|
|
148
|
+
const one = report.unreadable === 1;
|
|
149
|
+
return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/parsers/bun.ts
|
|
153
|
+
async function parseBunLockfile(lockfilePath, report) {
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = await (0, import_promises2.readFile)(lockfilePath, "utf8");
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
159
|
+
}
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(stripTrailingCommas(raw));
|
|
163
|
+
} catch (cause) {
|
|
164
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
|
|
165
|
+
}
|
|
166
|
+
const packages = parsed?.packages;
|
|
167
|
+
if (typeof packages !== "object" || packages === null) {
|
|
168
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} has no "packages" entries`, "LOCKFILE_PARSE_ERROR");
|
|
169
|
+
}
|
|
170
|
+
const direct = directDependencyNames(parsed);
|
|
171
|
+
const entries = [];
|
|
172
|
+
let skipped = 0;
|
|
173
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
174
|
+
const descriptor = Array.isArray(value) ? value[0] : value;
|
|
175
|
+
if (typeof descriptor !== "string") {
|
|
176
|
+
skipped++;
|
|
177
|
+
recordUnreadable(report, key);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const split = splitDescriptor(descriptor);
|
|
181
|
+
if (split === null) {
|
|
182
|
+
skipped++;
|
|
183
|
+
if (!isDeliberatelyExcluded(descriptor)) recordUnreadable(report, key);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
entries.push({
|
|
187
|
+
name: split.name,
|
|
188
|
+
version: split.version,
|
|
189
|
+
...direct.has(split.name) ? { direct: true } : {},
|
|
190
|
+
...key === split.name ? {} : { path: key }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (entries.length === 0) {
|
|
194
|
+
throw new PatchstackError(
|
|
195
|
+
`Lockfile at ${lockfilePath} lists no registry packages${skipped > 0 ? ` (${skipped} non-registry entr${skipped === 1 ? "y" : "ies"} skipped)` : ""}`,
|
|
196
|
+
"LOCKFILE_PARSE_ERROR"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return entries;
|
|
200
|
+
}
|
|
201
|
+
function splitDescriptor(descriptor) {
|
|
202
|
+
const at = descriptor.lastIndexOf("@");
|
|
203
|
+
if (at <= 0) return null;
|
|
204
|
+
const name = descriptor.slice(0, at);
|
|
205
|
+
const version = descriptor.slice(at + 1);
|
|
206
|
+
if (name === "" || version === "") return null;
|
|
207
|
+
if (NON_REGISTRY_VERSION.test(version)) return null;
|
|
208
|
+
return { name, version };
|
|
209
|
+
}
|
|
210
|
+
var NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
|
|
211
|
+
function isDeliberatelyExcluded(descriptor) {
|
|
212
|
+
const at = descriptor.lastIndexOf("@");
|
|
213
|
+
if (at <= 0) return false;
|
|
214
|
+
return NON_REGISTRY_VERSION.test(descriptor.slice(at + 1));
|
|
215
|
+
}
|
|
216
|
+
function directDependencyNames(parsed) {
|
|
217
|
+
const names = /* @__PURE__ */ new Set();
|
|
218
|
+
const workspaces = parsed?.workspaces;
|
|
219
|
+
if (typeof workspaces !== "object" || workspaces === null) return names;
|
|
220
|
+
for (const workspace of Object.values(workspaces)) {
|
|
221
|
+
if (typeof workspace !== "object" || workspace === null) continue;
|
|
222
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
223
|
+
const deps = workspace[group];
|
|
224
|
+
if (typeof deps !== "object" || deps === null) continue;
|
|
225
|
+
for (const name of Object.keys(deps)) names.add(name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return names;
|
|
229
|
+
}
|
|
230
|
+
function stripTrailingCommas(source) {
|
|
231
|
+
const out = [];
|
|
232
|
+
let inString = false;
|
|
233
|
+
let pendingComma = -1;
|
|
234
|
+
for (let i = 0; i < source.length; i++) {
|
|
235
|
+
const ch = source[i];
|
|
236
|
+
if (inString) {
|
|
237
|
+
out.push(ch);
|
|
238
|
+
if (ch === "\\") {
|
|
239
|
+
const next = source[i + 1];
|
|
240
|
+
if (next !== void 0) {
|
|
241
|
+
out.push(next);
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
} else if (ch === '"') {
|
|
245
|
+
inString = false;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ch === '"') {
|
|
250
|
+
inString = true;
|
|
251
|
+
pendingComma = -1;
|
|
252
|
+
out.push(ch);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ch === ",") {
|
|
256
|
+
pendingComma = out.length;
|
|
257
|
+
out.push(ch);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (ch === "}" || ch === "]") {
|
|
261
|
+
if (pendingComma !== -1) out[pendingComma] = " ";
|
|
262
|
+
pendingComma = -1;
|
|
263
|
+
out.push(ch);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!/\s/.test(ch)) pendingComma = -1;
|
|
267
|
+
out.push(ch);
|
|
268
|
+
}
|
|
269
|
+
return out.join("");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/parsers/npm.ts
|
|
273
|
+
var import_promises3 = require("fs/promises");
|
|
117
274
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
118
275
|
async function parseNpmLockfile(lockfilePath) {
|
|
119
276
|
let raw;
|
|
120
277
|
try {
|
|
121
|
-
raw = await (0,
|
|
278
|
+
raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
|
|
122
279
|
} catch (cause) {
|
|
123
280
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
124
281
|
}
|
|
@@ -199,12 +356,13 @@ function isDirectV2(pkgPath) {
|
|
|
199
356
|
}
|
|
200
357
|
|
|
201
358
|
// src/parsers/node_modules.ts
|
|
202
|
-
var
|
|
359
|
+
var import_promises4 = require("fs/promises");
|
|
203
360
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
361
|
+
var PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
|
|
204
362
|
async function walkNodeModules(cwd) {
|
|
205
363
|
const root = import_node_path3.default.join(cwd, "node_modules");
|
|
206
364
|
try {
|
|
207
|
-
const info = await (0,
|
|
365
|
+
const info = await (0, import_promises4.stat)(root);
|
|
208
366
|
if (!info.isDirectory()) {
|
|
209
367
|
throw new PatchstackError(
|
|
210
368
|
`${root} exists but is not a directory.`,
|
|
@@ -221,96 +379,122 @@ async function walkNodeModules(cwd) {
|
|
|
221
379
|
cause
|
|
222
380
|
);
|
|
223
381
|
}
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
382
|
+
const seen = /* @__PURE__ */ new Map();
|
|
383
|
+
try {
|
|
384
|
+
seen.set(await (0, import_promises4.realpath)(cwd), -1);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
return dedupeByShallowest(await walk(root, 0, seen));
|
|
388
|
+
}
|
|
389
|
+
function dedupeByShallowest(found) {
|
|
390
|
+
const best = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const item of found) {
|
|
392
|
+
const current = best.get(item.realpath);
|
|
393
|
+
if (current === void 0 || item.depth < current.depth) best.set(item.realpath, item);
|
|
394
|
+
}
|
|
395
|
+
return [...best.values()].map(({ entry }) => entry);
|
|
227
396
|
}
|
|
228
|
-
|
|
397
|
+
var CONCURRENCY = 32;
|
|
398
|
+
async function walk(dir, depth, seen) {
|
|
229
399
|
let names;
|
|
230
400
|
try {
|
|
231
|
-
names = await (0,
|
|
401
|
+
names = await (0, import_promises4.readdir)(dir);
|
|
232
402
|
} catch {
|
|
233
|
-
return;
|
|
403
|
+
return [];
|
|
234
404
|
}
|
|
235
|
-
|
|
236
|
-
if (name.startsWith("."))
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
405
|
+
const perEntry = await mapLimit(names, async (name) => {
|
|
406
|
+
if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
|
|
239
407
|
const fullPath = import_node_path3.default.join(dir, name);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
408
|
+
const resolved = await claim(fullPath, seen, depth);
|
|
409
|
+
if (resolved === null) return [];
|
|
410
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
|
|
243
411
|
if (name.startsWith("@")) {
|
|
244
412
|
let subNames;
|
|
245
413
|
try {
|
|
246
|
-
subNames = await (0,
|
|
414
|
+
subNames = await (0, import_promises4.readdir)(fullPath);
|
|
247
415
|
} catch {
|
|
248
|
-
|
|
416
|
+
return [];
|
|
249
417
|
}
|
|
250
|
-
|
|
251
|
-
if (sub.startsWith("."))
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
418
|
+
const scoped = await mapLimit(subNames, async (sub) => {
|
|
419
|
+
if (sub.startsWith(".")) return [];
|
|
254
420
|
const scopedDir = import_node_path3.default.join(fullPath, sub);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
continue;
|
|
421
|
+
const scopedReal = await claim(scopedDir, seen, depth);
|
|
422
|
+
if (scopedReal === null) return [];
|
|
423
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen);
|
|
424
|
+
});
|
|
425
|
+
return scoped.flat();
|
|
262
426
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
427
|
+
return packageAndNested(fullPath, resolved, depth, seen);
|
|
428
|
+
});
|
|
429
|
+
return perEntry.flat();
|
|
430
|
+
}
|
|
431
|
+
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
432
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
433
|
+
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
266
434
|
}
|
|
267
|
-
async function
|
|
435
|
+
async function mapLimit(items, fn) {
|
|
436
|
+
const results = new Array(items.length);
|
|
437
|
+
let next = 0;
|
|
438
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, items.length) }, async () => {
|
|
439
|
+
while (next < items.length) {
|
|
440
|
+
const index = next++;
|
|
441
|
+
results[index] = await fn(items[index]);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
await Promise.all(workers);
|
|
445
|
+
return results;
|
|
446
|
+
}
|
|
447
|
+
async function readPackage(pkgDir, depth) {
|
|
268
448
|
let raw;
|
|
269
449
|
try {
|
|
270
|
-
raw = await (0,
|
|
450
|
+
raw = await (0, import_promises4.readFile)(import_node_path3.default.join(pkgDir, "package.json"), "utf8");
|
|
271
451
|
} catch {
|
|
272
|
-
return;
|
|
452
|
+
return null;
|
|
273
453
|
}
|
|
274
454
|
let parsed;
|
|
275
455
|
try {
|
|
276
456
|
parsed = JSON.parse(raw);
|
|
277
457
|
} catch {
|
|
278
|
-
return;
|
|
458
|
+
return null;
|
|
279
459
|
}
|
|
280
460
|
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
281
|
-
return;
|
|
461
|
+
return null;
|
|
282
462
|
}
|
|
283
463
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
284
|
-
return;
|
|
464
|
+
return null;
|
|
285
465
|
}
|
|
286
|
-
|
|
287
|
-
name: parsed.name,
|
|
288
|
-
version: parsed.version,
|
|
289
|
-
direct: depth === 0
|
|
290
|
-
});
|
|
466
|
+
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
291
467
|
}
|
|
292
|
-
async function walkNested(pkgDir,
|
|
468
|
+
async function walkNested(pkgDir, depth, seen) {
|
|
293
469
|
const nested = import_node_path3.default.join(pkgDir, "node_modules");
|
|
294
|
-
if (
|
|
295
|
-
return;
|
|
470
|
+
if (await claim(nested, seen, depth + 1) === null) {
|
|
471
|
+
return [];
|
|
296
472
|
}
|
|
297
|
-
|
|
473
|
+
return walk(nested, depth + 1, seen);
|
|
298
474
|
}
|
|
299
|
-
async function
|
|
475
|
+
async function claim(dir, seen, depth) {
|
|
476
|
+
let resolved;
|
|
300
477
|
try {
|
|
301
|
-
const info = await (0,
|
|
302
|
-
|
|
478
|
+
const info = await (0, import_promises4.lstat)(dir);
|
|
479
|
+
if (!info.isDirectory() && !info.isSymbolicLink()) return null;
|
|
480
|
+
resolved = await (0, import_promises4.realpath)(dir);
|
|
481
|
+
const target = await (0, import_promises4.stat)(resolved);
|
|
482
|
+
if (!target.isDirectory()) return null;
|
|
303
483
|
} catch {
|
|
304
|
-
return
|
|
484
|
+
return null;
|
|
305
485
|
}
|
|
486
|
+
const claimed = seen.get(resolved);
|
|
487
|
+
if (claimed !== void 0 && claimed <= depth) return null;
|
|
488
|
+
seen.set(resolved, depth);
|
|
489
|
+
return resolved;
|
|
306
490
|
}
|
|
307
491
|
|
|
308
492
|
// src/parsers/pnpm.ts
|
|
309
|
-
var
|
|
310
|
-
async function parsePnpmLockfile(lockfilePath) {
|
|
493
|
+
var import_promises5 = require("fs/promises");
|
|
494
|
+
async function parsePnpmLockfile(lockfilePath, report) {
|
|
311
495
|
let raw;
|
|
312
496
|
try {
|
|
313
|
-
raw = await (0,
|
|
497
|
+
raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
|
|
314
498
|
} catch (cause) {
|
|
315
499
|
throw new PatchstackError(
|
|
316
500
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -331,6 +515,7 @@ async function parsePnpmLockfile(lockfilePath) {
|
|
|
331
515
|
for (const key of packageKeys) {
|
|
332
516
|
const parsed = parsePackageKey(key);
|
|
333
517
|
if (parsed === null) {
|
|
518
|
+
recordUnreadable(report, key);
|
|
334
519
|
continue;
|
|
335
520
|
}
|
|
336
521
|
entries.push({
|
|
@@ -558,12 +743,12 @@ function extractLeafName(trimmed) {
|
|
|
558
743
|
}
|
|
559
744
|
|
|
560
745
|
// src/parsers/yarn.ts
|
|
561
|
-
var
|
|
746
|
+
var import_promises6 = require("fs/promises");
|
|
562
747
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
563
|
-
async function parseYarnLockfile(lockfilePath) {
|
|
748
|
+
async function parseYarnLockfile(lockfilePath, report) {
|
|
564
749
|
let raw;
|
|
565
750
|
try {
|
|
566
|
-
raw = await (0,
|
|
751
|
+
raw = await (0, import_promises6.readFile)(lockfilePath, "utf8");
|
|
567
752
|
} catch (cause) {
|
|
568
753
|
throw new PatchstackError(
|
|
569
754
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -571,7 +756,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
571
756
|
cause
|
|
572
757
|
);
|
|
573
758
|
}
|
|
574
|
-
const blocks = parseBlocks(raw);
|
|
759
|
+
const blocks = parseBlocks(raw, report);
|
|
575
760
|
if (blocks.length === 0) {
|
|
576
761
|
throw new PatchstackError(
|
|
577
762
|
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
@@ -600,13 +785,19 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
600
785
|
}
|
|
601
786
|
return entries;
|
|
602
787
|
}
|
|
603
|
-
|
|
788
|
+
var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
|
|
789
|
+
function parseBlocks(raw, report) {
|
|
604
790
|
const lines = raw.split(/\r?\n/);
|
|
605
791
|
const blocks = [];
|
|
606
792
|
let current = null;
|
|
607
793
|
const finalize = () => {
|
|
608
|
-
if (current
|
|
794
|
+
if (current === null) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (current.version.length > 0 && current.names.size > 0) {
|
|
609
798
|
blocks.push(current);
|
|
799
|
+
} else if (current.unreadable) {
|
|
800
|
+
recordUnreadable(report, current.key);
|
|
610
801
|
}
|
|
611
802
|
current = null;
|
|
612
803
|
};
|
|
@@ -623,13 +814,13 @@ function parseBlocks(raw) {
|
|
|
623
814
|
}
|
|
624
815
|
const keyLine = trimmed.slice(0, -1);
|
|
625
816
|
const names = /* @__PURE__ */ new Set();
|
|
817
|
+
let unreadable = false;
|
|
626
818
|
for (const spec of splitDescriptors(keyLine)) {
|
|
627
|
-
const
|
|
628
|
-
if (name !==
|
|
629
|
-
|
|
630
|
-
}
|
|
819
|
+
const classified = classifyDescriptor(spec);
|
|
820
|
+
if (classified.name !== void 0) names.add(classified.name);
|
|
821
|
+
else if (!classified.excluded) unreadable = true;
|
|
631
822
|
}
|
|
632
|
-
current = { names, version: "" };
|
|
823
|
+
current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
|
|
633
824
|
continue;
|
|
634
825
|
}
|
|
635
826
|
if (current === null) {
|
|
@@ -684,20 +875,37 @@ function splitDescriptors(keyLine) {
|
|
|
684
875
|
}
|
|
685
876
|
return parts;
|
|
686
877
|
}
|
|
687
|
-
|
|
878
|
+
var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
|
|
879
|
+
function classifyDescriptor(rawSpec) {
|
|
688
880
|
let s = rawSpec.trim();
|
|
689
881
|
if (s.length === 0) {
|
|
690
|
-
return
|
|
882
|
+
return { excluded: true };
|
|
691
883
|
}
|
|
692
884
|
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
693
885
|
s = s.slice(1, -1);
|
|
694
886
|
}
|
|
695
|
-
|
|
887
|
+
if (NON_PACKAGE_KEYS.has(s)) {
|
|
888
|
+
return { excluded: true };
|
|
889
|
+
}
|
|
890
|
+
const atIdx = s.indexOf("@", 1);
|
|
696
891
|
if (atIdx <= 0) {
|
|
697
|
-
return
|
|
892
|
+
return {};
|
|
698
893
|
}
|
|
699
894
|
const name = s.slice(0, atIdx);
|
|
700
|
-
|
|
895
|
+
const range = s.slice(atIdx + 1);
|
|
896
|
+
if (name.length === 0) {
|
|
897
|
+
return {};
|
|
898
|
+
}
|
|
899
|
+
if (NON_REGISTRY_PROTOCOLS.test(range)) {
|
|
900
|
+
return { excluded: true };
|
|
901
|
+
}
|
|
902
|
+
const alias = /^npm:(.+)$/i.exec(range);
|
|
903
|
+
if (alias !== null) {
|
|
904
|
+
const target = alias[1] ?? "";
|
|
905
|
+
const targetAt = target.indexOf("@", 1);
|
|
906
|
+
return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
|
|
907
|
+
}
|
|
908
|
+
return { name };
|
|
701
909
|
}
|
|
702
910
|
function parseVersionField(content) {
|
|
703
911
|
if (!content.startsWith("version")) {
|
|
@@ -722,7 +930,7 @@ async function readDirectDepNames(cwd) {
|
|
|
722
930
|
const names = /* @__PURE__ */ new Set();
|
|
723
931
|
let raw;
|
|
724
932
|
try {
|
|
725
|
-
raw = await (0,
|
|
933
|
+
raw = await (0, import_promises6.readFile)(import_node_path4.default.join(cwd, "package.json"), "utf8");
|
|
726
934
|
} catch {
|
|
727
935
|
return names;
|
|
728
936
|
}
|
|
@@ -751,7 +959,10 @@ async function readDirectDepNames(cwd) {
|
|
|
751
959
|
// src/parsers/index.ts
|
|
752
960
|
var LOCKFILE_CANDIDATES = [
|
|
753
961
|
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
754
|
-
|
|
962
|
+
// Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
|
|
963
|
+
// top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
|
|
964
|
+
// for new workspaces.
|
|
965
|
+
{ filename: "bun.lock", strategy: "bun-lockfile" },
|
|
755
966
|
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
756
967
|
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
757
968
|
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
@@ -786,34 +997,58 @@ async function scanLockfile(cwd) {
|
|
|
786
997
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
787
998
|
const warnings = [];
|
|
788
999
|
let firstParsed = null;
|
|
789
|
-
let
|
|
1000
|
+
let walked = null;
|
|
1001
|
+
let walkWasCandidate = false;
|
|
1002
|
+
const parsedSources = [];
|
|
790
1003
|
for (const candidate of candidates) {
|
|
791
1004
|
let packages;
|
|
1005
|
+
const report = newReport();
|
|
792
1006
|
try {
|
|
793
|
-
packages = await runStrategy(candidate, cwd);
|
|
1007
|
+
packages = await runStrategy(candidate, cwd, report);
|
|
794
1008
|
} catch {
|
|
795
1009
|
continue;
|
|
796
1010
|
}
|
|
797
|
-
|
|
1011
|
+
const unreadable = unreadableWarning(candidate.filename, report);
|
|
1012
|
+
if (unreadable !== null) warnings.push(unreadable);
|
|
1013
|
+
if (candidate.strategy === "node-modules-walk") {
|
|
1014
|
+
walked = packages;
|
|
1015
|
+
walkWasCandidate = true;
|
|
1016
|
+
}
|
|
798
1017
|
firstParsed ??= { packages, filename: candidate.filename };
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1018
|
+
parsedSources.push({ packages, filename: candidate.filename });
|
|
1019
|
+
}
|
|
1020
|
+
for (const source of parsedSources) {
|
|
1021
|
+
const missing = missingDependencies(declared, source.packages);
|
|
1022
|
+
if (missing.length > 0) {
|
|
1023
|
+
warnings.push(staleWarning(source.filename, missing));
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
1027
|
+
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
1028
|
+
);
|
|
1029
|
+
if (conflicts.length === 0) {
|
|
1030
|
+
return manifestWith(source.packages, warnings);
|
|
802
1031
|
}
|
|
803
|
-
warnings.push(
|
|
1032
|
+
warnings.push(conflictWarning(source.filename, conflicts));
|
|
1033
|
+
break;
|
|
804
1034
|
}
|
|
805
|
-
if (
|
|
1035
|
+
if (walked === null) {
|
|
806
1036
|
try {
|
|
807
|
-
|
|
808
|
-
const missing = missingDependencies(declared, packages);
|
|
809
|
-
if (missing.length > 0) {
|
|
810
|
-
warnings.push(staleWarning("node_modules/", missing));
|
|
811
|
-
}
|
|
812
|
-
warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
|
|
813
|
-
return manifestWith(packages, warnings);
|
|
1037
|
+
walked = await walkNodeModules(cwd);
|
|
814
1038
|
} catch {
|
|
1039
|
+
walked = null;
|
|
815
1040
|
}
|
|
816
1041
|
}
|
|
1042
|
+
if (walked !== null) {
|
|
1043
|
+
const missing = missingDependencies(declared, walked);
|
|
1044
|
+
if (missing.length > 0) {
|
|
1045
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
1046
|
+
}
|
|
1047
|
+
warnings.push(
|
|
1048
|
+
walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
|
|
1049
|
+
);
|
|
1050
|
+
return manifestWith(walked, warnings);
|
|
1051
|
+
}
|
|
817
1052
|
if (firstParsed === null) {
|
|
818
1053
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
819
1054
|
throw new PatchstackError(
|
|
@@ -829,6 +1064,15 @@ async function scanLockfile(cwd) {
|
|
|
829
1064
|
function manifestWith(packages, warnings) {
|
|
830
1065
|
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
831
1066
|
}
|
|
1067
|
+
function otherCompleteSources(all, current, declared) {
|
|
1068
|
+
return all.filter(
|
|
1069
|
+
(source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
function conflictWarning(filename, conflicts) {
|
|
1073
|
+
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
1074
|
+
return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
|
|
1075
|
+
}
|
|
832
1076
|
function staleWarning(source, missing) {
|
|
833
1077
|
const sample = missing.slice(0, 3).join(", ");
|
|
834
1078
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
@@ -843,21 +1087,25 @@ async function presentLockfiles(cwd) {
|
|
|
843
1087
|
);
|
|
844
1088
|
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
845
1089
|
}
|
|
846
|
-
async function runStrategy(detected, cwd) {
|
|
1090
|
+
async function runStrategy(detected, cwd, report) {
|
|
847
1091
|
switch (detected.strategy) {
|
|
848
1092
|
case "npm-lockfile":
|
|
849
1093
|
return parseNpmLockfile(detected.filePath);
|
|
1094
|
+
case "bun-lockfile":
|
|
1095
|
+
return parseBunLockfile(detected.filePath, report);
|
|
1096
|
+
// The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
|
|
1097
|
+
// many it dropped, because a manifest short a package reads exactly like a project without it.
|
|
850
1098
|
case "pnpm-lockfile":
|
|
851
|
-
return parsePnpmLockfile(detected.filePath);
|
|
1099
|
+
return parsePnpmLockfile(detected.filePath, report);
|
|
852
1100
|
case "yarn-lockfile":
|
|
853
|
-
return parseYarnLockfile(detected.filePath);
|
|
1101
|
+
return parseYarnLockfile(detected.filePath, report);
|
|
854
1102
|
case "node-modules-walk":
|
|
855
1103
|
return walkNodeModules(cwd);
|
|
856
1104
|
}
|
|
857
1105
|
}
|
|
858
1106
|
async function exists(filePath) {
|
|
859
1107
|
try {
|
|
860
|
-
await (0,
|
|
1108
|
+
await (0, import_promises7.access)(filePath);
|
|
861
1109
|
return true;
|
|
862
1110
|
} catch {
|
|
863
1111
|
return false;
|
|
@@ -1143,12 +1391,14 @@ function isTimeoutError(cause) {
|
|
|
1143
1391
|
}
|
|
1144
1392
|
|
|
1145
1393
|
// src/config.ts
|
|
1146
|
-
var
|
|
1394
|
+
var import_promises8 = require("fs/promises");
|
|
1147
1395
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
1148
1396
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
1397
|
+
var SECRET_FILENAME = ".patchstackrc.local.json";
|
|
1149
1398
|
var DEFAULT_ENVIRONMENT = "production";
|
|
1150
1399
|
async function resolveConfig(options) {
|
|
1151
1400
|
const fromFile = await readConfigFile(options.cwd);
|
|
1401
|
+
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1152
1402
|
const fromEnv = readEnv();
|
|
1153
1403
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1154
1404
|
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
@@ -1173,8 +1423,8 @@ async function resolveConfig(options) {
|
|
|
1173
1423
|
"CONFIG_MISSING"
|
|
1174
1424
|
);
|
|
1175
1425
|
}
|
|
1176
|
-
const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
|
|
1177
|
-
const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1426
|
+
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1427
|
+
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1178
1428
|
return {
|
|
1179
1429
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1180
1430
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1188,26 +1438,91 @@ async function resolveConfig(options) {
|
|
|
1188
1438
|
async function writeConfigFile(cwd, config) {
|
|
1189
1439
|
const target = import_node_path6.default.join(cwd, CONFIG_FILENAME);
|
|
1190
1440
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
1191
|
-
await (0,
|
|
1441
|
+
await (0, import_promises8.writeFile)(target, content, "utf8");
|
|
1192
1442
|
return target;
|
|
1193
1443
|
}
|
|
1444
|
+
async function writeSecretFile(cwd, secrets) {
|
|
1445
|
+
const target = import_node_path6.default.join(cwd, SECRET_FILENAME);
|
|
1446
|
+
await (0, import_promises8.writeFile)(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1447
|
+
try {
|
|
1448
|
+
await (0, import_promises8.chmod)(target, 384);
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1451
|
+
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1452
|
+
return { path: target, ...ignore };
|
|
1453
|
+
}
|
|
1454
|
+
async function ensureIgnored(cwd, entry) {
|
|
1455
|
+
const target = import_node_path6.default.join(cwd, ".gitignore");
|
|
1456
|
+
let existing = "";
|
|
1457
|
+
try {
|
|
1458
|
+
existing = await (0, import_promises8.readFile)(target, "utf8");
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
const code = err.code;
|
|
1461
|
+
if (code !== "ENOENT") {
|
|
1462
|
+
return { ignored: false, reason: `.gitignore could not be read (${code ?? "unknown error"})` };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (ignoresEntry(existing, entry)) return { ignored: true };
|
|
1466
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1467
|
+
const block = `${separator}
|
|
1468
|
+
# Patchstack credential \u2014 never commit this
|
|
1469
|
+
${entry}
|
|
1470
|
+
`;
|
|
1471
|
+
try {
|
|
1472
|
+
await (0, import_promises8.writeFile)(target, existing + block, "utf8");
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
const code = err.code;
|
|
1475
|
+
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
1476
|
+
}
|
|
1477
|
+
try {
|
|
1478
|
+
const written = await (0, import_promises8.readFile)(target, "utf8");
|
|
1479
|
+
if (ignoresEntry(written, entry)) return { ignored: true };
|
|
1480
|
+
return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
const code = err.code;
|
|
1483
|
+
return { ignored: false, reason: `.gitignore could not be read back (${code ?? "unknown error"})` };
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function ignoresEntry(contents, entry) {
|
|
1487
|
+
let ignored = false;
|
|
1488
|
+
for (const raw of contents.split("\n")) {
|
|
1489
|
+
const line = raw.trim();
|
|
1490
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1491
|
+
const negated = line.startsWith("!");
|
|
1492
|
+
const pattern = negated ? line.slice(1).trim() : line;
|
|
1493
|
+
if (pattern === entry || pattern === `/${entry}`) ignored = !negated;
|
|
1494
|
+
}
|
|
1495
|
+
return ignored;
|
|
1496
|
+
}
|
|
1194
1497
|
async function persistSiteUuid(cwd, siteUuid) {
|
|
1195
1498
|
const existing = await readConfigFile(cwd);
|
|
1196
1499
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
1197
1500
|
}
|
|
1198
1501
|
async function persistApiKey(cwd, apiKey) {
|
|
1199
|
-
const { pulseAuth:
|
|
1200
|
-
|
|
1502
|
+
const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
|
|
1503
|
+
const existingSecrets = await readSecretFile(cwd);
|
|
1504
|
+
const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
|
|
1505
|
+
if (_movedKey !== void 0 || _movedPulse !== void 0) {
|
|
1506
|
+
await writeConfigFile(cwd, publicConfig);
|
|
1507
|
+
}
|
|
1508
|
+
return target;
|
|
1201
1509
|
}
|
|
1202
1510
|
async function persistPulseAuth(cwd, pulseAuth) {
|
|
1203
|
-
const existing = await
|
|
1204
|
-
return
|
|
1511
|
+
const existing = await readSecretFile(cwd);
|
|
1512
|
+
return writeSecretFile(cwd, { ...existing, pulseAuth });
|
|
1513
|
+
}
|
|
1514
|
+
async function readSecretFile(cwd) {
|
|
1515
|
+
const file = await readJsonFile(cwd, SECRET_FILENAME);
|
|
1516
|
+
return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
|
|
1205
1517
|
}
|
|
1206
1518
|
async function readConfigFile(cwd) {
|
|
1207
|
-
|
|
1519
|
+
return readJsonFile(cwd, CONFIG_FILENAME);
|
|
1520
|
+
}
|
|
1521
|
+
async function readJsonFile(cwd, filename) {
|
|
1522
|
+
const target = import_node_path6.default.join(cwd, filename);
|
|
1208
1523
|
let raw;
|
|
1209
1524
|
try {
|
|
1210
|
-
raw = await (0,
|
|
1525
|
+
raw = await (0, import_promises8.readFile)(target, "utf8");
|
|
1211
1526
|
} catch (err) {
|
|
1212
1527
|
if (err.code === "ENOENT") {
|
|
1213
1528
|
return {};
|