@liiift-studio/deploy-vercel-from-sanity 0.2.2 → 1.0.0
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 +37 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +55 -19
- package/dist/index.mjs +59 -22
- package/package.json +1 -1
- package/src/components/DeployItem.tsx +46 -16
- package/src/components/DeployTargetForm.tsx +3 -1
- package/src/components/DeployTool.tsx +9 -2
- package/src/lib/api.ts +8 -2
- package/src/lib/helpers.ts +12 -1
- package/src/types.ts +4 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
- **Live status** with automatic polling — Queued → Building → Ready / Error
|
|
17
17
|
- **Build timer** showing elapsed time while a deploy is in progress
|
|
18
18
|
- **Cancel** in-progress deployments
|
|
19
|
+
- **Deploy-complete notifications** — Studio toast when a build finishes, errors, or is canceled
|
|
19
20
|
- **Copy deployment URL** with one click
|
|
21
|
+
- **GitHub commit links** — commit SHA links directly to the GitHub commit when repo metadata is available
|
|
20
22
|
- **Inline error log viewer** — see build errors without leaving the studio
|
|
21
23
|
- **Deployment history** per target
|
|
22
24
|
- **"Open in Vercel"** link to your project dashboard
|
|
@@ -122,6 +124,41 @@ tools: (prev, { currentUser }) => {
|
|
|
122
124
|
3. While a deployment is active (Queued / Initializing / Building), it polls every 5 seconds.
|
|
123
125
|
4. Clicking **Deploy** POSTs to the hook URL — Vercel queues a new build.
|
|
124
126
|
5. If a deploy fails, clicking **Show error details** fetches the last 30 build log lines from the Vercel API inline.
|
|
127
|
+
6. A Studio toast notification fires when a deployment completes (Ready, Error, or Canceled).
|
|
128
|
+
|
|
129
|
+
> **Polling and rate limits** — Active deployments are polled every 5 seconds per target. With many simultaneous active deploys, API call volume adds up. Vercel's rate limit is generous for normal use, but studios with a large number of targets triggering concurrently may hit `429` errors. The plugin surfaces these with a clear message.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Troubleshooting
|
|
134
|
+
|
|
135
|
+
### "Token is invalid or expired"
|
|
136
|
+
|
|
137
|
+
Your Vercel API token has been revoked or expired. Go to **Vercel → Settings → Tokens**, create a new token with **Full Account** scope, and reconnect it in the Deploy tab (top-right → *Token connected* button).
|
|
138
|
+
|
|
139
|
+
### "Token lacks the required permissions"
|
|
140
|
+
|
|
141
|
+
The token exists but was created with insufficient scope. Vercel tokens need **Full Account** scope to read deployments. Delete the token and create a new one with the correct scope.
|
|
142
|
+
|
|
143
|
+
### "Resource not found — check the deploy hook URL and team ID"
|
|
144
|
+
|
|
145
|
+
Either the deploy hook URL is incorrect, or the project belongs to a Vercel team and the **Team ID** field is missing from the deploy target. Find your Team ID at **Vercel → Settings → General → Team ID** (starts with `team_`) and add it to the deploy target via the edit menu.
|
|
146
|
+
|
|
147
|
+
### "Rate limit reached"
|
|
148
|
+
|
|
149
|
+
The plugin is making too many API calls at once (common when many targets are all actively building). Wait a few seconds — polling will resume automatically.
|
|
150
|
+
|
|
151
|
+
### Deploy triggers but status never updates
|
|
152
|
+
|
|
153
|
+
This usually means the token is missing. The plugin can trigger deploys via hook URL without a token, but it needs an API token to read back deployment status. Connect a token using the button in the top-right of the Deploy tab.
|
|
154
|
+
|
|
155
|
+
### Commit SHA does not link to GitHub
|
|
156
|
+
|
|
157
|
+
The SHA link requires Vercel to return GitHub repo metadata with the deployment. This is present on deployments triggered by GitHub pushes but not on manually triggered hook deploys. Manually triggered deploys will show the SHA as plain text with a tooltip.
|
|
158
|
+
|
|
159
|
+
### No error logs shown after a failed build
|
|
160
|
+
|
|
161
|
+
If "No stderr or stdout was captured" appears, the build may have failed before producing log output, or the events API returned no lines. Use **Open in Vercel** to view the full build log in the Vercel dashboard.
|
|
125
162
|
|
|
126
163
|
---
|
|
127
164
|
|
package/dist/index.d.mts
CHANGED
|
@@ -33,6 +33,10 @@ interface VercelDeployment {
|
|
|
33
33
|
githubCommitRef?: string;
|
|
34
34
|
githubCommitSha?: string;
|
|
35
35
|
githubCommitAuthorName?: string;
|
|
36
|
+
/** GitHub repo in "org/repo" format — used to construct commit links */
|
|
37
|
+
githubRepo?: string;
|
|
38
|
+
/** GitHub org slug — fallback when githubRepo is absent */
|
|
39
|
+
githubCommitOrg?: string;
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
42
|
/** Plugin configuration options */
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ interface VercelDeployment {
|
|
|
33
33
|
githubCommitRef?: string;
|
|
34
34
|
githubCommitSha?: string;
|
|
35
35
|
githubCommitAuthorName?: string;
|
|
36
|
+
/** GitHub repo in "org/repo" format — used to construct commit links */
|
|
37
|
+
githubRepo?: string;
|
|
38
|
+
/** GitHub org slug — fallback when githubRepo is absent */
|
|
39
|
+
githubCommitOrg?: string;
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
42
|
/** Plugin configuration options */
|
package/dist/index.js
CHANGED
|
@@ -51,8 +51,8 @@ async function vercelFetch(path, token, init) {
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
if (!res.ok) {
|
|
54
|
-
const
|
|
55
|
-
throw new Error(`Vercel API ${res.status}
|
|
54
|
+
const hint = res.status === 401 ? " \u2014 token is invalid or expired. Reconnect your API token." : res.status === 403 ? " \u2014 token lacks the required permissions. Ensure it has Full Account scope." : res.status === 404 ? " \u2014 resource not found. Check the deploy hook URL and team ID." : res.status === 429 ? " \u2014 rate limit reached. Wait a moment and try again." : res.status >= 500 ? " \u2014 Vercel is experiencing issues. Try again shortly." : "";
|
|
55
|
+
throw new Error(`Vercel API ${res.status}${hint}`);
|
|
56
56
|
}
|
|
57
57
|
return res.json();
|
|
58
58
|
}
|
|
@@ -160,6 +160,12 @@ function stateLabel(state) {
|
|
|
160
160
|
return { label: "Unknown", tone: "default" };
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
function githubCommitHref(meta) {
|
|
164
|
+
if (!meta?.githubCommitSha) return null;
|
|
165
|
+
const repo = meta.githubRepo ?? null;
|
|
166
|
+
if (!repo) return null;
|
|
167
|
+
return `https://github.com/${repo}/commit/${meta.githubCommitSha}`;
|
|
168
|
+
}
|
|
163
169
|
function projectHref(inspectorUrl) {
|
|
164
170
|
if (!inspectorUrl) return null;
|
|
165
171
|
try {
|
|
@@ -278,6 +284,7 @@ var POLL_INTERVAL_MS = 5e3;
|
|
|
278
284
|
var LABEL_WIDTH = 64;
|
|
279
285
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
280
286
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
287
|
+
const toast = (0, import_ui3.useToast)();
|
|
281
288
|
const [deployments, setDeployments] = (0, import_react2.useState)([]);
|
|
282
289
|
const [loadingInitial, setLoadingInitial] = (0, import_react2.useState)(true);
|
|
283
290
|
const [triggering, setTriggering] = (0, import_react2.useState)(false);
|
|
@@ -313,6 +320,21 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
313
320
|
(0, import_react2.useEffect)(() => {
|
|
314
321
|
if (triggering && latest && latest.state !== void 0) setTriggering(false);
|
|
315
322
|
}, [triggering, latest]);
|
|
323
|
+
const prevStateRef = (0, import_react2.useRef)(void 0);
|
|
324
|
+
(0, import_react2.useEffect)(() => {
|
|
325
|
+
const current = latest?.state;
|
|
326
|
+
const prev = prevStateRef.current;
|
|
327
|
+
if (prev && isActiveState(prev) && current && !isActiveState(current)) {
|
|
328
|
+
if (current === "READY") {
|
|
329
|
+
toast.push({ status: "success", title: `${target.name} deployed`, description: "Build completed successfully" });
|
|
330
|
+
} else if (current === "ERROR") {
|
|
331
|
+
toast.push({ status: "error", title: `${target.name} failed`, description: "Build encountered an error \u2014 check error details" });
|
|
332
|
+
} else if (current === "CANCELED") {
|
|
333
|
+
toast.push({ status: "warning", title: `${target.name} canceled`, description: "Deployment was canceled" });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
prevStateRef.current = current;
|
|
337
|
+
}, [latest?.state, target.name, toast]);
|
|
316
338
|
(0, import_react2.useEffect)(() => {
|
|
317
339
|
setShowErrorLogs(false);
|
|
318
340
|
setErrorLines([]);
|
|
@@ -359,11 +381,12 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
359
381
|
}, [latest?.uid, token, target.teamId, fetchDeployments]);
|
|
360
382
|
const copyUrl = (0, import_react2.useCallback)(() => {
|
|
361
383
|
if (!latest?.url) return;
|
|
362
|
-
|
|
384
|
+
const fullUrl = `https://${latest.url}`;
|
|
385
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
363
386
|
setCopied(true);
|
|
364
387
|
setTimeout(() => setCopied(false), 2e3);
|
|
365
|
-
}).catch((
|
|
366
|
-
|
|
388
|
+
}).catch(() => {
|
|
389
|
+
window.prompt("Copy deployment URL:", fullUrl);
|
|
367
390
|
});
|
|
368
391
|
}, [latest?.url]);
|
|
369
392
|
const fetchErrorLogs = (0, import_react2.useCallback)(async () => {
|
|
@@ -377,7 +400,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
377
400
|
teamId: target.teamId
|
|
378
401
|
});
|
|
379
402
|
const lines = events.filter((e) => e.type === "stderr" || e.type === "stdout").map((e) => e.text ?? "").filter(Boolean).reverse().slice(-30);
|
|
380
|
-
setErrorLines(lines.length > 0 ? lines : ["No
|
|
403
|
+
setErrorLines(lines.length > 0 ? lines : ["No stderr or stdout was captured for this build. Open the full build log in Vercel for details."]);
|
|
381
404
|
} catch (err) {
|
|
382
405
|
setLogError(err instanceof Error ? err.message : "Failed to load build logs");
|
|
383
406
|
} finally {
|
|
@@ -392,6 +415,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
392
415
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
393
416
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
394
417
|
const fullSha = latest?.meta?.githubCommitSha;
|
|
418
|
+
const commitHref = githubCommitHref(latest?.meta);
|
|
395
419
|
const creator = latest?.creator?.username;
|
|
396
420
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
397
421
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
@@ -509,15 +533,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
509
533
|
{
|
|
510
534
|
content: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Box, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: commitMsg ?? sha }) }),
|
|
511
535
|
portal: true,
|
|
512
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
513
|
-
|
|
536
|
+
children: commitHref ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
537
|
+
"a",
|
|
514
538
|
{
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
539
|
+
href: commitHref,
|
|
540
|
+
target: "_blank",
|
|
541
|
+
rel: "noreferrer",
|
|
542
|
+
style: { color: "inherit", textDecoration: "none" },
|
|
543
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, style: { cursor: "pointer", fontFamily: "monospace" }, children: sha })
|
|
519
544
|
}
|
|
520
|
-
)
|
|
545
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, style: { cursor: "default", fontFamily: "monospace" }, children: sha })
|
|
521
546
|
}
|
|
522
547
|
),
|
|
523
548
|
creator && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Text, { size: 1, muted: true, children: [
|
|
@@ -580,7 +605,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
580
605
|
] }) })
|
|
581
606
|
] }),
|
|
582
607
|
!loadingLogs && !logError && errorLines.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 2, children: [
|
|
583
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Box, { style: { maxHeight: 240, overflowY: "auto", fontFamily: "monospace", fontSize:
|
|
608
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Box, { style: { maxHeight: 240, overflowY: "auto", fontFamily: "monospace", fontSize: 13, lineHeight: 1.6 }, children: errorLines.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Code, { size: 1, style: { display: "block", whiteSpace: "pre-wrap", wordBreak: "break-all" }, children: line }, i)) }),
|
|
584
609
|
safeHref(latest?.inspectorUrl) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: safeHref(latest?.inspectorUrl), target: "_blank", rel: "noreferrer", style: { color: "inherit" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
|
|
585
610
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.LaunchIcon, {}),
|
|
586
611
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: "View full logs in Vercel" })
|
|
@@ -605,7 +630,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
605
630
|
style: { width: "100%", justifyContent: "flex-start", borderRadius: 0, cursor: "pointer" }
|
|
606
631
|
}
|
|
607
632
|
),
|
|
608
|
-
showDetails && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Card, { tone: "primary", padding: 3, style: { borderRadius: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 2, children: [
|
|
633
|
+
showDetails && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Card, { tone: "primary", padding: 3, className: "dvfs-accordion-content", style: { borderRadius: 0, borderTop: "1px solid rgba(128,128,128,0.15)" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Stack, { space: 2, children: [
|
|
609
634
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { gap: 2, align: "center", children: [
|
|
610
635
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, weight: "semibold", style: { minWidth: LABEL_WIDTH }, children: "Project" }),
|
|
611
636
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, style: { fontFamily: "monospace" }, children: projectId || "\u2014" })
|
|
@@ -878,7 +903,11 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
878
903
|
placeholder: "team_xxxxxxxx"
|
|
879
904
|
}
|
|
880
905
|
),
|
|
881
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.
|
|
906
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Text, { size: 0, muted: true, children: [
|
|
907
|
+
"Required for team-owned Vercel projects. Find it at Vercel \u2192 Settings \u2192 General \u2192 Team ID (starts with ",
|
|
908
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { children: "team_" }),
|
|
909
|
+
")."
|
|
910
|
+
] })
|
|
882
911
|
] }),
|
|
883
912
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { align: "center", gap: 3, children: [
|
|
884
913
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
@@ -901,7 +930,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
901
930
|
}
|
|
902
931
|
|
|
903
932
|
// src/version.ts
|
|
904
|
-
var VERSION = "0.
|
|
933
|
+
var VERSION = "1.0.0";
|
|
905
934
|
|
|
906
935
|
// src/components/DeployTool.tsx
|
|
907
936
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
@@ -941,7 +970,7 @@ function DeployTool() {
|
|
|
941
970
|
const style = document.createElement("style");
|
|
942
971
|
style.id = "dvfs-styles";
|
|
943
972
|
style.textContent = `
|
|
944
|
-
@media (max-width:
|
|
973
|
+
@media (max-width: 768px) {
|
|
945
974
|
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
946
975
|
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
947
976
|
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
@@ -949,6 +978,13 @@ function DeployTool() {
|
|
|
949
978
|
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
950
979
|
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
951
980
|
}
|
|
981
|
+
@keyframes dvfs-open {
|
|
982
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
983
|
+
to { opacity: 1; transform: translateY(0); }
|
|
984
|
+
}
|
|
985
|
+
.dvfs-accordion-content {
|
|
986
|
+
animation: dvfs-open 0.15s ease-out;
|
|
987
|
+
}
|
|
952
988
|
`;
|
|
953
989
|
document.head.appendChild(style);
|
|
954
990
|
return () => {
|
|
@@ -1047,7 +1083,7 @@ function DeployTool() {
|
|
|
1047
1083
|
] }) }),
|
|
1048
1084
|
targets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dvfs-grid", style: {
|
|
1049
1085
|
display: "grid",
|
|
1050
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(540px, 1fr))",
|
|
1086
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(min(540px, 100%), 1fr))",
|
|
1051
1087
|
gap: "16px",
|
|
1052
1088
|
alignItems: "start"
|
|
1053
1089
|
}, children: targets.map((target) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Spinner as Spinner4,
|
|
16
16
|
Button as Button5,
|
|
17
17
|
Dialog as Dialog4,
|
|
18
|
-
useToast
|
|
18
|
+
useToast as useToast2
|
|
19
19
|
} from "@sanity/ui";
|
|
20
20
|
import { TokenIcon, TrashIcon as TrashIcon2, WarningOutlineIcon as WarningOutlineIcon2, AddIcon } from "@sanity/icons";
|
|
21
21
|
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
MenuButton,
|
|
35
35
|
Menu,
|
|
36
36
|
MenuItem,
|
|
37
|
-
Code
|
|
37
|
+
Code,
|
|
38
|
+
useToast
|
|
38
39
|
} from "@sanity/ui";
|
|
39
40
|
import {
|
|
40
41
|
ClockIcon,
|
|
@@ -62,8 +63,8 @@ async function vercelFetch(path, token, init) {
|
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
65
|
if (!res.ok) {
|
|
65
|
-
const
|
|
66
|
-
throw new Error(`Vercel API ${res.status}
|
|
66
|
+
const hint = res.status === 401 ? " \u2014 token is invalid or expired. Reconnect your API token." : res.status === 403 ? " \u2014 token lacks the required permissions. Ensure it has Full Account scope." : res.status === 404 ? " \u2014 resource not found. Check the deploy hook URL and team ID." : res.status === 429 ? " \u2014 rate limit reached. Wait a moment and try again." : res.status >= 500 ? " \u2014 Vercel is experiencing issues. Try again shortly." : "";
|
|
67
|
+
throw new Error(`Vercel API ${res.status}${hint}`);
|
|
67
68
|
}
|
|
68
69
|
return res.json();
|
|
69
70
|
}
|
|
@@ -171,6 +172,12 @@ function stateLabel(state) {
|
|
|
171
172
|
return { label: "Unknown", tone: "default" };
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
function githubCommitHref(meta) {
|
|
176
|
+
if (!meta?.githubCommitSha) return null;
|
|
177
|
+
const repo = meta.githubRepo ?? null;
|
|
178
|
+
if (!repo) return null;
|
|
179
|
+
return `https://github.com/${repo}/commit/${meta.githubCommitSha}`;
|
|
180
|
+
}
|
|
174
181
|
function projectHref(inspectorUrl) {
|
|
175
182
|
if (!inspectorUrl) return null;
|
|
176
183
|
try {
|
|
@@ -299,6 +306,7 @@ var POLL_INTERVAL_MS = 5e3;
|
|
|
299
306
|
var LABEL_WIDTH = 64;
|
|
300
307
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
301
308
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
309
|
+
const toast = useToast();
|
|
302
310
|
const [deployments, setDeployments] = useState2([]);
|
|
303
311
|
const [loadingInitial, setLoadingInitial] = useState2(true);
|
|
304
312
|
const [triggering, setTriggering] = useState2(false);
|
|
@@ -334,6 +342,21 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
334
342
|
useEffect2(() => {
|
|
335
343
|
if (triggering && latest && latest.state !== void 0) setTriggering(false);
|
|
336
344
|
}, [triggering, latest]);
|
|
345
|
+
const prevStateRef = useRef(void 0);
|
|
346
|
+
useEffect2(() => {
|
|
347
|
+
const current = latest?.state;
|
|
348
|
+
const prev = prevStateRef.current;
|
|
349
|
+
if (prev && isActiveState(prev) && current && !isActiveState(current)) {
|
|
350
|
+
if (current === "READY") {
|
|
351
|
+
toast.push({ status: "success", title: `${target.name} deployed`, description: "Build completed successfully" });
|
|
352
|
+
} else if (current === "ERROR") {
|
|
353
|
+
toast.push({ status: "error", title: `${target.name} failed`, description: "Build encountered an error \u2014 check error details" });
|
|
354
|
+
} else if (current === "CANCELED") {
|
|
355
|
+
toast.push({ status: "warning", title: `${target.name} canceled`, description: "Deployment was canceled" });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
prevStateRef.current = current;
|
|
359
|
+
}, [latest?.state, target.name, toast]);
|
|
337
360
|
useEffect2(() => {
|
|
338
361
|
setShowErrorLogs(false);
|
|
339
362
|
setErrorLines([]);
|
|
@@ -380,11 +403,12 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
380
403
|
}, [latest?.uid, token, target.teamId, fetchDeployments]);
|
|
381
404
|
const copyUrl = useCallback2(() => {
|
|
382
405
|
if (!latest?.url) return;
|
|
383
|
-
|
|
406
|
+
const fullUrl = `https://${latest.url}`;
|
|
407
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
384
408
|
setCopied(true);
|
|
385
409
|
setTimeout(() => setCopied(false), 2e3);
|
|
386
|
-
}).catch((
|
|
387
|
-
|
|
410
|
+
}).catch(() => {
|
|
411
|
+
window.prompt("Copy deployment URL:", fullUrl);
|
|
388
412
|
});
|
|
389
413
|
}, [latest?.url]);
|
|
390
414
|
const fetchErrorLogs = useCallback2(async () => {
|
|
@@ -398,7 +422,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
398
422
|
teamId: target.teamId
|
|
399
423
|
});
|
|
400
424
|
const lines = events.filter((e) => e.type === "stderr" || e.type === "stdout").map((e) => e.text ?? "").filter(Boolean).reverse().slice(-30);
|
|
401
|
-
setErrorLines(lines.length > 0 ? lines : ["No
|
|
425
|
+
setErrorLines(lines.length > 0 ? lines : ["No stderr or stdout was captured for this build. Open the full build log in Vercel for details."]);
|
|
402
426
|
} catch (err) {
|
|
403
427
|
setLogError(err instanceof Error ? err.message : "Failed to load build logs");
|
|
404
428
|
} finally {
|
|
@@ -413,6 +437,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
413
437
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
414
438
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
415
439
|
const fullSha = latest?.meta?.githubCommitSha;
|
|
440
|
+
const commitHref = githubCommitHref(latest?.meta);
|
|
416
441
|
const creator = latest?.creator?.username;
|
|
417
442
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
418
443
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
@@ -530,15 +555,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
530
555
|
{
|
|
531
556
|
content: /* @__PURE__ */ jsx3(Box2, { padding: 2, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: commitMsg ?? sha }) }),
|
|
532
557
|
portal: true,
|
|
533
|
-
children: /* @__PURE__ */ jsx3(
|
|
534
|
-
|
|
558
|
+
children: commitHref ? /* @__PURE__ */ jsx3(
|
|
559
|
+
"a",
|
|
535
560
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
561
|
+
href: commitHref,
|
|
562
|
+
target: "_blank",
|
|
563
|
+
rel: "noreferrer",
|
|
564
|
+
style: { color: "inherit", textDecoration: "none" },
|
|
565
|
+
children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, style: { cursor: "pointer", fontFamily: "monospace" }, children: sha })
|
|
540
566
|
}
|
|
541
|
-
)
|
|
567
|
+
) : /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, style: { cursor: "default", fontFamily: "monospace" }, children: sha })
|
|
542
568
|
}
|
|
543
569
|
),
|
|
544
570
|
creator && /* @__PURE__ */ jsxs3(Text2, { size: 1, muted: true, children: [
|
|
@@ -601,7 +627,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
601
627
|
] }) })
|
|
602
628
|
] }),
|
|
603
629
|
!loadingLogs && !logError && errorLines.length > 0 && /* @__PURE__ */ jsxs3(Stack2, { space: 2, children: [
|
|
604
|
-
/* @__PURE__ */ jsx3(Box2, { style: { maxHeight: 240, overflowY: "auto", fontFamily: "monospace", fontSize:
|
|
630
|
+
/* @__PURE__ */ jsx3(Box2, { style: { maxHeight: 240, overflowY: "auto", fontFamily: "monospace", fontSize: 13, lineHeight: 1.6 }, children: errorLines.map((line, i) => /* @__PURE__ */ jsx3(Code, { size: 1, style: { display: "block", whiteSpace: "pre-wrap", wordBreak: "break-all" }, children: line }, i)) }),
|
|
605
631
|
safeHref(latest?.inspectorUrl) && /* @__PURE__ */ jsx3("a", { href: safeHref(latest?.inspectorUrl), target: "_blank", rel: "noreferrer", style: { color: "inherit" }, children: /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
|
|
606
632
|
/* @__PURE__ */ jsx3(LaunchIcon2, {}),
|
|
607
633
|
/* @__PURE__ */ jsx3(Text2, { size: 1, children: "View full logs in Vercel" })
|
|
@@ -626,7 +652,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
626
652
|
style: { width: "100%", justifyContent: "flex-start", borderRadius: 0, cursor: "pointer" }
|
|
627
653
|
}
|
|
628
654
|
),
|
|
629
|
-
showDetails && /* @__PURE__ */ jsx3(Card2, { tone: "primary", padding: 3, style: { borderRadius: 0 }, children: /* @__PURE__ */ jsxs3(Stack2, { space: 2, children: [
|
|
655
|
+
showDetails && /* @__PURE__ */ jsx3(Card2, { tone: "primary", padding: 3, className: "dvfs-accordion-content", style: { borderRadius: 0, borderTop: "1px solid rgba(128,128,128,0.15)" }, children: /* @__PURE__ */ jsxs3(Stack2, { space: 2, children: [
|
|
630
656
|
/* @__PURE__ */ jsxs3(Flex3, { gap: 2, align: "center", children: [
|
|
631
657
|
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, weight: "semibold", style: { minWidth: LABEL_WIDTH }, children: "Project" }),
|
|
632
658
|
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, style: { fontFamily: "monospace" }, children: projectId || "\u2014" })
|
|
@@ -910,7 +936,11 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
910
936
|
placeholder: "team_xxxxxxxx"
|
|
911
937
|
}
|
|
912
938
|
),
|
|
913
|
-
/* @__PURE__ */
|
|
939
|
+
/* @__PURE__ */ jsxs5(Text4, { size: 0, muted: true, children: [
|
|
940
|
+
"Required for team-owned Vercel projects. Find it at Vercel \u2192 Settings \u2192 General \u2192 Team ID (starts with ",
|
|
941
|
+
/* @__PURE__ */ jsx5("code", { children: "team_" }),
|
|
942
|
+
")."
|
|
943
|
+
] })
|
|
914
944
|
] }),
|
|
915
945
|
/* @__PURE__ */ jsxs5(Flex5, { align: "center", gap: 3, children: [
|
|
916
946
|
/* @__PURE__ */ jsx5(
|
|
@@ -933,7 +963,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
933
963
|
}
|
|
934
964
|
|
|
935
965
|
// src/version.ts
|
|
936
|
-
var VERSION = "0.
|
|
966
|
+
var VERSION = "1.0.0";
|
|
937
967
|
|
|
938
968
|
// src/components/DeployTool.tsx
|
|
939
969
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
@@ -941,7 +971,7 @@ var TOKEN_QUERY = `*[_id == "config.vercelDeploy"][0].accessToken`;
|
|
|
941
971
|
var TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)`;
|
|
942
972
|
function DeployTool() {
|
|
943
973
|
const client = useClient3({ apiVersion: "2025-01-01" });
|
|
944
|
-
const toast =
|
|
974
|
+
const toast = useToast2();
|
|
945
975
|
const [token, setToken] = useState5(null);
|
|
946
976
|
const [targets, setTargets] = useState5([]);
|
|
947
977
|
const [loading, setLoading] = useState5(true);
|
|
@@ -973,7 +1003,7 @@ function DeployTool() {
|
|
|
973
1003
|
const style = document.createElement("style");
|
|
974
1004
|
style.id = "dvfs-styles";
|
|
975
1005
|
style.textContent = `
|
|
976
|
-
@media (max-width:
|
|
1006
|
+
@media (max-width: 768px) {
|
|
977
1007
|
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
978
1008
|
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
979
1009
|
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
@@ -981,6 +1011,13 @@ function DeployTool() {
|
|
|
981
1011
|
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
982
1012
|
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
983
1013
|
}
|
|
1014
|
+
@keyframes dvfs-open {
|
|
1015
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1016
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1017
|
+
}
|
|
1018
|
+
.dvfs-accordion-content {
|
|
1019
|
+
animation: dvfs-open 0.15s ease-out;
|
|
1020
|
+
}
|
|
984
1021
|
`;
|
|
985
1022
|
document.head.appendChild(style);
|
|
986
1023
|
return () => {
|
|
@@ -1079,7 +1116,7 @@ function DeployTool() {
|
|
|
1079
1116
|
] }) }),
|
|
1080
1117
|
targets.length > 0 && /* @__PURE__ */ jsx6("div", { className: "dvfs-grid", style: {
|
|
1081
1118
|
display: "grid",
|
|
1082
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(540px, 1fr))",
|
|
1119
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(min(540px, 100%), 1fr))",
|
|
1083
1120
|
gap: "16px",
|
|
1084
1121
|
alignItems: "start"
|
|
1085
1122
|
}, children: targets.map((target) => /* @__PURE__ */ jsx6(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports v3, v4, and v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
3
3
|
import {
|
|
4
4
|
Card, Box, Stack, Flex, Text, Button, Tooltip, Badge, Spinner,
|
|
5
|
-
MenuButton, Menu, MenuItem, Code,
|
|
5
|
+
MenuButton, Menu, MenuItem, Code, useToast,
|
|
6
6
|
} from '@sanity/ui'
|
|
7
7
|
import {
|
|
8
8
|
ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
|
|
9
9
|
CopyIcon, CheckmarkIcon, WarningOutlineIcon, ChevronDownIcon, ChevronUpIcon, EditIcon, SchemaIcon
|
|
10
10
|
} from '@sanity/icons'
|
|
11
11
|
import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
|
|
12
|
-
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref } from '../lib/helpers'
|
|
12
|
+
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref, githubCommitHref } from '../lib/helpers'
|
|
13
13
|
import { StatusBadge } from './StatusBadge'
|
|
14
14
|
import { DeployHistory } from './DeployHistory'
|
|
15
15
|
import type { DeployTarget, VercelDeployment } from '../types'
|
|
@@ -26,6 +26,7 @@ interface DeployItemProps {
|
|
|
26
26
|
|
|
27
27
|
export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps) {
|
|
28
28
|
const { projectId, hookId } = parseHookUrl(target.url)
|
|
29
|
+
const toast = useToast()
|
|
29
30
|
|
|
30
31
|
const [deployments, setDeployments] = useState<VercelDeployment[]>([])
|
|
31
32
|
const [loadingInitial, setLoadingInitial] = useState(true)
|
|
@@ -69,6 +70,23 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
69
70
|
if (triggering && latest && latest.state !== undefined) setTriggering(false)
|
|
70
71
|
}, [triggering, latest])
|
|
71
72
|
|
|
73
|
+
// ── Deploy-complete toast ─────────────────────────────────────────────────
|
|
74
|
+
const prevStateRef = useRef<string | undefined>(undefined)
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const current = latest?.state
|
|
77
|
+
const prev = prevStateRef.current
|
|
78
|
+
if (prev && isActiveState(prev as never) && current && !isActiveState(current as never)) {
|
|
79
|
+
if (current === 'READY') {
|
|
80
|
+
toast.push({ status: 'success', title: `${target.name} deployed`, description: 'Build completed successfully' })
|
|
81
|
+
} else if (current === 'ERROR') {
|
|
82
|
+
toast.push({ status: 'error', title: `${target.name} failed`, description: 'Build encountered an error — check error details' })
|
|
83
|
+
} else if (current === 'CANCELED') {
|
|
84
|
+
toast.push({ status: 'warning', title: `${target.name} canceled`, description: 'Deployment was canceled' })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
prevStateRef.current = current
|
|
88
|
+
}, [latest?.state, target.name, toast])
|
|
89
|
+
|
|
72
90
|
useEffect(() => {
|
|
73
91
|
setShowErrorLogs(false)
|
|
74
92
|
setErrorLines([])
|
|
@@ -119,11 +137,13 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
119
137
|
|
|
120
138
|
const copyUrl = useCallback(() => {
|
|
121
139
|
if (!latest?.url) return
|
|
122
|
-
|
|
140
|
+
const fullUrl = `https://${latest.url}`
|
|
141
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
123
142
|
setCopied(true)
|
|
124
143
|
setTimeout(() => setCopied(false), 2000)
|
|
125
|
-
}).catch(
|
|
126
|
-
|
|
144
|
+
}).catch(() => {
|
|
145
|
+
// Clipboard API unavailable — surface the URL for manual copy
|
|
146
|
+
window.prompt('Copy deployment URL:', fullUrl)
|
|
127
147
|
})
|
|
128
148
|
}, [latest?.url])
|
|
129
149
|
|
|
@@ -143,7 +163,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
143
163
|
.filter(Boolean)
|
|
144
164
|
.reverse()
|
|
145
165
|
.slice(-30)
|
|
146
|
-
setErrorLines(lines.length > 0 ? lines : ['No
|
|
166
|
+
setErrorLines(lines.length > 0 ? lines : ['No stderr or stdout was captured for this build. Open the full build log in Vercel for details.'])
|
|
147
167
|
} catch (err) {
|
|
148
168
|
setLogError(err instanceof Error ? err.message : 'Failed to load build logs')
|
|
149
169
|
} finally {
|
|
@@ -161,6 +181,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
161
181
|
const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
|
|
162
182
|
const sha = shortSha(latest?.meta?.githubCommitSha)
|
|
163
183
|
const fullSha = latest?.meta?.githubCommitSha
|
|
184
|
+
const commitHref = githubCommitHref(latest?.meta)
|
|
164
185
|
const creator = latest?.creator?.username
|
|
165
186
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null
|
|
166
187
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl)
|
|
@@ -304,7 +325,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
304
325
|
</>
|
|
305
326
|
)}
|
|
306
327
|
|
|
307
|
-
{/* Commit SHA — tooltip shows full message */}
|
|
328
|
+
{/* Commit SHA — links to GitHub if repo info available, tooltip shows full message */}
|
|
308
329
|
{sha && (
|
|
309
330
|
<Tooltip
|
|
310
331
|
content={
|
|
@@ -314,13 +335,22 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
314
335
|
}
|
|
315
336
|
portal
|
|
316
337
|
>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
338
|
+
{commitHref ? (
|
|
339
|
+
<a
|
|
340
|
+
href={commitHref}
|
|
341
|
+
target="_blank"
|
|
342
|
+
rel="noreferrer"
|
|
343
|
+
style={{ color: 'inherit', textDecoration: 'none' }}
|
|
344
|
+
>
|
|
345
|
+
<Text size={1} muted style={{ cursor: 'pointer', fontFamily: 'monospace' }}>
|
|
346
|
+
{sha}
|
|
347
|
+
</Text>
|
|
348
|
+
</a>
|
|
349
|
+
) : (
|
|
350
|
+
<Text size={1} muted style={{ cursor: 'default', fontFamily: 'monospace' }}>
|
|
351
|
+
{sha}
|
|
352
|
+
</Text>
|
|
353
|
+
)}
|
|
324
354
|
</Tooltip>
|
|
325
355
|
)}
|
|
326
356
|
|
|
@@ -399,7 +429,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
399
429
|
)}
|
|
400
430
|
{!loadingLogs && !logError && errorLines.length > 0 && (
|
|
401
431
|
<Stack space={2}>
|
|
402
|
-
<Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize:
|
|
432
|
+
<Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize: 13, lineHeight: 1.6 }}>
|
|
403
433
|
{errorLines.map((line, i) => (
|
|
404
434
|
<Code key={i} size={1} style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
|
405
435
|
{line}
|
|
@@ -445,7 +475,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
445
475
|
style={{ width: '100%', justifyContent: 'flex-start', borderRadius: 0, cursor: 'pointer' }}
|
|
446
476
|
/>
|
|
447
477
|
{showDetails && (
|
|
448
|
-
<Card tone="primary" padding={3} style={{ borderRadius: 0 }}>
|
|
478
|
+
<Card tone="primary" padding={3} className="dvfs-accordion-content" style={{ borderRadius: 0, borderTop: '1px solid rgba(128,128,128,0.15)' }}>
|
|
449
479
|
<Stack space={2}>
|
|
450
480
|
<Flex gap={2} align="center">
|
|
451
481
|
<Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Project</Text>
|
|
@@ -114,7 +114,9 @@ export function DeployTargetForm({ initial, onSaved, onClose }: DeployTargetForm
|
|
|
114
114
|
onChange={e => setTeamId((e.target as HTMLInputElement).value)}
|
|
115
115
|
placeholder="team_xxxxxxxx"
|
|
116
116
|
/>
|
|
117
|
-
<Text size={0} muted>
|
|
117
|
+
<Text size={0} muted>
|
|
118
|
+
Required for team-owned Vercel projects. Find it at Vercel → Settings → General → Team ID (starts with <code>team_</code>).
|
|
119
|
+
</Text>
|
|
118
120
|
</Stack>
|
|
119
121
|
|
|
120
122
|
{/* Disable delete */}
|
|
@@ -52,7 +52,7 @@ export function DeployTool() {
|
|
|
52
52
|
const style = document.createElement('style')
|
|
53
53
|
style.id = 'dvfs-styles'
|
|
54
54
|
style.textContent = `
|
|
55
|
-
@media (max-width:
|
|
55
|
+
@media (max-width: 768px) {
|
|
56
56
|
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
57
57
|
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
58
58
|
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
@@ -60,6 +60,13 @@ export function DeployTool() {
|
|
|
60
60
|
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
61
61
|
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
62
62
|
}
|
|
63
|
+
@keyframes dvfs-open {
|
|
64
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
65
|
+
to { opacity: 1; transform: translateY(0); }
|
|
66
|
+
}
|
|
67
|
+
.dvfs-accordion-content {
|
|
68
|
+
animation: dvfs-open 0.15s ease-out;
|
|
69
|
+
}
|
|
63
70
|
`
|
|
64
71
|
document.head.appendChild(style)
|
|
65
72
|
return () => { document.getElementById('dvfs-styles')?.remove() }
|
|
@@ -181,7 +188,7 @@ export function DeployTool() {
|
|
|
181
188
|
{targets.length > 0 && (
|
|
182
189
|
<div className="dvfs-grid" style={{
|
|
183
190
|
display: 'grid',
|
|
184
|
-
gridTemplateColumns: 'repeat(auto-fill, minmax(540px, 1fr))',
|
|
191
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(min(540px, 100%), 1fr))',
|
|
185
192
|
gap: '16px',
|
|
186
193
|
alignItems: 'start',
|
|
187
194
|
}}>
|
package/src/lib/api.ts
CHANGED
|
@@ -16,8 +16,14 @@ async function vercelFetch<T>(path: string, token: string, init?: RequestInit):
|
|
|
16
16
|
},
|
|
17
17
|
})
|
|
18
18
|
if (!res.ok) {
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const hint =
|
|
20
|
+
res.status === 401 ? ' — token is invalid or expired. Reconnect your API token.' :
|
|
21
|
+
res.status === 403 ? ' — token lacks the required permissions. Ensure it has Full Account scope.' :
|
|
22
|
+
res.status === 404 ? ' — resource not found. Check the deploy hook URL and team ID.' :
|
|
23
|
+
res.status === 429 ? ' — rate limit reached. Wait a moment and try again.' :
|
|
24
|
+
res.status >= 500 ? ' — Vercel is experiencing issues. Try again shortly.' :
|
|
25
|
+
''
|
|
26
|
+
throw new Error(`Vercel API ${res.status}${hint}`)
|
|
21
27
|
}
|
|
22
28
|
return res.json() as Promise<T>
|
|
23
29
|
}
|
package/src/lib/helpers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// URL parsing and time formatting utilities
|
|
2
|
-
import type { VercelDeployState } from '../types'
|
|
2
|
+
import type { VercelDeployment, VercelDeployState } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Extracts projectId and hookId from a Vercel deploy hook URL.
|
|
@@ -85,6 +85,17 @@ export function stateLabel(state: VercelDeployState | undefined): {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Constructs a GitHub commit URL from deployment meta fields.
|
|
90
|
+
* Returns null if the required repo or SHA info is not present.
|
|
91
|
+
*/
|
|
92
|
+
export function githubCommitHref(meta: VercelDeployment['meta']): string | null {
|
|
93
|
+
if (!meta?.githubCommitSha) return null
|
|
94
|
+
const repo = meta.githubRepo ?? null
|
|
95
|
+
if (!repo) return null
|
|
96
|
+
return `https://github.com/${repo}/commit/${meta.githubCommitSha}`
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
/**
|
|
89
100
|
* Extracts the Vercel project dashboard URL from a deployment's inspectorUrl.
|
|
90
101
|
* inspectorUrl format: https://vercel.com/{team}/{project}/{deploymentId}
|
package/src/types.ts
CHANGED
|
@@ -42,6 +42,10 @@ export interface VercelDeployment {
|
|
|
42
42
|
githubCommitRef?: string
|
|
43
43
|
githubCommitSha?: string
|
|
44
44
|
githubCommitAuthorName?: string
|
|
45
|
+
/** GitHub repo in "org/repo" format — used to construct commit links */
|
|
46
|
+
githubRepo?: string
|
|
47
|
+
/** GitHub org slug — fallback when githubRepo is absent */
|
|
48
|
+
githubCommitOrg?: string
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Package version — keep in sync with package.json
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '1.0.0'
|