@netlify/spark-ui 1.31.0-alpha.7 → 1.31.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/preact/DropZone/DropZone.js +2 -0
- package/dist/components/preact/DropZone/useDropDeploy.js +139 -134
- package/dist/drop-core/authedApi.d.ts +4 -12
- package/dist/drop-core/authedApi.js +19 -20
- package/dist/drop-core/client.js +31 -23
- package/dist/drop-core/client.test.d.ts +1 -0
- package/dist/drop-core/pollDeploy.d.ts +31 -0
- package/dist/drop-core/pollDeploy.js +14 -0
- package/dist/drop-core/pollDeploy.test.d.ts +1 -0
- package/package.json +1 -1
- package/packages/components/preact/DropZone/DropZone.tsx +2 -0
- package/packages/components/preact/DropZone/useDropDeploy.test.ts +3 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +12 -3
- package/packages/drop-core/authedApi.ts +15 -29
- package/packages/drop-core/client.test.ts +29 -0
- package/packages/drop-core/client.ts +20 -12
- package/packages/drop-core/pollDeploy.test.ts +50 -0
- package/packages/drop-core/pollDeploy.ts +46 -0
|
@@ -160,6 +160,8 @@ function ne({
|
|
|
160
160
|
return /* @__PURE__ */ e(o, { label: "Site’s live — taking you to claim it…" });
|
|
161
161
|
case "dashboard":
|
|
162
162
|
return /* @__PURE__ */ e(o, { label: "Site’s live — taking you to your new project…" });
|
|
163
|
+
default:
|
|
164
|
+
return null;
|
|
163
165
|
}
|
|
164
166
|
case "uploading": {
|
|
165
167
|
const c = r.total ? Math.round(r.uploaded / r.total * 100) : 0;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { d as
|
|
2
|
-
import { humanizeDropError as rt, isNotAuthenticatedError as
|
|
1
|
+
import { d as R, A as _, y as nt } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
2
|
+
import { humanizeDropError as rt, isNotAuthenticatedError as v } from "../../../drop-core/errors.js";
|
|
3
3
|
import { DROPZONE_EVENTS as c } from "../../../drop-core/analytics.js";
|
|
4
4
|
import { renameSingleNonIndexHtmlToIndex as at, getSingleNonIndexHtmlFileName as ot, hasMultipleHtmlFilesWithoutIndex as st } from "../../../drop-core/readFiles.js";
|
|
5
5
|
import { detectBuild as dt } from "../../../drop-core/detectBuild.js";
|
|
6
|
-
import { zipFiles as
|
|
7
|
-
import { AnonymousDropClient as
|
|
6
|
+
import { zipFiles as lt } from "../../../drop-core/zip.js";
|
|
7
|
+
import { AnonymousDropClient as ut } from "../../../drop-core/client.js";
|
|
8
8
|
import { AuthenticatedDropClient as ct } from "../../../drop-core/authedDropClient.js";
|
|
9
9
|
import { AuthenticatedBuildClient as mt } from "../../../drop-core/buildClient.js";
|
|
10
|
-
import { deployZippedBuild as ft, deployFiles as
|
|
11
|
-
import { hasDropStashMarker as gt, loadDropStash as ht, clearDropStash as
|
|
12
|
-
function
|
|
13
|
-
switch (
|
|
10
|
+
import { deployZippedBuild as ft, deployFiles as z } from "../../../drop-core/deploy.js";
|
|
11
|
+
import { hasDropStashMarker as gt, loadDropStash as ht, clearDropStash as I, saveDropStash as pt } from "../../../drop-core/buildStash.js";
|
|
12
|
+
function N(l) {
|
|
13
|
+
switch (l.phase) {
|
|
14
14
|
case "zipping":
|
|
15
15
|
return { kind: "zipping" };
|
|
16
16
|
case "creating":
|
|
17
17
|
return { kind: "creating" };
|
|
18
18
|
case "uploading":
|
|
19
|
-
return { kind: "uploading", uploaded:
|
|
19
|
+
return { kind: "uploading", uploaded: l.uploaded ?? 0, total: l.total ?? 0 };
|
|
20
20
|
case "building":
|
|
21
21
|
return { kind: "building" };
|
|
22
22
|
case "processing":
|
|
@@ -24,196 +24,201 @@ function I(u) {
|
|
|
24
24
|
return { kind: "processing" };
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
function wt(
|
|
28
|
-
const m = ot(
|
|
29
|
-
return m ? `No index.html found — "${m}" won’t be served at the site root.` : st(
|
|
27
|
+
function wt(l) {
|
|
28
|
+
const m = ot(l);
|
|
29
|
+
return m ? `No index.html found — "${m}" won’t be served at the site root.` : st(l) ? "No index.html found — your site may not have a landing page." : null;
|
|
30
30
|
}
|
|
31
|
-
function St(
|
|
31
|
+
function St(l, m) {
|
|
32
32
|
try {
|
|
33
|
-
localStorage.setItem("drop-token",
|
|
33
|
+
localStorage.setItem("drop-token", l), localStorage.setItem("dropSiteName", m);
|
|
34
34
|
} catch {
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
function Ft({
|
|
38
|
-
apiBase:
|
|
38
|
+
apiBase: l,
|
|
39
39
|
client: m,
|
|
40
|
-
claimUrl:
|
|
41
|
-
siteDashboardUrl:
|
|
42
|
-
buildSignupUrl:
|
|
43
|
-
buildClient:
|
|
44
|
-
authenticatedDropClient:
|
|
45
|
-
getUploadToken:
|
|
46
|
-
proxyBase:
|
|
47
|
-
accountSlug:
|
|
48
|
-
buildDeployUrl:
|
|
49
|
-
waitForBuild:
|
|
50
|
-
onBuildRequired:
|
|
51
|
-
onBuildSignupHandoff:
|
|
52
|
-
resumeStashedBuilds:
|
|
53
|
-
onDeploy:
|
|
40
|
+
claimUrl: Y,
|
|
41
|
+
siteDashboardUrl: H,
|
|
42
|
+
buildSignupUrl: W,
|
|
43
|
+
buildClient: M,
|
|
44
|
+
authenticatedDropClient: Z,
|
|
45
|
+
getUploadToken: w,
|
|
46
|
+
proxyBase: A,
|
|
47
|
+
accountSlug: L,
|
|
48
|
+
buildDeployUrl: C,
|
|
49
|
+
waitForBuild: $,
|
|
50
|
+
onBuildRequired: F,
|
|
51
|
+
onBuildSignupHandoff: P,
|
|
52
|
+
resumeStashedBuilds: q = !0,
|
|
53
|
+
onDeploy: k,
|
|
54
54
|
onBuildDeploy: f,
|
|
55
55
|
onError: g,
|
|
56
|
-
track:
|
|
56
|
+
track: U
|
|
57
57
|
}) {
|
|
58
|
-
const
|
|
58
|
+
const u = (n, t) => (
|
|
59
59
|
// The event interfaces carry no index signature, hence the cast to TrackFn's record.
|
|
60
|
-
|
|
61
|
-
), [
|
|
62
|
-
|
|
63
|
-
).current,
|
|
64
|
-
|
|
65
|
-
).current,
|
|
66
|
-
async function
|
|
60
|
+
U(n, t)
|
|
61
|
+
), [b, o] = R({ kind: "idle" }), [j, D] = R(null), J = _(m ?? new ut(l)).current, S = _(
|
|
62
|
+
M ?? (w ? new mt({ proxyBase: A, getUploadToken: w, accountSlug: L }) : null)
|
|
63
|
+
).current, E = _(
|
|
64
|
+
Z ?? (w ? new ct({ proxyBase: A, getUploadToken: w, accountSlug: L }) : null)
|
|
65
|
+
).current, Q = b.kind !== "idle" && b.kind !== "error";
|
|
66
|
+
async function x(n, t) {
|
|
67
67
|
try {
|
|
68
|
-
const { site:
|
|
69
|
-
onProgress: (
|
|
70
|
-
waitForBuild:
|
|
68
|
+
const { site: i, build: r, buildSettings: s, url: a } = await ft(n, t, {
|
|
69
|
+
onProgress: (e) => o(N(e)),
|
|
70
|
+
waitForBuild: $
|
|
71
71
|
});
|
|
72
72
|
return {
|
|
73
|
-
url:
|
|
74
|
-
siteName:
|
|
75
|
-
siteId:
|
|
73
|
+
url: a,
|
|
74
|
+
siteName: i.name,
|
|
75
|
+
siteId: i.id,
|
|
76
76
|
buildId: r.id,
|
|
77
77
|
deployId: r.deploy_id,
|
|
78
|
-
buildSettings:
|
|
78
|
+
buildSettings: s
|
|
79
79
|
};
|
|
80
|
-
} catch (
|
|
81
|
-
if (
|
|
82
|
-
throw
|
|
80
|
+
} catch (i) {
|
|
81
|
+
if (v(i)) return null;
|
|
82
|
+
throw i;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
const r =
|
|
88
|
-
|
|
85
|
+
async function T(n, t, i) {
|
|
86
|
+
D(null), o({ kind: "reading" });
|
|
87
|
+
const r = i.length ? { file_types: i } : {}, s = (e, d, h) => {
|
|
88
|
+
o({ kind: "error", message: e }), u(c.DEPLOY_FAILED, { method: t, reason: d, ...r, ...h }), g == null || g({ message: e, reason: d });
|
|
89
89
|
};
|
|
90
|
+
let a;
|
|
90
91
|
try {
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
93
|
-
|
|
92
|
+
const e = at(await n());
|
|
93
|
+
if (a = e.length, !e.length) {
|
|
94
|
+
s("No files found to deploy.", "No files found to deploy.", { file_count: 0 });
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
method:
|
|
98
|
-
file_count:
|
|
97
|
+
t === "drag" && u(c.FILES_DROPPED, {
|
|
98
|
+
method: t,
|
|
99
|
+
file_count: e.length,
|
|
99
100
|
...r
|
|
100
101
|
});
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
} catch (
|
|
104
|
-
const
|
|
105
|
-
|
|
102
|
+
const d = { source: t, fileCount: e.length, typeProps: r }, h = dt(e);
|
|
103
|
+
h.buildRequired ? await V(e, h, d) : await K(e, d);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const d = e instanceof Error ? e.message : String(e);
|
|
106
|
+
s(
|
|
107
|
+
rt(e),
|
|
108
|
+
d,
|
|
109
|
+
a !== void 0 ? { file_count: a } : void 0
|
|
110
|
+
);
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
|
-
async function
|
|
109
|
-
if (
|
|
110
|
-
method:
|
|
111
|
-
reason:
|
|
113
|
+
async function V(n, t, { source: i, fileCount: r, typeProps: s }) {
|
|
114
|
+
if (u(c.BUILD_REQUIRED, {
|
|
115
|
+
method: i,
|
|
116
|
+
reason: t.reason,
|
|
112
117
|
file_count: r,
|
|
113
|
-
...
|
|
114
|
-
}),
|
|
115
|
-
|
|
118
|
+
...s
|
|
119
|
+
}), F) {
|
|
120
|
+
o({ kind: "idle" }), F(t);
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
118
|
-
let
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
const
|
|
123
|
+
let a = !1;
|
|
124
|
+
if (S) {
|
|
125
|
+
o({ kind: "zipping" });
|
|
126
|
+
const e = await lt(n), d = await x(S, e);
|
|
122
127
|
if (d) {
|
|
123
|
-
|
|
124
|
-
method:
|
|
125
|
-
reason:
|
|
128
|
+
o({ kind: "redirecting", target: "build" }), u(c.BUILD_DEPLOY_SUCCEEDED, {
|
|
129
|
+
method: i,
|
|
130
|
+
reason: t.reason,
|
|
126
131
|
site_name: d.siteName,
|
|
127
132
|
file_count: r,
|
|
128
133
|
...d.buildSettings ? { framework: d.buildSettings.framework } : {},
|
|
129
|
-
...
|
|
130
|
-
}), f == null || f(d), window.location.assign(
|
|
134
|
+
...s
|
|
135
|
+
}), f == null || f(d), window.location.assign(C(d));
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
|
-
|
|
134
|
-
method:
|
|
135
|
-
reason:
|
|
138
|
+
a = await pt(e.file, e.buildSettings), a && u(c.PROJECT_STASHED, {
|
|
139
|
+
method: i,
|
|
140
|
+
reason: t.reason,
|
|
136
141
|
file_count: r,
|
|
137
|
-
...
|
|
142
|
+
...s
|
|
138
143
|
});
|
|
139
144
|
}
|
|
140
|
-
if (
|
|
141
|
-
method:
|
|
145
|
+
if (a || u(c.DEPLOY_FAILED, {
|
|
146
|
+
method: i,
|
|
142
147
|
reason: "anonymous_build_not_supported",
|
|
143
148
|
file_count: r,
|
|
144
|
-
...
|
|
145
|
-
}),
|
|
146
|
-
|
|
149
|
+
...s
|
|
150
|
+
}), P) {
|
|
151
|
+
o({ kind: "idle" }), P({ stashed: a, detection: t });
|
|
147
152
|
return;
|
|
148
153
|
}
|
|
149
|
-
|
|
154
|
+
o({ kind: "redirecting", target: "signup" }), window.location.assign(W);
|
|
150
155
|
}
|
|
151
|
-
async function
|
|
156
|
+
async function G(n, t) {
|
|
152
157
|
try {
|
|
153
|
-
return await
|
|
154
|
-
onProgress: (
|
|
158
|
+
return await z(n, t, {
|
|
159
|
+
onProgress: (i) => o(N(i))
|
|
155
160
|
});
|
|
156
|
-
} catch (
|
|
157
|
-
if (
|
|
158
|
-
throw
|
|
161
|
+
} catch (i) {
|
|
162
|
+
if (v(i)) return null;
|
|
163
|
+
throw i;
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
|
-
async function
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
onProgress: (et) =>
|
|
165
|
-
}), { url:
|
|
166
|
-
|
|
167
|
-
method:
|
|
168
|
-
site_name:
|
|
169
|
-
file_count:
|
|
170
|
-
authenticated:
|
|
171
|
-
...
|
|
166
|
+
async function K(n, { source: t, fileCount: i, typeProps: r }) {
|
|
167
|
+
D(wt(n));
|
|
168
|
+
const s = E ? await G(E, n) : null, a = s !== null, e = E !== null && !a, d = s ?? await z(J, n, {
|
|
169
|
+
onProgress: (et) => o(N(et))
|
|
170
|
+
}), { url: h, token: y, deploy: p } = d;
|
|
171
|
+
a || St(y, p.subdomain), o({ kind: "redirecting", target: a ? "dashboard" : "claim" }), u(c.DEPLOY_SUCCEEDED, {
|
|
172
|
+
method: t,
|
|
173
|
+
site_name: p.subdomain,
|
|
174
|
+
file_count: i,
|
|
175
|
+
authenticated: a,
|
|
176
|
+
...e ? { auth_fallback: !0 } : {},
|
|
172
177
|
...r
|
|
173
|
-
}),
|
|
174
|
-
url:
|
|
175
|
-
...
|
|
176
|
-
siteName:
|
|
177
|
-
siteId:
|
|
178
|
-
authenticated:
|
|
179
|
-
}),
|
|
178
|
+
}), k == null || k({
|
|
179
|
+
url: h,
|
|
180
|
+
...a ? {} : { token: y },
|
|
181
|
+
siteName: p.subdomain,
|
|
182
|
+
siteId: p.id,
|
|
183
|
+
authenticated: a
|
|
184
|
+
}), X(a, p.subdomain, y);
|
|
180
185
|
}
|
|
181
|
-
function
|
|
182
|
-
n ? window.location.assign(
|
|
186
|
+
function X(n, t, i) {
|
|
187
|
+
n ? window.location.assign(H(t)) : window.location.assign(`${Y(t)}#drop_token=${i}`);
|
|
183
188
|
}
|
|
184
|
-
async function
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
await
|
|
189
|
+
async function B(n) {
|
|
190
|
+
const t = await ht();
|
|
191
|
+
if (!t) {
|
|
192
|
+
await I();
|
|
188
193
|
return;
|
|
189
194
|
}
|
|
190
|
-
|
|
191
|
-
const
|
|
195
|
+
o({ kind: "creating" });
|
|
196
|
+
const i = { file: t.zipFile, buildSettings: t.buildSettings };
|
|
192
197
|
let r;
|
|
193
198
|
try {
|
|
194
|
-
r = await
|
|
195
|
-
} catch (
|
|
196
|
-
await
|
|
199
|
+
r = await x(n, i);
|
|
200
|
+
} catch (s) {
|
|
201
|
+
await I(), tt(s instanceof Error ? s.message : String(s));
|
|
197
202
|
return;
|
|
198
203
|
}
|
|
199
204
|
if (!r) {
|
|
200
|
-
|
|
205
|
+
o({ kind: "idle" });
|
|
201
206
|
return;
|
|
202
207
|
}
|
|
203
|
-
await
|
|
208
|
+
u(c.RESUMED, {}), await I(), o({ kind: "redirecting", target: "build" }), f == null || f(r), window.location.assign(C(r));
|
|
204
209
|
}
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
|
|
210
|
+
function tt(n) {
|
|
211
|
+
const t = "We couldn’t automatically deploy your project. Drop it again to publish it.";
|
|
212
|
+
o({ kind: "error", message: t }), u(c.DEPLOY_FAILED, { reason: "resume_failed" }), g == null || g({ message: t, reason: n });
|
|
208
213
|
}
|
|
209
|
-
const
|
|
214
|
+
const O = _(!1);
|
|
210
215
|
nt(() => {
|
|
211
|
-
|
|
216
|
+
O.current || !q || !S || !gt() || (O.current = !0, B(S));
|
|
212
217
|
}, []);
|
|
213
|
-
function
|
|
214
|
-
|
|
218
|
+
function it() {
|
|
219
|
+
D(null), o({ kind: "idle" });
|
|
215
220
|
}
|
|
216
|
-
return { status:
|
|
221
|
+
return { status: b, warning: j, busy: Q, deploy: T, reset: it };
|
|
217
222
|
}
|
|
218
223
|
export {
|
|
219
224
|
Ft as useDropDeploy
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BuildSite } from './types';
|
|
2
|
+
import { SettledDeploy } from './pollDeploy';
|
|
3
|
+
export type { SettledDeploy } from './pollDeploy';
|
|
2
4
|
/**
|
|
3
5
|
* Transport shared by both authenticated clients: JSON rides the consumer's
|
|
4
6
|
* access-control rewrite on the session cookie. Uploads are each client's own
|
|
@@ -10,19 +12,9 @@ import { BuildSite } from './types';
|
|
|
10
12
|
* fallback is still safe, hence NotAuthenticatedError.
|
|
11
13
|
*/
|
|
12
14
|
export declare function createSiteInAccount(proxyBase: string, accountSlug?: string): Promise<BuildSite>;
|
|
13
|
-
/** How a watched deploy ended. `errorMessage` is the API's own explanation, when it gave one. */
|
|
14
|
-
export type SettledDeploy = {
|
|
15
|
-
outcome: 'ready';
|
|
16
|
-
} | {
|
|
17
|
-
outcome: 'failed';
|
|
18
|
-
errorMessage?: string;
|
|
19
|
-
} | {
|
|
20
|
-
outcome: 'timed-out';
|
|
21
|
-
};
|
|
22
15
|
/**
|
|
23
|
-
* Polls
|
|
24
|
-
*
|
|
25
|
-
* not fatal: a proxy hiccup shouldn't kill a progressing deploy.
|
|
16
|
+
* Polls a deploy over the session-cookie proxy — the shared `pollUntilSettled`
|
|
17
|
+
* loop with the cookie transport bound in. A non-ok read is skipped, not fatal.
|
|
26
18
|
*/
|
|
27
19
|
export declare function pollDeployUntilSettled(options: {
|
|
28
20
|
proxyBase: string;
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import { NotAuthenticatedError as
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { NotAuthenticatedError as a, SiteCreateError as l } from "./errors.js";
|
|
2
|
+
import { pollUntilSettled as c } from "./pollDeploy.js";
|
|
3
|
+
async function u(o, e) {
|
|
4
|
+
const n = e ? `/${e}/sites` : "/sites", t = await fetch(`${o}${n}`, {
|
|
4
5
|
method: "POST",
|
|
5
6
|
credentials: "include",
|
|
6
7
|
headers: { "Content-Type": "application/json" },
|
|
7
8
|
body: JSON.stringify({ created_via: "drop" })
|
|
8
9
|
});
|
|
9
|
-
if (
|
|
10
|
-
if (!
|
|
11
|
-
return
|
|
10
|
+
if (t.status === 401 || t.status === 403) throw new a();
|
|
11
|
+
if (!t.ok) throw new l(t.status);
|
|
12
|
+
return t.json();
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
function f(o) {
|
|
15
|
+
const { proxyBase: e, deployId: n, pollIntervalMs: t, timeoutMs: s } = o;
|
|
16
|
+
return c({
|
|
17
|
+
deployId: n,
|
|
18
|
+
pollIntervalMs: t,
|
|
19
|
+
timeoutMs: s,
|
|
20
|
+
readState: async (i) => {
|
|
21
|
+
const r = await fetch(`${e}/deploys/${i}`, { credentials: "include" });
|
|
22
|
+
return r.ok ? r.json() : null;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
return { outcome: "timed-out" };
|
|
24
|
+
});
|
|
26
25
|
}
|
|
27
26
|
export {
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
u as createSiteInAccount,
|
|
28
|
+
f as pollDeployUntilSettled
|
|
30
29
|
};
|
package/dist/drop-core/client.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { DEFAULT_API_BASE as
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { DEFAULT_API_BASE as n, DEFAULT_POLL_INTERVAL_MS as l, DEFAULT_TIMEOUT_MS as p } from "./constants.js";
|
|
2
|
+
import { DeployFailedError as d, DeployTimeoutError as c } from "./errors.js";
|
|
3
|
+
import { pollUntilSettled as h } from "./pollDeploy.js";
|
|
4
|
+
function a(i, t) {
|
|
5
|
+
return Object.assign(new Error(`${i} failed: ${t}`), { status: t });
|
|
5
6
|
}
|
|
6
|
-
class
|
|
7
|
-
constructor(t =
|
|
7
|
+
class m {
|
|
8
|
+
constructor(t = n, o = l, e = p) {
|
|
8
9
|
this.apiBase = t, this.pollIntervalMs = o, this.timeoutMs = e;
|
|
9
10
|
}
|
|
10
11
|
async getToken() {
|
|
11
12
|
const t = await fetch(`${this.apiBase}/drop/token`, { method: "POST" });
|
|
12
|
-
if (!t.ok) throw
|
|
13
|
+
if (!t.ok) throw a("drop/token", t.status);
|
|
13
14
|
return (await t.json()).token;
|
|
14
15
|
}
|
|
15
16
|
async createDeploy(t, o) {
|
|
@@ -18,30 +19,37 @@ class y {
|
|
|
18
19
|
headers: { "Content-Type": "application/json" },
|
|
19
20
|
body: JSON.stringify({ files: t, token: o })
|
|
20
21
|
});
|
|
21
|
-
if (!e.ok) throw
|
|
22
|
+
if (!e.ok) throw a("drop", e.status);
|
|
22
23
|
return e.json();
|
|
23
24
|
}
|
|
24
|
-
async uploadFile(t, o, e,
|
|
25
|
-
const
|
|
25
|
+
async uploadFile(t, o, e, s) {
|
|
26
|
+
const r = await fetch(`${this.apiBase}/deploys/${t}/files${o}`, {
|
|
26
27
|
method: "PUT",
|
|
27
|
-
headers: { "Content-Type": "application/octet-stream", Authorization: `Bearer ${
|
|
28
|
+
headers: { "Content-Type": "application/octet-stream", Authorization: `Bearer ${s}` },
|
|
28
29
|
body: e
|
|
29
30
|
});
|
|
30
|
-
if (!
|
|
31
|
+
if (!r.ok) throw a(`upload ${o}`, r.status);
|
|
31
32
|
}
|
|
32
33
|
async waitUntilReady(t, o) {
|
|
33
|
-
const e =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const e = await h({
|
|
35
|
+
deployId: t.deploy_id,
|
|
36
|
+
pollIntervalMs: this.pollIntervalMs,
|
|
37
|
+
timeoutMs: this.timeoutMs,
|
|
38
|
+
// `/deploys/{id}` expects the deploy_id (BSON), not the site id (UUID) that
|
|
39
|
+
// lives at `deploy.id`. See drop_controller.rb; Deploy.find(uuid) is nil and
|
|
40
|
+
// the request 401s misleadingly via require_current_actor!.
|
|
41
|
+
readState: async (s) => {
|
|
42
|
+
const r = await fetch(`${this.apiBase}/deploys/${s}`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${o}` }
|
|
44
|
+
});
|
|
45
|
+
return r.ok ? r.json() : null;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (e.outcome !== "ready")
|
|
49
|
+
throw e.outcome === "failed" ? new d(e.errorMessage) : new c();
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
export {
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
m as AnonymousDropClient,
|
|
54
|
+
a as apiError
|
|
47
55
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single deploy poller both drop clients share. The loop and its
|
|
3
|
+
* terminal-state policy live here once; transport (cookie proxy vs bearer) is
|
|
4
|
+
* injected via `readState`, so the anonymous and authenticated paths can't
|
|
5
|
+
* drift on what "done" means.
|
|
6
|
+
*/
|
|
7
|
+
/** How a watched deploy ended. `errorMessage` is the API's own explanation, when it gave one. */
|
|
8
|
+
export type SettledDeploy = {
|
|
9
|
+
outcome: 'ready';
|
|
10
|
+
} | {
|
|
11
|
+
outcome: 'failed';
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
} | {
|
|
14
|
+
outcome: 'timed-out';
|
|
15
|
+
};
|
|
16
|
+
/** The slice of a deploy record the poller reads; `null` = a read that failed and should be skipped. */
|
|
17
|
+
export interface DeployState {
|
|
18
|
+
state?: string;
|
|
19
|
+
error_message?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Poll until the deploy reaches a terminal state or the timeout elapses.
|
|
23
|
+
* A `null` read is skipped, not fatal — a transient hiccup shouldn't kill a
|
|
24
|
+
* progressing deploy. Callers map the outcome onto their own error vocabulary.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pollUntilSettled(options: {
|
|
27
|
+
deployId: string;
|
|
28
|
+
pollIntervalMs: number;
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
readState: (deployId: string) => Promise<DeployState | null>;
|
|
31
|
+
}): Promise<SettledDeploy>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const c = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
|
|
2
|
+
async function l(r) {
|
|
3
|
+
const { deployId: a, pollIntervalMs: t, timeoutMs: s, readState: n } = r, i = s / t;
|
|
4
|
+
for (let o = 0; o < i; o++) {
|
|
5
|
+
const e = await n(a);
|
|
6
|
+
if (e != null && e.state && c.has(e.state))
|
|
7
|
+
return e.state === "ready" ? { outcome: "ready" } : { outcome: "failed", errorMessage: e.error_message };
|
|
8
|
+
await new Promise((d) => setTimeout(d, t));
|
|
9
|
+
}
|
|
10
|
+
return { outcome: "timed-out" };
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
l as pollUntilSettled
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/spark-ui",
|
|
3
3
|
"description": "Assets, design tokens, components, and utilities",
|
|
4
|
-
"version": "1.31.0-alpha.
|
|
4
|
+
"version": "1.31.0-alpha.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/components/preact/index.js",
|
|
7
7
|
"types": "dist/components/preact/index.d.ts",
|
|
@@ -309,6 +309,8 @@ function StatusView({
|
|
|
309
309
|
return <Spinner label="Site’s live — taking you to claim it…" />;
|
|
310
310
|
case 'dashboard':
|
|
311
311
|
return <Spinner label="Site’s live — taking you to your new project…" />;
|
|
312
|
+
default:
|
|
313
|
+
return null;
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
case 'uploading': {
|
|
@@ -346,6 +346,9 @@ describe('stash resume on mount', () => {
|
|
|
346
346
|
expect(clearDropStash).not.toHaveBeenCalled(); // preserved for the next authenticated visit
|
|
347
347
|
expect(harness.api.status.kind).toBe('idle');
|
|
348
348
|
expect(harness.errors).toHaveLength(0);
|
|
349
|
+
// Nothing deployed, so no resume was counted — otherwise every logged-out
|
|
350
|
+
// reload with a stash would inflate the metric.
|
|
351
|
+
expect(harness.events.map(e => e.event)).not.toContain(DROPZONE_EVENTS.RESUMED);
|
|
349
352
|
});
|
|
350
353
|
|
|
351
354
|
it('surfaces a real resume failure with re-drop copy, stash consumed', async () => {
|
|
@@ -268,11 +268,15 @@ export function useDropDeploy({
|
|
|
268
268
|
onError?.({ message, reason });
|
|
269
269
|
};
|
|
270
270
|
|
|
271
|
+
// Set once the file set is read, so a later throw still reports file_count.
|
|
272
|
+
let knownFileCount: number | undefined;
|
|
273
|
+
|
|
271
274
|
try {
|
|
272
275
|
// Rename a lone non-index HTML file so it is served at the site root.
|
|
273
276
|
// Warnings are computed on the renamed set, so the fixable single-file
|
|
274
277
|
// case no longer warns and only the multi-HTML case does.
|
|
275
278
|
const files = renameSingleNonIndexHtmlToIndex(await read());
|
|
279
|
+
knownFileCount = files.length;
|
|
276
280
|
if (!files.length) {
|
|
277
281
|
fail('No files found to deploy.', 'No files found to deploy.', { file_count: 0 });
|
|
278
282
|
return;
|
|
@@ -297,7 +301,11 @@ export function useDropDeploy({
|
|
|
297
301
|
}
|
|
298
302
|
} catch (err) {
|
|
299
303
|
const reason = err instanceof Error ? err.message : String(err);
|
|
300
|
-
fail(
|
|
304
|
+
fail(
|
|
305
|
+
humanizeDropError(err),
|
|
306
|
+
reason,
|
|
307
|
+
knownFileCount !== undefined ? { file_count: knownFileCount } : undefined
|
|
308
|
+
);
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
|
|
@@ -481,7 +489,6 @@ export function useDropDeploy({
|
|
|
481
489
|
return;
|
|
482
490
|
}
|
|
483
491
|
|
|
484
|
-
emit(DROPZONE_EVENTS.RESUMED, {});
|
|
485
492
|
setStatus({ kind: 'creating' });
|
|
486
493
|
|
|
487
494
|
const archive: ZipResult = { file: stash.zipFile, buildSettings: stash.buildSettings };
|
|
@@ -496,11 +503,13 @@ export function useDropDeploy({
|
|
|
496
503
|
|
|
497
504
|
if (!built) {
|
|
498
505
|
// Still no session — the visitor came back logged out. Keep the stash for
|
|
499
|
-
// their next authenticated visit.
|
|
506
|
+
// their next authenticated visit. No RESUMED event: nothing deployed.
|
|
500
507
|
setStatus({ kind: 'idle' });
|
|
501
508
|
return;
|
|
502
509
|
}
|
|
503
510
|
|
|
511
|
+
// Only now is a resume real — a logged-out attempt above never counts.
|
|
512
|
+
emit(DROPZONE_EVENTS.RESUMED, {});
|
|
504
513
|
await clearDropStash();
|
|
505
514
|
setStatus({ kind: 'redirecting', target: 'build' });
|
|
506
515
|
onBuildDeploy?.(built);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { BuildSite } from './types';
|
|
2
2
|
import { NotAuthenticatedError, SiteCreateError } from './errors';
|
|
3
|
+
import { pollUntilSettled, type SettledDeploy } from './pollDeploy';
|
|
4
|
+
|
|
5
|
+
export type { SettledDeploy } from './pollDeploy';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Transport shared by both authenticated clients: JSON rides the consumer's
|
|
@@ -28,41 +31,24 @@ export async function createSiteInAccount(
|
|
|
28
31
|
return res.json();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
/** How a watched deploy ended. `errorMessage` is the API's own explanation, when it gave one. */
|
|
32
|
-
export type SettledDeploy =
|
|
33
|
-
{ outcome: 'ready' } | { outcome: 'failed'; errorMessage?: string } | { outcome: 'timed-out' };
|
|
34
|
-
|
|
35
|
-
/** Deploy states that end the wait — `ready` succeeds, the rest are failures. */
|
|
36
|
-
const TERMINAL_STATES = new Set(['ready', 'error', 'rejected']);
|
|
37
|
-
|
|
38
|
-
interface DeployState {
|
|
39
|
-
state?: string;
|
|
40
|
-
error_message?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
34
|
/**
|
|
44
|
-
* Polls
|
|
45
|
-
*
|
|
46
|
-
* not fatal: a proxy hiccup shouldn't kill a progressing deploy.
|
|
35
|
+
* Polls a deploy over the session-cookie proxy — the shared `pollUntilSettled`
|
|
36
|
+
* loop with the cookie transport bound in. A non-ok read is skipped, not fatal.
|
|
47
37
|
*/
|
|
48
|
-
export
|
|
38
|
+
export function pollDeployUntilSettled(options: {
|
|
49
39
|
proxyBase: string;
|
|
50
40
|
deployId: string;
|
|
51
41
|
pollIntervalMs: number;
|
|
52
42
|
timeoutMs: number;
|
|
53
43
|
}): Promise<SettledDeploy> {
|
|
54
44
|
const { proxyBase, deployId, pollIntervalMs, timeoutMs } = options;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
66
|
-
}
|
|
67
|
-
return { outcome: 'timed-out' };
|
|
45
|
+
return pollUntilSettled({
|
|
46
|
+
deployId,
|
|
47
|
+
pollIntervalMs,
|
|
48
|
+
timeoutMs,
|
|
49
|
+
readState: async id => {
|
|
50
|
+
const res = await fetch(`${proxyBase}/deploys/${id}`, { credentials: 'include' });
|
|
51
|
+
return res.ok ? res.json() : null;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
68
54
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { AnonymousDropClient } from './client';
|
|
3
|
+
import { DeployFailedError } from './errors';
|
|
4
|
+
import type { DropResponse } from './types';
|
|
5
|
+
|
|
6
|
+
const deploy: DropResponse = { id: 'site', deploy_id: 'd', subdomain: 'x', required: [] };
|
|
7
|
+
const client = () => new AnonymousDropClient('https://api', 1, 50);
|
|
8
|
+
|
|
9
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
10
|
+
|
|
11
|
+
describe('AnonymousDropClient.waitUntilReady', () => {
|
|
12
|
+
it('resolves once the deploy is ready', async () => {
|
|
13
|
+
vi.stubGlobal('fetch', async () => ({ ok: true, json: async () => ({ state: 'ready' }) }));
|
|
14
|
+
await expect(client().waitUntilReady(deploy, 'tok')).resolves.toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Regression: a failed anonymous deploy used to poll to the full timeout.
|
|
18
|
+
it('fails fast on a failed deploy instead of timing out', async () => {
|
|
19
|
+
vi.stubGlobal('fetch', async () => ({
|
|
20
|
+
ok: true,
|
|
21
|
+
json: async () => ({ state: 'error', error_message: 'nope' }),
|
|
22
|
+
}));
|
|
23
|
+
const err = await client()
|
|
24
|
+
.waitUntilReady(deploy, 'tok')
|
|
25
|
+
.catch(e => e);
|
|
26
|
+
expect(err).toBeInstanceOf(DeployFailedError);
|
|
27
|
+
expect(err.reason).toBe('nope');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* interface, so `deployFiles` runs the same pipeline anonymous or authenticated.
|
|
4
4
|
*/
|
|
5
5
|
import { DEFAULT_API_BASE, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS } from './constants';
|
|
6
|
-
import { DeployTimeoutError } from './errors';
|
|
6
|
+
import { DeployFailedError, DeployTimeoutError } from './errors';
|
|
7
|
+
import { pollUntilSettled } from './pollDeploy';
|
|
7
8
|
import type { DropResponse, Digest } from './types';
|
|
8
9
|
|
|
9
10
|
/** A normal Error with the response's HTTP status attached, so callers can branch on it directly. */
|
|
@@ -61,17 +62,24 @@ export class AnonymousDropClient implements DropClient {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
async waitUntilReady(deploy: DropResponse, token: string): Promise<void> {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
const settled = await pollUntilSettled({
|
|
66
|
+
deployId: deploy.deploy_id,
|
|
67
|
+
pollIntervalMs: this.pollIntervalMs,
|
|
68
|
+
timeoutMs: this.timeoutMs,
|
|
69
|
+
// `/deploys/{id}` expects the deploy_id (BSON), not the site id (UUID) that
|
|
70
|
+
// lives at `deploy.id`. See drop_controller.rb; Deploy.find(uuid) is nil and
|
|
71
|
+
// the request 401s misleadingly via require_current_actor!.
|
|
72
|
+
readState: async id => {
|
|
73
|
+
const res = await fetch(`${this.apiBase}/deploys/${id}`, {
|
|
74
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
75
|
+
});
|
|
76
|
+
return res.ok ? res.json() : null;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (settled.outcome === 'ready') return;
|
|
80
|
+
// A failed deploy now surfaces immediately instead of waiting out the full
|
|
81
|
+
// timeout, matching the authenticated path.
|
|
82
|
+
if (settled.outcome === 'failed') throw new DeployFailedError(settled.errorMessage);
|
|
75
83
|
throw new DeployTimeoutError();
|
|
76
84
|
}
|
|
77
85
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { pollUntilSettled } from './pollDeploy';
|
|
3
|
+
|
|
4
|
+
const base = { deployId: 'd', pollIntervalMs: 1, timeoutMs: 50 };
|
|
5
|
+
|
|
6
|
+
describe('pollUntilSettled', () => {
|
|
7
|
+
it('resolves ready when the deploy reaches ready', async () => {
|
|
8
|
+
const settled = await pollUntilSettled({ ...base, readState: async () => ({ state: 'ready' }) });
|
|
9
|
+
expect(settled).toEqual({ outcome: 'ready' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('reports failure with the API message on error/rejected', async () => {
|
|
13
|
+
for (const state of ['error', 'rejected']) {
|
|
14
|
+
const settled = await pollUntilSettled({
|
|
15
|
+
...base,
|
|
16
|
+
readState: async () => ({ state, error_message: 'boom' }),
|
|
17
|
+
});
|
|
18
|
+
expect(settled).toEqual({ outcome: 'failed', errorMessage: 'boom' });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('times out when the deploy never settles (does not hang on failure states either)', async () => {
|
|
23
|
+
const settled = await pollUntilSettled({
|
|
24
|
+
...base,
|
|
25
|
+
timeoutMs: 3,
|
|
26
|
+
readState: async () => ({ state: 'building' }),
|
|
27
|
+
});
|
|
28
|
+
expect(settled).toEqual({ outcome: 'timed-out' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('skips a null read without settling, then resolves', async () => {
|
|
32
|
+
let calls = 0;
|
|
33
|
+
const settled = await pollUntilSettled({
|
|
34
|
+
...base,
|
|
35
|
+
readState: async () => (++calls < 3 ? null : { state: 'ready' }),
|
|
36
|
+
});
|
|
37
|
+
expect(settled).toEqual({ outcome: 'ready' });
|
|
38
|
+
expect(calls).toBeGreaterThanOrEqual(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('waits through non-terminal states then settles on the terminal one', async () => {
|
|
42
|
+
const states = ['uploading', 'processing', 'ready'];
|
|
43
|
+
let i = 0;
|
|
44
|
+
const settled = await pollUntilSettled({
|
|
45
|
+
...base,
|
|
46
|
+
readState: async () => ({ state: states[Math.min(i++, states.length - 1)] }),
|
|
47
|
+
});
|
|
48
|
+
expect(settled).toEqual({ outcome: 'ready' });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single deploy poller both drop clients share. The loop and its
|
|
3
|
+
* terminal-state policy live here once; transport (cookie proxy vs bearer) is
|
|
4
|
+
* injected via `readState`, so the anonymous and authenticated paths can't
|
|
5
|
+
* drift on what "done" means.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** How a watched deploy ended. `errorMessage` is the API's own explanation, when it gave one. */
|
|
9
|
+
export type SettledDeploy =
|
|
10
|
+
| { outcome: 'ready' }
|
|
11
|
+
| { outcome: 'failed'; errorMessage?: string }
|
|
12
|
+
| { outcome: 'timed-out' };
|
|
13
|
+
|
|
14
|
+
/** The slice of a deploy record the poller reads; `null` = a read that failed and should be skipped. */
|
|
15
|
+
export interface DeployState {
|
|
16
|
+
state?: string;
|
|
17
|
+
error_message?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `ready` succeeds; `error`/`rejected` are failures; anything else keeps waiting. */
|
|
21
|
+
const TERMINAL_STATES = new Set(['ready', 'error', 'rejected']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Poll until the deploy reaches a terminal state or the timeout elapses.
|
|
25
|
+
* A `null` read is skipped, not fatal — a transient hiccup shouldn't kill a
|
|
26
|
+
* progressing deploy. Callers map the outcome onto their own error vocabulary.
|
|
27
|
+
*/
|
|
28
|
+
export async function pollUntilSettled(options: {
|
|
29
|
+
deployId: string;
|
|
30
|
+
pollIntervalMs: number;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
readState: (deployId: string) => Promise<DeployState | null>;
|
|
33
|
+
}): Promise<SettledDeploy> {
|
|
34
|
+
const { deployId, pollIntervalMs, timeoutMs, readState } = options;
|
|
35
|
+
const deadline = timeoutMs / pollIntervalMs;
|
|
36
|
+
for (let i = 0; i < deadline; i++) {
|
|
37
|
+
const deploy = await readState(deployId);
|
|
38
|
+
if (deploy?.state && TERMINAL_STATES.has(deploy.state)) {
|
|
39
|
+
return deploy.state === 'ready'
|
|
40
|
+
? { outcome: 'ready' }
|
|
41
|
+
: { outcome: 'failed', errorMessage: deploy.error_message };
|
|
42
|
+
}
|
|
43
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
44
|
+
}
|
|
45
|
+
return { outcome: 'timed-out' };
|
|
46
|
+
}
|