@netlify/spark-ui 1.29.0 → 1.31.0-alpha.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.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/chunks/browser.Ca2rTNTu.js +769 -0
  3. package/dist/components/articleCopyActions.d.ts +9 -0
  4. package/dist/components/articleCopyActions.test.d.ts +1 -0
  5. package/dist/components/index.d.ts +2 -2
  6. package/dist/components/preact/DropZone/DropZone.d.ts +84 -15
  7. package/dist/components/preact/DropZone/DropZone.js +117 -85
  8. package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +80 -0
  9. package/dist/components/preact/DropZone/drop-core/authedDropClient.js +99 -0
  10. package/dist/components/preact/DropZone/drop-core/buildClient.d.ts +98 -0
  11. package/dist/components/preact/DropZone/drop-core/buildClient.js +92 -0
  12. package/dist/components/preact/DropZone/drop-core/client.d.ts +4 -0
  13. package/dist/components/preact/DropZone/drop-core/client.js +2 -1
  14. package/dist/components/preact/DropZone/drop-core/deploy.d.ts +31 -0
  15. package/dist/components/preact/DropZone/drop-core/deploy.js +36 -18
  16. package/dist/components/preact/DropZone/drop-core/detectBuild.d.ts +26 -0
  17. package/dist/components/preact/DropZone/drop-core/detectBuild.js +80 -53
  18. package/dist/components/preact/DropZone/drop-core/errors.d.ts +38 -0
  19. package/dist/components/preact/DropZone/drop-core/errors.js +50 -5
  20. package/dist/components/preact/DropZone/drop-core/fileTypes.d.ts +17 -0
  21. package/dist/components/preact/DropZone/drop-core/fileTypes.js +11 -0
  22. package/dist/components/preact/DropZone/drop-core/index.d.ts +4 -0
  23. package/dist/components/preact/DropZone/drop-core/index.js +32 -15
  24. package/dist/components/preact/DropZone/drop-core/readFiles.js +1 -1
  25. package/dist/components/preact/DropZone/drop-core/types.d.ts +6 -1
  26. package/dist/components/preact/DropZone/drop-core/zip.d.ts +46 -0
  27. package/dist/components/preact/DropZone/drop-core/zip.js +43 -0
  28. package/dist/components/preact/DropZone/index.d.ts +3 -2
  29. package/dist/components/preact/DropZone/index.js +27 -9
  30. package/dist/components/preact/DropZone/useDropDeploy.d.ts +36 -5
  31. package/dist/components/preact/DropZone/useDropDeploy.js +140 -65
  32. package/dist/components/preact/index.d.ts +2 -1
  33. package/dist/components/preact/index.js +17 -12
  34. package/package.json +5 -2
  35. package/packages/components/ArticleCopyActions.astro +122 -0
  36. package/packages/components/articleCopyActions.test.ts +143 -0
  37. package/packages/components/articleCopyActions.ts +41 -0
  38. package/packages/components/index.ts +15 -0
  39. package/packages/components/preact/DropZone/DropZone.tsx +145 -24
  40. package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +194 -0
  41. package/packages/components/preact/DropZone/drop-core/buildClient.ts +190 -0
  42. package/packages/components/preact/DropZone/drop-core/client.ts +1 -1
  43. package/packages/components/preact/DropZone/drop-core/deploy.ts +65 -0
  44. package/packages/components/preact/DropZone/drop-core/detectBuild.ts +125 -41
  45. package/packages/components/preact/DropZone/drop-core/errors.ts +84 -1
  46. package/packages/components/preact/DropZone/drop-core/fileTypes.ts +34 -0
  47. package/packages/components/preact/DropZone/drop-core/index.ts +4 -0
  48. package/packages/components/preact/DropZone/drop-core/readFiles.ts +1 -1
  49. package/packages/components/preact/DropZone/drop-core/types.ts +12 -1
  50. package/packages/components/preact/DropZone/drop-core/zip.ts +118 -0
  51. package/packages/components/preact/DropZone/index.tsx +24 -0
  52. package/packages/components/preact/DropZone/useDropDeploy.ts +219 -34
  53. package/packages/components/preact/index.ts +2 -1
  54. package/dist/chunks/browser.D58rdxmc.js +0 -444
