@netlify/spark-ui 1.31.0-alpha.4 → 1.31.0-alpha.6
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 +3 -4
- package/dist/components/preact/DropZone/DropZone.d.ts +23 -98
- package/dist/components/preact/DropZone/DropZone.js +5 -5
- package/dist/components/preact/DropZone/index.js +29 -29
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +0 -1
- package/dist/components/preact/DropZone/useDropDeploy.js +25 -27
- package/dist/components/preact/index.js +10 -10
- package/dist/drop-core/analytics.d.ts +4 -15
- package/dist/drop-core/authedApi.d.ts +9 -32
- package/dist/drop-core/authedApi.js +15 -20
- package/dist/drop-core/authedDropClient.d.ts +15 -64
- package/dist/drop-core/authedDropClient.js +27 -49
- package/dist/drop-core/buildClient.d.ts +19 -53
- package/dist/drop-core/buildClient.js +32 -44
- package/dist/drop-core/buildStash.d.ts +6 -20
- package/dist/drop-core/client.d.ts +4 -2
- package/dist/drop-core/client.js +19 -17
- package/dist/drop-core/constants.d.ts +9 -0
- package/dist/drop-core/constants.js +10 -0
- package/dist/drop-core/deploy.d.ts +7 -17
- package/dist/drop-core/deploy.js +56 -43
- package/dist/drop-core/detectBuild.d.ts +8 -26
- package/dist/drop-core/errors.d.ts +27 -17
- package/dist/drop-core/errors.js +70 -33
- package/dist/drop-core/fileTypes.d.ts +4 -14
- package/dist/drop-core/index.d.ts +22 -11
- package/dist/drop-core/index.js +45 -49
- package/dist/drop-core/readFiles.d.ts +5 -11
- package/dist/drop-core/types.d.ts +2 -4
- package/dist/drop-core/zip.d.ts +7 -30
- package/package.json +1 -1
- package/packages/components/preact/DropZone/DropZone.tsx +27 -122
- package/packages/components/preact/DropZone/useDropDeploy.ts +31 -73
- package/packages/drop-core/README.md +91 -0
- package/packages/drop-core/analytics.ts +4 -15
- package/packages/drop-core/authedApi.ts +9 -35
- package/packages/drop-core/authedDropClient.ts +23 -86
- package/packages/drop-core/buildClient.test.ts +19 -4
- package/packages/drop-core/buildClient.ts +23 -66
- package/packages/drop-core/buildStash.ts +9 -25
- package/packages/drop-core/client.ts +19 -11
- package/packages/drop-core/constants.ts +25 -0
- package/packages/drop-core/deploy.test.ts +42 -0
- package/packages/drop-core/deploy.ts +42 -22
- package/packages/drop-core/detectBuild.ts +15 -50
- package/packages/drop-core/errors.test.ts +34 -3
- package/packages/drop-core/errors.ts +63 -27
- package/packages/drop-core/fileTypes.ts +5 -19
- package/packages/drop-core/index.ts +41 -11
- package/packages/drop-core/readFiles.test.ts +0 -1
- package/packages/drop-core/readFiles.ts +5 -12
- package/packages/drop-core/types.ts +2 -4
- package/packages/drop-core/zip.ts +7 -30
package/dist/drop-core/zip.d.ts
CHANGED
|
@@ -1,46 +1,23 @@
|
|
|
1
1
|
import { InferredBuildSettings } from './detectBuild';
|
|
2
2
|
import { ProcessedFile } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Render inferred settings as a `[build]` table. Only `command` and `publish`
|
|
5
|
-
* are written: everything else (functions directory, plugins, node version)
|
|
6
|
-
* is zero-config on Netlify's side and guessing at it would override detection
|
|
7
|
-
* that is better informed than we are.
|
|
8
|
-
*/
|
|
3
|
+
/** Renders a [build] table — command and publish only; everything else is zero-config server-side. */
|
|
9
4
|
export declare function generateNetlifyToml({ command, publish }: InferredBuildSettings): string;
|
|
10
5
|
export interface ZipOptions {
|
|
11
6
|
/** Filename given to the produced `File`. Shows up as the build's source archive. */
|
|
12
7
|
name?: string;
|
|
13
|
-
/**
|
|
14
|
-
* Synthesise a `netlify.toml` `[build]` table from `inferBuildSettings` when the
|
|
15
|
-
* drop doesn't declare one (default `true`). Set `false` to upload the files
|
|
16
|
-
* exactly as dropped and let buildbot resolve every setting itself.
|
|
17
|
-
*/
|
|
8
|
+
/** Synthesise a [build] table when the drop lacks one (default true); false uploads as-dropped. */
|
|
18
9
|
synthesizeNetlifyToml?: boolean;
|
|
19
10
|
}
|
|
20
11
|
export interface ZipResult {
|
|
21
12
|
/** The archive, ready to hand to `createBuildFromZip`. */
|
|
22
13
|
file: File;
|
|
23
|
-
/**
|
|
24
|
-
* The settings written into `netlify.toml`, or `null` when the archive is a
|
|
25
|
-
* faithful copy of the drop — either because it already declared a `[build]`
|
|
26
|
-
* table, or because nothing could be inferred with confidence.
|
|
27
|
-
*/
|
|
14
|
+
/** Settings written into netlify.toml, or null when the archive is a faithful copy. */
|
|
28
15
|
buildSettings: InferredBuildSettings | null;
|
|
29
16
|
}
|
|
30
17
|
/**
|
|
31
|
-
* Zip
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* uses (see `readFiles`), so junk — dotfiles, `node_modules`, macOS metadata — is
|
|
36
|
-
* already gone and paths sit at the deploy root. Output directories (`dist/`,
|
|
37
|
-
* `build/`) are deliberately *not* stripped: buildbot regenerates them when
|
|
38
|
-
* there's a build command, and a drop whose `netlify.toml` publishes a committed
|
|
39
|
-
* directory without a command would break if we removed it.
|
|
40
|
-
*
|
|
41
|
-
* When the drop needs a build but declares no `[build]` table, an inferred one is
|
|
42
|
-
* written in (appended to an existing `netlify.toml`, or created if there is
|
|
43
|
-
* none) so buildbot doesn't publish the project's source. See
|
|
44
|
-
* `inferBuildSettings` for how conservative that inference is.
|
|
18
|
+
* Zip a read file set into the archive `POST /sites/{id}/builds` expects.
|
|
19
|
+
* Junk is already gone (readFiles); dist/build are deliberately kept — a drop
|
|
20
|
+
* publishing a committed directory without a command would break without them.
|
|
21
|
+
* A [build] table is synthesised when missing (appended, so redirects survive).
|
|
45
22
|
*/
|
|
46
23
|
export declare function zipFiles(files: ProcessedFile[], { name, synthesizeNetlifyToml }?: ZipOptions): Promise<ZipResult>;
|
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.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/components/preact/index.js",
|
|
7
7
|
"types": "dist/components/preact/index.d.ts",
|
|
@@ -25,26 +25,13 @@ const DRAG_INACTIVE_MS = 120;
|
|
|
25
25
|
|
|
26
26
|
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Where an authenticated static deploy lands: the new site's project overview,
|
|
30
|
-
* the same place the app's own logged-in drop sends you.
|
|
31
|
-
*/
|
|
28
|
+
/** The project overview — where the app's own logged-in drop lands too. */
|
|
32
29
|
const defaultSiteDashboardUrl = (siteName: string) =>
|
|
33
30
|
`https://app.netlify.com/projects/${siteName}`;
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
* Where a build-required drop is sent by default. Mirrors the app's own Drop
|
|
37
|
-
* page: signup with `next=/drop`, so after signup + email verification the user
|
|
38
|
-
* lands back on the authenticated Drop page and can build there.
|
|
39
|
-
*/
|
|
32
|
+
// Mirrors the app's own Drop page hand-off.
|
|
40
33
|
const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
|
|
41
34
|
|
|
42
|
-
/**
|
|
43
|
-
* Where an authenticated build drop lands: the new site's project overview —
|
|
44
|
-
* the same destination as a static deploy, and where the app's own Drop page
|
|
45
|
-
* sends build drops. The overview surfaces the in-progress deploy; consumers
|
|
46
|
-
* who want the deploy-log page instead can override with `build.deployId`.
|
|
47
|
-
*/
|
|
48
35
|
const defaultBuildDeployUrl = ({ siteName }: DeployedBuild) =>
|
|
49
36
|
`https://app.netlify.com/projects/${siteName}`;
|
|
50
37
|
|
|
@@ -57,30 +44,17 @@ interface Props {
|
|
|
57
44
|
client?: DropClient;
|
|
58
45
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
59
46
|
claimUrl?: (siteName: string) => string;
|
|
60
|
-
/**
|
|
61
|
-
* Where an authenticated static deploy sends the visitor — the site already
|
|
62
|
-
* lives in their account, so there is no claim step. Defaults to the new
|
|
63
|
-
* site's project overview on app.netlify.com.
|
|
64
|
-
*/
|
|
47
|
+
/** Destination after an authenticated static deploy (no claim step). Default: the project overview. */
|
|
65
48
|
siteDashboardUrl?: (siteName: string) => string;
|
|
66
49
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* visitor who isn't logged in. Such projects can't deploy through the anonymous
|
|
70
|
-
* static drop API, so by default the user is redirected here — the app signup
|
|
71
|
-
* page with `next=/drop` — to build on the authenticated Drop page. Override to
|
|
72
|
-
* point at your own destination.
|
|
50
|
+
* Signup hand-off for a build drop with no session. The drop is stashed on
|
|
51
|
+
* this origin first — return the visitor here and it deploys automatically.
|
|
73
52
|
*/
|
|
74
53
|
buildSignupUrl?: string;
|
|
75
54
|
/**
|
|
76
|
-
* Mint
|
|
77
|
-
*
|
|
78
|
-
* drops
|
|
79
|
-
* build-required drops build in place instead of being handed off to the app.
|
|
80
|
-
* On netlify.com this reads `GET /access-control/generate-access-control-token`.
|
|
81
|
-
* Resolving `null` means "no session": static drops fall back to the anonymous
|
|
82
|
-
* claim flow, build drops to `buildSignupUrl`.
|
|
83
|
-
* See [Logged-in visitors](#logged-in-visitors).
|
|
55
|
+
* Mint the upload bearer — passing this switches on the whole authenticated
|
|
56
|
+
* path. `null` means "no session": static drops fall back to the anonymous
|
|
57
|
+
* claim flow; build drops stash and hand off. See the docs' Logged-in visitors.
|
|
84
58
|
*/
|
|
85
59
|
getUploadToken?: () => Promise<string | null>;
|
|
86
60
|
/**
|
|
@@ -88,108 +62,46 @@ interface Props {
|
|
|
88
62
|
* Takes precedence over `getUploadToken` for the build path.
|
|
89
63
|
*/
|
|
90
64
|
buildClient?: BuildClient;
|
|
91
|
-
/**
|
|
92
|
-
* Inject a custom authenticated `DropClient` for static drops (tests, staging,
|
|
93
|
-
* a different account flow). Takes precedence over `getUploadToken` for the
|
|
94
|
-
* static path. Distinct from `client`, which is the anonymous fallback.
|
|
95
|
-
*/
|
|
65
|
+
/** Custom authenticated static-drop client; wins over `getUploadToken`. (`client` is the anonymous fallback.) */
|
|
96
66
|
authenticatedDropClient?: DropClient;
|
|
97
|
-
/**
|
|
98
|
-
* API root for the authenticated paths' JSON calls, which go through your
|
|
99
|
-
* access-control rewrite so the session cookie authorizes them. Defaults to
|
|
100
|
-
* `/access-control/bb-api/api/v1`. (Uploads always go direct to
|
|
101
|
-
* `api.netlify.com` — the proxy can't carry bodies that large.)
|
|
102
|
-
*/
|
|
67
|
+
/** Root for the session-cookie JSON calls, via your access-control rewrite. Uploads always go direct. */
|
|
103
68
|
proxyBase?: string;
|
|
104
69
|
/** Create the visitor's new project in this account. Omit for their default account. */
|
|
105
70
|
accountSlug?: string;
|
|
106
|
-
/**
|
|
107
|
-
* Where an enqueued build sends the visitor. Defaults to the new site's
|
|
108
|
-
* project overview, same as a static deploy; `build.deployId` is available for
|
|
109
|
-
* consumers who want the deploy-log page instead.
|
|
110
|
-
*/
|
|
71
|
+
/** Destination after a build is enqueued. Default: the project overview (`build.deployId` for the logs page). */
|
|
111
72
|
buildDeployUrl?: (build: DeployedBuild) => string;
|
|
112
73
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* its own error message here (via `onError` and the overlay) rather than the
|
|
116
|
-
* visitor discovering it on the deploy page — at the cost of holding them on a
|
|
117
|
-
* spinner for the length of the build, which is minutes. Off by default.
|
|
74
|
+
* Hold the visitor until the build finishes (off by default — builds take
|
|
75
|
+
* minutes). On is the only way a failing build's own message reaches `onError`.
|
|
118
76
|
*/
|
|
119
77
|
waitForBuild?: boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Called when a build-required drop is detected, instead of building it or
|
|
122
|
-
* redirecting. Use it to own the hand-off yourself (custom routing, persisting
|
|
123
|
-
* the drop, gating on auth). When set, the component neither builds nor
|
|
124
|
-
* redirects. The static drop path is unaffected either way.
|
|
125
|
-
*/
|
|
78
|
+
/** Take over build-required drops entirely — when set, the component neither builds nor redirects. */
|
|
126
79
|
onBuildRequired?: (info: BuildDetection) => void;
|
|
127
80
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* however fits your auth flow, e.g. open a signup popup that keeps the
|
|
132
|
-
* visitor on the page. Once they're back authenticated, a remount of the
|
|
133
|
-
* zone deploys the stash automatically.
|
|
81
|
+
* Own the signup hand-off instead of the `buildSignupUrl` redirect (e.g. an
|
|
82
|
+
* auth popup). The drop is already stashed; a remount while authenticated
|
|
83
|
+
* deploys it.
|
|
134
84
|
*/
|
|
135
85
|
onBuildSignupHandoff?: (info: { stashed: boolean; detection: BuildDetection }) => void;
|
|
136
|
-
/**
|
|
137
|
-
* Deploy a previously stashed build drop automatically when the zone mounts
|
|
138
|
-
* with an authenticated visitor (default `true`). The stash is written when a
|
|
139
|
-
* logged-out build drop is handed to signup; see
|
|
140
|
-
* [Signup hand-off and auto-resume](#signup-hand-off-and-auto-resume).
|
|
141
|
-
*/
|
|
86
|
+
/** Auto-deploy a stashed build drop on mount when authenticated (default true). */
|
|
142
87
|
resumeStashedBuilds?: boolean;
|
|
143
88
|
/** Called once the deploy is live. */
|
|
144
89
|
onDeploy?: (result: DeployedSite) => void;
|
|
145
|
-
/**
|
|
146
|
-
* Called once a build-required drop has been zipped, uploaded and enqueued —
|
|
147
|
-
* before the redirect to `buildDeployUrl`. The site exists at this point but
|
|
148
|
-
* won't serve content until the build finishes.
|
|
149
|
-
*/
|
|
90
|
+
/** Called once a build is enqueued, before the `buildDeployUrl` redirect. */
|
|
150
91
|
onBuildDeploy?: (build: DeployedBuild) => void;
|
|
151
|
-
/**
|
|
152
|
-
* Called when a deploy fails, with a humanized `message` (ready to show a
|
|
153
|
-
* user — the same copy the built-in overlay uses) and the raw `reason`. Pair
|
|
154
|
-
* this with `showStatus={false}` to render your own error UI.
|
|
155
|
-
*/
|
|
92
|
+
/** Deploy failures: humanized `message` (the overlay's copy) + raw `reason`. Pair with showStatus={false}. */
|
|
156
93
|
onError?: (error: DeployError) => void;
|
|
157
94
|
/**
|
|
158
|
-
* Render the built-in
|
|
159
|
-
*
|
|
160
|
-
* drive your own feedback from the `data-state` attribute on the root element.
|
|
161
|
-
* Note: in this mode you're responsible for surfacing errors yourself — use
|
|
162
|
-
* `onError` to get the message.
|
|
95
|
+
* Render the built-in overlay. `false` keeps children visible — drive your
|
|
96
|
+
* own feedback from `data-state`, and surface errors yourself via `onError`.
|
|
163
97
|
*/
|
|
164
98
|
showStatus?: boolean;
|
|
165
99
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
|
|
169
|
-
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
170
|
-
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
|
|
171
|
-
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
172
|
-
* - `dropzone_build_required` — drop needs a build (built here, or handed off) ({ method, reason, file_count, file_types })
|
|
173
|
-
* - `dropzone_deploy_succeeded` — static deploy went live ({ method, site_name, file_count, authenticated, auth_fallback?, file_types })
|
|
174
|
-
* - `dropzone_build_deploy_succeeded` — build enqueued for a logged-in visitor ({ method, reason, site_name, file_count, framework?, file_types })
|
|
175
|
-
* - `dropzone_deploy_failed` — deploy or build errored out ({ method, reason, file_count?, file_types })
|
|
176
|
-
*
|
|
177
|
-
* `authenticated` says whether the deploy landed in the visitor's account;
|
|
178
|
-
* `auth_fallback: true` is sent only when an authenticated attempt bounced
|
|
179
|
-
* (stale auth hint) and the drop completed anonymously — omitted otherwise.
|
|
180
|
-
* `file_types` is the sorted, de-duplicated list of MIME types in the
|
|
181
|
-
* selection, with `application/octet-stream` standing in wherever the browser
|
|
182
|
-
* reports none (a dragged folder, an `.exe`). It is omitted when the browser
|
|
183
|
-
* exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
|
|
184
|
-
* a file-picker selection reports its types on the outcome events, as
|
|
185
|
-
* `dropzone_browse_opened` fires before any file exists.
|
|
100
|
+
* Provider-agnostic analytics hook. Event names and payload shapes live in
|
|
101
|
+
* DROPZONE_EVENTS / DropZoneEventProperties (drop-core/analytics.ts).
|
|
186
102
|
*/
|
|
187
103
|
onTrack?: TrackFn;
|
|
188
|
-
/**
|
|
189
|
-
* Stable identifier for this instance, attached to every tracked event as
|
|
190
|
-
* `dropzone_id`. Set it when a page renders more than one Drop Zone so the
|
|
191
|
-
* events can be told apart in Amplitude.
|
|
192
|
-
*/
|
|
104
|
+
/** Stamped on every event as `dropzone_id` — set when a page has more than one zone. */
|
|
193
105
|
analyticsId?: string;
|
|
194
106
|
className?: string;
|
|
195
107
|
}
|
|
@@ -219,8 +131,6 @@ export default function DropZone({
|
|
|
219
131
|
analyticsId,
|
|
220
132
|
className,
|
|
221
133
|
}: Props) {
|
|
222
|
-
// Tag every event with this instance's id so multiple zones on a page can be
|
|
223
|
-
// told apart, then hand it off to the consumer's analytics.
|
|
224
134
|
const track: TrackFn = (event, properties = {}) =>
|
|
225
135
|
onTrack?.(event, analyticsId ? { dropzone_id: analyticsId, ...properties } : properties);
|
|
226
136
|
|
|
@@ -254,8 +164,6 @@ export default function DropZone({
|
|
|
254
164
|
inputRef.current?.click();
|
|
255
165
|
};
|
|
256
166
|
|
|
257
|
-
// The overlay only paints when there's something to show — dragging, deploying,
|
|
258
|
-
// or an error. With `showStatus` off, the consumer drives their own feedback.
|
|
259
167
|
const showOverlay = showStatus && (active || busy || status.kind === 'error');
|
|
260
168
|
const rootClass = [
|
|
261
169
|
'n-dropzone',
|
|
@@ -344,11 +252,8 @@ export default function DropZone({
|
|
|
344
252
|
}
|
|
345
253
|
|
|
346
254
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* stream of `dragover` events — so we stay active only while they keep arriving
|
|
350
|
-
* and clear shortly after they stop. This also handles drags over children,
|
|
351
|
-
* where `dragleave` targets a child rather than the zone.
|
|
255
|
+
* Drag-over tracking by dragover decay: drags have no reliable cancel event,
|
|
256
|
+
* and dragleave targets children — so stay active while events keep arriving.
|
|
352
257
|
*/
|
|
353
258
|
function useDragActive() {
|
|
354
259
|
const [active, setActive] = useState(false);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getSingleNonIndexHtmlFileName,
|
|
16
16
|
hasMultipleHtmlFilesWithoutIndex,
|
|
17
17
|
humanizeDropError,
|
|
18
|
-
|
|
18
|
+
isNotAuthenticatedError,
|
|
19
19
|
renameSingleNonIndexHtmlToIndex,
|
|
20
20
|
type BuildClient,
|
|
21
21
|
type BuildDetection,
|
|
@@ -88,7 +88,6 @@ export type DeployStatus =
|
|
|
88
88
|
| { kind: 'redirecting'; target: 'claim' | 'dashboard' | 'signup' | 'build' }
|
|
89
89
|
| { kind: 'error'; message: string };
|
|
90
90
|
|
|
91
|
-
/** Re-exported from the drop-core analytics contract, where the shape now lives. */
|
|
92
91
|
export type { TrackFn };
|
|
93
92
|
|
|
94
93
|
interface UseDropDeployOptions {
|
|
@@ -113,7 +112,6 @@ interface UseDropDeployOptions {
|
|
|
113
112
|
track: TrackFn;
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
/** Map a low-level deploy progress event onto our UI status. */
|
|
117
115
|
function progressToStatus(e: DeployProgress): DeployStatus {
|
|
118
116
|
switch (e.phase) {
|
|
119
117
|
case 'zipping':
|
|
@@ -130,7 +128,7 @@ function progressToStatus(e: DeployProgress): DeployStatus {
|
|
|
130
128
|
}
|
|
131
129
|
}
|
|
132
130
|
|
|
133
|
-
/**
|
|
131
|
+
/** Advisory only — a missing index.html never fails the deploy. */
|
|
134
132
|
function indexHtmlWarning(files: ProcessedFile[]): string | null {
|
|
135
133
|
const singleNonIndex = getSingleNonIndexHtmlFileName(files);
|
|
136
134
|
if (singleNonIndex) {
|
|
@@ -143,12 +141,9 @@ function indexHtmlWarning(files: ProcessedFile[]): string | null {
|
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
/**
|
|
146
|
-
* Best-effort
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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.
|
|
144
|
+
* Best-effort claim persistence. localStorage is per-origin, so this never
|
|
145
|
+
* reaches app.netlify.com — the URL fragment is the real cross-origin handoff.
|
|
146
|
+
* Authenticated deploys skip it: no claim token, and bearers are never persisted.
|
|
152
147
|
*/
|
|
153
148
|
function persistAnonymousClaimLocally(token: string, siteName: string): void {
|
|
154
149
|
try {
|
|
@@ -184,16 +179,9 @@ export function useDropDeploy({
|
|
|
184
179
|
onError,
|
|
185
180
|
track,
|
|
186
181
|
}: 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
|
-
*/
|
|
182
|
+
/** Typed veneer over `track` — a renamed property fails to compile instead of forking analytics history. */
|
|
193
183
|
const emit = <E extends DropZoneEventName>(event: E, props: DropZoneEventProperties[E]) =>
|
|
194
|
-
// The
|
|
195
|
-
// signature (so payloads stay closed for consumers), and the checking that
|
|
196
|
-
// matters happened against DropZoneEventProperties[E] above.
|
|
184
|
+
// The event interfaces carry no index signature, hence the cast to TrackFn's record.
|
|
197
185
|
track(event, props as Record<string, unknown>);
|
|
198
186
|
|
|
199
187
|
const [status, setStatus] = useState<DeployStatus>({ kind: 'idle' });
|
|
@@ -224,10 +212,8 @@ export function useDropDeploy({
|
|
|
224
212
|
const busy = status.kind !== 'idle' && status.kind !== 'error';
|
|
225
213
|
|
|
226
214
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
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`.
|
|
215
|
+
* Site + build for a zipped drop. `null` = not logged in (nothing created,
|
|
216
|
+
* so the caller can still stash and hand off); other failures throw.
|
|
231
217
|
*/
|
|
232
218
|
async function enqueueZippedBuild(
|
|
233
219
|
client: BuildClient,
|
|
@@ -247,7 +233,7 @@ export function useDropDeploy({
|
|
|
247
233
|
buildSettings,
|
|
248
234
|
};
|
|
249
235
|
} catch (err) {
|
|
250
|
-
if (err
|
|
236
|
+
if (isNotAuthenticatedError(err)) return null;
|
|
251
237
|
throw err;
|
|
252
238
|
}
|
|
253
239
|
}
|
|
@@ -261,10 +247,8 @@ export function useDropDeploy({
|
|
|
261
247
|
}
|
|
262
248
|
|
|
263
249
|
/**
|
|
264
|
-
* `fileTypes` is
|
|
265
|
-
*
|
|
266
|
-
* captured before `read()` runs so it survives a read that throws. Omitted
|
|
267
|
-
* from events when empty rather than sent as `[]`.
|
|
250
|
+
* `fileTypes` is captured at the drop boundary, before `read()` runs, so it
|
|
251
|
+
* survives a read that throws. Omitted from events when empty, never `[]`.
|
|
268
252
|
*/
|
|
269
253
|
async function deploy(
|
|
270
254
|
read: () => Promise<ProcessedFile[]>,
|
|
@@ -276,17 +260,8 @@ export function useDropDeploy({
|
|
|
276
260
|
|
|
277
261
|
const typeProps = fileTypes.length ? { file_types: fileTypes } : {};
|
|
278
262
|
|
|
279
|
-
//
|
|
280
|
-
// analytics
|
|
281
|
-
// read/deploy throw) reports uniformly — the mirror of the
|
|
282
|
-
// `dropzone_deploy_succeeded` event on the happy path. `file_count` is only
|
|
283
|
-
// included where the file set is known (it isn't if `read()` itself threw);
|
|
284
|
-
// `file_types` rides along on every failure precisely because it *is* known
|
|
285
|
-
// in that case — a rejected single file is the main thing we want to see.
|
|
286
|
-
// `reason` goes to analytics as-is (raw, e.g. "drop failed: 429"); `message`
|
|
287
|
-
// is what the user sees, which for a caught error is humanized separately.
|
|
288
|
-
// `onError` gets both so a consumer (e.g. with `showStatus={false}`) can show
|
|
289
|
-
// the humanized message while keeping the raw reason for their own logging.
|
|
263
|
+
// Single failure funnel. `file_count` only when the file set is known;
|
|
264
|
+
// `reason` is raw for analytics, `message` is the humanized copy; `onError` gets both.
|
|
290
265
|
const fail = (message: string, reason: string, extra?: Record<string, unknown>) => {
|
|
291
266
|
setStatus({ kind: 'error', message });
|
|
292
267
|
emit(DROPZONE_EVENTS.DEPLOY_FAILED, { method: source, reason, ...typeProps, ...extra });
|
|
@@ -294,10 +269,9 @@ export function useDropDeploy({
|
|
|
294
269
|
};
|
|
295
270
|
|
|
296
271
|
try {
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// single-file case no longer warns and only the multi-HTML case does.
|
|
272
|
+
// Rename a lone non-index HTML file so it is served at the site root.
|
|
273
|
+
// Warnings are computed on the renamed set, so the fixable single-file
|
|
274
|
+
// case no longer warns and only the multi-HTML case does.
|
|
301
275
|
const files = renameSingleNonIndexHtmlToIndex(await read());
|
|
302
276
|
if (!files.length) {
|
|
303
277
|
fail('No files found to deploy.', 'No files found to deploy.', { file_count: 0 });
|
|
@@ -328,12 +302,8 @@ export function useDropDeploy({
|
|
|
328
302
|
}
|
|
329
303
|
|
|
330
304
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
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`.
|
|
305
|
+
* Build-required drops can't use the anonymous API (static-only): build in
|
|
306
|
+
* the account, or stash + hand off to signup when there's no session.
|
|
337
307
|
*/
|
|
338
308
|
async function handleBuildRequiredDrop(
|
|
339
309
|
files: ProcessedFile[],
|
|
@@ -381,8 +351,7 @@ export function useDropDeploy({
|
|
|
381
351
|
|
|
382
352
|
// No session after all (the auth hint cookie can outlive the session).
|
|
383
353
|
// Nothing was created — stash the archive so the drop survives the
|
|
384
|
-
// signup round-trip.
|
|
385
|
-
// authenticated, the mount-time resume deploys it for them.
|
|
354
|
+
// signup round-trip.
|
|
386
355
|
stashed = await saveDropStash(archive.file, archive.buildSettings);
|
|
387
356
|
if (stashed) {
|
|
388
357
|
emit(DROPZONE_EVENTS.PROJECT_STASHED, {
|
|
@@ -408,13 +377,9 @@ export function useDropDeploy({
|
|
|
408
377
|
}
|
|
409
378
|
|
|
410
379
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
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.
|
|
380
|
+
* `null` = no session (pre-mutation, so the anonymous fallback is safe).
|
|
381
|
+
* Anything else rethrows — falling back after the site exists would strand
|
|
382
|
+
* an empty site and deploy a duplicate.
|
|
418
383
|
*/
|
|
419
384
|
async function attemptAuthenticatedStaticDeploy(
|
|
420
385
|
client: DropClient,
|
|
@@ -425,7 +390,7 @@ export function useDropDeploy({
|
|
|
425
390
|
onProgress: e => setStatus(progressToStatus(e)),
|
|
426
391
|
});
|
|
427
392
|
} catch (err) {
|
|
428
|
-
if (err
|
|
393
|
+
if (isNotAuthenticatedError(err)) return null;
|
|
429
394
|
throw err;
|
|
430
395
|
}
|
|
431
396
|
}
|
|
@@ -456,11 +421,10 @@ export function useDropDeploy({
|
|
|
456
421
|
}
|
|
457
422
|
|
|
458
423
|
setStatus({ kind: 'redirecting', target: authenticated ? 'dashboard' : 'claim' });
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// otherwise, like `file_types`.
|
|
424
|
+
// The redirect below can cut a fire-and-forget analytics request short;
|
|
425
|
+
// harden delivery with sendBeacon or by deferring the redirect.
|
|
426
|
+
// `auth_fallback` is only sent when an authenticated attempt bounced
|
|
427
|
+
// (stale auth hint) — omitted otherwise, like `file_types`.
|
|
464
428
|
emit(DROPZONE_EVENTS.DEPLOY_SUCCEEDED, {
|
|
465
429
|
method: source,
|
|
466
430
|
site_name: result.subdomain,
|
|
@@ -480,7 +444,6 @@ export function useDropDeploy({
|
|
|
480
444
|
redirectAfterStaticDeploy(authenticated, result.subdomain, token);
|
|
481
445
|
}
|
|
482
446
|
|
|
483
|
-
/** Hand the visitor to their new site: its dashboard page, or the claim page. */
|
|
484
447
|
function redirectAfterStaticDeploy(authenticated: boolean, siteName: string, token: string) {
|
|
485
448
|
if (authenticated) {
|
|
486
449
|
// The site already lives in the visitor's account — send them straight
|
|
@@ -495,13 +458,8 @@ export function useDropDeploy({
|
|
|
495
458
|
}
|
|
496
459
|
|
|
497
460
|
/**
|
|
498
|
-
* Deploy
|
|
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.
|
|
461
|
+
* Deploy the stashed drop. Consumed only once provably authenticated (a
|
|
462
|
+
* bounced attempt keeps it), then consumed win or lose — no retry loops.
|
|
505
463
|
*/
|
|
506
464
|
async function resumeStashedBuild(client: BuildClient) {
|
|
507
465
|
const stash = await loadDropStash();
|
|
@@ -526,7 +484,7 @@ export function useDropDeploy({
|
|
|
526
484
|
|
|
527
485
|
if (!built) {
|
|
528
486
|
// Still no session — the visitor came back logged out. Keep the stash for
|
|
529
|
-
// their next authenticated visit
|
|
487
|
+
// their next authenticated visit.
|
|
530
488
|
setStatus({ kind: 'idle' });
|
|
531
489
|
return;
|
|
532
490
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# drop-core
|
|
2
|
+
|
|
3
|
+
The framework-free engine behind the Drop Zone: reading a dropped folder/zip
|
|
4
|
+
into deployable files, deciding whether it needs a build, and deploying it —
|
|
5
|
+
anonymously (claim flow) or into a logged-in visitor's account — plus the
|
|
6
|
+
stash that carries a build drop across the signup round-trip.
|
|
7
|
+
|
|
8
|
+
Consumed three ways: by the Preact `DropZone` in this repo, by anything
|
|
9
|
+
importing `@netlify/spark-ui/drop-core`, and eventually by other Netlify
|
|
10
|
+
surfaces implementing their own Drop UI over the same core.
|
|
11
|
+
|
|
12
|
+
## Conventions
|
|
13
|
+
|
|
14
|
+
These follow the practices shared by the libraries doing our two jobs —
|
|
15
|
+
react-dropzone/Uppy/FilePond on the component side, netlify-cli's deploy
|
|
16
|
+
engine, @vercel/client, and tus-js-client on the pipeline side. Where we
|
|
17
|
+
deliberately differ, that's recorded too, so it isn't re-litigated by accident.
|
|
18
|
+
|
|
19
|
+
### Boundaries
|
|
20
|
+
|
|
21
|
+
- **Nothing in `drop-core` may import from `preact`** (or any UI framework).
|
|
22
|
+
Anything that imports preact may not contain deploy/read/detect logic — it
|
|
23
|
+
adapts this package to a view, nothing more.
|
|
24
|
+
- **The barrel is the public API.** Nothing is public unless `index.ts` names
|
|
25
|
+
it; `export *` is reserved for modules that are contracts in their entirety
|
|
26
|
+
(`types`, `errors`, `analytics`). New helpers default to private.
|
|
27
|
+
- **Granular pure modules, one cohesive orchestrator.** Pure helpers get their
|
|
28
|
+
own small file named after what they export; the deploy orchestration stays
|
|
29
|
+
together in `deploy.ts` rather than fragmenting a state machine.
|
|
30
|
+
- **Shared tunables live in `constants.ts`** with a unit-bearing comment each
|
|
31
|
+
(`// 10 minutes`, not `// the timeout`). Module-specific constants stay with
|
|
32
|
+
their module. No magic numbers inline.
|
|
33
|
+
|
|
34
|
+
### Naming
|
|
35
|
+
|
|
36
|
+
| Kind | Convention | Examples here |
|
|
37
|
+
| ------------------ | ---------------------------------- | ------------------------------------------------------------- |
|
|
38
|
+
| Outcome callbacks | `onVerb` / past participle | `onDeploy`, `onBuildRequired`, `onError` |
|
|
39
|
+
| Boolean state | `isX` | `busy` (exception, pre-dates rule), `isNotAuthenticatedError` |
|
|
40
|
+
| Strategy injection | bare noun | `client`, `buildClient`, `validator`-style |
|
|
41
|
+
| Analytics events | `dropzone_snake_case` — **frozen** | `DROPZONE_EVENTS` (test-pinned wire names) |
|
|
42
|
+
| Error predicates | `isXError(err)` | `isNotAuthenticatedError` |
|
|
43
|
+
| Files | filename = main export | `detectBuild.ts`, `buildStash.ts` |
|
|
44
|
+
|
|
45
|
+
### Comments
|
|
46
|
+
|
|
47
|
+
- **WHY over WHAT.** A comment earns its place by stating something the code
|
|
48
|
+
can't: an invariant, an observed API behavior, a browser quirk, a
|
|
49
|
+
don't-simplify warning. Never narrate the next line.
|
|
50
|
+
- **Record observed API behavior, with receipts.** The model comment in this
|
|
51
|
+
package: readiness polling keys off `deploy_id` (BSON), not `deploy.id` (site
|
|
52
|
+
UUID), "or the request 401s misleadingly" — naming the server-side file. Say
|
|
53
|
+
what the server _actually does_, and link the server source when you can.
|
|
54
|
+
- **Repeat load-bearing quirk notes at every call site.** The `deploy_id` note
|
|
55
|
+
appears in the client _and_ the orchestrator on purpose — a note that only
|
|
56
|
+
exists where nobody is reading protects nothing.
|
|
57
|
+
- **"Don't simplify" notes name what breaks** — the invariant, and ideally the
|
|
58
|
+
test that pins it (e.g. the `NotAuthenticatedError` boundary below).
|
|
59
|
+
- **No `@param`/`@returns`.** Types carry the shape; JSDoc carries rationale
|
|
60
|
+
and edge cases. (react-dropzone: zero `@param` across its entire source.)
|
|
61
|
+
- File-header purpose blocks only where the filename undersells the contents.
|
|
62
|
+
|
|
63
|
+
### Errors
|
|
64
|
+
|
|
65
|
+
- **Class per failure, each with a readonly kebab-case `code` brand.** This
|
|
66
|
+
package ships as raw TS (`./components`) _and_ compiled dist (`./preact`,
|
|
67
|
+
`./drop-core`) — two copies of a class can coexist in one app and defeat
|
|
68
|
+
`instanceof`. Detection that steers control flow must go through a
|
|
69
|
+
code-checking predicate (`isNotAuthenticatedError`), never bare `instanceof`.
|
|
70
|
+
- **`NotAuthenticatedError` is pre-mutation only.** It may be thrown solely
|
|
71
|
+
before anything is created (token gate, site-create 401) — it is the signal
|
|
72
|
+
that an anonymous retry is safe. Throwing it after a site exists would strand
|
|
73
|
+
an empty project and deploy a duplicate. Pinned by tests.
|
|
74
|
+
- **Humanized copy lives in `humanizeDropError`, in this package.** A
|
|
75
|
+
deliberate divergence from @vercel/client (which leaves rendering to the
|
|
76
|
+
CLI): our consumers render our copy directly, and the docs promise it.
|
|
77
|
+
- Transient upload failures retry on `UPLOAD_RETRY_DELAYS_MS` (backoff as
|
|
78
|
+
data, the tus idiom); **400/422 never retry** — the request itself is wrong.
|
|
79
|
+
|
|
80
|
+
### Considered and rejected
|
|
81
|
+
|
|
82
|
+
- **Async-generator lifecycles** (@vercel/client): our phase-callback design is
|
|
83
|
+
equivalent and churn-free; `DeployPhase` is a closed union, which netlify-cli's
|
|
84
|
+
own author wished theirs was.
|
|
85
|
+
- **Kebab-case event renames**: `dropzone_*` wire names are pinned analytics
|
|
86
|
+
history; `analytics.test.ts` fails on any rename. Append-only.
|
|
87
|
+
- **`getRootProps`/`getInputProps` attribute getters** (react-dropzone, Uppy):
|
|
88
|
+
the right shape for a future framework-free UI tier shared with the app —
|
|
89
|
+
not something to bolt onto the shipped wrapper component.
|
|
90
|
+
- **A `defaultOptions` object** (tus, Uppy): our destructured constructor
|
|
91
|
+
defaults are react-dropzone's pattern; either is fine, we have this one.
|