@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 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 npmLock = path4.join(cwd, "package-lock.json");
658
- if (await exists(npmLock)) {
659
- return {
660
- ecosystem: "npm",
661
- filePath: npmLock,
662
- filename: "package-lock.json",
663
- strategy: "npm-lockfile"
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: yarnLock,
698
- filename: "yarn.lock",
699
- strategy: "yarn-lockfile"
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: package-lock.json, bun.lock, bun.lockb, yarn.lock, pnpm-lock.yaml.`,
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/buildFlag.ts
836
- import { promises as fs } from "fs";
837
- import path5 from "path";
838
- var FLAG_MARKER = "__PATCHSTACK_PROD__";
839
- var SNIPPET = "<script>window.__PATCHSTACK_PROD__=true;/*patchstack:production*/</script>";
840
- var DEFAULT_DIRS = ["dist", "build", "out", ".output/public"];
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
- return found;
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
- await fs.writeFile(file, next, "utf8");
912
- patched.push(file);
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 path6 from "path";
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 = path6.join(cwd, CONFIG_FILENAME);
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 = path6.join(cwd, CONFIG_FILENAME);
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 [--dir <path>] Inject the production flag into
1091
- the built HTML (run as a postbuild
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 dir (default: auto-detect dist/, build/,
1103
- out/, .output/public)
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: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
1233
- console.log(`Endpoint: ${config.endpoint}`);
1234
- console.log(`Timeout: ${config.timeoutMs}ms`);
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 result = await markProductionBuild(process.cwd(), getStringFlag(args.flags, "dir"));
1242
- if (result.dir === null) {
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
- "patchstack: no build output found (looked for dist/, build/, out/, .output/public). Pass --dir <path> if your build outputs elsewhere. Skipping production flag."
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
- if (result.patched.length === 0 && result.skipped.length === 0) {
1249
- console.warn(`patchstack: no HTML files found under ${result.dir}; skipping production flag.`);
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 already = result.skipped.length > 0 ? ` (${result.skipped.length} already marked)` : "";
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
- `patchstack: marked ${result.patched.length} HTML file(s) as a production build in ${result.dir}${already}.`
1259
+ `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
1255
1260
  );
1256
1261
  return 0;
1257
1262
  }