@liiift-studio/deploy-vercel-from-sanity 0.2.2 → 1.0.1
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 +58 -24
- package/dist/index.mjs +62 -27
- package/package.json +1 -1
- package/src/components/DeployItem.tsx +49 -21
- 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
|
@@ -35,6 +35,7 @@ var import_icons5 = require("@sanity/icons");
|
|
|
35
35
|
|
|
36
36
|
// src/components/DeployItem.tsx
|
|
37
37
|
var import_react2 = require("react");
|
|
38
|
+
var import_react_dom = require("react-dom");
|
|
38
39
|
var import_ui3 = require("@sanity/ui");
|
|
39
40
|
var import_icons2 = require("@sanity/icons");
|
|
40
41
|
|
|
@@ -51,8 +52,8 @@ async function vercelFetch(path, token, init) {
|
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
if (!res.ok) {
|
|
54
|
-
const
|
|
55
|
-
throw new Error(`Vercel API ${res.status}
|
|
55
|
+
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." : "";
|
|
56
|
+
throw new Error(`Vercel API ${res.status}${hint}`);
|
|
56
57
|
}
|
|
57
58
|
return res.json();
|
|
58
59
|
}
|
|
@@ -160,6 +161,12 @@ function stateLabel(state) {
|
|
|
160
161
|
return { label: "Unknown", tone: "default" };
|
|
161
162
|
}
|
|
162
163
|
}
|
|
164
|
+
function githubCommitHref(meta) {
|
|
165
|
+
if (!meta?.githubCommitSha) return null;
|
|
166
|
+
const repo = meta.githubRepo ?? null;
|
|
167
|
+
if (!repo) return null;
|
|
168
|
+
return `https://github.com/${repo}/commit/${meta.githubCommitSha}`;
|
|
169
|
+
}
|
|
163
170
|
function projectHref(inspectorUrl) {
|
|
164
171
|
if (!inspectorUrl) return null;
|
|
165
172
|
try {
|
|
@@ -278,6 +285,7 @@ var POLL_INTERVAL_MS = 5e3;
|
|
|
278
285
|
var LABEL_WIDTH = 64;
|
|
279
286
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
280
287
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
288
|
+
const toast = (0, import_ui3.useToast)();
|
|
281
289
|
const [deployments, setDeployments] = (0, import_react2.useState)([]);
|
|
282
290
|
const [loadingInitial, setLoadingInitial] = (0, import_react2.useState)(true);
|
|
283
291
|
const [triggering, setTriggering] = (0, import_react2.useState)(false);
|
|
@@ -313,6 +321,21 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
313
321
|
(0, import_react2.useEffect)(() => {
|
|
314
322
|
if (triggering && latest && latest.state !== void 0) setTriggering(false);
|
|
315
323
|
}, [triggering, latest]);
|
|
324
|
+
const prevStateRef = (0, import_react2.useRef)(void 0);
|
|
325
|
+
(0, import_react2.useEffect)(() => {
|
|
326
|
+
const current = latest?.state;
|
|
327
|
+
const prev = prevStateRef.current;
|
|
328
|
+
if (prev && isActiveState(prev) && current && !isActiveState(current)) {
|
|
329
|
+
if (current === "READY") {
|
|
330
|
+
toast.push({ status: "success", title: `${target.name} deployed`, description: "Build completed successfully" });
|
|
331
|
+
} else if (current === "ERROR") {
|
|
332
|
+
toast.push({ status: "error", title: `${target.name} failed`, description: "Build encountered an error \u2014 check error details" });
|
|
333
|
+
} else if (current === "CANCELED") {
|
|
334
|
+
toast.push({ status: "warning", title: `${target.name} canceled`, description: "Deployment was canceled" });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
prevStateRef.current = current;
|
|
338
|
+
}, [latest?.state, target.name, toast]);
|
|
316
339
|
(0, import_react2.useEffect)(() => {
|
|
317
340
|
setShowErrorLogs(false);
|
|
318
341
|
setErrorLines([]);
|
|
@@ -336,7 +359,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
336
359
|
}, [isActive, latest?.created]);
|
|
337
360
|
const deploy = (0, import_react2.useCallback)(async () => {
|
|
338
361
|
setDeployError(null);
|
|
339
|
-
setTriggering(true);
|
|
362
|
+
(0, import_react_dom.flushSync)(() => setTriggering(true));
|
|
340
363
|
try {
|
|
341
364
|
await triggerDeploy(target.url);
|
|
342
365
|
setTimeout(fetchDeployments, 2e3);
|
|
@@ -359,11 +382,12 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
359
382
|
}, [latest?.uid, token, target.teamId, fetchDeployments]);
|
|
360
383
|
const copyUrl = (0, import_react2.useCallback)(() => {
|
|
361
384
|
if (!latest?.url) return;
|
|
362
|
-
|
|
385
|
+
const fullUrl = `https://${latest.url}`;
|
|
386
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
363
387
|
setCopied(true);
|
|
364
388
|
setTimeout(() => setCopied(false), 2e3);
|
|
365
|
-
}).catch((
|
|
366
|
-
|
|
389
|
+
}).catch(() => {
|
|
390
|
+
window.prompt("Copy deployment URL:", fullUrl);
|
|
367
391
|
});
|
|
368
392
|
}, [latest?.url]);
|
|
369
393
|
const fetchErrorLogs = (0, import_react2.useCallback)(async () => {
|
|
@@ -377,7 +401,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
377
401
|
teamId: target.teamId
|
|
378
402
|
});
|
|
379
403
|
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
|
|
404
|
+
setErrorLines(lines.length > 0 ? lines : ["No stderr or stdout was captured for this build. Open the full build log in Vercel for details."]);
|
|
381
405
|
} catch (err) {
|
|
382
406
|
setLogError(err instanceof Error ? err.message : "Failed to load build logs");
|
|
383
407
|
} finally {
|
|
@@ -392,6 +416,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
392
416
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
393
417
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
394
418
|
const fullSha = latest?.meta?.githubCommitSha;
|
|
419
|
+
const commitHref = githubCommitHref(latest?.meta);
|
|
395
420
|
const creator = latest?.creator?.username;
|
|
396
421
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
397
422
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
@@ -408,10 +433,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
408
433
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", style: { marginLeft: "-0.1em", opacity: 0.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) })
|
|
409
434
|
] }) }),
|
|
410
435
|
token && !loadingInitial && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
411
|
-
triggering ? /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
412
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
|
|
413
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "caution", padding: 2, children: "Triggering\u2026" })
|
|
414
|
-
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
436
|
+
triggering ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "caution", padding: 2, children: "Triggering\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
415
437
|
isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
416
438
|
import_ui3.Button,
|
|
417
439
|
{
|
|
@@ -509,15 +531,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
509
531
|
{
|
|
510
532
|
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
533
|
portal: true,
|
|
512
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
513
|
-
|
|
534
|
+
children: commitHref ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
535
|
+
"a",
|
|
514
536
|
{
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
537
|
+
href: commitHref,
|
|
538
|
+
target: "_blank",
|
|
539
|
+
rel: "noreferrer",
|
|
540
|
+
style: { color: "inherit", textDecoration: "none" },
|
|
541
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, style: { cursor: "pointer", fontFamily: "monospace" }, children: sha })
|
|
519
542
|
}
|
|
520
|
-
)
|
|
543
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, style: { cursor: "default", fontFamily: "monospace" }, children: sha })
|
|
521
544
|
}
|
|
522
545
|
),
|
|
523
546
|
creator && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Text, { size: 1, muted: true, children: [
|
|
@@ -580,7 +603,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
580
603
|
] }) })
|
|
581
604
|
] }),
|
|
582
605
|
!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:
|
|
606
|
+
/* @__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
607
|
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
608
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.LaunchIcon, {}),
|
|
586
609
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, children: "View full logs in Vercel" })
|
|
@@ -605,7 +628,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
605
628
|
style: { width: "100%", justifyContent: "flex-start", borderRadius: 0, cursor: "pointer" }
|
|
606
629
|
}
|
|
607
630
|
),
|
|
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: [
|
|
631
|
+
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
632
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { gap: 2, align: "center", children: [
|
|
610
633
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, weight: "semibold", style: { minWidth: LABEL_WIDTH }, children: "Project" }),
|
|
611
634
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 0, muted: true, style: { fontFamily: "monospace" }, children: projectId || "\u2014" })
|
|
@@ -878,7 +901,11 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
878
901
|
placeholder: "team_xxxxxxxx"
|
|
879
902
|
}
|
|
880
903
|
),
|
|
881
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.
|
|
904
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Text, { size: 0, muted: true, children: [
|
|
905
|
+
"Required for team-owned Vercel projects. Find it at Vercel \u2192 Settings \u2192 General \u2192 Team ID (starts with ",
|
|
906
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { children: "team_" }),
|
|
907
|
+
")."
|
|
908
|
+
] })
|
|
882
909
|
] }),
|
|
883
910
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_ui5.Flex, { align: "center", gap: 3, children: [
|
|
884
911
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
@@ -901,7 +928,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
901
928
|
}
|
|
902
929
|
|
|
903
930
|
// src/version.ts
|
|
904
|
-
var VERSION = "0.
|
|
931
|
+
var VERSION = "1.0.0";
|
|
905
932
|
|
|
906
933
|
// src/components/DeployTool.tsx
|
|
907
934
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
@@ -941,7 +968,7 @@ function DeployTool() {
|
|
|
941
968
|
const style = document.createElement("style");
|
|
942
969
|
style.id = "dvfs-styles";
|
|
943
970
|
style.textContent = `
|
|
944
|
-
@media (max-width:
|
|
971
|
+
@media (max-width: 768px) {
|
|
945
972
|
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
946
973
|
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
947
974
|
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
@@ -949,6 +976,13 @@ function DeployTool() {
|
|
|
949
976
|
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
950
977
|
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
951
978
|
}
|
|
979
|
+
@keyframes dvfs-open {
|
|
980
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
981
|
+
to { opacity: 1; transform: translateY(0); }
|
|
982
|
+
}
|
|
983
|
+
.dvfs-accordion-content {
|
|
984
|
+
animation: dvfs-open 0.15s ease-out;
|
|
985
|
+
}
|
|
952
986
|
`;
|
|
953
987
|
document.head.appendChild(style);
|
|
954
988
|
return () => {
|
|
@@ -1047,7 +1081,7 @@ function DeployTool() {
|
|
|
1047
1081
|
] }) }),
|
|
1048
1082
|
targets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dvfs-grid", style: {
|
|
1049
1083
|
display: "grid",
|
|
1050
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(540px, 1fr))",
|
|
1084
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(min(540px, 100%), 1fr))",
|
|
1051
1085
|
gap: "16px",
|
|
1052
1086
|
alignItems: "start"
|
|
1053
1087
|
}, children: targets.map((target) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
package/dist/index.mjs
CHANGED
|
@@ -15,12 +15,13 @@ 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
|
|
|
22
22
|
// src/components/DeployItem.tsx
|
|
23
23
|
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef } from "react";
|
|
24
|
+
import { flushSync } from "react-dom";
|
|
24
25
|
import {
|
|
25
26
|
Card as Card2,
|
|
26
27
|
Box as Box2,
|
|
@@ -34,7 +35,8 @@ import {
|
|
|
34
35
|
MenuButton,
|
|
35
36
|
Menu,
|
|
36
37
|
MenuItem,
|
|
37
|
-
Code
|
|
38
|
+
Code,
|
|
39
|
+
useToast
|
|
38
40
|
} from "@sanity/ui";
|
|
39
41
|
import {
|
|
40
42
|
ClockIcon,
|
|
@@ -62,8 +64,8 @@ async function vercelFetch(path, token, init) {
|
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
66
|
if (!res.ok) {
|
|
65
|
-
const
|
|
66
|
-
throw new Error(`Vercel API ${res.status}
|
|
67
|
+
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." : "";
|
|
68
|
+
throw new Error(`Vercel API ${res.status}${hint}`);
|
|
67
69
|
}
|
|
68
70
|
return res.json();
|
|
69
71
|
}
|
|
@@ -171,6 +173,12 @@ function stateLabel(state) {
|
|
|
171
173
|
return { label: "Unknown", tone: "default" };
|
|
172
174
|
}
|
|
173
175
|
}
|
|
176
|
+
function githubCommitHref(meta) {
|
|
177
|
+
if (!meta?.githubCommitSha) return null;
|
|
178
|
+
const repo = meta.githubRepo ?? null;
|
|
179
|
+
if (!repo) return null;
|
|
180
|
+
return `https://github.com/${repo}/commit/${meta.githubCommitSha}`;
|
|
181
|
+
}
|
|
174
182
|
function projectHref(inspectorUrl) {
|
|
175
183
|
if (!inspectorUrl) return null;
|
|
176
184
|
try {
|
|
@@ -299,6 +307,7 @@ var POLL_INTERVAL_MS = 5e3;
|
|
|
299
307
|
var LABEL_WIDTH = 64;
|
|
300
308
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
301
309
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
310
|
+
const toast = useToast();
|
|
302
311
|
const [deployments, setDeployments] = useState2([]);
|
|
303
312
|
const [loadingInitial, setLoadingInitial] = useState2(true);
|
|
304
313
|
const [triggering, setTriggering] = useState2(false);
|
|
@@ -334,6 +343,21 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
334
343
|
useEffect2(() => {
|
|
335
344
|
if (triggering && latest && latest.state !== void 0) setTriggering(false);
|
|
336
345
|
}, [triggering, latest]);
|
|
346
|
+
const prevStateRef = useRef(void 0);
|
|
347
|
+
useEffect2(() => {
|
|
348
|
+
const current = latest?.state;
|
|
349
|
+
const prev = prevStateRef.current;
|
|
350
|
+
if (prev && isActiveState(prev) && current && !isActiveState(current)) {
|
|
351
|
+
if (current === "READY") {
|
|
352
|
+
toast.push({ status: "success", title: `${target.name} deployed`, description: "Build completed successfully" });
|
|
353
|
+
} else if (current === "ERROR") {
|
|
354
|
+
toast.push({ status: "error", title: `${target.name} failed`, description: "Build encountered an error \u2014 check error details" });
|
|
355
|
+
} else if (current === "CANCELED") {
|
|
356
|
+
toast.push({ status: "warning", title: `${target.name} canceled`, description: "Deployment was canceled" });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
prevStateRef.current = current;
|
|
360
|
+
}, [latest?.state, target.name, toast]);
|
|
337
361
|
useEffect2(() => {
|
|
338
362
|
setShowErrorLogs(false);
|
|
339
363
|
setErrorLines([]);
|
|
@@ -357,7 +381,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
357
381
|
}, [isActive, latest?.created]);
|
|
358
382
|
const deploy = useCallback2(async () => {
|
|
359
383
|
setDeployError(null);
|
|
360
|
-
setTriggering(true);
|
|
384
|
+
flushSync(() => setTriggering(true));
|
|
361
385
|
try {
|
|
362
386
|
await triggerDeploy(target.url);
|
|
363
387
|
setTimeout(fetchDeployments, 2e3);
|
|
@@ -380,11 +404,12 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
380
404
|
}, [latest?.uid, token, target.teamId, fetchDeployments]);
|
|
381
405
|
const copyUrl = useCallback2(() => {
|
|
382
406
|
if (!latest?.url) return;
|
|
383
|
-
|
|
407
|
+
const fullUrl = `https://${latest.url}`;
|
|
408
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
384
409
|
setCopied(true);
|
|
385
410
|
setTimeout(() => setCopied(false), 2e3);
|
|
386
|
-
}).catch((
|
|
387
|
-
|
|
411
|
+
}).catch(() => {
|
|
412
|
+
window.prompt("Copy deployment URL:", fullUrl);
|
|
388
413
|
});
|
|
389
414
|
}, [latest?.url]);
|
|
390
415
|
const fetchErrorLogs = useCallback2(async () => {
|
|
@@ -398,7 +423,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
398
423
|
teamId: target.teamId
|
|
399
424
|
});
|
|
400
425
|
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
|
|
426
|
+
setErrorLines(lines.length > 0 ? lines : ["No stderr or stdout was captured for this build. Open the full build log in Vercel for details."]);
|
|
402
427
|
} catch (err) {
|
|
403
428
|
setLogError(err instanceof Error ? err.message : "Failed to load build logs");
|
|
404
429
|
} finally {
|
|
@@ -413,6 +438,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
413
438
|
const commitMsg = latest?.meta?.githubCommitMessage?.split("\n")[0];
|
|
414
439
|
const sha = shortSha(latest?.meta?.githubCommitSha);
|
|
415
440
|
const fullSha = latest?.meta?.githubCommitSha;
|
|
441
|
+
const commitHref = githubCommitHref(latest?.meta);
|
|
416
442
|
const creator = latest?.creator?.username;
|
|
417
443
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null;
|
|
418
444
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl);
|
|
@@ -429,10 +455,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
429
455
|
/* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", style: { marginLeft: "-0.1em", opacity: 0.5 }, children: /* @__PURE__ */ jsx3("path", { d: "M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) })
|
|
430
456
|
] }) }),
|
|
431
457
|
token && !loadingInitial && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
432
|
-
triggering ? /* @__PURE__ */
|
|
433
|
-
/* @__PURE__ */ jsx3(Spinner3, { muted: true }),
|
|
434
|
-
/* @__PURE__ */ jsx3(Badge3, { tone: "caution", padding: 2, children: "Triggering\u2026" })
|
|
435
|
-
] }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
458
|
+
triggering ? /* @__PURE__ */ jsx3(Badge3, { tone: "caution", padding: 2, children: "Triggering\u2026" }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state, showSpinner: true }),
|
|
436
459
|
isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
|
|
437
460
|
Button2,
|
|
438
461
|
{
|
|
@@ -530,15 +553,16 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
530
553
|
{
|
|
531
554
|
content: /* @__PURE__ */ jsx3(Box2, { padding: 2, children: /* @__PURE__ */ jsx3(Text2, { size: 1, children: commitMsg ?? sha }) }),
|
|
532
555
|
portal: true,
|
|
533
|
-
children: /* @__PURE__ */ jsx3(
|
|
534
|
-
|
|
556
|
+
children: commitHref ? /* @__PURE__ */ jsx3(
|
|
557
|
+
"a",
|
|
535
558
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
559
|
+
href: commitHref,
|
|
560
|
+
target: "_blank",
|
|
561
|
+
rel: "noreferrer",
|
|
562
|
+
style: { color: "inherit", textDecoration: "none" },
|
|
563
|
+
children: /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, style: { cursor: "pointer", fontFamily: "monospace" }, children: sha })
|
|
540
564
|
}
|
|
541
|
-
)
|
|
565
|
+
) : /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, style: { cursor: "default", fontFamily: "monospace" }, children: sha })
|
|
542
566
|
}
|
|
543
567
|
),
|
|
544
568
|
creator && /* @__PURE__ */ jsxs3(Text2, { size: 1, muted: true, children: [
|
|
@@ -601,7 +625,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
601
625
|
] }) })
|
|
602
626
|
] }),
|
|
603
627
|
!loadingLogs && !logError && errorLines.length > 0 && /* @__PURE__ */ jsxs3(Stack2, { space: 2, children: [
|
|
604
|
-
/* @__PURE__ */ jsx3(Box2, { style: { maxHeight: 240, overflowY: "auto", fontFamily: "monospace", fontSize:
|
|
628
|
+
/* @__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
629
|
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
630
|
/* @__PURE__ */ jsx3(LaunchIcon2, {}),
|
|
607
631
|
/* @__PURE__ */ jsx3(Text2, { size: 1, children: "View full logs in Vercel" })
|
|
@@ -626,7 +650,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
626
650
|
style: { width: "100%", justifyContent: "flex-start", borderRadius: 0, cursor: "pointer" }
|
|
627
651
|
}
|
|
628
652
|
),
|
|
629
|
-
showDetails && /* @__PURE__ */ jsx3(Card2, { tone: "primary", padding: 3, style: { borderRadius: 0 }, children: /* @__PURE__ */ jsxs3(Stack2, { space: 2, children: [
|
|
653
|
+
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
654
|
/* @__PURE__ */ jsxs3(Flex3, { gap: 2, align: "center", children: [
|
|
631
655
|
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, weight: "semibold", style: { minWidth: LABEL_WIDTH }, children: "Project" }),
|
|
632
656
|
/* @__PURE__ */ jsx3(Text2, { size: 0, muted: true, style: { fontFamily: "monospace" }, children: projectId || "\u2014" })
|
|
@@ -910,7 +934,11 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
910
934
|
placeholder: "team_xxxxxxxx"
|
|
911
935
|
}
|
|
912
936
|
),
|
|
913
|
-
/* @__PURE__ */
|
|
937
|
+
/* @__PURE__ */ jsxs5(Text4, { size: 0, muted: true, children: [
|
|
938
|
+
"Required for team-owned Vercel projects. Find it at Vercel \u2192 Settings \u2192 General \u2192 Team ID (starts with ",
|
|
939
|
+
/* @__PURE__ */ jsx5("code", { children: "team_" }),
|
|
940
|
+
")."
|
|
941
|
+
] })
|
|
914
942
|
] }),
|
|
915
943
|
/* @__PURE__ */ jsxs5(Flex5, { align: "center", gap: 3, children: [
|
|
916
944
|
/* @__PURE__ */ jsx5(
|
|
@@ -933,7 +961,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
933
961
|
}
|
|
934
962
|
|
|
935
963
|
// src/version.ts
|
|
936
|
-
var VERSION = "0.
|
|
964
|
+
var VERSION = "1.0.0";
|
|
937
965
|
|
|
938
966
|
// src/components/DeployTool.tsx
|
|
939
967
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
@@ -941,7 +969,7 @@ var TOKEN_QUERY = `*[_id == "config.vercelDeploy"][0].accessToken`;
|
|
|
941
969
|
var TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)`;
|
|
942
970
|
function DeployTool() {
|
|
943
971
|
const client = useClient3({ apiVersion: "2025-01-01" });
|
|
944
|
-
const toast =
|
|
972
|
+
const toast = useToast2();
|
|
945
973
|
const [token, setToken] = useState5(null);
|
|
946
974
|
const [targets, setTargets] = useState5([]);
|
|
947
975
|
const [loading, setLoading] = useState5(true);
|
|
@@ -973,7 +1001,7 @@ function DeployTool() {
|
|
|
973
1001
|
const style = document.createElement("style");
|
|
974
1002
|
style.id = "dvfs-styles";
|
|
975
1003
|
style.textContent = `
|
|
976
|
-
@media (max-width:
|
|
1004
|
+
@media (max-width: 768px) {
|
|
977
1005
|
.dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; }
|
|
978
1006
|
.dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; }
|
|
979
1007
|
.dvfs-grid { grid-template-columns: 1fr !important; }
|
|
@@ -981,6 +1009,13 @@ function DeployTool() {
|
|
|
981
1009
|
.dvfs-deploy-col { width: 100% !important; align-self: auto !important; }
|
|
982
1010
|
.dvfs-deploy-col button { border-radius: 3px !important; }
|
|
983
1011
|
}
|
|
1012
|
+
@keyframes dvfs-open {
|
|
1013
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1014
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1015
|
+
}
|
|
1016
|
+
.dvfs-accordion-content {
|
|
1017
|
+
animation: dvfs-open 0.15s ease-out;
|
|
1018
|
+
}
|
|
984
1019
|
`;
|
|
985
1020
|
document.head.appendChild(style);
|
|
986
1021
|
return () => {
|
|
@@ -1079,7 +1114,7 @@ function DeployTool() {
|
|
|
1079
1114
|
] }) }),
|
|
1080
1115
|
targets.length > 0 && /* @__PURE__ */ jsx6("div", { className: "dvfs-grid", style: {
|
|
1081
1116
|
display: "grid",
|
|
1082
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(540px, 1fr))",
|
|
1117
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(min(540px, 100%), 1fr))",
|
|
1083
1118
|
gap: "16px",
|
|
1084
1119
|
alignItems: "start"
|
|
1085
1120
|
}, 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.1",
|
|
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",
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Per-deploy-target card — shows status, build timer, history, cancel, deploy, copy URL, and error logs
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
3
|
+
import { flushSync } from 'react-dom'
|
|
3
4
|
import {
|
|
4
5
|
Card, Box, Stack, Flex, Text, Button, Tooltip, Badge, Spinner,
|
|
5
|
-
MenuButton, Menu, MenuItem, Code,
|
|
6
|
+
MenuButton, Menu, MenuItem, Code, useToast,
|
|
6
7
|
} from '@sanity/ui'
|
|
7
8
|
import {
|
|
8
9
|
ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
|
|
9
10
|
CopyIcon, CheckmarkIcon, WarningOutlineIcon, ChevronDownIcon, ChevronUpIcon, EditIcon, SchemaIcon
|
|
10
11
|
} from '@sanity/icons'
|
|
11
12
|
import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
|
|
12
|
-
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref } from '../lib/helpers'
|
|
13
|
+
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref, githubCommitHref } from '../lib/helpers'
|
|
13
14
|
import { StatusBadge } from './StatusBadge'
|
|
14
15
|
import { DeployHistory } from './DeployHistory'
|
|
15
16
|
import type { DeployTarget, VercelDeployment } from '../types'
|
|
@@ -26,6 +27,7 @@ interface DeployItemProps {
|
|
|
26
27
|
|
|
27
28
|
export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps) {
|
|
28
29
|
const { projectId, hookId } = parseHookUrl(target.url)
|
|
30
|
+
const toast = useToast()
|
|
29
31
|
|
|
30
32
|
const [deployments, setDeployments] = useState<VercelDeployment[]>([])
|
|
31
33
|
const [loadingInitial, setLoadingInitial] = useState(true)
|
|
@@ -69,6 +71,23 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
69
71
|
if (triggering && latest && latest.state !== undefined) setTriggering(false)
|
|
70
72
|
}, [triggering, latest])
|
|
71
73
|
|
|
74
|
+
// ── Deploy-complete toast ─────────────────────────────────────────────────
|
|
75
|
+
const prevStateRef = useRef<string | undefined>(undefined)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const current = latest?.state
|
|
78
|
+
const prev = prevStateRef.current
|
|
79
|
+
if (prev && isActiveState(prev as never) && current && !isActiveState(current as never)) {
|
|
80
|
+
if (current === 'READY') {
|
|
81
|
+
toast.push({ status: 'success', title: `${target.name} deployed`, description: 'Build completed successfully' })
|
|
82
|
+
} else if (current === 'ERROR') {
|
|
83
|
+
toast.push({ status: 'error', title: `${target.name} failed`, description: 'Build encountered an error — check error details' })
|
|
84
|
+
} else if (current === 'CANCELED') {
|
|
85
|
+
toast.push({ status: 'warning', title: `${target.name} canceled`, description: 'Deployment was canceled' })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
prevStateRef.current = current
|
|
89
|
+
}, [latest?.state, target.name, toast])
|
|
90
|
+
|
|
72
91
|
useEffect(() => {
|
|
73
92
|
setShowErrorLogs(false)
|
|
74
93
|
setErrorLines([])
|
|
@@ -94,7 +113,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
94
113
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
95
114
|
const deploy = useCallback(async () => {
|
|
96
115
|
setDeployError(null)
|
|
97
|
-
setTriggering(true)
|
|
116
|
+
flushSync(() => setTriggering(true))
|
|
98
117
|
try {
|
|
99
118
|
await triggerDeploy(target.url)
|
|
100
119
|
setTimeout(fetchDeployments, 2000)
|
|
@@ -119,11 +138,13 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
119
138
|
|
|
120
139
|
const copyUrl = useCallback(() => {
|
|
121
140
|
if (!latest?.url) return
|
|
122
|
-
|
|
141
|
+
const fullUrl = `https://${latest.url}`
|
|
142
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
123
143
|
setCopied(true)
|
|
124
144
|
setTimeout(() => setCopied(false), 2000)
|
|
125
|
-
}).catch(
|
|
126
|
-
|
|
145
|
+
}).catch(() => {
|
|
146
|
+
// Clipboard API unavailable — surface the URL for manual copy
|
|
147
|
+
window.prompt('Copy deployment URL:', fullUrl)
|
|
127
148
|
})
|
|
128
149
|
}, [latest?.url])
|
|
129
150
|
|
|
@@ -143,7 +164,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
143
164
|
.filter(Boolean)
|
|
144
165
|
.reverse()
|
|
145
166
|
.slice(-30)
|
|
146
|
-
setErrorLines(lines.length > 0 ? lines : ['No
|
|
167
|
+
setErrorLines(lines.length > 0 ? lines : ['No stderr or stdout was captured for this build. Open the full build log in Vercel for details.'])
|
|
147
168
|
} catch (err) {
|
|
148
169
|
setLogError(err instanceof Error ? err.message : 'Failed to load build logs')
|
|
149
170
|
} finally {
|
|
@@ -161,6 +182,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
161
182
|
const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
|
|
162
183
|
const sha = shortSha(latest?.meta?.githubCommitSha)
|
|
163
184
|
const fullSha = latest?.meta?.githubCommitSha
|
|
185
|
+
const commitHref = githubCommitHref(latest?.meta)
|
|
164
186
|
const creator = latest?.creator?.username
|
|
165
187
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null
|
|
166
188
|
const vercelProjectUrl = projectHref(latest?.inspectorUrl)
|
|
@@ -193,10 +215,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
193
215
|
{token && !loadingInitial && (
|
|
194
216
|
<>
|
|
195
217
|
{triggering ? (
|
|
196
|
-
<
|
|
197
|
-
<Spinner muted />
|
|
198
|
-
<Badge tone="caution" padding={2}>Triggering…</Badge>
|
|
199
|
-
</Flex>
|
|
218
|
+
<Badge tone="caution" padding={2}>Triggering…</Badge>
|
|
200
219
|
) : (
|
|
201
220
|
<StatusBadge state={latest?.state} showSpinner />
|
|
202
221
|
)}
|
|
@@ -304,7 +323,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
304
323
|
</>
|
|
305
324
|
)}
|
|
306
325
|
|
|
307
|
-
{/* Commit SHA — tooltip shows full message */}
|
|
326
|
+
{/* Commit SHA — links to GitHub if repo info available, tooltip shows full message */}
|
|
308
327
|
{sha && (
|
|
309
328
|
<Tooltip
|
|
310
329
|
content={
|
|
@@ -314,13 +333,22 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
314
333
|
}
|
|
315
334
|
portal
|
|
316
335
|
>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
336
|
+
{commitHref ? (
|
|
337
|
+
<a
|
|
338
|
+
href={commitHref}
|
|
339
|
+
target="_blank"
|
|
340
|
+
rel="noreferrer"
|
|
341
|
+
style={{ color: 'inherit', textDecoration: 'none' }}
|
|
342
|
+
>
|
|
343
|
+
<Text size={1} muted style={{ cursor: 'pointer', fontFamily: 'monospace' }}>
|
|
344
|
+
{sha}
|
|
345
|
+
</Text>
|
|
346
|
+
</a>
|
|
347
|
+
) : (
|
|
348
|
+
<Text size={1} muted style={{ cursor: 'default', fontFamily: 'monospace' }}>
|
|
349
|
+
{sha}
|
|
350
|
+
</Text>
|
|
351
|
+
)}
|
|
324
352
|
</Tooltip>
|
|
325
353
|
)}
|
|
326
354
|
|
|
@@ -399,7 +427,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
399
427
|
)}
|
|
400
428
|
{!loadingLogs && !logError && errorLines.length > 0 && (
|
|
401
429
|
<Stack space={2}>
|
|
402
|
-
<Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize:
|
|
430
|
+
<Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize: 13, lineHeight: 1.6 }}>
|
|
403
431
|
{errorLines.map((line, i) => (
|
|
404
432
|
<Code key={i} size={1} style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
|
405
433
|
{line}
|
|
@@ -445,7 +473,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
445
473
|
style={{ width: '100%', justifyContent: 'flex-start', borderRadius: 0, cursor: 'pointer' }}
|
|
446
474
|
/>
|
|
447
475
|
{showDetails && (
|
|
448
|
-
<Card tone="primary" padding={3} style={{ borderRadius: 0 }}>
|
|
476
|
+
<Card tone="primary" padding={3} className="dvfs-accordion-content" style={{ borderRadius: 0, borderTop: '1px solid rgba(128,128,128,0.15)' }}>
|
|
449
477
|
<Stack space={2}>
|
|
450
478
|
<Flex gap={2} align="center">
|
|
451
479
|
<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'
|