@netlify/spark-ui 1.31.0-alpha.5 → 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 -97
- package/dist/drop-core/analytics.d.ts +4 -15
- package/dist/drop-core/authedApi.d.ts +9 -22
- package/dist/drop-core/authedDropClient.d.ts +13 -55
- package/dist/drop-core/authedDropClient.js +5 -18
- package/dist/drop-core/buildClient.d.ts +19 -53
- package/dist/drop-core/buildClient.js +27 -40
- package/dist/drop-core/buildStash.d.ts +6 -20
- package/dist/drop-core/client.d.ts +1 -1
- package/dist/drop-core/constants.d.ts +1 -21
- package/dist/drop-core/deploy.d.ts +5 -17
- package/dist/drop-core/detectBuild.d.ts +8 -26
- package/dist/drop-core/errors.d.ts +8 -27
- package/dist/drop-core/fileTypes.d.ts +4 -14
- 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 -121
- package/packages/components/preact/DropZone/useDropDeploy.ts +27 -60
- package/packages/drop-core/analytics.ts +4 -15
- package/packages/drop-core/authedApi.ts +9 -22
- package/packages/drop-core/authedDropClient.ts +13 -55
- package/packages/drop-core/buildClient.test.ts +19 -4
- package/packages/drop-core/buildClient.ts +21 -63
- package/packages/drop-core/buildStash.ts +9 -25
- package/packages/drop-core/client.ts +6 -10
- package/packages/drop-core/constants.ts +7 -21
- package/packages/drop-core/deploy.ts +6 -22
- package/packages/drop-core/detectBuild.ts +15 -50
- package/packages/drop-core/errors.test.ts +3 -1
- package/packages/drop-core/errors.ts +9 -30
- package/packages/drop-core/fileTypes.ts +5 -19
- package/packages/drop-core/readFiles.ts +5 -12
- package/packages/drop-core/types.ts +2 -4
- package/packages/drop-core/zip.ts +7 -30
|
@@ -5,19 +5,13 @@ import {
|
|
|
5
5
|
DEFAULT_PROXY_BASE,
|
|
6
6
|
DEFAULT_TIMEOUT_MS,
|
|
7
7
|
} from './constants';
|
|
8
|
-
import {
|
|
9
|
-
BuildCreateError,
|
|
10
|
-
BuildFailedError,
|
|
11
|
-
BuildTimeoutError,
|
|
12
|
-
NotAuthenticatedError,
|
|
13
|
-
} from './errors';
|
|
8
|
+
import { BuildCreateError, BuildFailedError, BuildTimeoutError } from './errors';
|
|
14
9
|
import type { BuildSite } from './types';
|
|
15
10
|
|
|
16
11
|
export type { BuildSite } from './types';
|
|
17
12
|
|
|
18
13
|
/** The subset of a Netlify Build returned by `POST /sites/{id}/builds`. */
|
|
19
14
|
export interface BuildResponse {
|
|
20
|
-
/** build id */
|
|
21
15
|
id: string;
|
|
22
16
|
/** the deploy the build produces — present as soon as the build is enqueued */
|
|
23
17
|
deploy_id?: string;
|
|
@@ -30,28 +24,14 @@ export interface BuildClient {
|
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
export interface AuthenticatedBuildClientConfig {
|
|
33
|
-
/**
|
|
34
|
-
* Base for JSON API calls, routed through the consumer's access-control
|
|
35
|
-
* rewrite so the session cookie authenticates them. On netlify.com that
|
|
36
|
-
* rewrite is `/access-control/*` → the app's access-control function, which
|
|
37
|
-
* makes `/access-control/bb-api/api/v1` the API root (the default here).
|
|
38
|
-
*/
|
|
27
|
+
/** Root for session-cookie JSON calls, via the consumer's access-control rewrite. */
|
|
39
28
|
proxyBase?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Direct API base for the zip upload. The proxy above runs in a Lambda with a
|
|
42
|
-
* ~6MB request body limit, so the multipart POST has to go straight to
|
|
43
|
-
* `api.netlify.com` (whose `/api/*` CORS policy allows any origin).
|
|
44
|
-
*/
|
|
29
|
+
/** Direct base for the zip upload — the proxy's ~6MB Lambda body limit can't carry it. */
|
|
45
30
|
apiBase?: string;
|
|
46
31
|
/**
|
|
47
|
-
* Mint
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
50
|
-
* Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
|
|
51
|
-
*
|
|
52
|
-
* Note the token issued to a non-app origin lives only 30 seconds. It is
|
|
53
|
-
* minted immediately before the upload, but a zip that takes longer than that
|
|
54
|
-
* to transfer will still be rejected mid-flight — see `createBuildFromZip`.
|
|
32
|
+
* Mint the upload bearer; `null` means "no session". Non-app origins get
|
|
33
|
+
* 30-second tokens — a zip slower than that fails mid-flight (see
|
|
34
|
+
* `createBuildFromZip`).
|
|
55
35
|
*/
|
|
56
36
|
getUploadToken: () => Promise<string | null>;
|
|
57
37
|
/** Create the site in this account. Omit to use the user's default account. */
|
|
@@ -65,16 +45,9 @@ export interface AuthenticatedBuildClientConfig {
|
|
|
65
45
|
}
|
|
66
46
|
|
|
67
47
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* hands buildbot a source archive to build, mirroring what app.netlify.com does
|
|
72
|
-
* for a drag-and-drop build.
|
|
73
|
-
*
|
|
74
|
-
* Requests split across two hosts by necessity: JSON calls go through the
|
|
75
|
-
* consumer's access-control proxy (cookie session, no token handling in the
|
|
76
|
-
* browser), while the zip upload goes direct to `api.netlify.com` with a
|
|
77
|
-
* short-lived bearer token because the proxy can't carry a body that large.
|
|
48
|
+
* Builds a drop in a logged-in visitor's account: create the site, hand
|
|
49
|
+
* buildbot the source zip. JSON rides the cookie proxy; the zip goes direct
|
|
50
|
+
* with a bearer (the proxy can't carry it).
|
|
78
51
|
*/
|
|
79
52
|
export class AuthenticatedBuildClient implements BuildClient {
|
|
80
53
|
private proxyBase: string;
|
|
@@ -103,29 +76,24 @@ export class AuthenticatedBuildClient implements BuildClient {
|
|
|
103
76
|
this.timeoutMs = timeoutMs;
|
|
104
77
|
}
|
|
105
78
|
|
|
106
|
-
/**
|
|
107
|
-
* Create the site the build deploys into. A logged-out visitor fails here,
|
|
108
|
-
* before anything is created — see `createSiteInAccount` for the error
|
|
109
|
-
* mapping that makes the signup fallback safe.
|
|
110
|
-
*/
|
|
79
|
+
/** A logged-out visitor fails here, before anything is created (see `createSiteInAccount`). */
|
|
111
80
|
async createSite(): Promise<BuildSite> {
|
|
112
81
|
return createSiteInAccount(this.proxyBase, this.accountSlug);
|
|
113
82
|
}
|
|
114
83
|
|
|
115
84
|
/**
|
|
116
|
-
*
|
|
117
|
-
* (
|
|
118
|
-
*
|
|
119
|
-
* boundary itself.
|
|
85
|
+
* One multipart request on a fresh 30s bearer — a slower zip fails mid-flight
|
|
86
|
+
* (the app origin gets 300s; raising ours is a platform-side change).
|
|
87
|
+
* Content-Type stays unset so the browser adds the multipart boundary.
|
|
120
88
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
89
|
+
* Never NotAuthenticatedError, including for a 401: the site exists by the
|
|
90
|
+
* time this runs, so signalling "no session" would strand it and re-stash
|
|
91
|
+
* the drop for the next visit to strand another. `createSite` is the auth
|
|
92
|
+
* gate for this path.
|
|
125
93
|
*/
|
|
126
94
|
async createBuildFromZip(siteId: string, zip: File): Promise<BuildResponse> {
|
|
127
95
|
const token = await this.getUploadToken();
|
|
128
|
-
if (!token) throw new
|
|
96
|
+
if (!token) throw new BuildCreateError();
|
|
129
97
|
|
|
130
98
|
const body = new FormData();
|
|
131
99
|
body.append('zip', zip);
|
|
@@ -136,22 +104,14 @@ export class AuthenticatedBuildClient implements BuildClient {
|
|
|
136
104
|
headers: { Authorization: `Bearer ${token}` },
|
|
137
105
|
body,
|
|
138
106
|
});
|
|
139
|
-
if (res.status === 401 || res.status === 403) throw new NotAuthenticatedError();
|
|
140
107
|
if (!res.ok) throw new BuildCreateError(res.status);
|
|
141
108
|
return res.json();
|
|
142
109
|
}
|
|
143
110
|
|
|
144
111
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* deploy id yet has nothing pollable, so it resolves immediately and the
|
|
149
|
-
* caller falls back to the site page.
|
|
150
|
-
*
|
|
151
|
-
* Optional in the drop flow, and off by default: a build takes minutes, so
|
|
152
|
-
* the useful hand-off is usually the project page rather than a spinner.
|
|
153
|
-
* Polling rides the session cookie, so it cannot outlive its credentials the
|
|
154
|
-
* way a 30s bearer would.
|
|
112
|
+
* Optional (a build takes minutes; the useful hand-off is the project page).
|
|
113
|
+
* Polls on the cookie, so it can't outlive its credentials; no deploy id yet
|
|
114
|
+
* means nothing pollable, so it resolves immediately.
|
|
155
115
|
*/
|
|
156
116
|
async waitUntilBuildReady(build: BuildResponse): Promise<void> {
|
|
157
117
|
if (!build.deploy_id) return;
|
|
@@ -163,8 +123,6 @@ export class AuthenticatedBuildClient implements BuildClient {
|
|
|
163
123
|
timeoutMs: this.timeoutMs,
|
|
164
124
|
});
|
|
165
125
|
if (settled.outcome === 'ready') return;
|
|
166
|
-
// The build's own error message is the only actionable part of a failure,
|
|
167
|
-
// so it travels on the typed error rather than being flattened away.
|
|
168
126
|
if (settled.outcome === 'failed') throw new BuildFailedError(settled.errorMessage);
|
|
169
127
|
throw new BuildTimeoutError();
|
|
170
128
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import type { InferredBuildSettings } from './detectBuild';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* budget. When they return to this origin authenticated, the zone deploys the
|
|
10
|
-
* stash automatically instead of asking them to drop again.
|
|
11
|
-
*
|
|
12
|
-
* Storage is per-origin: a stash written on www.netlify.com can only ever be
|
|
13
|
-
* resumed on www.netlify.com. That is the design, not a limitation — it is
|
|
14
|
-
* exactly why the hand-off page must bring the visitor back here.
|
|
15
|
-
*
|
|
16
|
-
* Every operation is failure-swallowing: the stash is an enhancement, and a
|
|
17
|
-
* browser without IndexedDB (or with a full quota) degrades to the plain
|
|
18
|
-
* signup redirect the zone always supported.
|
|
4
|
+
* Carries a build drop across the signup round-trip: stash the zip in
|
|
5
|
+
* IndexedDB before the hand-off, deploy it when the visitor returns
|
|
6
|
+
* authenticated. Storage is per-origin by design — the hand-off must bring
|
|
7
|
+
* them back here. Every operation swallows failures: no storage means the
|
|
8
|
+
* plain signup redirect the zone always supported.
|
|
19
9
|
*/
|
|
20
10
|
|
|
21
11
|
export interface DropBuildStash {
|
|
@@ -92,11 +82,7 @@ const clearMarker = () => {
|
|
|
92
82
|
localStorage.removeItem(MARKER_KEY);
|
|
93
83
|
};
|
|
94
84
|
|
|
95
|
-
/**
|
|
96
|
-
* Stash a drop's source archive for an automatic deploy after signup. Returns
|
|
97
|
-
* whether it was actually stored — callers use that only to phrase the
|
|
98
|
-
* hand-off, never to block it.
|
|
99
|
-
*/
|
|
85
|
+
/** Stash the archive for auto-deploy after signup. The result phrases the hand-off, never blocks it. */
|
|
100
86
|
export async function saveDropStash(
|
|
101
87
|
zipFile: File,
|
|
102
88
|
buildSettings: InferredBuildSettings | null = null
|
|
@@ -139,11 +125,9 @@ export async function loadDropStash(): Promise<DropBuildStash | null> {
|
|
|
139
125
|
}
|
|
140
126
|
|
|
141
127
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* named by the File). A Blob-shaped survivor is rewrapped; anything else is
|
|
146
|
-
* treated as a corrupt record.
|
|
128
|
+
* Rewraps a Blob-shaped survivor into a named File — structured-clone
|
|
129
|
+
* implementations vary in preserving File identity, and the multipart upload
|
|
130
|
+
* needs the name. Anything less Blob-shaped is a corrupt record.
|
|
147
131
|
*/
|
|
148
132
|
function rehydrateZipFile(stored: unknown): File | null {
|
|
149
133
|
if (stored instanceof File && stored.name) return stored;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
* so `deployFiles` runs the same pipeline whether the deploy is anonymous
|
|
5
|
-
* (this file's `AnonymousDropClient`, claimed later by the visitor) or lands
|
|
6
|
-
* directly in an account (`authedDropClient.ts`).
|
|
2
|
+
* The DropClient seam: the four calls a static deploy needs, behind one
|
|
3
|
+
* interface, so `deployFiles` runs the same pipeline anonymous or authenticated.
|
|
7
4
|
*/
|
|
8
5
|
import { DEFAULT_API_BASE, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS } from './constants';
|
|
9
6
|
import { DeployTimeoutError } from './errors';
|
|
@@ -23,7 +20,7 @@ export interface DropClient {
|
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Unauthenticated drop client. Authorized server-side by the Referer header,
|
|
26
|
-
* so it only works from an allowlisted origin
|
|
23
|
+
* so it only works from an allowlisted origin.
|
|
27
24
|
*/
|
|
28
25
|
export class AnonymousDropClient implements DropClient {
|
|
29
26
|
constructor(
|
|
@@ -64,10 +61,9 @@ export class AnonymousDropClient implements DropClient {
|
|
|
64
61
|
}
|
|
65
62
|
|
|
66
63
|
async waitUntilReady(deploy: DropResponse, token: string): Promise<void> {
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// and the request 401s misleadingly via require_current_actor!.
|
|
64
|
+
// `/deploys/{id}` expects the deploy_id (BSON), not the site id (UUID) that
|
|
65
|
+
// lives at `deploy.id`. See drop_controller.rb; Deploy.find(uuid) is nil and
|
|
66
|
+
// the request 401s misleadingly via require_current_actor!.
|
|
71
67
|
const deadline = this.timeoutMs / this.pollIntervalMs;
|
|
72
68
|
for (let i = 0; i < deadline; i++) {
|
|
73
69
|
const res = await fetch(`${this.apiBase}/deploys/${deploy.deploy_id}`, {
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The tunables shared across drop-core, one place and one comment each.
|
|
3
|
-
* Module-specific constants (e.g. buildStash's storage caps) stay with their
|
|
4
|
-
* module; anything two modules read belongs here.
|
|
5
|
-
*/
|
|
1
|
+
/** Tunables shared across drop-core. Module-specific constants stay with their module. */
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* rewrite. On netlify.com, `/access-control/*` targets the app's
|
|
10
|
-
* access-control function; the paths an origin may call are allowlisted
|
|
11
|
-
* server-side.
|
|
12
|
-
*/
|
|
3
|
+
// Root for session-cookie JSON calls via the consumer's access-control rewrite
|
|
4
|
+
// (paths per origin are allowlisted server-side)
|
|
13
5
|
export const DEFAULT_PROXY_BASE = '/access-control/bb-api/api/v1';
|
|
14
6
|
|
|
15
7
|
/** Direct api.netlify.com root for the uploads the proxy's ~6MB Lambda body limit can't carry. */
|
|
@@ -24,16 +16,10 @@ export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
24
16
|
// Concurrent file uploads (matches netlify-cli's deploy engine)
|
|
25
17
|
export const MAX_CONCURRENT_UPLOADS = 5;
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* The server issues non-app origins a 30s token; the gap leaves margin for the
|
|
30
|
-
* request the token authorizes to finish.
|
|
31
|
-
*/
|
|
19
|
+
// Reuse window for a minted upload token: 20s (server TTL is 30s; the gap is
|
|
20
|
+
// margin for the request it authorizes to finish)
|
|
32
21
|
export const TOKEN_REUSE_MS = 20 * 1000;
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* can be overridden (the tus-js-client `retryDelays` idiom). Three retries,
|
|
37
|
-
* linear backoff; 400/422 responses never retry — see `uploadWithRetry`.
|
|
38
|
-
*/
|
|
23
|
+
// Waits between upload retries, as data so the curve is overridable.
|
|
24
|
+
// 400/422 never retry — see uploadWithRetry.
|
|
39
25
|
export const UPLOAD_RETRY_DELAYS_MS: readonly number[] = [1_000, 2_000, 3_000];
|
|
@@ -9,11 +9,7 @@ import { zipFiles, type ZipResult } from './zip';
|
|
|
9
9
|
/** Responses that mean the request itself is wrong — retrying can only repeat the answer. */
|
|
10
10
|
const NON_RETRIABLE_STATUSES = new Set([400, 422]);
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Upload one file, retrying transient failures (5xx, 429, network errors) on
|
|
14
|
-
* the given delay curve. A single flaky PUT among hundreds shouldn't kill the
|
|
15
|
-
* whole deploy — every deploy client we know of retries here.
|
|
16
|
-
*/
|
|
12
|
+
/** Retries transient failures (5xx/429/network) — one flaky PUT shouldn't kill the deploy. */
|
|
17
13
|
async function uploadWithRetry(
|
|
18
14
|
client: DropClient,
|
|
19
15
|
deployId: string,
|
|
@@ -96,7 +92,6 @@ export async function deployFiles(
|
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
export interface BuildDeployResult {
|
|
99
|
-
/** The site created for this build. */
|
|
100
95
|
site: BuildSite;
|
|
101
96
|
/** The enqueued build; `deploy_id` is what the app's deploy page keys off. */
|
|
102
97
|
build: BuildResponse;
|
|
@@ -108,22 +103,15 @@ export interface BuildDeployResult {
|
|
|
108
103
|
|
|
109
104
|
export interface BuildDeployOptions extends DeployOptions {
|
|
110
105
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* app's deploy page with its live logs, not a spinner. Turn it on when the
|
|
114
|
-
* caller needs to know the outcome — only then can a `BuildFailedError` reach
|
|
115
|
-
* it with the build's own error message.
|
|
106
|
+
* Wait for the build instead of returning at enqueue. Off by default (builds
|
|
107
|
+
* take minutes); on is the only way a BuildFailedError reaches the caller.
|
|
116
108
|
*/
|
|
117
109
|
waitForBuild?: boolean;
|
|
118
110
|
}
|
|
119
111
|
|
|
120
112
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* client — builds have no anonymous equivalent.
|
|
124
|
-
*
|
|
125
|
-
* Site creation deliberately happens *after* zipping, so a zip that fails
|
|
126
|
-
* doesn't leave an empty site behind in the user's account.
|
|
113
|
+
* Zip the drop, create a site, hand buildbot the archive. Zipping comes first
|
|
114
|
+
* on purpose: a failed zip must not leave an empty site behind.
|
|
127
115
|
*/
|
|
128
116
|
export async function deployBuild(
|
|
129
117
|
client: BuildClient,
|
|
@@ -138,11 +126,7 @@ export async function deployBuild(
|
|
|
138
126
|
return deployZippedBuild(client, archive, opts);
|
|
139
127
|
}
|
|
140
128
|
|
|
141
|
-
/**
|
|
142
|
-
* Deploy an archive that was already zipped — the resume path for a drop
|
|
143
|
-
* stashed across the signup round-trip (see `buildStash`), where `source.zip`
|
|
144
|
-
* and its synthesised settings were produced before the visitor left.
|
|
145
|
-
*/
|
|
129
|
+
/** Deploy an already-zipped archive — the resume path for a stashed drop (see `buildStash`). */
|
|
146
130
|
export async function deployZippedBuild(
|
|
147
131
|
client: BuildClient,
|
|
148
132
|
{ file: zip, buildSettings }: ZipResult,
|
|
@@ -8,23 +8,16 @@ export type BuildReason =
|
|
|
8
8
|
| 'functions-directory';
|
|
9
9
|
|
|
10
10
|
export interface BuildDetection {
|
|
11
|
-
/**
|
|
11
|
+
/** The drop can't be served as static files. */
|
|
12
12
|
buildRequired: boolean;
|
|
13
13
|
/** The signal that decided it (first match wins), or null for a plain static drop. */
|
|
14
14
|
reason: BuildReason | null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* The value is the framework's default publish directory, or `null` where that
|
|
23
|
-
* directory isn't a fixed string we can state with confidence — Nuxt, SvelteKit,
|
|
24
|
-
* Remix and Angular all resolve it from an adapter or project name. `null` means
|
|
25
|
-
* "a build is required, but don't guess a publish directory": see
|
|
26
|
-
* `inferBuildSettings`, which only synthesises a `netlify.toml` when it knows
|
|
27
|
-
* both halves of the answer.
|
|
18
|
+
* Frameworks that imply a build even without a `build` script, mapped to their
|
|
19
|
+
* default publish dir — `null` where it isn't a fixed string (Nuxt, SvelteKit,
|
|
20
|
+
* Remix, Angular resolve theirs), meaning "build required, don't guess".
|
|
28
21
|
*/
|
|
29
22
|
const FRAMEWORK_DEPENDENCIES = new Map<string, string | null>([
|
|
30
23
|
['astro', 'dist'],
|
|
@@ -49,11 +42,7 @@ const FRAMEWORK_DEPENDENCIES = new Map<string, string | null>([
|
|
|
49
42
|
['@solidjs/start', null],
|
|
50
43
|
]);
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
* A `netlify.toml` build config: a bare `[build]` table or any of its
|
|
54
|
-
* sub-tables (`[build.environment]`, `[build.processing]`, …). Anchored to the
|
|
55
|
-
* line start so a `[build]` mention inside a string/comment doesn't count.
|
|
56
|
-
*/
|
|
45
|
+
// A [build] table or sub-table, line-anchored so a mention in a comment doesn't count.
|
|
57
46
|
const NETLIFY_TOML_BUILD_SECTION = /^\s*\[build(\.[\w-]+)?\]/m;
|
|
58
47
|
|
|
59
48
|
/** Zero-config functions directories — their presence alone requires bundling. */
|
|
@@ -61,7 +50,7 @@ const FUNCTIONS_DIR_PREFIXES = ['/netlify/functions/', '/netlify/edge-functions/
|
|
|
61
50
|
|
|
62
51
|
const decoder = new TextDecoder();
|
|
63
52
|
|
|
64
|
-
/**
|
|
53
|
+
/** Root paths are normalized with a leading slash. */
|
|
65
54
|
function rootFile(files: ProcessedFile[], path: string): ProcessedFile | undefined {
|
|
66
55
|
return files.find(f => f.path === path);
|
|
67
56
|
}
|
|
@@ -90,23 +79,14 @@ function findFrameworkDependency(pkg: Record<string, unknown>): string | null {
|
|
|
90
79
|
return null;
|
|
91
80
|
}
|
|
92
81
|
|
|
93
|
-
/** True if any dependency (prod or dev) is a known build-requiring framework. */
|
|
94
82
|
function hasFrameworkDependency(pkg: Record<string, unknown>): boolean {
|
|
95
83
|
return findFrameworkDependency(pkg) !== null;
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* root
|
|
102
|
-
* deploy root, e.g. `/netlify.toml`, `/package.json`.
|
|
103
|
-
*
|
|
104
|
-
* The anonymous drop API only performs static deploys, so a build-required
|
|
105
|
-
* project can't go through it — the consumer redirects such drops to the
|
|
106
|
-
* authenticated Drop page instead. This heuristic is deliberately lightweight
|
|
107
|
-
* (no framework-detection dependency): it reads the two config files and looks
|
|
108
|
-
* for the zero-config functions directories. A plain static site (HTML/CSS/JS,
|
|
109
|
-
* no build config) returns `{ buildRequired: false }`.
|
|
87
|
+
* Does this drop need a build? Deliberately lightweight (two config files +
|
|
88
|
+
* the functions dirs; no framework-detection dependency). Input is
|
|
89
|
+
* root-normalized by `readFiles`, so config sits at `/netlify.toml` etc.
|
|
110
90
|
*/
|
|
111
91
|
export function detectBuild(files: ProcessedFile[]): BuildDetection {
|
|
112
92
|
const toml = rootFile(files, '/netlify.toml');
|
|
@@ -165,13 +145,8 @@ const LOCKFILE_PACKAGE_MANAGERS: ReadonlyArray<readonly [string, string]> = [
|
|
|
165
145
|
['/package-lock.json', 'npm'],
|
|
166
146
|
];
|
|
167
147
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* `NETLIFY_TOML_BUILD_SECTION` on purpose: a config with only
|
|
171
|
-
* `[build.environment]` carries no command or publish directory, so it still
|
|
172
|
-
* needs a `[build]` table synthesised. (TOML permits defining a super-table
|
|
173
|
-
* after its sub-tables, so appending one is valid.)
|
|
174
|
-
*/
|
|
148
|
+
// Bare [build] only — stricter than the detection regex, since a config with
|
|
149
|
+
// just [build.environment] still needs a [build] table synthesised.
|
|
175
150
|
const NETLIFY_TOML_BUILD_TABLE = /^\s*\[build\]/m;
|
|
176
151
|
|
|
177
152
|
function detectPackageManager(files: ProcessedFile[]): string {
|
|
@@ -182,20 +157,10 @@ function detectPackageManager(files: ProcessedFile[]): string {
|
|
|
182
157
|
}
|
|
183
158
|
|
|
184
159
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* known — a `build` script (which gives the command) and a recognised framework
|
|
190
|
-
* with a fixed default publish directory (which gives the output). Anything else
|
|
191
|
-
* returns `null`, leaving the zip without a synthesised `[build]` table so
|
|
192
|
-
* buildbot's own framework detection resolves the settings instead. That matters
|
|
193
|
-
* because a `[build]` table with a command but no `publish` makes buildbot
|
|
194
|
-
* publish the deploy root — shipping the project's *source* rather than its
|
|
195
|
-
* built output, which is worse than not guessing at all.
|
|
196
|
-
*
|
|
197
|
-
* Returns `null` too when the drop already declares a `[build]` table: the
|
|
198
|
-
* author's own config always wins.
|
|
160
|
+
* Build settings for the synthesised netlify.toml — only when BOTH halves are
|
|
161
|
+
* known (build script → command, known framework → publish). Guessing publish
|
|
162
|
+
* wrong ships source instead of output, so `null` beats a bad guess. An
|
|
163
|
+
* author-declared [build] table always wins (also `null`).
|
|
199
164
|
*/
|
|
200
165
|
export function inferBuildSettings(files: ProcessedFile[]): InferredBuildSettings | null {
|
|
201
166
|
const toml = rootFile(files, '/netlify.toml');
|
|
@@ -76,7 +76,9 @@ describe('humanizeDropError', () => {
|
|
|
76
76
|
expect(humanizeDropError(new DeployFailedError('no index.html'))).toBe(
|
|
77
77
|
'Your deploy failed: no index.html'
|
|
78
78
|
);
|
|
79
|
-
expect(humanizeDropError(new DeployFailedError())).toBe(
|
|
79
|
+
expect(humanizeDropError(new DeployFailedError())).toBe(
|
|
80
|
+
'Your deploy failed. Please try again.'
|
|
81
|
+
);
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
it('reads a fetch TypeError as a connectivity problem', () => {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* HTML page (served as index.html). A folder, multiple files, a zip, or a lone
|
|
5
|
-
* HTML file are all fine; any other single file is rejected rather than
|
|
6
|
-
* deployed as a one-asset site (matches react-ui).
|
|
2
|
+
* A single loose file that can't stand up a site — only a zip, folder,
|
|
3
|
+
* multiple files, or a lone HTML page may deploy (matches react-ui).
|
|
7
4
|
*/
|
|
8
5
|
export class UnsupportedDropError extends Error {
|
|
9
6
|
readonly code = 'unsupported-drop';
|
|
@@ -15,12 +12,8 @@ export class UnsupportedDropError extends Error {
|
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* session-cookie site create was refused, so nothing was created. For static
|
|
21
|
-
* drops the flow falls back to the anonymous drop + claim; builds have no
|
|
22
|
-
* anonymous equivalent, so the consumer's only recourse is to send the visitor
|
|
23
|
-
* through login/signup and have them drop again.
|
|
15
|
+
* The pre-mutation "no session" signal: nothing was created yet, so static
|
|
16
|
+
* drops may fall back to the anonymous flow and build drops stash + hand off.
|
|
24
17
|
*/
|
|
25
18
|
export class NotAuthenticatedError extends Error {
|
|
26
19
|
readonly code = 'not-authenticated';
|
|
@@ -32,12 +25,8 @@ export class NotAuthenticatedError extends Error {
|
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* compiled dist (`./preact`, `./drop-core`), so two copies of the class can
|
|
38
|
-
* coexist in one app and defeat `instanceof` — the `code` brand is the
|
|
39
|
-
* copy-proof fallback. Control flow that decides whether an anonymous retry
|
|
40
|
-
* is safe must use this, never a bare `instanceof`.
|
|
28
|
+
* Copy-proof check for the "no session" error — `instanceof` misses class
|
|
29
|
+
* copies from the package's other entrypoint. Fallback gates must use this.
|
|
41
30
|
*/
|
|
42
31
|
export function isNotAuthenticatedError(err: unknown): err is NotAuthenticatedError {
|
|
43
32
|
if (err instanceof NotAuthenticatedError) return true;
|
|
@@ -71,12 +60,7 @@ export class BuildCreateError extends Error {
|
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
62
|
|
|
74
|
-
/**
|
|
75
|
-
* The build ran and ended in `error`/`rejected`. `reason` is the API's
|
|
76
|
-
* `error_message` when there is one — it's the only part of a build failure that
|
|
77
|
-
* says anything actionable (a failing build command, a missing dependency), so
|
|
78
|
-
* it is surfaced to the user rather than swallowed.
|
|
79
|
-
*/
|
|
63
|
+
/** The build ran and broke. `reason` carries the API's `error_message` — the only actionable part. */
|
|
80
64
|
export class BuildFailedError extends Error {
|
|
81
65
|
readonly code = 'build-failed';
|
|
82
66
|
|
|
@@ -96,10 +80,7 @@ export class BuildTimeoutError extends Error {
|
|
|
96
80
|
}
|
|
97
81
|
}
|
|
98
82
|
|
|
99
|
-
/**
|
|
100
|
-
* A static deploy ran and ended in `error`/`rejected`. Like a build failure,
|
|
101
|
-
* the API's `error_message` is the actionable part, so it travels on the error.
|
|
102
|
-
*/
|
|
83
|
+
/** A static deploy ran and ended in `error`/`rejected`; `reason` carries the API's `error_message`. */
|
|
103
84
|
export class DeployFailedError extends Error {
|
|
104
85
|
readonly code = 'deploy-failed';
|
|
105
86
|
|
|
@@ -152,9 +133,7 @@ export function humanizeDropError(err: unknown): string {
|
|
|
152
133
|
}
|
|
153
134
|
if (code === 'deploy-failed') {
|
|
154
135
|
const reason = (err as DeployFailedError).reason;
|
|
155
|
-
return reason
|
|
156
|
-
? `Your deploy failed: ${reason}`
|
|
157
|
-
: 'Your deploy failed. Please try again.';
|
|
136
|
+
return reason ? `Your deploy failed: ${reason}` : 'Your deploy failed. Please try again.';
|
|
158
137
|
}
|
|
159
138
|
|
|
160
139
|
if (code === 'build-timeout') {
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
/** What we report when the browser gives us no MIME type at all. */
|
|
2
2
|
const UNKNOWN_TYPE = 'application/octet-stream';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Distinct types kept per event. Deduplication alone keeps this small for a real
|
|
6
|
-
* site (a handful of types across hundreds of files); the cap is a backstop so an
|
|
7
|
-
* unusual folder can't push an oversized property into the analytics payload.
|
|
8
|
-
*/
|
|
4
|
+
// Cap on distinct types per event — a backstop against oversized analytics payloads.
|
|
9
5
|
const MAX_TYPES = 20;
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
|
-
* Distinct MIME types
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `ProcessedFile`s: the MIME type only exists on the original `File`, and reading
|
|
17
|
-
* can throw before any `ProcessedFile` does (an unsupported single file) — the
|
|
18
|
-
* very case the file type is most worth knowing about.
|
|
19
|
-
*
|
|
20
|
-
* Browsers leave `File.type` empty for anything they don't recognise — a dragged
|
|
21
|
-
* folder, an `.exe`, an extensionless file — so those collapse to
|
|
22
|
-
* `application/octet-stream` and the property never carries an empty string.
|
|
23
|
-
*
|
|
24
|
-
* Call this synchronously inside the drop handler: a `DataTransfer` detaches once
|
|
25
|
-
* the event turn ends.
|
|
8
|
+
* Distinct, sorted MIME types of a selection. Read at the drop boundary — the
|
|
9
|
+
* type only exists on the original File, and reading can throw before any
|
|
10
|
+
* ProcessedFile exists. Call synchronously in the drop handler: a DataTransfer
|
|
11
|
+
* detaches after the event turn.
|
|
26
12
|
*/
|
|
27
13
|
export function getFileTypes(files: FileList | null | undefined): string[] {
|
|
28
14
|
if (!files?.length) return [];
|
|
@@ -53,15 +53,11 @@ async function readDirEntry(entry: any): Promise<ProcessedFile[]> {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Read a
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* handler — the DataTransfer is detached once the event turn ends. The
|
|
60
|
-
* component captures entries first (see `readDroppedEntries`) then calls the
|
|
61
|
-
* async readers, so this function is safe to `await`.
|
|
56
|
+
* Read a drop's DataTransfer into ProcessedFiles. `webkitGetAsEntry()` runs
|
|
57
|
+
* synchronously before the first await — the DataTransfer detaches once the
|
|
58
|
+
* event turn ends.
|
|
62
59
|
*/
|
|
63
60
|
export async function readDataTransfer(dt: DataTransfer): Promise<ProcessedFile[]> {
|
|
64
|
-
// single zip?
|
|
65
61
|
const first = dt.files?.[0];
|
|
66
62
|
if (dt.files.length === 1 && first && isZipName(first.name)) {
|
|
67
63
|
return readZip(first);
|
|
@@ -164,11 +160,8 @@ export function hasMultipleHtmlFilesWithoutIndex(files: ProcessedFile[]): boolea
|
|
|
164
160
|
}
|
|
165
161
|
|
|
166
162
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* index.html — keeping its directory — so it is served at the site root
|
|
170
|
-
* instead of 404ing. Content and sha are untouched. Any other set (already has
|
|
171
|
-
* an index, multiple HTML files, or no HTML) is returned unchanged.
|
|
163
|
+
* A lone non-index HTML page is renamed to index.html (same directory,
|
|
164
|
+
* content/sha untouched) so it serves instead of 404ing. Other sets unchanged.
|
|
172
165
|
*/
|
|
173
166
|
export function renameSingleNonIndexHtmlToIndex(files: ProcessedFile[]): ProcessedFile[] {
|
|
174
167
|
const name = getSingleNonIndexHtmlFileName(files);
|
|
@@ -11,10 +11,8 @@ export interface ProcessedFile {
|
|
|
11
11
|
export type Digest = Record<string, string>; // { "/index.html": "<sha1>" }
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Response
|
|
15
|
-
*
|
|
16
|
-
* so the client has everything it needs to upload, poll, and hand off to the
|
|
17
|
-
* user in one round trip.
|
|
14
|
+
* Response of `POST /api/v1/drop` — a mixed site+deploy shape, everything the
|
|
15
|
+
* client needs to upload, poll, and hand off in one round trip.
|
|
18
16
|
*/
|
|
19
17
|
export interface DropResponse {
|
|
20
18
|
/** site id (UUID) — surfaced to callers so they can build claim links */
|