@patchstack/connect 0.2.6 → 0.2.7
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/cli.js +147 -142
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +38 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +38 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
5
|
+
|
|
3
6
|
// src/parsers/index.ts
|
|
4
7
|
import { access } from "fs/promises";
|
|
5
8
|
import path4 from "path";
|
|
@@ -653,54 +656,32 @@ async function readDirectDepNames(cwd) {
|
|
|
653
656
|
}
|
|
654
657
|
|
|
655
658
|
// src/parsers/index.ts
|
|
659
|
+
var LOCKFILE_CANDIDATES = [
|
|
660
|
+
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
661
|
+
{ filename: "bun.lock", strategy: "node-modules-walk" },
|
|
662
|
+
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
663
|
+
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
664
|
+
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
665
|
+
];
|
|
656
666
|
async function detectLockfile(cwd) {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
const bunLock = path4.join(cwd, "bun.lock");
|
|
667
|
-
if (await exists(bunLock)) {
|
|
668
|
-
return {
|
|
669
|
-
ecosystem: "npm",
|
|
670
|
-
filePath: bunLock,
|
|
671
|
-
filename: "bun.lock",
|
|
672
|
-
strategy: "node-modules-walk"
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
const bunLockB = path4.join(cwd, "bun.lockb");
|
|
676
|
-
if (await exists(bunLockB)) {
|
|
677
|
-
return {
|
|
678
|
-
ecosystem: "npm",
|
|
679
|
-
filePath: bunLockB,
|
|
680
|
-
filename: "bun.lockb",
|
|
681
|
-
strategy: "node-modules-walk"
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
const pnpmLock = path4.join(cwd, "pnpm-lock.yaml");
|
|
685
|
-
if (await exists(pnpmLock)) {
|
|
686
|
-
return {
|
|
687
|
-
ecosystem: "npm",
|
|
688
|
-
filePath: pnpmLock,
|
|
689
|
-
filename: "pnpm-lock.yaml",
|
|
690
|
-
strategy: "pnpm-lockfile"
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
const yarnLock = path4.join(cwd, "yarn.lock");
|
|
694
|
-
if (await exists(yarnLock)) {
|
|
667
|
+
const probed = await Promise.all(
|
|
668
|
+
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
669
|
+
const filePath = path4.join(cwd, candidate.filename);
|
|
670
|
+
return { ...candidate, filePath, present: await exists(filePath) };
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
const match = probed.find((candidate) => candidate.present);
|
|
674
|
+
if (match) {
|
|
695
675
|
return {
|
|
696
676
|
ecosystem: "npm",
|
|
697
|
-
filePath:
|
|
698
|
-
filename:
|
|
699
|
-
strategy:
|
|
677
|
+
filePath: match.filePath,
|
|
678
|
+
filename: match.filename,
|
|
679
|
+
strategy: match.strategy
|
|
700
680
|
};
|
|
701
681
|
}
|
|
682
|
+
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
702
683
|
throw new PatchstackError(
|
|
703
|
-
`No lockfile found in ${cwd}. Expected one of:
|
|
684
|
+
`No lockfile found in ${cwd}. Expected one of: ${expected}.`,
|
|
704
685
|
"LOCKFILE_NOT_FOUND"
|
|
705
686
|
);
|
|
706
687
|
}
|
|
@@ -832,86 +813,19 @@ function compareSegments(a, b) {
|
|
|
832
813
|
return 0;
|
|
833
814
|
}
|
|
834
815
|
|
|
835
|
-
// src/
|
|
836
|
-
import {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
async function pathExists(target) {
|
|
842
|
-
try {
|
|
843
|
-
await fs.access(target);
|
|
844
|
-
return true;
|
|
845
|
-
} catch {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
async function resolveBuildDir(cwd, override) {
|
|
850
|
-
if (override !== void 0 && override.length > 0) {
|
|
851
|
-
const dir = path5.resolve(cwd, override);
|
|
852
|
-
return await pathExists(dir) ? dir : null;
|
|
853
|
-
}
|
|
854
|
-
for (const candidate of DEFAULT_DIRS) {
|
|
855
|
-
const dir = path5.resolve(cwd, candidate);
|
|
856
|
-
if (await pathExists(dir)) {
|
|
857
|
-
return dir;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
async function findHtmlFiles(dir, depth = 3) {
|
|
863
|
-
const found = [];
|
|
864
|
-
let entries;
|
|
865
|
-
try {
|
|
866
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
867
|
-
} catch {
|
|
868
|
-
return found;
|
|
869
|
-
}
|
|
870
|
-
for (const entry of entries) {
|
|
871
|
-
const full = path5.join(dir, entry.name);
|
|
872
|
-
if (entry.isDirectory()) {
|
|
873
|
-
if (depth > 0) {
|
|
874
|
-
found.push(...await findHtmlFiles(full, depth - 1));
|
|
875
|
-
}
|
|
876
|
-
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
877
|
-
found.push(full);
|
|
816
|
+
// src/checksum.ts
|
|
817
|
+
import { createHash } from "crypto";
|
|
818
|
+
function computeManifestChecksum(packages) {
|
|
819
|
+
const canonical = packages.map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((a, b) => {
|
|
820
|
+
if (a.name !== b.name) {
|
|
821
|
+
return a.name < b.name ? -1 : 1;
|
|
878
822
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
function injectFlag(html) {
|
|
883
|
-
if (html.includes(FLAG_MARKER)) {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
const headOpen = /<head[^>]*>/i;
|
|
887
|
-
if (headOpen.test(html)) {
|
|
888
|
-
return html.replace(headOpen, (match) => `${match}${SNIPPET}`);
|
|
889
|
-
}
|
|
890
|
-
const bodyOpen = /<body[^>]*>/i;
|
|
891
|
-
if (bodyOpen.test(html)) {
|
|
892
|
-
return html.replace(bodyOpen, (match) => `${match}${SNIPPET}`);
|
|
893
|
-
}
|
|
894
|
-
return `${SNIPPET}${html}`;
|
|
895
|
-
}
|
|
896
|
-
async function markProductionBuild(cwd, override) {
|
|
897
|
-
const dir = await resolveBuildDir(cwd, override);
|
|
898
|
-
if (dir === null) {
|
|
899
|
-
return { dir: null, patched: [], skipped: [] };
|
|
900
|
-
}
|
|
901
|
-
const files = await findHtmlFiles(dir);
|
|
902
|
-
const patched = [];
|
|
903
|
-
const skipped = [];
|
|
904
|
-
for (const file of files) {
|
|
905
|
-
const html = await fs.readFile(file, "utf8");
|
|
906
|
-
const next = injectFlag(html);
|
|
907
|
-
if (next === null) {
|
|
908
|
-
skipped.push(file);
|
|
909
|
-
continue;
|
|
823
|
+
if (a.version !== b.version) {
|
|
824
|
+
return a.version < b.version ? -1 : 1;
|
|
910
825
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
return { dir, patched, skipped };
|
|
826
|
+
return 0;
|
|
827
|
+
});
|
|
828
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 12);
|
|
915
829
|
}
|
|
916
830
|
|
|
917
831
|
// src/client.ts
|
|
@@ -937,7 +851,7 @@ async function postManifest(config, payload) {
|
|
|
937
851
|
Accept: "application/json",
|
|
938
852
|
"User-Agent": "@patchstack/connect"
|
|
939
853
|
},
|
|
940
|
-
body: JSON.stringify(payload),
|
|
854
|
+
body: JSON.stringify({ ...payload, environment: config.environment }),
|
|
941
855
|
signal: AbortSignal.timeout(timeoutMs)
|
|
942
856
|
});
|
|
943
857
|
} catch (cause) {
|
|
@@ -993,14 +907,23 @@ function isTimeoutError(cause) {
|
|
|
993
907
|
|
|
994
908
|
// src/config.ts
|
|
995
909
|
import { readFile as readFile5, writeFile } from "fs/promises";
|
|
996
|
-
import
|
|
910
|
+
import path5 from "path";
|
|
997
911
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
912
|
+
var DEFAULT_ENVIRONMENT = "production";
|
|
998
913
|
async function resolveConfig(options) {
|
|
999
914
|
const fromFile = await readConfigFile(options.cwd);
|
|
1000
915
|
const fromEnv = readEnv();
|
|
1001
916
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1002
917
|
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
1003
918
|
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
919
|
+
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
920
|
+
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
921
|
+
throw new PatchstackError(
|
|
922
|
+
`Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
|
|
923
|
+
"CONFIG_INVALID"
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
|
|
1004
927
|
if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
|
|
1005
928
|
throw new PatchstackError(
|
|
1006
929
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
@@ -1016,11 +939,12 @@ async function resolveConfig(options) {
|
|
|
1016
939
|
return {
|
|
1017
940
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1018
941
|
endpoint,
|
|
1019
|
-
timeoutMs
|
|
942
|
+
timeoutMs,
|
|
943
|
+
environment
|
|
1020
944
|
};
|
|
1021
945
|
}
|
|
1022
946
|
async function writeConfigFile(cwd, config) {
|
|
1023
|
-
const target =
|
|
947
|
+
const target = path5.join(cwd, CONFIG_FILENAME);
|
|
1024
948
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
1025
949
|
await writeFile(target, content, "utf8");
|
|
1026
950
|
return target;
|
|
@@ -1030,7 +954,7 @@ async function persistSiteUuid(cwd, siteUuid) {
|
|
|
1030
954
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
1031
955
|
}
|
|
1032
956
|
async function readConfigFile(cwd) {
|
|
1033
|
-
const target =
|
|
957
|
+
const target = path5.join(cwd, CONFIG_FILENAME);
|
|
1034
958
|
let raw;
|
|
1035
959
|
try {
|
|
1036
960
|
raw = await readFile5(target, "utf8");
|
|
@@ -1067,15 +991,74 @@ function readEnv() {
|
|
|
1067
991
|
}
|
|
1068
992
|
timeoutMs = parsed;
|
|
1069
993
|
}
|
|
994
|
+
const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
|
|
1070
995
|
return {
|
|
1071
996
|
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
|
|
1072
997
|
endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
|
|
1073
|
-
timeoutMs
|
|
998
|
+
timeoutMs,
|
|
999
|
+
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
1074
1000
|
};
|
|
1075
1001
|
}
|
|
1076
1002
|
function isUuid(value) {
|
|
1077
1003
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1078
1004
|
}
|
|
1005
|
+
function isEnvironment(value) {
|
|
1006
|
+
return value === "production" || value === "sandbox";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/mark-build.ts
|
|
1010
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
1011
|
+
import path6 from "path";
|
|
1012
|
+
var MARKER_ATTR = "data-patchstack-build";
|
|
1013
|
+
var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
|
|
1014
|
+
function resolveBuildDir(cwd, override) {
|
|
1015
|
+
if (override !== void 0 && override !== "") {
|
|
1016
|
+
const abs = path6.resolve(cwd, override);
|
|
1017
|
+
return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
|
|
1018
|
+
}
|
|
1019
|
+
for (const candidate of BUILD_DIR_CANDIDATES) {
|
|
1020
|
+
const abs = path6.resolve(cwd, candidate);
|
|
1021
|
+
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
1022
|
+
return abs;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
function findHtmlFiles(dir) {
|
|
1028
|
+
const out = [];
|
|
1029
|
+
const walk2 = (current) => {
|
|
1030
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1031
|
+
const full = path6.join(current, entry.name);
|
|
1032
|
+
if (entry.isDirectory()) {
|
|
1033
|
+
walk2(full);
|
|
1034
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
1035
|
+
out.push(full);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
walk2(dir);
|
|
1040
|
+
return out;
|
|
1041
|
+
}
|
|
1042
|
+
function buildInjectionSnippet(checksum) {
|
|
1043
|
+
const statements = ["window.__PATCHSTACK_PROD__=true;"];
|
|
1044
|
+
if (checksum !== null && checksum !== "") {
|
|
1045
|
+
statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
|
|
1046
|
+
}
|
|
1047
|
+
return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
|
|
1048
|
+
}
|
|
1049
|
+
function injectMarker(html, snippet) {
|
|
1050
|
+
const stripped = html.replace(
|
|
1051
|
+
new RegExp(`\\s*<script ${MARKER_ATTR}[^>]*>[\\s\\S]*?</script>`, "gi"),
|
|
1052
|
+
""
|
|
1053
|
+
);
|
|
1054
|
+
if (/<\/head>/i.test(stripped)) {
|
|
1055
|
+
return stripped.replace(/<\/head>/i, `${snippet}</head>`);
|
|
1056
|
+
}
|
|
1057
|
+
if (/<\/body>/i.test(stripped)) {
|
|
1058
|
+
return stripped.replace(/<\/body>/i, `${snippet}</body>`);
|
|
1059
|
+
}
|
|
1060
|
+
return stripped + snippet;
|
|
1061
|
+
}
|
|
1079
1062
|
|
|
1080
1063
|
// src/cli.ts
|
|
1081
1064
|
var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
|
|
@@ -1087,10 +1070,8 @@ Usage:
|
|
|
1087
1070
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
1088
1071
|
with an existing site UUID
|
|
1089
1072
|
patchstack-connect status [options] Show current configuration
|
|
1090
|
-
patchstack-connect mark-build [
|
|
1091
|
-
|
|
1092
|
-
step). Tells the widget it's live so
|
|
1093
|
-
it hides the claim flow.
|
|
1073
|
+
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
1074
|
+
build fingerprint (run as a postbuild step)
|
|
1094
1075
|
patchstack-connect help Print this message
|
|
1095
1076
|
|
|
1096
1077
|
Options (for scan and status):
|
|
@@ -1099,13 +1080,14 @@ Options (for scan and status):
|
|
|
1099
1080
|
--dry-run (scan only) Show the payload without posting
|
|
1100
1081
|
|
|
1101
1082
|
Options (for mark-build):
|
|
1102
|
-
--dir <path> Build output
|
|
1103
|
-
out
|
|
1083
|
+
--dir <path> Build output directory (default: auto-detect
|
|
1084
|
+
dist/ build/ out/ .output/public)
|
|
1104
1085
|
|
|
1105
1086
|
Environment:
|
|
1106
1087
|
PATCHSTACK_SITE_UUID Site UUID
|
|
1107
1088
|
PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
|
|
1108
1089
|
PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
|
|
1090
|
+
PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
|
|
1109
1091
|
|
|
1110
1092
|
Precedence: CLI flag > environment variable > .patchstackrc.json.
|
|
1111
1093
|
|
|
@@ -1179,6 +1161,7 @@ async function runScan(args) {
|
|
|
1179
1161
|
console.log(
|
|
1180
1162
|
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
|
|
1181
1163
|
);
|
|
1164
|
+
console.log(`Reporting under the ${config.environment} environment.`);
|
|
1182
1165
|
if (stats.duplicateNames.length > 0) {
|
|
1183
1166
|
console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
|
|
1184
1167
|
if (stats.duplicateNames.length <= 10) {
|
|
@@ -1229,29 +1212,51 @@ async function runStatus(args) {
|
|
|
1229
1212
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
1230
1213
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
1231
1214
|
});
|
|
1232
|
-
console.log(`Site UUID:
|
|
1233
|
-
console.log(`Endpoint:
|
|
1234
|
-
console.log(`Timeout:
|
|
1215
|
+
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
1216
|
+
console.log(`Endpoint: ${config.endpoint}`);
|
|
1217
|
+
console.log(`Timeout: ${config.timeoutMs}ms`);
|
|
1218
|
+
console.log(`Environment: ${config.environment}`);
|
|
1235
1219
|
if (config.siteUuid !== null) {
|
|
1236
1220
|
console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
1237
1221
|
}
|
|
1238
1222
|
return 0;
|
|
1239
1223
|
}
|
|
1240
1224
|
async function runMarkBuild(args) {
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1225
|
+
const cwd = process.cwd();
|
|
1226
|
+
let checksum = null;
|
|
1227
|
+
try {
|
|
1228
|
+
const manifest = await scanLockfile(cwd);
|
|
1229
|
+
const { payload } = buildWirePayload(manifest);
|
|
1230
|
+
checksum = computeManifestChecksum(payload.packages);
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
console.warn(
|
|
1233
|
+
`mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"));
|
|
1237
|
+
if (dir === null) {
|
|
1243
1238
|
console.warn(
|
|
1244
|
-
"
|
|
1239
|
+
"mark-build: no build output directory found (looked for dist/, build/, out/, .output/public). Pass --dir <path> if it is elsewhere. Nothing to mark."
|
|
1245
1240
|
);
|
|
1246
1241
|
return 0;
|
|
1247
1242
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1243
|
+
const files = findHtmlFiles(dir);
|
|
1244
|
+
if (files.length === 0) {
|
|
1245
|
+
console.warn(`mark-build: no HTML files found under ${dir}. Nothing to mark.`);
|
|
1250
1246
|
return 0;
|
|
1251
1247
|
}
|
|
1252
|
-
const
|
|
1248
|
+
const snippet = buildInjectionSnippet(checksum);
|
|
1249
|
+
let marked = 0;
|
|
1250
|
+
for (const file of files) {
|
|
1251
|
+
const before = readFileSync(file, "utf8");
|
|
1252
|
+
const after = injectMarker(before, snippet);
|
|
1253
|
+
if (after !== before) {
|
|
1254
|
+
writeFileSync(file, after);
|
|
1255
|
+
marked += 1;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1253
1258
|
console.log(
|
|
1254
|
-
`
|
|
1259
|
+
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
|
|
1255
1260
|
);
|
|
1256
1261
|
return 0;
|
|
1257
1262
|
}
|