@@ -1,15 +1,22 @@
1
1
  import { useRef, useState } from 'preact/hooks';
2
2
  import {
3
3
  AnonymousDropClient,
4
+ AuthenticatedBuildClient,
5
+ AuthenticatedDropClient,
6
+ deployBuild,
4
7
  deployFiles,
5
8
  detectBuild,
6
9
  getSingleNonIndexHtmlFileName,
7
10
  hasMultipleHtmlFilesWithoutIndex,
8
11
  humanizeDropError,
12
+ NotAuthenticatedError,
9
13
  renameSingleNonIndexHtmlToIndex,
14
+ type BuildClient,
10
15
  type BuildDetection,
11
16
  type DeployProgress,
17
+ type DeployResult,
12
18
  type DropClient,
19
+ type InferredBuildSettings,
13
20
  type ProcessedFile,
14
21
  } from './drop-core';
15
22
 
@@ -19,9 +26,15 @@ export type DeploySource = 'drag' | 'browse';
19
26
  /** The live site handed back once a deploy succeeds. */
20
27
  export interface DeployedSite {
21
28
  url: string;
22
- token: string;
29
+ /**
30
+ * Drop claim token — only present for anonymous deploys. An authenticated
31
+ * deploy lands directly in the visitor's account, so there is nothing to claim.
32
+ */
33
+ token?: string;
23
34
  siteName: string;
24
35
  siteId: string;
36
+ /** True when the deploy landed directly in the visitor's account. */
37
+ authenticated: boolean;
25
38
  }
26
39
 
27
40
  /** A deploy failure, handed to `onError` so consumers can render their own error UI. */
@@ -32,16 +45,36 @@ export interface DeployError {
32
45
  reason: string;
33
46
  }
34
47
 
48
+ /** The build handed back once a build-required drop is enqueued. */
49
+ export interface DeployedBuild {
50
+ /** The site created for the build — live once the build finishes. */
51
+ url: string;
52
+ siteName: string;
53
+ siteId: string;
54
+ buildId: string;
55
+ /** The deploy the build produces, if the API has assigned one yet. */
56
+ deployId?: string;
57
+ /** Build settings synthesised into the uploaded `netlify.toml`, if any. */
58
+ buildSettings: InferredBuildSettings | null;
59
+ }
60
+
35
61
  /** Finite states of a single deploy attempt. */
36
62
  export type DeployStatus =
37
63
  | { kind: 'idle' }
38
64
  | { kind: 'reading' }
65
+ // Build path only: packing the drop into the source archive buildbot builds.
66
+ | { kind: 'zipping' }
39
67
  | { kind: 'creating' }
40
68
  | { kind: 'uploading'; uploaded: number; total: number }
41
69
  | { kind: 'processing' }
42
- // `claim` follows a static deploy (hand off to the claim page); `signup`
43
- // follows a build-required drop (hand off to the authenticated Drop page).
44
- | { kind: 'redirecting'; target: 'claim' | 'signup' }
70
+ // Build path only: the archive is uploaded and buildbot is building it.
71
+ | { kind: 'building' }
72
+ // `claim` follows an anonymous static deploy (hand off to the claim page);
73
+ // `dashboard` follows an authenticated static deploy (hand off to the new
74
+ // site's project page); `build` follows an authenticated build (hand off to
75
+ // the app's project page while the build runs); `signup` follows a
76
+ // build-required drop with no session to build it with.
77
+ | { kind: 'redirecting'; target: 'claim' | 'dashboard' | 'signup' | 'build' }
45
78
  | { kind: 'error'; message: string };
46
79
 
47
80
  /** Provider-agnostic analytics sink. */
