@netlify/spark-ui 1.31.0-alpha.3 → 1.31.0-alpha.4
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/index.d.ts +2 -2
- package/dist/components/preact/DropZone/DropZone.d.ts +21 -2
- package/dist/components/preact/DropZone/DropZone.js +59 -54
- package/dist/components/preact/DropZone/DropZone.test.d.ts +1 -0
- package/dist/components/preact/DropZone/index.d.ts +3 -3
- package/dist/components/preact/DropZone/index.js +25 -23
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +10 -6
- package/dist/components/preact/DropZone/useDropDeploy.js +184 -131
- package/dist/components/preact/DropZone/useDropDeploy.test.d.ts +1 -0
- package/dist/components/preact/index.d.ts +2 -2
- package/dist/components/preact/index.js +25 -19
- package/dist/drop-core/analytics.d.ts +107 -0
- package/dist/drop-core/analytics.js +21 -0
- package/dist/drop-core/analytics.test.d.ts +1 -0
- package/dist/drop-core/authedApi.d.ts +55 -0
- package/dist/drop-core/authedApi.js +35 -0
- package/dist/drop-core/authedApi.test.d.ts +1 -0
- package/dist/drop-core/authedDropClient.js +109 -0
- package/dist/drop-core/authedDropClient.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/buildClient.d.ts +5 -13
- package/dist/drop-core/buildClient.js +84 -0
- package/dist/drop-core/buildClient.test.d.ts +1 -0
- package/dist/drop-core/buildStash.d.ts +37 -0
- package/dist/drop-core/buildStash.js +84 -0
- package/dist/drop-core/buildStash.test.d.ts +0 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/deploy.d.ts +7 -0
- package/dist/drop-core/deploy.js +53 -0
- package/dist/drop-core/deploy.test.d.ts +1 -0
- package/dist/drop-core/detectBuild.test.d.ts +1 -0
- package/dist/drop-core/digest.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/errors.js +1 -1
- package/dist/drop-core/errors.test.d.ts +1 -0
- package/dist/drop-core/fileTypes.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/index.d.ts +3 -0
- package/dist/drop-core/index.js +51 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.js +2 -2
- package/dist/drop-core/readFiles.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/types.d.ts +10 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.js +1 -1
- package/dist/drop-core/zip.test.d.ts +1 -0
- package/package.json +4 -1
- package/packages/components/index.ts +19 -0
- package/packages/components/preact/DropZone/DropZone.test.ts +71 -0
- package/packages/components/preact/DropZone/DropZone.tsx +23 -2
- package/packages/components/preact/DropZone/index.tsx +14 -3
- package/packages/components/preact/DropZone/useDropDeploy.test.ts +376 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +297 -132
- package/packages/components/preact/index.ts +19 -2
- package/packages/drop-core/analytics.test.ts +17 -0
- package/packages/drop-core/analytics.ts +122 -0
- package/packages/drop-core/authedApi.test.ts +100 -0
- package/packages/drop-core/authedApi.ts +94 -0
- package/packages/drop-core/authedDropClient.test.ts +196 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/authedDropClient.ts +30 -51
- package/packages/drop-core/buildClient.test.ts +112 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/buildClient.ts +28 -57
- package/packages/drop-core/buildStash.test.ts +85 -0
- package/packages/drop-core/buildStash.ts +185 -0
- package/packages/drop-core/deploy.test.ts +135 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/deploy.ts +18 -4
- package/packages/drop-core/detectBuild.test.ts +139 -0
- package/packages/drop-core/digest.test.ts +24 -0
- package/packages/drop-core/errors.test.ts +69 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/errors.ts +1 -1
- package/packages/drop-core/fileTypes.test.ts +37 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/index.ts +3 -0
- package/packages/drop-core/readFiles.test.ts +125 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/types.ts +12 -6
- package/packages/drop-core/zip.test.ts +92 -0
- package/dist/components/preact/DropZone/drop-core/authedDropClient.js +0 -123
- package/dist/components/preact/DropZone/drop-core/buildClient.js +0 -99
- package/dist/components/preact/DropZone/drop-core/deploy.js +0 -48
- package/dist/components/preact/DropZone/drop-core/index.js +0 -36
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/authedDropClient.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/client.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/client.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/errors.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/types.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.d.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/client.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/detectBuild.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/digest.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/fileTypes.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/readFiles.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/zip.ts +0 -0
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { useRef, useState } from 'preact/hooks';
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import {
|
|
3
3
|
AnonymousDropClient,
|
|
4
|
+
clearDropStash,
|
|
5
|
+
deployZippedBuild,
|
|
6
|
+
DROPZONE_EVENTS,
|
|
7
|
+
hasDropStashMarker,
|
|
8
|
+
loadDropStash,
|
|
9
|
+
saveDropStash,
|
|
10
|
+
zipFiles,
|
|
4
11
|
AuthenticatedBuildClient,
|
|
5
12
|
AuthenticatedDropClient,
|
|
6
|
-
deployBuild,
|
|
7
13
|
deployFiles,
|
|
8
14
|
detectBuild,
|
|
9
15
|
getSingleNonIndexHtmlFileName,
|
|
@@ -16,12 +22,17 @@ import {
|
|
|
16
22
|
type DeployProgress,
|
|
17
23
|
type DeployResult,
|
|
18
24
|
type DropClient,
|
|
25
|
+
type DropMethod,
|
|
26
|
+
type DropZoneEventName,
|
|
27
|
+
type DropZoneEventProperties,
|
|
28
|
+
type TrackFn,
|
|
19
29
|
type InferredBuildSettings,
|
|
20
30
|
type ProcessedFile,
|
|
21
|
-
|
|
31
|
+
type ZipResult,
|
|
32
|
+
} from '../../../drop-core';
|
|
22
33
|
|
|
23
34
|
/** How the files reached the zone — carried through to analytics. */
|
|
24
|
-
export type DeploySource =
|
|
35
|
+
export type DeploySource = DropMethod;
|
|
25
36
|
|
|
26
37
|
/** The live site handed back once a deploy succeeds. */
|
|
27
38
|
export interface DeployedSite {
|
|
@@ -77,8 +88,8 @@ export type DeployStatus =
|
|
|
77
88
|
| { kind: 'redirecting'; target: 'claim' | 'dashboard' | 'signup' | 'build' }
|
|
78
89
|
| { kind: 'error'; message: string };
|
|
79
90
|
|
|
80
|
-
/**
|
|
81
|
-
export type TrackFn
|
|
91
|
+
/** Re-exported from the drop-core analytics contract, where the shape now lives. */
|
|
92
|
+
export type { TrackFn };
|
|
82
93
|
|
|
83
94
|
interface UseDropDeployOptions {
|
|
84
95
|
apiBase?: string;
|
|
@@ -94,6 +105,8 @@ interface UseDropDeployOptions {
|
|
|
94
105
|
buildDeployUrl: (build: DeployedBuild) => string;
|
|
95
106
|
waitForBuild?: boolean;
|
|
96
107
|
onBuildRequired?: (info: BuildDetection) => void;
|
|
108
|
+
onBuildSignupHandoff?: (info: { stashed: boolean; detection: BuildDetection }) => void;
|
|
109
|
+
resumeStashedBuilds?: boolean;
|
|
97
110
|
onDeploy?: (site: DeployedSite) => void;
|
|
98
111
|
onBuildDeploy?: (build: DeployedBuild) => void;
|
|
99
112
|
onError?: (error: DeployError) => void;
|
|
@@ -129,6 +142,23 @@ function indexHtmlWarning(files: ProcessedFile[]): string | null {
|
|
|
129
142
|
return null;
|
|
130
143
|
}
|
|
131
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Best-effort local persistence of the anonymous claim handoff. Note this does
|
|
147
|
+
* NOT reach the dashboard: localStorage is per-origin, so a token written here
|
|
148
|
+
* (e.g. www.netlify.com) is invisible to app.netlify.com. The cross-origin
|
|
149
|
+
* handoff is the URL fragment on the claim redirect; this only helps if the
|
|
150
|
+
* drop and claim share an origin. Authenticated deploys never call this —
|
|
151
|
+
* there is no claim token, and the upload bearer must never be persisted.
|
|
152
|
+
*/
|
|
153
|
+
function persistAnonymousClaimLocally(token: string, siteName: string): void {
|
|
154
|
+
try {
|
|
155
|
+
localStorage.setItem('drop-token', token);
|
|
156
|
+
localStorage.setItem('dropSiteName', siteName);
|
|
157
|
+
} catch {
|
|
158
|
+
/* private mode / storage disabled — non-fatal */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
132
162
|
/**
|
|
133
163
|
* Owns the deploy state machine and its side effects (upload, local persistence,
|
|
134
164
|
* analytics, redirect), keeping DropZone a thin view over the returned `status`.
|
|
@@ -147,11 +177,25 @@ export function useDropDeploy({
|
|
|
147
177
|
buildDeployUrl,
|
|
148
178
|
waitForBuild,
|
|
149
179
|
onBuildRequired,
|
|
180
|
+
onBuildSignupHandoff,
|
|
181
|
+
resumeStashedBuilds = true,
|
|
150
182
|
onDeploy,
|
|
151
183
|
onBuildDeploy,
|
|
152
184
|
onError,
|
|
153
185
|
track,
|
|
154
186
|
}: UseDropDeployOptions) {
|
|
187
|
+
/**
|
|
188
|
+
* Typed veneer over the consumer's `track`: event names come from the
|
|
189
|
+
* DROPZONE_EVENTS contract and each payload is checked against its shape,
|
|
190
|
+
* so a renamed property fails to compile instead of silently forking the
|
|
191
|
+
* analytics history.
|
|
192
|
+
*/
|
|
193
|
+
const emit = <E extends DropZoneEventName>(event: E, props: DropZoneEventProperties[E]) =>
|
|
194
|
+
// The cast bridges to TrackFn's loose record: the interfaces have no index
|
|
195
|
+
// signature (so payloads stay closed for consumers), and the checking that
|
|
196
|
+
// matters happened against DropZoneEventProperties[E] above.
|
|
197
|
+
track(event, props as Record<string, unknown>);
|
|
198
|
+
|
|
155
199
|
const [status, setStatus] = useState<DeployStatus>({ kind: 'idle' });
|
|
156
200
|
const [warning, setWarning] = useState<string | null>(null);
|
|
157
201
|
const dropClient = useRef(client ?? new AnonymousDropClient(apiBase)).current;
|
|
@@ -180,17 +224,17 @@ export function useDropDeploy({
|
|
|
180
224
|
const busy = status.kind !== 'idle' && status.kind !== 'error';
|
|
181
225
|
|
|
182
226
|
/**
|
|
183
|
-
*
|
|
184
|
-
* visitor turns out not to be logged in — nothing is created
|
|
185
|
-
* the caller can still
|
|
186
|
-
* throws and is reported by the caller's `catch`.
|
|
227
|
+
* Create a site and enqueue a build for an already-zipped drop. Returns
|
|
228
|
+
* `null` when the visitor turns out not to be logged in — nothing is created
|
|
229
|
+
* in that case, so the caller can still stash the archive and hand off to
|
|
230
|
+
* signup. Every other failure throws and is reported by the caller's `catch`.
|
|
187
231
|
*/
|
|
188
|
-
async function
|
|
232
|
+
async function enqueueZippedBuild(
|
|
189
233
|
client: BuildClient,
|
|
190
|
-
|
|
234
|
+
archive: ZipResult
|
|
191
235
|
): Promise<DeployedBuild | null> {
|
|
192
236
|
try {
|
|
193
|
-
const { site, build, buildSettings, url } = await
|
|
237
|
+
const { site, build, buildSettings, url } = await deployZippedBuild(client, archive, {
|
|
194
238
|
onProgress: e => setStatus(progressToStatus(e)),
|
|
195
239
|
waitForBuild,
|
|
196
240
|
});
|
|
@@ -208,6 +252,14 @@ export function useDropDeploy({
|
|
|
208
252
|
}
|
|
209
253
|
}
|
|
210
254
|
|
|
255
|
+
/** One attempt's analytics context, threaded to the branch handlers below. */
|
|
256
|
+
interface AttemptContext {
|
|
257
|
+
source: DeploySource;
|
|
258
|
+
fileCount: number;
|
|
259
|
+
/** `{ file_types }` when the drop boundary captured any, `{}` otherwise — spread into every event. */
|
|
260
|
+
typeProps: { file_types?: string[] };
|
|
261
|
+
}
|
|
262
|
+
|
|
211
263
|
/**
|
|
212
264
|
* `fileTypes` is the MIME types of the selection, read at the drop boundary
|
|
213
265
|
* (see `getFileTypes`) and passed in rather than derived here — it has to be
|
|
@@ -237,15 +289,10 @@ export function useDropDeploy({
|
|
|
237
289
|
// the humanized message while keeping the raw reason for their own logging.
|
|
238
290
|
const fail = (message: string, reason: string, extra?: Record<string, unknown>) => {
|
|
239
291
|
setStatus({ kind: 'error', message });
|
|
240
|
-
|
|
292
|
+
emit(DROPZONE_EVENTS.DEPLOY_FAILED, { method: source, reason, ...typeProps, ...extra });
|
|
241
293
|
onError?.({ message, reason });
|
|
242
294
|
};
|
|
243
295
|
|
|
244
|
-
const failFromError = (err: unknown, extra?: Record<string, unknown>) => {
|
|
245
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
246
|
-
fail(humanizeDropError(err), reason, extra);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
296
|
try {
|
|
250
297
|
// Auto-rename a lone non-index HTML file to index.html so it is served at
|
|
251
298
|
// the site root. Detection (getSingleNonIndexHtmlFileName) sits on the
|
|
@@ -260,140 +307,258 @@ export function useDropDeploy({
|
|
|
260
307
|
// A drop is the "user provided files" signal; the browse path is already
|
|
261
308
|
// tracked when the picker opens.
|
|
262
309
|
if (source === 'drag') {
|
|
263
|
-
|
|
310
|
+
emit(DROPZONE_EVENTS.FILES_DROPPED, {
|
|
264
311
|
method: source,
|
|
265
312
|
file_count: files.length,
|
|
266
313
|
...typeProps,
|
|
267
314
|
});
|
|
268
315
|
}
|
|
269
316
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
317
|
+
const ctx: AttemptContext = { source, fileCount: files.length, typeProps };
|
|
318
|
+
const detection = detectBuild(files);
|
|
319
|
+
if (detection.buildRequired) {
|
|
320
|
+
await handleBuildRequiredDrop(files, detection, ctx);
|
|
321
|
+
} else {
|
|
322
|
+
await deployStaticDrop(files, ctx);
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
326
|
+
fail(humanizeDropError(err), reason);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A drop that needs a build. Build-required projects can't deploy through the
|
|
332
|
+
* anonymous drop API — it only performs static deploys, so building one this
|
|
333
|
+
* way yields a broken site. They're routed to the authenticated build path
|
|
334
|
+
* instead, or stashed and handed off to signup when there is no session.
|
|
335
|
+
* Consumers can take over entirely with `onBuildRequired`, or own just the
|
|
336
|
+
* signup hand-off with `onBuildSignupHandoff`.
|
|
337
|
+
*/
|
|
338
|
+
async function handleBuildRequiredDrop(
|
|
339
|
+
files: ProcessedFile[],
|
|
340
|
+
detection: BuildDetection,
|
|
341
|
+
{ source, fileCount, typeProps }: AttemptContext
|
|
342
|
+
) {
|
|
343
|
+
emit(DROPZONE_EVENTS.BUILD_REQUIRED, {
|
|
344
|
+
method: source,
|
|
345
|
+
reason: detection.reason,
|
|
346
|
+
file_count: fileCount,
|
|
347
|
+
...typeProps,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (onBuildRequired) {
|
|
351
|
+
// The consumer owns the hand-off now; drop back to idle so the zone
|
|
352
|
+
// stays usable rather than sitting on a spinner.
|
|
353
|
+
setStatus({ kind: 'idle' });
|
|
354
|
+
onBuildRequired(detection);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let stashed = false;
|
|
359
|
+
if (authedBuildClient) {
|
|
360
|
+
// Zip before anything is created: the archive serves whichever path
|
|
361
|
+
// runs — an authenticated build right now, or the stash that carries
|
|
362
|
+
// the drop across signup.
|
|
363
|
+
setStatus({ kind: 'zipping' });
|
|
364
|
+
const archive = await zipFiles(files);
|
|
365
|
+
|
|
366
|
+
const built = await enqueueZippedBuild(authedBuildClient, archive);
|
|
367
|
+
if (built) {
|
|
368
|
+
setStatus({ kind: 'redirecting', target: 'build' });
|
|
369
|
+
emit(DROPZONE_EVENTS.BUILD_DEPLOY_SUCCEEDED, {
|
|
279
370
|
method: source,
|
|
280
|
-
reason:
|
|
281
|
-
|
|
371
|
+
reason: detection.reason,
|
|
372
|
+
site_name: built.siteName,
|
|
373
|
+
file_count: fileCount,
|
|
374
|
+
...(built.buildSettings ? { framework: built.buildSettings.framework } : {}),
|
|
282
375
|
...typeProps,
|
|
283
376
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// stays usable rather than sitting on a spinner.
|
|
287
|
-
setStatus({ kind: 'idle' });
|
|
288
|
-
onBuildRequired(build);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (authedBuildClient) {
|
|
292
|
-
const built = await buildProject(authedBuildClient, files);
|
|
293
|
-
// No session after all (the auth hint cookie can outlive the session).
|
|
294
|
-
// Nothing was created, so fall through to the signup hand-off rather
|
|
295
|
-
// than dead-ending on an error the visitor can't act on from here.
|
|
296
|
-
if (built) {
|
|
297
|
-
setStatus({ kind: 'redirecting', target: 'build' });
|
|
298
|
-
track('dropzone_build_deploy_succeeded', {
|
|
299
|
-
method: source,
|
|
300
|
-
reason: build.reason,
|
|
301
|
-
site_name: built.siteName,
|
|
302
|
-
file_count: files.length,
|
|
303
|
-
...(built.buildSettings ? { framework: built.buildSettings.framework } : {}),
|
|
304
|
-
...typeProps,
|
|
305
|
-
});
|
|
306
|
-
onBuildDeploy?.(built);
|
|
307
|
-
window.location.assign(buildDeployUrl(built));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
setStatus({ kind: 'redirecting', target: 'signup' });
|
|
312
|
-
window.location.assign(buildSignupUrl);
|
|
377
|
+
onBuildDeploy?.(built);
|
|
378
|
+
window.location.assign(buildDeployUrl(built));
|
|
313
379
|
return;
|
|
314
380
|
}
|
|
315
381
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (authedDropClient) {
|
|
328
|
-
try {
|
|
329
|
-
deployed = await deployFiles(authedDropClient, files, {
|
|
330
|
-
onProgress: e => setStatus(progressToStatus(e)),
|
|
331
|
-
});
|
|
332
|
-
authenticated = true;
|
|
333
|
-
} catch (err) {
|
|
334
|
-
if (!(err instanceof NotAuthenticatedError)) throw err;
|
|
335
|
-
authFallback = true;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (!deployed) {
|
|
339
|
-
deployed = await deployFiles(dropClient, files, {
|
|
340
|
-
onProgress: e => setStatus(progressToStatus(e)),
|
|
382
|
+
// No session after all (the auth hint cookie can outlive the session).
|
|
383
|
+
// Nothing was created — stash the archive so the drop survives the
|
|
384
|
+
// signup round-trip. When the visitor is back on this origin
|
|
385
|
+
// authenticated, the mount-time resume deploys it for them.
|
|
386
|
+
stashed = await saveDropStash(archive.file, archive.buildSettings);
|
|
387
|
+
if (stashed) {
|
|
388
|
+
emit(DROPZONE_EVENTS.PROJECT_STASHED, {
|
|
389
|
+
method: source,
|
|
390
|
+
reason: detection.reason,
|
|
391
|
+
file_count: fileCount,
|
|
392
|
+
...typeProps,
|
|
341
393
|
});
|
|
342
394
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// Best-effort local persistence. Note this does NOT reach the dashboard:
|
|
347
|
-
// localStorage is per-origin, so a token written here (e.g. www.netlify.com)
|
|
348
|
-
// is invisible to app.netlify.com. The cross-origin handoff is the URL
|
|
349
|
-
// fragment below; this only helps if the drop and claim share an origin.
|
|
350
|
-
// Authenticated deploys skip this entirely — there is no claim token,
|
|
351
|
-
// and the upload bearer must never be persisted.
|
|
352
|
-
try {
|
|
353
|
-
localStorage.setItem('drop-token', token);
|
|
354
|
-
localStorage.setItem('dropSiteName', result.subdomain);
|
|
355
|
-
} catch {
|
|
356
|
-
/* private mode / storage disabled — non-fatal */
|
|
357
|
-
}
|
|
358
|
-
}
|
|
395
|
+
}
|
|
396
|
+
// Without build wiring nothing here could deploy a stash later, so nothing
|
|
397
|
+
// is stashed — the hand-off is all that's left.
|
|
359
398
|
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
399
|
+
if (onBuildSignupHandoff) {
|
|
400
|
+
// The consumer owns the hand-off (e.g. an auth popup that keeps the
|
|
401
|
+
// visitor on this page); back to idle so the zone stays usable.
|
|
402
|
+
setStatus({ kind: 'idle' });
|
|
403
|
+
onBuildSignupHandoff({ stashed, detection });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
setStatus({ kind: 'redirecting', target: 'signup' });
|
|
407
|
+
window.location.assign(buildSignupUrl);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Try the authenticated static deploy, or report that the visitor has no
|
|
412
|
+
* session. NotAuthenticatedError is the one failure that returns null and
|
|
413
|
+
* lets the caller fall back to the anonymous flow — it's thrown before
|
|
414
|
+
* anything is created (the auth hint cookie can outlive the session), so
|
|
415
|
+
* retrying anonymously can't strand a half-made site. Any later failure is
|
|
416
|
+
* real and rethrows: falling back then would leave an empty site in the
|
|
417
|
+
* account and deploy a duplicate.
|
|
418
|
+
*/
|
|
419
|
+
async function attemptAuthenticatedStaticDeploy(
|
|
420
|
+
client: DropClient,
|
|
421
|
+
files: ProcessedFile[]
|
|
422
|
+
): Promise<DeployResult | null> {
|
|
423
|
+
try {
|
|
424
|
+
return await deployFiles(client, files, {
|
|
425
|
+
onProgress: e => setStatus(progressToStatus(e)),
|
|
380
426
|
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err instanceof NotAuthenticatedError) return null;
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
381
432
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
433
|
+
/** A static drop: into the visitor's account when possible, anonymous otherwise. */
|
|
434
|
+
async function deployStaticDrop(
|
|
435
|
+
files: ProcessedFile[],
|
|
436
|
+
{ source, fileCount, typeProps }: AttemptContext
|
|
437
|
+
) {
|
|
438
|
+
setWarning(indexHtmlWarning(files));
|
|
439
|
+
|
|
440
|
+
const authedResult = authedDropClient
|
|
441
|
+
? await attemptAuthenticatedStaticDeploy(authedDropClient, files)
|
|
442
|
+
: null;
|
|
443
|
+
const authenticated = authedResult !== null;
|
|
444
|
+
// A fallback is an authed attempt that bounced — distinct from never trying.
|
|
445
|
+
const authFallback = authedDropClient !== null && !authenticated;
|
|
446
|
+
|
|
447
|
+
const deployed =
|
|
448
|
+
authedResult ??
|
|
449
|
+
(await deployFiles(dropClient, files, {
|
|
450
|
+
onProgress: e => setStatus(progressToStatus(e)),
|
|
451
|
+
}));
|
|
452
|
+
const { url, token, deploy: result } = deployed;
|
|
453
|
+
|
|
454
|
+
if (!authenticated) {
|
|
455
|
+
persistAnonymousClaimLocally(token, result.subdomain);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
setStatus({ kind: 'redirecting', target: authenticated ? 'dashboard' : 'claim' });
|
|
459
|
+
// Fired before the redirect below. Note: the navigation can cut a
|
|
460
|
+
// fire-and-forget analytics request short — see the docs for delivery
|
|
461
|
+
// hardening (sendBeacon / deferring the redirect). `auth_fallback` is only
|
|
462
|
+
// sent when an authenticated attempt bounced (stale auth hint) — omitted
|
|
463
|
+
// otherwise, like `file_types`.
|
|
464
|
+
emit(DROPZONE_EVENTS.DEPLOY_SUCCEEDED, {
|
|
465
|
+
method: source,
|
|
466
|
+
site_name: result.subdomain,
|
|
467
|
+
file_count: fileCount,
|
|
468
|
+
authenticated,
|
|
469
|
+
...(authFallback ? { auth_fallback: true } : {}),
|
|
470
|
+
...typeProps,
|
|
471
|
+
});
|
|
472
|
+
onDeploy?.({
|
|
473
|
+
url,
|
|
474
|
+
...(authenticated ? {} : { token }),
|
|
475
|
+
siteName: result.subdomain,
|
|
476
|
+
siteId: result.id,
|
|
477
|
+
authenticated,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
redirectAfterStaticDeploy(authenticated, result.subdomain, token);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Hand the visitor to their new site: its dashboard page, or the claim page. */
|
|
484
|
+
function redirectAfterStaticDeploy(authenticated: boolean, siteName: string, token: string) {
|
|
485
|
+
if (authenticated) {
|
|
486
|
+
// The site already lives in the visitor's account — send them straight
|
|
487
|
+
// to its dashboard page. No claim step, no token in the URL.
|
|
488
|
+
window.location.assign(siteDashboardUrl(siteName));
|
|
489
|
+
} else {
|
|
490
|
+
// Hand the drop token to the dashboard via the URL fragment — localStorage
|
|
491
|
+
// does not cross origins. The dashboard's /drop/:name page reads
|
|
492
|
+
// `#drop_token=` and persists it for the claim + post-signup steps.
|
|
493
|
+
window.location.assign(`${claimUrl(siteName)}#drop_token=${token}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Deploy a drop stashed before the signup hand-off (see `buildStash`).
|
|
499
|
+
*
|
|
500
|
+
* The stash is consumed only once the visitor is provably authenticated —
|
|
501
|
+
* the marker (or the auth hint behind `getUploadToken`) can outlive the
|
|
502
|
+
* session, and consuming on a bounced attempt would quietly lose the drop.
|
|
503
|
+
* Once past that gate it is consumed whether the deploy succeeds or fails,
|
|
504
|
+
* so a broken project can never retry-loop.
|
|
505
|
+
*/
|
|
506
|
+
async function resumeStashedBuild(client: BuildClient) {
|
|
507
|
+
const stash = await loadDropStash();
|
|
508
|
+
if (!stash) {
|
|
509
|
+
// Expired or unavailable — clear the dangling marker and stay idle.
|
|
510
|
+
await clearDropStash();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
emit(DROPZONE_EVENTS.RESUMED, {});
|
|
515
|
+
setStatus({ kind: 'creating' });
|
|
516
|
+
|
|
517
|
+
const archive: ZipResult = { file: stash.zipFile, buildSettings: stash.buildSettings };
|
|
518
|
+
let built: DeployedBuild | null;
|
|
519
|
+
try {
|
|
520
|
+
built = await enqueueZippedBuild(client, archive);
|
|
392
521
|
} catch (err) {
|
|
393
|
-
|
|
522
|
+
await clearDropStash();
|
|
523
|
+
failResume(err instanceof Error ? err.message : String(err));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!built) {
|
|
528
|
+
// Still no session — the visitor came back logged out. Keep the stash for
|
|
529
|
+
// their next authenticated visit and return to idle without fuss.
|
|
530
|
+
setStatus({ kind: 'idle' });
|
|
531
|
+
return;
|
|
394
532
|
}
|
|
533
|
+
|
|
534
|
+
await clearDropStash();
|
|
535
|
+
setStatus({ kind: 'redirecting', target: 'build' });
|
|
536
|
+
onBuildDeploy?.(built);
|
|
537
|
+
window.location.assign(buildDeployUrl(built));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Resume failures re-use the normal error surface, with re-drop copy. */
|
|
541
|
+
function failResume(reason: string) {
|
|
542
|
+
const message =
|
|
543
|
+
'We couldn\u2019t automatically deploy your project. Drop it again to publish it.';
|
|
544
|
+
setStatus({ kind: 'error', message });
|
|
545
|
+
// No `method`: the failure comes from storage, not a live drop gesture.
|
|
546
|
+
emit(DROPZONE_EVENTS.DEPLOY_FAILED, { reason: 'resume_failed' });
|
|
547
|
+
onError?.({ message, reason });
|
|
395
548
|
}
|
|
396
549
|
|
|
550
|
+
// Attempt a resume once per mount — guarded by the cheap synchronous marker
|
|
551
|
+
// check so ordinary page loads never open IndexedDB, and only when this zone
|
|
552
|
+
// has the wiring to build (a zone without it could not deploy the stash).
|
|
553
|
+
const resumeAttempted = useRef(false);
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if (resumeAttempted.current || !resumeStashedBuilds) return;
|
|
556
|
+
if (!authedBuildClient || !hasDropStashMarker()) return;
|
|
557
|
+
resumeAttempted.current = true;
|
|
558
|
+
void resumeStashedBuild(authedBuildClient);
|
|
559
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- deliberately mount-only
|
|
560
|
+
}, []);
|
|
561
|
+
|
|
397
562
|
function reset() {
|
|
398
563
|
setWarning(null);
|
|
399
564
|
setStatus({ kind: 'idle' });
|
|
@@ -6,7 +6,24 @@ export { default as SearchHeading } from './SearchModal/components/SearchHeading
|
|
|
6
6
|
export { default as SearchToggle } from './SearchModal/components/SearchToggle/SearchToggle';
|
|
7
7
|
export { default as AskNetlify } from './AskNetlify/AskNetlify';
|
|
8
8
|
export { SearchModal } from './SearchModal';
|
|
9
|
-
export {
|
|
10
|
-
|
|
9
|
+
export {
|
|
10
|
+
DropZone,
|
|
11
|
+
AuthenticatedBuildClient,
|
|
12
|
+
AuthenticatedDropClient,
|
|
13
|
+
DROPZONE_EVENTS,
|
|
14
|
+
getFileTypes,
|
|
15
|
+
humanizeDropError,
|
|
16
|
+
} from './DropZone';
|
|
17
|
+
export type {
|
|
18
|
+
BuildClient,
|
|
19
|
+
DeployedBuild,
|
|
20
|
+
DeployedSite,
|
|
21
|
+
DeployError,
|
|
22
|
+
DeployStatus,
|
|
23
|
+
DropClient,
|
|
24
|
+
DropZoneEventName,
|
|
25
|
+
DropZoneEventProperties,
|
|
26
|
+
TrackFn,
|
|
27
|
+
} from './DropZone';
|
|
11
28
|
|
|
12
29
|
export { type IconName } from './Icon';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DROPZONE_EVENTS } from './analytics';
|
|
3
|
+
|
|
4
|
+
describe('DROPZONE_EVENTS', () => {
|
|
5
|
+
it('pins the wire names — renaming one silently forks Amplitude/Segment history', () => {
|
|
6
|
+
expect(DROPZONE_EVENTS).toEqual({
|
|
7
|
+
FILES_DROPPED: 'dropzone_files_dropped',
|
|
8
|
+
BROWSE_OPENED: 'dropzone_browse_opened',
|
|
9
|
+
BUILD_REQUIRED: 'dropzone_build_required',
|
|
10
|
+
DEPLOY_SUCCEEDED: 'dropzone_deploy_succeeded',
|
|
11
|
+
BUILD_DEPLOY_SUCCEEDED: 'dropzone_build_deploy_succeeded',
|
|
12
|
+
DEPLOY_FAILED: 'dropzone_deploy_failed',
|
|
13
|
+
PROJECT_STASHED: 'dropzone_project_stashed',
|
|
14
|
+
RESUMED: 'dropzone_resumed',
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|