@netlify/spark-ui 1.27.6 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/preact/DropZone/DropZone.d.ts +27 -4
- package/dist/components/preact/DropZone/DropZone.js +50 -38
- package/dist/components/preact/DropZone/drop-core/detectBuild.d.ts +23 -0
- package/dist/components/preact/DropZone/drop-core/detectBuild.js +68 -0
- package/dist/components/preact/DropZone/drop-core/index.d.ts +1 -0
- package/dist/components/preact/DropZone/drop-core/index.js +9 -7
- package/dist/components/preact/DropZone/index.d.ts +2 -2
- package/dist/components/preact/DropZone/index.js +9 -7
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +13 -2
- package/dist/components/preact/DropZone/useDropDeploy.js +62 -44
- package/dist/components/preact/index.js +17 -16
- package/package.json +1 -1
- package/packages/components/preact/DropZone/DropZone.tsx +54 -4
- package/packages/components/preact/DropZone/drop-core/detectBuild.ts +130 -0
- package/packages/components/preact/DropZone/drop-core/index.ts +1 -0
- package/packages/components/preact/DropZone/index.tsx +3 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +47 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentChildren } from 'preact';
|
|
2
|
-
import { DropClient } from './drop-core';
|
|
3
|
-
import { DeployedSite, TrackFn } from './useDropDeploy';
|
|
2
|
+
import { BuildDetection, DropClient } from './drop-core';
|
|
3
|
+
import { DeployedSite, DeployError, TrackFn } from './useDropDeploy';
|
|
4
4
|
interface Props {
|
|
5
5
|
/** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
|
|
6
6
|
children?: ComponentChildren;
|
|
@@ -10,13 +10,35 @@ interface Props {
|
|
|
10
10
|
client?: DropClient;
|
|
11
11
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
12
12
|
claimUrl?: (siteName: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Where to send a build-required drop (framework source, build command,
|
|
15
|
+
* functions) when `onBuildRequired` isn't set. Such projects can't deploy
|
|
16
|
+
* through the anonymous static drop API, so by default the user is redirected
|
|
17
|
+
* here — the app signup page with `next=/drop` — to build on the
|
|
18
|
+
* authenticated Drop page. Override to point at your own destination.
|
|
19
|
+
*/
|
|
20
|
+
buildSignupUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Called when a build-required drop is detected, instead of the default
|
|
23
|
+
* `buildSignupUrl` redirect. Use it to own the hand-off yourself (custom
|
|
24
|
+
* routing, persisting the drop, gating on auth). When set, the component does
|
|
25
|
+
* not redirect. The static drop path is unaffected either way.
|
|
26
|
+
*/
|
|
27
|
+
onBuildRequired?: (info: BuildDetection) => void;
|
|
13
28
|
/** Called once the deploy is live. */
|
|
14
29
|
onDeploy?: (result: DeployedSite) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Called when a deploy fails, with a humanized `message` (ready to show a
|
|
32
|
+
* user — the same copy the built-in overlay uses) and the raw `reason`. Pair
|
|
33
|
+
* this with `showStatus={false}` to render your own error UI.
|
|
34
|
+
*/
|
|
35
|
+
onError?: (error: DeployError) => void;
|
|
15
36
|
/**
|
|
16
37
|
* Render the built-in status overlay (drag tint, spinner, progress bar, error).
|
|
17
38
|
* Set to `false` to suppress it entirely and keep your children visible, then
|
|
18
39
|
* drive your own feedback from the `data-state` attribute on the root element.
|
|
19
|
-
* Note: in this mode you're responsible for surfacing errors yourself
|
|
40
|
+
* Note: in this mode you're responsible for surfacing errors yourself — use
|
|
41
|
+
* `onError` to get the message.
|
|
20
42
|
*/
|
|
21
43
|
showStatus?: boolean;
|
|
22
44
|
/**
|
|
@@ -26,6 +48,7 @@ interface Props {
|
|
|
26
48
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
27
49
|
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
28
50
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
51
|
+
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count })
|
|
29
52
|
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
|
|
30
53
|
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count? })
|
|
31
54
|
*/
|
|
@@ -38,5 +61,5 @@ interface Props {
|
|
|
38
61
|
analyticsId?: string;
|
|
39
62
|
className?: string;
|
|
40
63
|
}
|
|
41
|
-
export default function DropZone({ children, apiBase, client, claimUrl, onDeploy, showStatus, onTrack, analyticsId, className, }: Props): import("preact").JSX.Element;
|
|
64
|
+
export default function DropZone({ children, apiBase, client, claimUrl, buildSignupUrl, onBuildRequired, onDeploy, onError, showStatus, onTrack, analyticsId, className, }: Props): import("preact").JSX.Element;
|
|
42
65
|
export {};
|
|
@@ -1,52 +1,59 @@
|
|
|
1
1
|
import { u as e } from "../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
|
|
2
|
-
import { A as
|
|
3
|
-
import { readFileList as
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
2
|
+
import { A as D, d as E, y as I } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
3
|
+
import { readFileList as P, readDataTransfer as U } from "./drop-core/readFiles.js";
|
|
4
|
+
import "./drop-core/detectBuild.js";
|
|
5
|
+
import { useDropDeploy as j } from "./useDropDeploy.js";
|
|
6
|
+
import { Fragment as k } from "preact";
|
|
7
|
+
import '../../../assets/DropZone.css';const B = 120, F = (n) => `https://app.netlify.com/drop/${n}`, M = "https://app.netlify.com/signup?next=/drop&utm_campaign=drop";
|
|
8
|
+
function H({
|
|
8
9
|
children: n,
|
|
9
10
|
apiBase: l,
|
|
10
11
|
client: i,
|
|
11
12
|
claimUrl: s = F,
|
|
12
|
-
|
|
13
|
+
buildSignupUrl: u = M,
|
|
14
|
+
onBuildRequired: w,
|
|
15
|
+
onDeploy: A,
|
|
16
|
+
onError: C,
|
|
13
17
|
showStatus: f = !0,
|
|
14
|
-
onTrack:
|
|
18
|
+
onTrack: c,
|
|
15
19
|
analyticsId: g,
|
|
16
|
-
className:
|
|
20
|
+
className: T
|
|
17
21
|
}) {
|
|
18
|
-
const
|
|
22
|
+
const h = (r, t = {}) => c == null ? void 0 : c(r, g ? { dropzone_id: g, ...t } : t), { status: a, warning: m, busy: o, deploy: v, reset: _ } = j({
|
|
19
23
|
apiBase: l,
|
|
20
24
|
client: i,
|
|
21
25
|
claimUrl: s,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
buildSignupUrl: u,
|
|
27
|
+
onBuildRequired: w,
|
|
28
|
+
onDeploy: A,
|
|
29
|
+
onError: C,
|
|
30
|
+
track: h
|
|
31
|
+
}), { active: p, keepActive: S, endDrag: R } = N(), b = D(null), y = (r) => {
|
|
25
32
|
var t;
|
|
26
|
-
|
|
27
|
-
},
|
|
33
|
+
h("dropzone_browse_opened", { method: r }), (t = b.current) == null || t.click();
|
|
34
|
+
}, $ = f && (p || o || a.kind === "error"), x = [
|
|
28
35
|
"n-dropzone",
|
|
29
36
|
p && "is-active",
|
|
30
37
|
o && "is-busy",
|
|
31
38
|
!f && "is-bare",
|
|
32
|
-
|
|
39
|
+
T
|
|
33
40
|
].filter(Boolean).join(" ");
|
|
34
41
|
return /* @__PURE__ */ e(
|
|
35
42
|
"div",
|
|
36
43
|
{
|
|
37
|
-
class:
|
|
44
|
+
class: x,
|
|
38
45
|
"data-state": a.kind,
|
|
39
46
|
role: "button",
|
|
40
47
|
tabIndex: o ? -1 : 0,
|
|
41
48
|
"aria-label": "Drop a site folder or zip here to deploy it, or press Enter to browse for files",
|
|
42
49
|
"aria-busy": o,
|
|
43
50
|
onDragOver: (r) => {
|
|
44
|
-
o || (r.preventDefault(),
|
|
51
|
+
o || (r.preventDefault(), S());
|
|
45
52
|
},
|
|
46
53
|
onDrop: (r) => {
|
|
47
|
-
if (r.preventDefault(),
|
|
54
|
+
if (r.preventDefault(), R(), o) return;
|
|
48
55
|
const t = r.dataTransfer;
|
|
49
|
-
t &&
|
|
56
|
+
t && v(() => U(t), "drag");
|
|
50
57
|
},
|
|
51
58
|
onClick: (r) => {
|
|
52
59
|
!o && r.target === r.currentTarget && y("click");
|
|
@@ -59,7 +66,7 @@ function K({
|
|
|
59
66
|
/* @__PURE__ */ e(
|
|
60
67
|
"input",
|
|
61
68
|
{
|
|
62
|
-
ref:
|
|
69
|
+
ref: b,
|
|
63
70
|
type: "file",
|
|
64
71
|
hidden: !0,
|
|
65
72
|
multiple: !0,
|
|
@@ -68,17 +75,17 @@ function K({
|
|
|
68
75
|
onChange: (r) => {
|
|
69
76
|
var z;
|
|
70
77
|
const t = r.currentTarget;
|
|
71
|
-
(z = t.files) != null && z.length &&
|
|
78
|
+
(z = t.files) != null && z.length && v(() => P(t.files), "browse"), t.value = "";
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
),
|
|
75
|
-
|
|
82
|
+
$ && /* @__PURE__ */ e(
|
|
76
83
|
"div",
|
|
77
84
|
{
|
|
78
85
|
class: "n-dropzone-overlay n-fadeIn",
|
|
79
86
|
children: [
|
|
80
|
-
/* @__PURE__ */ e(O, { status: a, active: p, onReset:
|
|
81
|
-
|
|
87
|
+
/* @__PURE__ */ e(O, { status: a, active: p, onReset: _ }),
|
|
88
|
+
m && a.kind !== "error" && o && /* @__PURE__ */ e("p", { class: "n-dropzone-warning", role: "status", children: m })
|
|
82
89
|
]
|
|
83
90
|
},
|
|
84
91
|
a.kind === "idle" ? "active" : a.kind
|
|
@@ -87,10 +94,10 @@ function K({
|
|
|
87
94
|
}
|
|
88
95
|
);
|
|
89
96
|
}
|
|
90
|
-
function
|
|
91
|
-
const [n, l] =
|
|
92
|
-
return
|
|
93
|
-
l(!0), clearTimeout(i.current), i.current = setTimeout(() => l(!1),
|
|
97
|
+
function N() {
|
|
98
|
+
const [n, l] = E(!1), i = D();
|
|
99
|
+
return I(() => () => clearTimeout(i.current), []), { active: n, keepActive: () => {
|
|
100
|
+
l(!0), clearTimeout(i.current), i.current = setTimeout(() => l(!1), B);
|
|
94
101
|
}, endDrag: () => {
|
|
95
102
|
clearTimeout(i.current), l(!1);
|
|
96
103
|
} };
|
|
@@ -105,29 +112,34 @@ function O({
|
|
|
105
112
|
case "idle":
|
|
106
113
|
return l ? /* @__PURE__ */ e("p", { class: "n-dropzone-hint", children: "Drop to deploy" }) : null;
|
|
107
114
|
case "reading":
|
|
108
|
-
return /* @__PURE__ */ e(
|
|
115
|
+
return /* @__PURE__ */ e(d, { label: "Reading files…" });
|
|
109
116
|
case "creating":
|
|
110
|
-
return /* @__PURE__ */ e(
|
|
117
|
+
return /* @__PURE__ */ e(d, { label: "Creating deploy…" });
|
|
111
118
|
case "processing":
|
|
112
|
-
return /* @__PURE__ */ e(
|
|
119
|
+
return /* @__PURE__ */ e(d, { label: "Processing — almost live…" });
|
|
113
120
|
case "redirecting":
|
|
114
|
-
return /* @__PURE__ */ e(
|
|
121
|
+
return /* @__PURE__ */ e(
|
|
122
|
+
d,
|
|
123
|
+
{
|
|
124
|
+
label: n.target === "signup" ? "This project needs a build — taking you to Netlify…" : "Site’s live — taking you to claim it…"
|
|
125
|
+
}
|
|
126
|
+
);
|
|
115
127
|
case "uploading": {
|
|
116
128
|
const s = n.total ? Math.round(n.uploaded / n.total * 100) : 0;
|
|
117
|
-
return /* @__PURE__ */ e(
|
|
118
|
-
/* @__PURE__ */ e(
|
|
129
|
+
return /* @__PURE__ */ e(k, { children: [
|
|
130
|
+
/* @__PURE__ */ e(d, { label: `Uploading ${n.uploaded}/${n.total} files…` }),
|
|
119
131
|
/* @__PURE__ */ e("div", { class: "n-dropzone-progress", role: "progressbar", "aria-valuenow": s, children: /* @__PURE__ */ e("div", { class: "n-dropzone-progress-bar", style: { width: `${s}%` } }) })
|
|
120
132
|
] });
|
|
121
133
|
}
|
|
122
134
|
case "error":
|
|
123
|
-
return /* @__PURE__ */ e(
|
|
135
|
+
return /* @__PURE__ */ e(k, { children: [
|
|
124
136
|
/* @__PURE__ */ e("p", { class: "n-dropzone-title n-dropzone-title--error", children: "Something went wrong" }),
|
|
125
137
|
/* @__PURE__ */ e("p", { class: "n-dropzone-subtitle", role: "alert", children: n.message }),
|
|
126
138
|
/* @__PURE__ */ e("button", { type: "button", class: "n-dropzone-browse", onClick: i, children: "Try again" })
|
|
127
139
|
] });
|
|
128
140
|
}
|
|
129
141
|
}
|
|
130
|
-
function
|
|
142
|
+
function d({ label: n }) {
|
|
131
143
|
return /* @__PURE__ */ e("div", { class: "n-dropzone-loading", children: [
|
|
132
144
|
/* @__PURE__ */ e("span", { class: "n-dropzone-spinner", "aria-hidden": "true" }),
|
|
133
145
|
/* @__PURE__ */ e("p", { class: "n-dropzone-subtitle", "aria-live": "polite", children: [
|
|
@@ -138,5 +150,5 @@ function c({ label: n }) {
|
|
|
138
150
|
] });
|
|
139
151
|
}
|
|
140
152
|
export {
|
|
141
|
-
|
|
153
|
+
H as default
|
|
142
154
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ProcessedFile } from './types';
|
|
2
|
+
/** Why a drop was judged to need a build — surfaced to analytics and `onBuildRequired`. */
|
|
3
|
+
export type BuildReason = 'netlify-toml-build' | 'package-json-build-script' | 'framework-dependency' | 'functions-directory';
|
|
4
|
+
export interface BuildDetection {
|
|
5
|
+
/** True when the dropped project needs a build step and can't be served as static files. */
|
|
6
|
+
buildRequired: boolean;
|
|
7
|
+
/** The signal that decided it (first match wins), or null for a plain static drop. */
|
|
8
|
+
reason: BuildReason | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Decide whether a dropped project needs a build. Runs on the already-read,
|
|
12
|
+
* root-normalized `ProcessedFile[]` (a dropped `.zip` is expanded and the common
|
|
13
|
+
* root folder stripped upstream in `readFiles`), so build config sits at the
|
|
14
|
+
* deploy root, e.g. `/netlify.toml`, `/package.json`.
|
|
15
|
+
*
|
|
16
|
+
* The anonymous drop API only performs static deploys, so a build-required
|
|
17
|
+
* project can't go through it — the consumer redirects such drops to the
|
|
18
|
+
* authenticated Drop page instead. This heuristic is deliberately lightweight
|
|
19
|
+
* (no framework-detection dependency): it reads the two config files and looks
|
|
20
|
+
* for the zero-config functions directories. A plain static site (HTML/CSS/JS,
|
|
21
|
+
* no build config) returns `{ buildRequired: false }`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectBuild(files: ProcessedFile[]): BuildDetection;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const u = /* @__PURE__ */ new Set([
|
|
2
|
+
"astro",
|
|
3
|
+
"next",
|
|
4
|
+
"nuxt",
|
|
5
|
+
"gatsby",
|
|
6
|
+
"vite",
|
|
7
|
+
"react-scripts",
|
|
8
|
+
"parcel",
|
|
9
|
+
"expo",
|
|
10
|
+
"gridsome",
|
|
11
|
+
"vuepress",
|
|
12
|
+
"hexo",
|
|
13
|
+
"@remix-run/dev",
|
|
14
|
+
"@sveltejs/kit",
|
|
15
|
+
"@angular/cli",
|
|
16
|
+
"@vue/cli-service",
|
|
17
|
+
"@11ty/eleventy",
|
|
18
|
+
"@docusaurus/core",
|
|
19
|
+
"@builder.io/qwik",
|
|
20
|
+
"@analogjs/platform",
|
|
21
|
+
"@solidjs/start"
|
|
22
|
+
]), c = /^\s*\[build(\.[\w-]+)?\]/m, d = ["/netlify/functions/", "/netlify/edge-functions/"], l = new TextDecoder();
|
|
23
|
+
function i(e, n) {
|
|
24
|
+
return e.find((t) => t.path === n);
|
|
25
|
+
}
|
|
26
|
+
function o(e) {
|
|
27
|
+
return l.decode(e.content);
|
|
28
|
+
}
|
|
29
|
+
function a(e) {
|
|
30
|
+
const n = e.scripts;
|
|
31
|
+
if (!n || typeof n != "object") return !1;
|
|
32
|
+
const t = n.build;
|
|
33
|
+
return typeof t == "string" && t.trim().length > 0;
|
|
34
|
+
}
|
|
35
|
+
function f(e) {
|
|
36
|
+
for (const n of ["dependencies", "devDependencies"]) {
|
|
37
|
+
const t = e[n];
|
|
38
|
+
if (!(!t || typeof t != "object")) {
|
|
39
|
+
for (const r of Object.keys(t))
|
|
40
|
+
if (u.has(r)) return !0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return !1;
|
|
44
|
+
}
|
|
45
|
+
function p(e) {
|
|
46
|
+
const n = i(e, "/netlify.toml");
|
|
47
|
+
if (n && c.test(o(n)))
|
|
48
|
+
return { buildRequired: !0, reason: "netlify-toml-build" };
|
|
49
|
+
const t = i(e, "/package.json");
|
|
50
|
+
if (t) {
|
|
51
|
+
let r = null;
|
|
52
|
+
try {
|
|
53
|
+
r = JSON.parse(o(t));
|
|
54
|
+
} catch {
|
|
55
|
+
r = null;
|
|
56
|
+
}
|
|
57
|
+
if (r) {
|
|
58
|
+
if (a(r))
|
|
59
|
+
return { buildRequired: !0, reason: "package-json-build-script" };
|
|
60
|
+
if (f(r))
|
|
61
|
+
return { buildRequired: !0, reason: "framework-dependency" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return e.some((r) => d.some((s) => r.path.startsWith(s))) ? { buildRequired: !0, reason: "functions-directory" } : { buildRequired: !1, reason: null };
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
p as detectBuild
|
|
68
|
+
};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { getDigest as o } from "./digest.js";
|
|
2
2
|
import { getSingleNonIndexHtmlFileName as n, hasMultipleHtmlFilesWithoutIndex as l, readDataTransfer as i, readFileList as m, renameSingleNonIndexHtmlToIndex as p } from "./readFiles.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { detectBuild as x } from "./detectBuild.js";
|
|
4
|
+
import { AnonymousDropClient as s } from "./client.js";
|
|
5
|
+
import { deployFiles as u } from "./deploy.js";
|
|
6
|
+
import { UnsupportedDropError as D, humanizeDropError as F } from "./errors.js";
|
|
6
7
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
s as AnonymousDropClient,
|
|
9
|
+
D as UnsupportedDropError,
|
|
10
|
+
u as deployFiles,
|
|
11
|
+
x as detectBuild,
|
|
10
12
|
o as getDigest,
|
|
11
13
|
n as getSingleNonIndexHtmlFileName,
|
|
12
14
|
l as hasMultipleHtmlFilesWithoutIndex,
|
|
13
|
-
|
|
15
|
+
F as humanizeDropError,
|
|
14
16
|
i as readDataTransfer,
|
|
15
17
|
m as readFileList,
|
|
16
18
|
p as renameSingleNonIndexHtmlToIndex
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default as DropZone } from './DropZone';
|
|
2
|
-
export { AnonymousDropClient, deployFiles, readDataTransfer, readFileList, } from './drop-core';
|
|
3
|
-
export type { DropClient, DropResponse, Digest, DeployResult, DeployProgress, DeployPhase, ProcessedFile, } from './drop-core';
|
|
2
|
+
export { AnonymousDropClient, deployFiles, detectBuild, readDataTransfer, readFileList, } from './drop-core';
|
|
3
|
+
export type { BuildDetection, BuildReason, DropClient, DropResponse, Digest, DeployResult, DeployProgress, DeployPhase, ProcessedFile, } from './drop-core';
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { default as
|
|
2
|
-
import { readDataTransfer as
|
|
3
|
-
import {
|
|
1
|
+
import { default as r } from "./DropZone.js";
|
|
2
|
+
import { readDataTransfer as p, readFileList as a } from "./drop-core/readFiles.js";
|
|
3
|
+
import { detectBuild as d } from "./drop-core/detectBuild.js";
|
|
4
|
+
import { AnonymousDropClient as m } from "./drop-core/client.js";
|
|
4
5
|
import { deployFiles as n } from "./drop-core/deploy.js";
|
|
5
6
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
m as AnonymousDropClient,
|
|
8
|
+
r as DropZone,
|
|
8
9
|
n as deployFiles,
|
|
9
|
-
|
|
10
|
-
p as
|
|
10
|
+
d as detectBuild,
|
|
11
|
+
p as readDataTransfer,
|
|
12
|
+
a as readFileList
|
|
11
13
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DropClient, ProcessedFile } from './drop-core';
|
|
1
|
+
import { BuildDetection, DropClient, ProcessedFile } from './drop-core';
|
|
2
2
|
/** How the files reached the zone — carried through to analytics. */
|
|
3
3
|
export type DeploySource = 'drag' | 'browse';
|
|
4
4
|
/** The live site handed back once a deploy succeeds. */
|
|
@@ -8,6 +8,13 @@ export interface DeployedSite {
|
|
|
8
8
|
siteName: string;
|
|
9
9
|
siteId: string;
|
|
10
10
|
}
|
|
11
|
+
/** A deploy failure, handed to `onError` so consumers can render their own error UI. */
|
|
12
|
+
export interface DeployError {
|
|
13
|
+
/** Human-readable text ready to show a user — the same copy the built-in overlay displays. */
|
|
14
|
+
message: string;
|
|
15
|
+
/** The raw underlying reason (e.g. "drop failed: 429"), useful for logging. */
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
11
18
|
/** Finite states of a single deploy attempt. */
|
|
12
19
|
export type DeployStatus = {
|
|
13
20
|
kind: 'idle';
|
|
@@ -23,6 +30,7 @@ export type DeployStatus = {
|
|
|
23
30
|
kind: 'processing';
|
|
24
31
|
} | {
|
|
25
32
|
kind: 'redirecting';
|
|
33
|
+
target: 'claim' | 'signup';
|
|
26
34
|
} | {
|
|
27
35
|
kind: 'error';
|
|
28
36
|
message: string;
|
|
@@ -33,14 +41,17 @@ interface UseDropDeployOptions {
|
|
|
33
41
|
apiBase?: string;
|
|
34
42
|
client?: DropClient;
|
|
35
43
|
claimUrl: (siteName: string) => string;
|
|
44
|
+
buildSignupUrl: string;
|
|
45
|
+
onBuildRequired?: (info: BuildDetection) => void;
|
|
36
46
|
onDeploy?: (site: DeployedSite) => void;
|
|
47
|
+
onError?: (error: DeployError) => void;
|
|
37
48
|
track: TrackFn;
|
|
38
49
|
}
|
|
39
50
|
/**
|
|
40
51
|
* Owns the deploy state machine and its side effects (upload, local persistence,
|
|
41
52
|
* analytics, redirect), keeping DropZone a thin view over the returned `status`.
|
|
42
53
|
*/
|
|
43
|
-
export declare function useDropDeploy({ apiBase, client, claimUrl, onDeploy, track, }: UseDropDeployOptions): {
|
|
54
|
+
export declare function useDropDeploy({ apiBase, client, claimUrl, buildSignupUrl, onBuildRequired, onDeploy, onError, track, }: UseDropDeployOptions): {
|
|
44
55
|
status: DeployStatus;
|
|
45
56
|
warning: string | null;
|
|
46
57
|
busy: boolean;
|
|
@@ -1,71 +1,89 @@
|
|
|
1
|
-
import { d as
|
|
2
|
-
import { renameSingleNonIndexHtmlToIndex as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { d as h, A as F } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
2
|
+
import { renameSingleNonIndexHtmlToIndex as H, getSingleNonIndexHtmlFileName as W, hasMultipleHtmlFilesWithoutIndex as $ } from "./drop-core/readFiles.js";
|
|
3
|
+
import { detectBuild as v } from "./drop-core/detectBuild.js";
|
|
4
|
+
import { AnonymousDropClient as A } from "./drop-core/client.js";
|
|
5
|
+
import { deployFiles as C } from "./drop-core/deploy.js";
|
|
6
|
+
import { humanizeDropError as T } from "./drop-core/errors.js";
|
|
7
|
+
function q(n) {
|
|
8
|
+
switch (n.phase) {
|
|
8
9
|
case "creating":
|
|
9
10
|
return { kind: "creating" };
|
|
10
11
|
case "uploading":
|
|
11
|
-
return { kind: "uploading", uploaded:
|
|
12
|
+
return { kind: "uploading", uploaded: n.uploaded ?? 0, total: n.total ?? 0 };
|
|
12
13
|
case "processing":
|
|
13
14
|
case "ready":
|
|
14
15
|
return { kind: "processing" };
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
return
|
|
18
|
+
function M(n) {
|
|
19
|
+
const l = W(n);
|
|
20
|
+
return l ? `No index.html found — "${l}" won’t be served at the site root.` : $(n) ? "No index.html found — your site may not have a landing page." : null;
|
|
20
21
|
}
|
|
21
|
-
function
|
|
22
|
-
apiBase:
|
|
23
|
-
client:
|
|
24
|
-
claimUrl:
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
function K({
|
|
23
|
+
apiBase: n,
|
|
24
|
+
client: l,
|
|
25
|
+
claimUrl: _,
|
|
26
|
+
buildSignupUrl: k,
|
|
27
|
+
onBuildRequired: g,
|
|
28
|
+
onDeploy: a,
|
|
29
|
+
onError: u,
|
|
30
|
+
track: s
|
|
27
31
|
}) {
|
|
28
|
-
const [
|
|
29
|
-
async function
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
const
|
|
35
|
-
|
|
32
|
+
const [c, i] = h({ kind: "idle" }), [y, m] = h(null), w = F(l ?? new A(n)).current, N = c.kind !== "idle" && c.kind !== "error";
|
|
33
|
+
async function b(I, o) {
|
|
34
|
+
m(null), i({ kind: "reading" });
|
|
35
|
+
const f = (e, t, d) => {
|
|
36
|
+
i({ kind: "error", message: e }), s("dropzone_deploy_failed", { method: o, reason: t, ...d }), u == null || u({ message: e, reason: t });
|
|
37
|
+
}, S = (e, t) => {
|
|
38
|
+
const d = e instanceof Error ? e.message : String(e);
|
|
39
|
+
f(T(e), d, t);
|
|
36
40
|
};
|
|
37
41
|
try {
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
|
|
42
|
+
const e = H(await I());
|
|
43
|
+
if (!e.length) {
|
|
44
|
+
f("No files found to deploy.", "No files found to deploy.", { file_count: 0 });
|
|
41
45
|
return;
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
o === "drag" && s("dropzone_files_dropped", { method: o, file_count: e.length });
|
|
48
|
+
const t = v(e);
|
|
49
|
+
if (t.buildRequired) {
|
|
50
|
+
if (s("dropzone_build_required", {
|
|
51
|
+
method: o,
|
|
52
|
+
reason: t.reason,
|
|
53
|
+
file_count: e.length
|
|
54
|
+
}), g) {
|
|
55
|
+
i({ kind: "idle" }), g(t);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
i({ kind: "redirecting", target: "signup" }), window.location.assign(k);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
m(M(e));
|
|
44
62
|
const {
|
|
45
|
-
url:
|
|
46
|
-
token:
|
|
63
|
+
url: d,
|
|
64
|
+
token: p,
|
|
47
65
|
deploy: r
|
|
48
|
-
} = await
|
|
49
|
-
onProgress: (
|
|
66
|
+
} = await C(w, e, {
|
|
67
|
+
onProgress: (z) => i(q(z))
|
|
50
68
|
});
|
|
51
69
|
try {
|
|
52
|
-
localStorage.setItem("drop-token",
|
|
70
|
+
localStorage.setItem("drop-token", p), localStorage.setItem("dropSiteName", r.subdomain);
|
|
53
71
|
} catch {
|
|
54
72
|
}
|
|
55
|
-
|
|
56
|
-
method:
|
|
73
|
+
i({ kind: "redirecting", target: "claim" }), s("dropzone_deploy_succeeded", {
|
|
74
|
+
method: o,
|
|
57
75
|
site_name: r.subdomain,
|
|
58
|
-
file_count:
|
|
59
|
-
}),
|
|
60
|
-
} catch (
|
|
61
|
-
|
|
76
|
+
file_count: e.length
|
|
77
|
+
}), a == null || a({ url: d, token: p, siteName: r.subdomain, siteId: r.id }), window.location.assign(`${_(r.subdomain)}#drop_token=${p}`);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
S(e);
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
82
|
+
function x() {
|
|
83
|
+
m(null), i({ kind: "idle" });
|
|
66
84
|
}
|
|
67
|
-
return { status:
|
|
85
|
+
return { status: c, warning: y, busy: N, deploy: b, reset: x };
|
|
68
86
|
}
|
|
69
87
|
export {
|
|
70
|
-
|
|
88
|
+
K as useDropDeploy
|
|
71
89
|
};
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { default as
|
|
1
|
+
import { default as p } from "./KeyboardKey/KeyboardKey.js";
|
|
2
2
|
import { default as d } from "./Modal/Modal.js";
|
|
3
|
-
import { default as
|
|
4
|
-
import { default as
|
|
5
|
-
import { default as
|
|
6
|
-
import { default as
|
|
7
|
-
import { A as
|
|
3
|
+
import { default as s } from "./Icon/index.js";
|
|
4
|
+
import { default as u } from "./SearchModal/components/SearchLink/SearchLink.js";
|
|
5
|
+
import { default as c } from "./SearchModal/components/SearchHeading/SearchHeading.js";
|
|
6
|
+
import { default as n } from "./SearchModal/components/SearchToggle/SearchToggle.js";
|
|
7
|
+
import { A as g } from "../../chunks/AskNetlify.bx0blyGV.js";
|
|
8
8
|
import "preact";
|
|
9
9
|
import "../../chunks/hooks.module.DRcxknIz.js";
|
|
10
|
-
import { default as
|
|
11
|
-
import { default as
|
|
10
|
+
import { default as k } from "./SearchModal/components/SearchModal/SearchModal.js";
|
|
11
|
+
import { default as K } from "./DropZone/DropZone.js";
|
|
12
12
|
import "./DropZone/drop-core/readFiles.js";
|
|
13
|
+
import "./DropZone/drop-core/detectBuild.js";
|
|
13
14
|
export {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
g as AskNetlify,
|
|
16
|
+
K as DropZone,
|
|
17
|
+
s as Icon,
|
|
18
|
+
p as KeyboardKey,
|
|
18
19
|
d as Modal,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
c as SearchHeading,
|
|
21
|
+
u as SearchLink,
|
|
22
|
+
k as SearchModal,
|
|
23
|
+
n as SearchToggle
|
|
23
24
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import type { ComponentChildren } from 'preact';
|
|
3
|
-
import { readDataTransfer, readFileList, type DropClient } from './drop-core';
|
|
4
|
-
import {
|
|
3
|
+
import { readDataTransfer, readFileList, type BuildDetection, type DropClient } from './drop-core';
|
|
4
|
+
import {
|
|
5
|
+
useDropDeploy,
|
|
6
|
+
type DeployedSite,
|
|
7
|
+
type DeployError,
|
|
8
|
+
type DeployStatus,
|
|
9
|
+
type TrackFn,
|
|
10
|
+
} from './useDropDeploy';
|
|
5
11
|
|
|
6
12
|
import './DropZone.css';
|
|
7
13
|
|
|
@@ -10,6 +16,13 @@ const DRAG_INACTIVE_MS = 120;
|
|
|
10
16
|
|
|
11
17
|
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Where a build-required drop is sent by default. Mirrors the app's own Drop
|
|
21
|
+
* page: signup with `next=/drop`, so after signup + email verification the user
|
|
22
|
+
* lands back on the authenticated Drop page and can build there.
|
|
23
|
+
*/
|
|
24
|
+
const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
|
|
25
|
+
|
|
13
26
|
interface Props {
|
|
14
27
|
/** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
|
|
15
28
|
children?: ComponentChildren;
|
|
@@ -19,13 +32,35 @@ interface Props {
|
|
|
19
32
|
client?: DropClient;
|
|
20
33
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
21
34
|
claimUrl?: (siteName: string) => string;
|
|
35
|
+
/**
|
|
36
|
+
* Where to send a build-required drop (framework source, build command,
|
|
37
|
+
* functions) when `onBuildRequired` isn't set. Such projects can't deploy
|
|
38
|
+
* through the anonymous static drop API, so by default the user is redirected
|
|
39
|
+
* here — the app signup page with `next=/drop` — to build on the
|
|
40
|
+
* authenticated Drop page. Override to point at your own destination.
|
|
41
|
+
*/
|
|
42
|
+
buildSignupUrl?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Called when a build-required drop is detected, instead of the default
|
|
45
|
+
* `buildSignupUrl` redirect. Use it to own the hand-off yourself (custom
|
|
46
|
+
* routing, persisting the drop, gating on auth). When set, the component does
|
|
47
|
+
* not redirect. The static drop path is unaffected either way.
|
|
48
|
+
*/
|
|
49
|
+
onBuildRequired?: (info: BuildDetection) => void;
|
|
22
50
|
/** Called once the deploy is live. */
|
|
23
51
|
onDeploy?: (result: DeployedSite) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Called when a deploy fails, with a humanized `message` (ready to show a
|
|
54
|
+
* user — the same copy the built-in overlay uses) and the raw `reason`. Pair
|
|
55
|
+
* this with `showStatus={false}` to render your own error UI.
|
|
56
|
+
*/
|
|
57
|
+
onError?: (error: DeployError) => void;
|
|
24
58
|
/**
|
|
25
59
|
* Render the built-in status overlay (drag tint, spinner, progress bar, error).
|
|
26
60
|
* Set to `false` to suppress it entirely and keep your children visible, then
|
|
27
61
|
* drive your own feedback from the `data-state` attribute on the root element.
|
|
28
|
-
* Note: in this mode you're responsible for surfacing errors yourself
|
|
62
|
+
* Note: in this mode you're responsible for surfacing errors yourself — use
|
|
63
|
+
* `onError` to get the message.
|
|
29
64
|
*/
|
|
30
65
|
showStatus?: boolean;
|
|
31
66
|
/**
|
|
@@ -35,6 +70,7 @@ interface Props {
|
|
|
35
70
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
36
71
|
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
37
72
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
73
|
+
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count })
|
|
38
74
|
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
|
|
39
75
|
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count? })
|
|
40
76
|
*/
|
|
@@ -53,7 +89,10 @@ export default function DropZone({
|
|
|
53
89
|
apiBase,
|
|
54
90
|
client,
|
|
55
91
|
claimUrl = defaultClaimUrl,
|
|
92
|
+
buildSignupUrl = defaultBuildSignupUrl,
|
|
93
|
+
onBuildRequired,
|
|
56
94
|
onDeploy,
|
|
95
|
+
onError,
|
|
57
96
|
showStatus = true,
|
|
58
97
|
onTrack,
|
|
59
98
|
analyticsId,
|
|
@@ -68,7 +107,10 @@ export default function DropZone({
|
|
|
68
107
|
apiBase,
|
|
69
108
|
client,
|
|
70
109
|
claimUrl,
|
|
110
|
+
buildSignupUrl,
|
|
111
|
+
onBuildRequired,
|
|
71
112
|
onDeploy,
|
|
113
|
+
onError,
|
|
72
114
|
track,
|
|
73
115
|
});
|
|
74
116
|
|
|
@@ -212,7 +254,15 @@ function StatusView({
|
|
|
212
254
|
case 'processing':
|
|
213
255
|
return <Spinner label="Processing — almost live…" />;
|
|
214
256
|
case 'redirecting':
|
|
215
|
-
return
|
|
257
|
+
return (
|
|
258
|
+
<Spinner
|
|
259
|
+
label={
|
|
260
|
+
status.target === 'signup'
|
|
261
|
+
? 'This project needs a build — taking you to Netlify…'
|
|
262
|
+
: 'Site’s live — taking you to claim it…'
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
216
266
|
|
|
217
267
|
case 'uploading': {
|
|
218
268
|
const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ProcessedFile } from './types';
|
|
2
|
+
|
|
3
|
+
/** Why a drop was judged to need a build — surfaced to analytics and `onBuildRequired`. */
|
|
4
|
+
export type BuildReason =
|
|
5
|
+
| 'netlify-toml-build'
|
|
6
|
+
| 'package-json-build-script'
|
|
7
|
+
| 'framework-dependency'
|
|
8
|
+
| 'functions-directory';
|
|
9
|
+
|
|
10
|
+
export interface BuildDetection {
|
|
11
|
+
/** True when the dropped project needs a build step and can't be served as static files. */
|
|
12
|
+
buildRequired: boolean;
|
|
13
|
+
/** The signal that decided it (first match wins), or null for a plain static drop. */
|
|
14
|
+
reason: BuildReason | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Dependencies whose presence implies a build even without an explicit `build`
|
|
19
|
+
* script (e.g. a framework wired through its own CLI). Not exhaustive — the
|
|
20
|
+
* `build` script check below catches the common case; this is the backstop.
|
|
21
|
+
*/
|
|
22
|
+
const FRAMEWORK_DEPENDENCIES = new Set([
|
|
23
|
+
'astro',
|
|
24
|
+
'next',
|
|
25
|
+
'nuxt',
|
|
26
|
+
'gatsby',
|
|
27
|
+
'vite',
|
|
28
|
+
'react-scripts',
|
|
29
|
+
'parcel',
|
|
30
|
+
'expo',
|
|
31
|
+
'gridsome',
|
|
32
|
+
'vuepress',
|
|
33
|
+
'hexo',
|
|
34
|
+
'@remix-run/dev',
|
|
35
|
+
'@sveltejs/kit',
|
|
36
|
+
'@angular/cli',
|
|
37
|
+
'@vue/cli-service',
|
|
38
|
+
'@11ty/eleventy',
|
|
39
|
+
'@docusaurus/core',
|
|
40
|
+
'@builder.io/qwik',
|
|
41
|
+
'@analogjs/platform',
|
|
42
|
+
'@solidjs/start',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A `netlify.toml` build config: a bare `[build]` table or any of its
|
|
47
|
+
* sub-tables (`[build.environment]`, `[build.processing]`, …). Anchored to the
|
|
48
|
+
* line start so a `[build]` mention inside a string/comment doesn't count.
|
|
49
|
+
*/
|
|
50
|
+
const NETLIFY_TOML_BUILD_SECTION = /^\s*\[build(\.[\w-]+)?\]/m;
|
|
51
|
+
|
|
52
|
+
/** Zero-config functions directories — their presence alone requires bundling. */
|
|
53
|
+
const FUNCTIONS_DIR_PREFIXES = ['/netlify/functions/', '/netlify/edge-functions/'];
|
|
54
|
+
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
|
|
57
|
+
/** Find a file at the deploy root by its normalized path (leading slash). */
|
|
58
|
+
function rootFile(files: ProcessedFile[], path: string): ProcessedFile | undefined {
|
|
59
|
+
return files.find(f => f.path === path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function text(file: ProcessedFile): string {
|
|
63
|
+
return decoder.decode(file.content);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True if `package.json` declares a non-empty `build` script. */
|
|
67
|
+
function hasBuildScript(pkg: Record<string, unknown>): boolean {
|
|
68
|
+
const scripts = pkg.scripts;
|
|
69
|
+
if (!scripts || typeof scripts !== 'object') return false;
|
|
70
|
+
const build = (scripts as Record<string, unknown>).build;
|
|
71
|
+
return typeof build === 'string' && build.trim().length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True if any dependency (prod or dev) is a known build-requiring framework. */
|
|
75
|
+
function hasFrameworkDependency(pkg: Record<string, unknown>): boolean {
|
|
76
|
+
for (const key of ['dependencies', 'devDependencies'] as const) {
|
|
77
|
+
const deps = pkg[key];
|
|
78
|
+
if (!deps || typeof deps !== 'object') continue;
|
|
79
|
+
for (const name of Object.keys(deps)) {
|
|
80
|
+
if (FRAMEWORK_DEPENDENCIES.has(name)) return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decide whether a dropped project needs a build. Runs on the already-read,
|
|
88
|
+
* root-normalized `ProcessedFile[]` (a dropped `.zip` is expanded and the common
|
|
89
|
+
* root folder stripped upstream in `readFiles`), so build config sits at the
|
|
90
|
+
* deploy root, e.g. `/netlify.toml`, `/package.json`.
|
|
91
|
+
*
|
|
92
|
+
* The anonymous drop API only performs static deploys, so a build-required
|
|
93
|
+
* project can't go through it — the consumer redirects such drops to the
|
|
94
|
+
* authenticated Drop page instead. This heuristic is deliberately lightweight
|
|
95
|
+
* (no framework-detection dependency): it reads the two config files and looks
|
|
96
|
+
* for the zero-config functions directories. A plain static site (HTML/CSS/JS,
|
|
97
|
+
* no build config) returns `{ buildRequired: false }`.
|
|
98
|
+
*/
|
|
99
|
+
export function detectBuild(files: ProcessedFile[]): BuildDetection {
|
|
100
|
+
const toml = rootFile(files, '/netlify.toml');
|
|
101
|
+
if (toml && NETLIFY_TOML_BUILD_SECTION.test(text(toml))) {
|
|
102
|
+
return { buildRequired: true, reason: 'netlify-toml-build' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pkg = rootFile(files, '/package.json');
|
|
106
|
+
if (pkg) {
|
|
107
|
+
let parsed: Record<string, unknown> | null = null;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(text(pkg)) as Record<string, unknown>;
|
|
110
|
+
} catch {
|
|
111
|
+
// A malformed package.json isn't a reliable build signal on its own —
|
|
112
|
+
// fall through and let the other heuristics decide.
|
|
113
|
+
parsed = null;
|
|
114
|
+
}
|
|
115
|
+
if (parsed) {
|
|
116
|
+
if (hasBuildScript(parsed)) {
|
|
117
|
+
return { buildRequired: true, reason: 'package-json-build-script' };
|
|
118
|
+
}
|
|
119
|
+
if (hasFrameworkDependency(parsed)) {
|
|
120
|
+
return { buildRequired: true, reason: 'framework-dependency' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (files.some(f => FUNCTIONS_DIR_PREFIXES.some(prefix => f.path.startsWith(prefix)))) {
|
|
126
|
+
return { buildRequired: true, reason: 'functions-directory' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { buildRequired: false, reason: null };
|
|
130
|
+
}
|
|
@@ -2,10 +2,13 @@ export { default as DropZone } from './DropZone';
|
|
|
2
2
|
export {
|
|
3
3
|
AnonymousDropClient,
|
|
4
4
|
deployFiles,
|
|
5
|
+
detectBuild,
|
|
5
6
|
readDataTransfer,
|
|
6
7
|
readFileList,
|
|
7
8
|
} from './drop-core';
|
|
8
9
|
export type {
|
|
10
|
+
BuildDetection,
|
|
11
|
+
BuildReason,
|
|
9
12
|
DropClient,
|
|
10
13
|
DropResponse,
|
|
11
14
|
Digest,
|
|
@@ -2,10 +2,12 @@ import { useRef, useState } from 'preact/hooks';
|
|
|
2
2
|
import {
|
|
3
3
|
AnonymousDropClient,
|
|
4
4
|
deployFiles,
|
|
5
|
+
detectBuild,
|
|
5
6
|
getSingleNonIndexHtmlFileName,
|
|
6
7
|
hasMultipleHtmlFilesWithoutIndex,
|
|
7
8
|
humanizeDropError,
|
|
8
9
|
renameSingleNonIndexHtmlToIndex,
|
|
10
|
+
type BuildDetection,
|
|
9
11
|
type DeployProgress,
|
|
10
12
|
type DropClient,
|
|
11
13
|
type ProcessedFile,
|
|
@@ -22,6 +24,14 @@ export interface DeployedSite {
|
|
|
22
24
|
siteId: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** A deploy failure, handed to `onError` so consumers can render their own error UI. */
|
|
28
|
+
export interface DeployError {
|
|
29
|
+
/** Human-readable text ready to show a user — the same copy the built-in overlay displays. */
|
|
30
|
+
message: string;
|
|
31
|
+
/** The raw underlying reason (e.g. "drop failed: 429"), useful for logging. */
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/** Finite states of a single deploy attempt. */
|
|
26
36
|
export type DeployStatus =
|
|
27
37
|
| { kind: 'idle' }
|
|
@@ -29,7 +39,9 @@ export type DeployStatus =
|
|
|
29
39
|
| { kind: 'creating' }
|
|
30
40
|
| { kind: 'uploading'; uploaded: number; total: number }
|
|
31
41
|
| { kind: 'processing' }
|
|
32
|
-
|
|
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' }
|
|
33
45
|
| { kind: 'error'; message: string };
|
|
34
46
|
|
|
35
47
|
/** Provider-agnostic analytics sink. */
|
|
@@ -39,7 +51,10 @@ interface UseDropDeployOptions {
|
|
|
39
51
|
apiBase?: string;
|
|
40
52
|
client?: DropClient;
|
|
41
53
|
claimUrl: (siteName: string) => string;
|
|
54
|
+
buildSignupUrl: string;
|
|
55
|
+
onBuildRequired?: (info: BuildDetection) => void;
|
|
42
56
|
onDeploy?: (site: DeployedSite) => void;
|
|
57
|
+
onError?: (error: DeployError) => void;
|
|
43
58
|
track: TrackFn;
|
|
44
59
|
}
|
|
45
60
|
|
|
@@ -76,7 +91,10 @@ export function useDropDeploy({
|
|
|
76
91
|
apiBase,
|
|
77
92
|
client,
|
|
78
93
|
claimUrl,
|
|
94
|
+
buildSignupUrl,
|
|
95
|
+
onBuildRequired,
|
|
79
96
|
onDeploy,
|
|
97
|
+
onError,
|
|
80
98
|
track,
|
|
81
99
|
}: UseDropDeployOptions) {
|
|
82
100
|
const [status, setStatus] = useState<DeployStatus>({ kind: 'idle' });
|
|
@@ -96,9 +114,12 @@ export function useDropDeploy({
|
|
|
96
114
|
// included where the file set is known (it isn't if `read()` itself threw).
|
|
97
115
|
// `reason` goes to analytics as-is (raw, e.g. "drop failed: 429"); `message`
|
|
98
116
|
// is what the user sees, which for a caught error is humanized separately.
|
|
117
|
+
// `onError` gets both so a consumer (e.g. with `showStatus={false}`) can show
|
|
118
|
+
// the humanized message while keeping the raw reason for their own logging.
|
|
99
119
|
const fail = (message: string, reason: string, extra?: Record<string, unknown>) => {
|
|
100
120
|
setStatus({ kind: 'error', message });
|
|
101
121
|
track('dropzone_deploy_failed', { method: source, reason, ...extra });
|
|
122
|
+
onError?.({ message, reason });
|
|
102
123
|
};
|
|
103
124
|
|
|
104
125
|
const failFromError = (err: unknown, extra?: Record<string, unknown>) => {
|
|
@@ -123,6 +144,30 @@ export function useDropDeploy({
|
|
|
123
144
|
track('dropzone_files_dropped', { method: source, file_count: files.length });
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
// Build-required projects can't deploy through the anonymous drop API —
|
|
148
|
+
// 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`.
|
|
152
|
+
const build = detectBuild(files);
|
|
153
|
+
if (build.buildRequired) {
|
|
154
|
+
track('dropzone_build_required', {
|
|
155
|
+
method: source,
|
|
156
|
+
reason: build.reason,
|
|
157
|
+
file_count: files.length,
|
|
158
|
+
});
|
|
159
|
+
if (onBuildRequired) {
|
|
160
|
+
// The consumer owns the hand-off now; drop back to idle so the zone
|
|
161
|
+
// stays usable rather than sitting on a spinner.
|
|
162
|
+
setStatus({ kind: 'idle' });
|
|
163
|
+
onBuildRequired(build);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
setStatus({ kind: 'redirecting', target: 'signup' });
|
|
167
|
+
window.location.assign(buildSignupUrl);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
126
171
|
setWarning(indexHtmlWarning(files));
|
|
127
172
|
|
|
128
173
|
const {
|
|
@@ -144,7 +189,7 @@ export function useDropDeploy({
|
|
|
144
189
|
/* private mode / storage disabled — non-fatal */
|
|
145
190
|
}
|
|
146
191
|
|
|
147
|
-
setStatus({ kind: 'redirecting' });
|
|
192
|
+
setStatus({ kind: 'redirecting', target: 'claim' });
|
|
148
193
|
// Fired before the redirect below. Note: the navigation can cut a
|
|
149
194
|
// fire-and-forget analytics request short — see the docs for delivery
|
|
150
195
|
// hardening (sendBeacon / deferring the redirect).
|