@@ -51,9 +84,18 @@ interface UseDropDeployOptions {
51
84
  apiBase?: string;
52
85
  client?: DropClient;
53
86
  claimUrl: (siteName: string) => string;
87
+ siteDashboardUrl: (siteName: string) => string;
54
88
  buildSignupUrl: string;
89
+ buildClient?: BuildClient;
90
+ authenticatedDropClient?: DropClient;
91
+ getUploadToken?: () => Promise<string | null>;
92
+ proxyBase?: string;
93
+ accountSlug?: string;
94
+ buildDeployUrl: (build: DeployedBuild) => string;
95
+ waitForBuild?: boolean;
55
96
  onBuildRequired?: (info: BuildDetection) => void;
56
97
  onDeploy?: (site: DeployedSite) => void;
98
+ onBuildDeploy?: (build: DeployedBuild) => void;
57
99
  onError?: (error: DeployError) => void;
58
100
  track: TrackFn;
59
101
  }
@@ -61,10 +103,14 @@ interface UseDropDeployOptions {
61
103
  /** Map a low-level deploy progress event onto our UI status. */
62
104
  function progressToStatus(e: DeployProgress): DeployStatus {
63
105
  switch (e.phase) {
106
+ case 'zipping':
107
+ return { kind: 'zipping' };
64
108
  case 'creating':
65
109
  return { kind: 'creating' };
66
110
  case 'uploading':
67
111
  return { kind: 'uploading', uploaded: e.uploaded ?? 0, total: e.total ?? 0 };
112
+ case 'building':
113
+ return { kind: 'building' };
68
114
  case 'processing':
69
115
  case 'ready':
70
116
  return { kind: 'processing' };
@@ -91,9 +137,18 @@ export function useDropDeploy({
91
137
  apiBase,
92
138
  client,
93
139
  claimUrl,
140
+ siteDashboardUrl,
94
141
  buildSignupUrl,
142
+ buildClient,
143
+ authenticatedDropClient,
144
+ getUploadToken,
145
+ proxyBase,
146
+ accountSlug,
147
+ buildDeployUrl,
148
+ waitForBuild,
95
149
  onBuildRequired,
96
150
  onDeploy,
151
+ onBuildDeploy,
97
152
  onError,
98
153
  track,
99
154
  }: UseDropDeployOptions) {
@@ -101,24 +156,88 @@ export function useDropDeploy({
101
156
  const [warning, setWarning] = useState<string | null>(null);
102
157
  const dropClient = useRef(client ?? new AnonymousDropClient(apiBase)).current;
103
158
 
159
+ // Building is opt-in: without an injected client or a way to mint an upload
160
+ // token there is no authenticated path, and build-required drops fall back to
161
+ // the signup hand-off. Built once and held so a re-render doesn't hand a
162
+ // fresh client (and a fresh token cache) to an in-flight deploy.
163
+ const authedBuildClient = useRef(
164
+ buildClient ??
165
+ (getUploadToken
166
+ ? new AuthenticatedBuildClient({ proxyBase, getUploadToken, accountSlug })
167
+ : null)
168
+ ).current;
169
+
170
+ // Same opt-in for static drops: with authenticated wiring present, deploy
171
+ // straight into the visitor's account and skip the claim step; without it,
172
+ // every static drop stays anonymous.
173
+ const authedDropClient = useRef(
174
+ authenticatedDropClient ??
175
+ (getUploadToken
176
+ ? new AuthenticatedDropClient({ proxyBase, getUploadToken, accountSlug })
177
+ : null)
178
+ ).current;
179
+
104
180
  const busy = status.kind !== 'idle' && status.kind !== 'error';
105
181
 
106
- async function deploy(read: () => Promise<ProcessedFile[]>, source: DeploySource) {
182
+ /**
183
+ * Zip the drop, create a site and enqueue a build. Returns `null` when the
184
+ * visitor turns out not to be logged in — nothing is created in that case, so
185
+ * the caller can still fall back to the signup hand-off. Every other failure
186
+ * throws and is reported by the caller's `catch`.
187
+ */
188
+ async function buildProject(
189
+ client: BuildClient,
190
+ files: ProcessedFile[]
191
+ ): Promise<DeployedBuild | null> {
192
+ try {
193
+ const { site, build, buildSettings, url } = await deployBuild(client, files, {
194
+ onProgress: e => setStatus(progressToStatus(e)),
195
+ waitForBuild,
196
+ });
197
+ return {
198
+ url,
199
+ siteName: site.name,
200
+ siteId: site.id,
201
+ buildId: build.id,
202
+ deployId: build.deploy_id,
203
+ buildSettings,
204
+ };
205
+ } catch (err) {
206
+ if (err instanceof NotAuthenticatedError) return null;
207
+ throw err;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * `fileTypes` is the MIME types of the selection, read at the drop boundary
213
+ * (see `getFileTypes`) and passed in rather than derived here — it has to be
214
+ * captured before `read()` runs so it survives a read that throws. Omitted
215
+ * from events when empty rather than sent as `[]`.
216
+ */
217
+ async function deploy(
218
+ read: () => Promise<ProcessedFile[]>,
219
+ source: DeploySource,
220
+ fileTypes: string[]
221
+ ) {
107
222
  setWarning(null);
108
223
  setStatus({ kind: 'reading' });
109
224
 
225
+ const typeProps = fileTypes.length ? { file_types: fileTypes } : {};
226
+
110
227
  // Surface a failure: move to the error state and emit the matching
111
228
  // analytics event. Centralised so every failure path (empty selection, a
112
229
  // read/deploy throw) reports uniformly — the mirror of the
113
230
  // `dropzone_deploy_succeeded` event on the happy path. `file_count` is only
114
- // included where the file set is known (it isn't if `read()` itself threw).
231
+ // included where the file set is known (it isn't if `read()` itself threw);
232
+ // `file_types` rides along on every failure precisely because it *is* known
233
+ // in that case — a rejected single file is the main thing we want to see.
115
234
  // `reason` goes to analytics as-is (raw, e.g. "drop failed: 429"); `message`
116
235
  // is what the user sees, which for a caught error is humanized separately.
117
236
  // `onError` gets both so a consumer (e.g. with `showStatus={false}`) can show
118
237
  // the humanized message while keeping the raw reason for their own logging.
119
238
  const fail = (message: string, reason: string, extra?: Record<string, unknown>) => {
120
239
  setStatus({ kind: 'error', message });
121
- track('dropzone_deploy_failed', { method: source, reason, ...extra });
240
+ track('dropzone_deploy_failed', { method: source, reason, ...typeProps, ...extra });
122
241
  onError?.({ message, reason });
123
242
  };
124
243
 
@@ -141,20 +260,26 @@ export function useDropDeploy({
141
260
  // A drop is the "user provided files" signal; the browse path is already
142
261
  // tracked when the picker opens.
143
262
  if (source === 'drag') {
144
- track('dropzone_files_dropped', { method: source, file_count: files.length });
263
+ track('dropzone_files_dropped', {
264
+ method: source,
265
+ file_count: files.length,
266
+ ...typeProps,
267
+ });
145
268
  }
146
269
 
147
270
  // Build-required projects can't deploy through the anonymous drop API —
148
271
  // it only performs static deploys, so building one this way yields a
149
- // broken site. Detect them up front and hand off to the authenticated
150
- // Drop page (via signup) instead. Consumers can override the hand-off
151
- // with `onBuildRequired`.
272
+ // broken site. Detect them up front and route them to the build path
273
+ // (which needs a logged-in user) or, failing that, hand off to the
274
+ // authenticated Drop page via signup. Consumers can override the hand-off
275
+ // entirely with `onBuildRequired`.
152
276
  const build = detectBuild(files);
153
277
  if (build.buildRequired) {
154
278
  track('dropzone_build_required', {
155
279
  method: source,
156
280
  reason: build.reason,
157
281
  file_count: files.length,
282
+ ...typeProps,
158
283
  });
159
284
  if (onBuildRequired) {
160
285
  // The consumer owns the hand-off now; drop back to idle so the zone
@@ -163,6 +288,26 @@ export function useDropDeploy({
163
288
  onBuildRequired(build);
164
289
  return;
165
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
+ }
166
311
  setStatus({ kind: 'redirecting', target: 'signup' });
167
312
  window.location.assign(buildSignupUrl);
168
313
  return;
@@ -170,40 +315,80 @@ export function useDropDeploy({
170
315
 
171
316
  setWarning(indexHtmlWarning(files));
172
317
 
173
- const {
174
- url,
175
- token,
176
- deploy: result,
177
- } = await deployFiles(dropClient, files, {
178
- onProgress: e => setStatus(progressToStatus(e)),
179
- });
318
+ // Deploy into the visitor's account when we can. NotAuthenticatedError is
319
+ // the one failure that falls back to the anonymous flow — it's thrown
320
+ // before anything is created (the auth hint cookie can outlive the
321
+ // session), so retrying anonymously can't strand a half-made site. Any
322
+ // later failure is real and surfaces as an error instead: falling back
323
+ // then would leave an empty site in the account and deploy a duplicate.
324
+ let deployed: DeployResult | null = null;
325
+ let authenticated = false;
326
+ let authFallback = false;
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)),
341
+ });
342
+ }
343
+ const { url, token, deploy: result } = deployed;
180
344
 
181
- // Best-effort local persistence. Note this does NOT reach the dashboard:
182
- // localStorage is per-origin, so a token written here (e.g. www.netlify.com)
183
- // is invisible to app.netlify.com. The cross-origin handoff is the URL
184
- // fragment below; this only helps if the drop and claim share an origin.
185
- try {
186
- localStorage.setItem('drop-token', token);
187
- localStorage.setItem('dropSiteName', result.subdomain);
188
- } catch {
189
- /* private mode / storage disabled — non-fatal */
345
+ if (!authenticated) {
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
+ }
190
358
  }
191
359
 
192
- setStatus({ kind: 'redirecting', target: 'claim' });
360
+ setStatus({ kind: 'redirecting', target: authenticated ? 'dashboard' : 'claim' });
193
361
  // Fired before the redirect below. Note: the navigation can cut a
194
362
  // fire-and-forget analytics request short — see the docs for delivery
195
- // hardening (sendBeacon / deferring the redirect).
363
+ // hardening (sendBeacon / deferring the redirect). `auth_fallback` is only
364
+ // sent when an authenticated attempt bounced (stale auth hint) — omitted
365
+ // otherwise, like `file_types`.
196
366
  track('dropzone_deploy_succeeded', {
197
367
  method: source,
198
368
  site_name: result.subdomain,
199
369
  file_count: files.length,
370
+ authenticated,
371
+ ...(authFallback ? { auth_fallback: true } : {}),
372
+ ...typeProps,
373
+ });
374
+ onDeploy?.({
375
+ url,
376
+ ...(authenticated ? {} : { token }),
377
+ siteName: result.subdomain,
378
+ siteId: result.id,
379
+ authenticated,
200
380
  });
201
- onDeploy?.({ url, token, siteName: result.subdomain, siteId: result.id });
202
381
 
203
- // Hand the drop token to the dashboard via the URL fragment — localStorage
204
- // does not cross origins. The dashboard's /drop/:name page reads
205
- // `#drop_token=` and persists it for the claim + post-signup steps.
206
- window.location.assign(`${claimUrl(result.subdomain)}#drop_token=${token}`);
382
+ if (authenticated) {
383
+ // The site already lives in the visitor's account — send them straight
384
+ // to its dashboard page. No claim step, no token in the URL.
385
+ window.location.assign(siteDashboardUrl(result.subdomain));
386
+ } else {
387
+ // Hand the drop token to the dashboard via the URL fragment — localStorage
388
+ // does not cross origins. The dashboard's /drop/:name page reads
389
+ // `#drop_token=` and persists it for the claim + post-signup steps.
390
+ window.location.assign(`${claimUrl(result.subdomain)}#drop_token=${token}`);
391
+ }
207
392
  } catch (err) {
208
393
  failFromError(err);
209
394
  }
@@ -6,6 +6,7 @@ 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 { DropZone } from './DropZone';
9
+ export { DropZone, AuthenticatedBuildClient, AuthenticatedDropClient } from './DropZone';
10
+ export type { BuildClient, DeployedBuild, DeployedSite, DropClient } from './DropZone';
10
11
 
11
12
  export { type IconName } from './Icon';