@liiift-studio/deploy-vercel-from-sanity 1.3.0 → 1.3.2

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 CHANGED
@@ -366,6 +366,21 @@ See [CHANGELOG.md](./CHANGELOG.md).
366
366
 
367
367
  ---
368
368
 
369
+ ## Tests and CI
370
+
371
+ ```bash
372
+ npm test # unit tests
373
+ npm run lint # includes the compat-seam import rule
374
+ npm run typecheck
375
+ ```
376
+
377
+ Unit tests cover the proxy's authorization boundary (fail-closed status key,
378
+ per-project deployment scoping, role gating, SSRF guard on hook URLs) and the URL
379
+ validators that guard every anchor the plugin renders. `prepublishOnly` runs
380
+ typecheck and tests before building, so a release cannot ship past them.
381
+
382
+ ---
383
+
369
384
  ## Contributing
370
385
 
371
386
  Issues and pull requests welcome at [github.com/Liiift-Studio/Deploy-Vercel-from-Sanity](https://github.com/Liiift-Studio/Deploy-Vercel-from-Sanity).
package/dist/index.js CHANGED
@@ -816,8 +816,14 @@ function DeployHistory({ target, token, onClose }) {
816
816
  const [loading, setLoading] = (0, import_react7.useState)(true);
817
817
  const [error, setError] = (0, import_react7.useState)(null);
818
818
  const { projectId, hookId } = parseHookUrl(target.url);
819
- const transport = pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token };
820
- const targetRef = { projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId };
819
+ const transport = (0, import_react7.useMemo)(
820
+ () => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
821
+ [pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
822
+ );
823
+ const targetRef = (0, import_react7.useMemo)(
824
+ () => ({ projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId }),
825
+ [projectId, hookId, target.proxyKey, target.teamId]
826
+ );
821
827
  const load = (0, import_react7.useCallback)(async () => {
822
828
  setLoading(true);
823
829
  setError(null);
@@ -829,7 +835,7 @@ function DeployHistory({ target, token, onClose }) {
829
835
  } finally {
830
836
  setLoading(false);
831
837
  }
832
- }, [projectId, hookId, token, target.teamId]);
838
+ }, [transport, targetRef]);
833
839
  (0, import_react7.useEffect)(() => {
834
840
  load();
835
841
  }, [load]);
@@ -910,8 +916,14 @@ function DeployItem({ target, token, onDelete, onEdit }) {
910
916
  const toast = useToast();
911
917
  const pluginConfig = usePluginConfig();
912
918
  const client = (0, import_sanity.useClient)({ apiVersion: "2025-01-01" });
913
- const transport = pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token };
914
- const targetRef = { projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId };
919
+ const transport = (0, import_react8.useMemo)(
920
+ () => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
921
+ [pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
922
+ );
923
+ const targetRef = (0, import_react8.useMemo)(
924
+ () => ({ projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId }),
925
+ [projectId, hookId, target.proxyKey, target.teamId]
926
+ );
915
927
  const [deployments, setDeployments] = (0, import_react8.useState)([]);
916
928
  const [loadingInitial, setLoadingInitial] = (0, import_react8.useState)(true);
917
929
  const [pendingSince, setPendingSince] = (0, import_react8.useState)(null);
@@ -939,7 +951,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
939
951
  const isPending = pendingSince !== null;
940
952
  const isActive = isPending || isActiveState(latest?.state);
941
953
  const fetchDeployments2 = (0, import_react8.useCallback)(async () => {
942
- const ready = transport.mode === "proxy" ? Boolean(target.proxyKey && pluginConfig.proxyUrl) : Boolean(projectId && hookId && token);
954
+ const ready = transport.mode === "proxy" ? Boolean(targetRef.proxyKey && transport.proxyUrl) : Boolean(targetRef.projectId && targetRef.hookId && transport.token);
943
955
  if (!ready) return;
944
956
  const seq = ++requestSeqRef.current;
945
957
  try {
@@ -952,7 +964,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
952
964
  setPollError(err instanceof Error ? err.message : "Could not reach the Vercel API");
953
965
  console.error("Deploy-vercel-from-sanity: fetch error", err);
954
966
  }
955
- }, [projectId, hookId, token, target.teamId, target.proxyKey, transport.mode, pluginConfig.proxyUrl]);
967
+ }, [transport, targetRef]);
956
968
  (0, import_react8.useEffect)(() => {
957
969
  fetchDeployments2().finally(() => setLoadingInitial(false));
958
970
  }, [fetchDeployments2]);
@@ -1043,7 +1055,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1043
1055
  } finally {
1044
1056
  setCanceling(false);
1045
1057
  }
1046
- }, [latest?.uid, token, target.teamId, fetchDeployments2]);
1058
+ }, [latest?.uid, transport, targetRef, fetchDeployments2]);
1047
1059
  const copyUrl = (0, import_react8.useCallback)(() => {
1048
1060
  if (!latest?.url) return;
1049
1061
  const fullUrl = deploymentHref(latest.url) ?? "";
@@ -1067,7 +1079,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1067
1079
  } finally {
1068
1080
  setLoadingLogs(false);
1069
1081
  }
1070
- }, [latest?.uid, token, target.teamId]);
1082
+ }, [latest?.uid, transport, targetRef]);
1071
1083
  const toggleErrorLogs = (0, import_react8.useCallback)(() => {
1072
1084
  if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs();
1073
1085
  setShowErrorLogs((v) => !v);
@@ -1632,7 +1644,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
1632
1644
  }
1633
1645
 
1634
1646
  // src/version.ts
1635
- var VERSION = "1.3.0";
1647
+ var VERSION = "1.3.2";
1636
1648
 
1637
1649
  // src/components/DeployTool.tsx
1638
1650
  var import_jsx_runtime13 = require("react/jsx-runtime");
package/dist/index.mjs CHANGED
@@ -84,7 +84,7 @@ function usePluginConfig() {
84
84
  }
85
85
 
86
86
  // src/components/DeployItem.tsx
87
- import { useState as useState5, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef2 } from "react";
87
+ import { useState as useState5, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo2, useRef as useRef2 } from "react";
88
88
  import { flushSync } from "react-dom";
89
89
 
90
90
  // src/compat/primitives.tsx
@@ -772,7 +772,7 @@ function StatusBadge({ state, showSpinner }) {
772
772
  }
773
773
 
774
774
  // src/components/DeployHistory.tsx
775
- import { useId as useId3, useEffect as useEffect3, useState as useState4, useCallback as useCallback3 } from "react";
775
+ import { useId as useId3, useEffect as useEffect3, useMemo, useState as useState4, useCallback as useCallback3 } from "react";
776
776
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
777
777
  function DeployHistory({ target, token, onClose }) {
778
778
  const dialogId = useId3();
@@ -781,8 +781,14 @@ function DeployHistory({ target, token, onClose }) {
781
781
  const [loading, setLoading] = useState4(true);
782
782
  const [error, setError] = useState4(null);
783
783
  const { projectId, hookId } = parseHookUrl(target.url);
784
- const transport = pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token };
785
- const targetRef = { projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId };
784
+ const transport = useMemo(
785
+ () => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
786
+ [pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
787
+ );
788
+ const targetRef = useMemo(
789
+ () => ({ projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId }),
790
+ [projectId, hookId, target.proxyKey, target.teamId]
791
+ );
786
792
  const load = useCallback3(async () => {
787
793
  setLoading(true);
788
794
  setError(null);
@@ -794,7 +800,7 @@ function DeployHistory({ target, token, onClose }) {
794
800
  } finally {
795
801
  setLoading(false);
796
802
  }
797
- }, [projectId, hookId, token, target.teamId]);
803
+ }, [transport, targetRef]);
798
804
  useEffect3(() => {
799
805
  load();
800
806
  }, [load]);
@@ -875,8 +881,14 @@ function DeployItem({ target, token, onDelete, onEdit }) {
875
881
  const toast = useToast();
876
882
  const pluginConfig = usePluginConfig();
877
883
  const client = useClient({ apiVersion: "2025-01-01" });
878
- const transport = pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token };
879
- const targetRef = { projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId };
884
+ const transport = useMemo2(
885
+ () => pluginConfig.mode === "proxy" ? { mode: "proxy", proxyUrl: pluginConfig.proxyUrl ?? "", statusKey: pluginConfig.statusKey } : { mode: "direct", token },
886
+ [pluginConfig.mode, pluginConfig.proxyUrl, pluginConfig.statusKey, token]
887
+ );
888
+ const targetRef = useMemo2(
889
+ () => ({ projectId, hookId, proxyKey: target.proxyKey, teamId: target.teamId }),
890
+ [projectId, hookId, target.proxyKey, target.teamId]
891
+ );
880
892
  const [deployments, setDeployments] = useState5([]);
881
893
  const [loadingInitial, setLoadingInitial] = useState5(true);
882
894
  const [pendingSince, setPendingSince] = useState5(null);
@@ -904,7 +916,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
904
916
  const isPending = pendingSince !== null;
905
917
  const isActive = isPending || isActiveState(latest?.state);
906
918
  const fetchDeployments2 = useCallback4(async () => {
907
- const ready = transport.mode === "proxy" ? Boolean(target.proxyKey && pluginConfig.proxyUrl) : Boolean(projectId && hookId && token);
919
+ const ready = transport.mode === "proxy" ? Boolean(targetRef.proxyKey && transport.proxyUrl) : Boolean(targetRef.projectId && targetRef.hookId && transport.token);
908
920
  if (!ready) return;
909
921
  const seq = ++requestSeqRef.current;
910
922
  try {
@@ -917,7 +929,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
917
929
  setPollError(err instanceof Error ? err.message : "Could not reach the Vercel API");
918
930
  console.error("Deploy-vercel-from-sanity: fetch error", err);
919
931
  }
920
- }, [projectId, hookId, token, target.teamId, target.proxyKey, transport.mode, pluginConfig.proxyUrl]);
932
+ }, [transport, targetRef]);
921
933
  useEffect4(() => {
922
934
  fetchDeployments2().finally(() => setLoadingInitial(false));
923
935
  }, [fetchDeployments2]);
@@ -1008,7 +1020,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1008
1020
  } finally {
1009
1021
  setCanceling(false);
1010
1022
  }
1011
- }, [latest?.uid, token, target.teamId, fetchDeployments2]);
1023
+ }, [latest?.uid, transport, targetRef, fetchDeployments2]);
1012
1024
  const copyUrl = useCallback4(() => {
1013
1025
  if (!latest?.url) return;
1014
1026
  const fullUrl = deploymentHref(latest.url) ?? "";
@@ -1032,7 +1044,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1032
1044
  } finally {
1033
1045
  setLoadingLogs(false);
1034
1046
  }
1035
- }, [latest?.uid, token, target.teamId]);
1047
+ }, [latest?.uid, transport, targetRef]);
1036
1048
  const toggleErrorLogs = useCallback4(() => {
1037
1049
  if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs();
1038
1050
  setShowErrorLogs((v) => !v);
@@ -1597,7 +1609,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
1597
1609
  }
1598
1610
 
1599
1611
  // src/version.ts
1600
- var VERSION = "1.3.0";
1612
+ var VERSION = "1.3.2";
1601
1613
 
1602
1614
  // src/components/DeployTool.tsx
1603
1615
  import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/deploy-vercel-from-sanity",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports Studio v3.30 through v6.",
5
5
  "license": "MIT",
6
6
  "author": "Liiift Studio",
@@ -41,7 +41,11 @@
41
41
  "scripts": {
42
42
  "build": "tsup",
43
43
  "dev": "tsup --watch",
44
- "prepublishOnly": "npm run build"
44
+ "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run build",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "typecheck": "tsc --noEmit -p tsconfig.json",
48
+ "lint": "eslint ."
45
49
  },
46
50
  "peerDependencies": {
47
51
  "@sanity/icons": ">=2 <6",
@@ -55,11 +59,16 @@
55
59
  "@sanity/ui": "^4",
56
60
  "@types/react": "^19",
57
61
  "@types/react-dom": "^19.2.4",
62
+ "eslint": "^9.39.5",
63
+ "eslint-plugin-react-hooks": "^5.2.0",
58
64
  "react": "^19",
59
65
  "react-dom": "^19",
60
66
  "sanity": "^6",
67
+ "styled-components": "^6.5.3",
61
68
  "tsup": "^8",
62
- "typescript": "^5"
69
+ "typescript": "^5",
70
+ "typescript-eslint": "^8.67.0",
71
+ "vitest": "^3.2.7"
63
72
  },
64
73
  "repository": {
65
74
  "type": "git",