drop2run 0.10.0 → 1.0.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/README.md +3 -3
- package/dist/index.js +1740 -1423
- package/dist/{package-CGI7Yt81.js → package-DgeW4aj9.js} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { join, dirname, relative, sep, resolve } from "node:path";
|
|
2
2
|
import { readFileSync, writeFileSync, chmodSync, renameSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
7
|
import { createServer } from "node:http";
|
|
7
|
-
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://dropto.run/api";
|
|
9
|
+
const PRODUCTION_SITE_ORIGIN = "https://dropto.run";
|
|
10
|
+
const PRODUCTION_DASHBOARD_ORIGIN = "https://app.dropto.run";
|
|
11
|
+
function dashboardUrlFor(apiBaseUrl) {
|
|
12
|
+
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
13
|
+
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
14
|
+
}
|
|
8
15
|
const TOKEN_VARIABLE = "DROP2RUN_TOKEN";
|
|
9
16
|
function configPath() {
|
|
10
17
|
return join(homedir(), ".config", "drop2run", "config.json");
|
|
11
18
|
}
|
|
12
|
-
const DEFAULT_API_BASE_URL = "https://dropto.run/api";
|
|
13
|
-
const PRODUCTION_SITE_ORIGIN = "https://dropto.run";
|
|
14
|
-
const PRODUCTION_DASHBOARD_ORIGIN = "https://app.dropto.run";
|
|
15
19
|
function loadCredentials(env = process.env, read = (path) => readFileSync(path, "utf8")) {
|
|
16
20
|
let stored = {};
|
|
17
21
|
try {
|
|
@@ -34,10 +38,6 @@ function resolveApiBaseUrl(env = process.env, read = (path) => readFileSync(path
|
|
|
34
38
|
const url = env.DROP2RUN_API_URL?.trim() || stored.apiBaseUrl?.trim() || DEFAULT_API_BASE_URL;
|
|
35
39
|
return url.replace(/\/+$/, "");
|
|
36
40
|
}
|
|
37
|
-
function dashboardUrlFor(apiBaseUrl) {
|
|
38
|
-
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
39
|
-
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
40
|
-
}
|
|
41
41
|
function saveToken(token, path = configPath()) {
|
|
42
42
|
let existing = {};
|
|
43
43
|
try {
|
|
@@ -100,705 +100,233 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
100
100
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
101
101
|
].join("\n");
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
function sizeOf(content) {
|
|
104
|
+
if (ArrayBuffer.isView(content)) return content.byteLength;
|
|
105
|
+
if (isArrayBuffer(content)) return content.byteLength;
|
|
106
|
+
if (typeof content.size === "number") return content.size;
|
|
107
|
+
throw new TypeError("File contents must be a Uint8Array, an ArrayBuffer or a Blob.");
|
|
108
|
+
}
|
|
109
|
+
function isArrayBuffer(value) {
|
|
110
|
+
return Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
111
|
+
}
|
|
112
|
+
async function bytesOf(content) {
|
|
113
|
+
if (ArrayBuffer.isView(content)) return content;
|
|
114
|
+
if (isArrayBuffer(content)) return new Uint8Array(content);
|
|
115
|
+
return new Uint8Array(await content.arrayBuffer());
|
|
116
|
+
}
|
|
117
|
+
class DeployError extends Error {
|
|
118
|
+
/**
|
|
119
|
+
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
120
|
+
* the client-side codes in {@link ClientErrorCode}.
|
|
121
|
+
* @param message Text safe to show the user.
|
|
122
|
+
* @param detail Anything extra worth logging, such as the RFC 9457 body from the API.
|
|
123
|
+
*/
|
|
124
|
+
constructor(code, message, detail) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.code = code;
|
|
127
|
+
this.detail = detail;
|
|
128
|
+
this.name = "DeployError";
|
|
115
129
|
}
|
|
116
|
-
return await response.json();
|
|
117
130
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
const ClientErrorCode = {
|
|
132
|
+
/** The drop held no files once junk was filtered out. */
|
|
133
|
+
Empty: "empty_drop",
|
|
134
|
+
/** No `index.html` at the root of the drop. */
|
|
135
|
+
MissingIndex: "missing_index",
|
|
136
|
+
/** The drop is larger than the plan allows; the server enforces this too. */
|
|
137
|
+
TooLarge: "too_large",
|
|
138
|
+
/** A single file is larger than the plan allows. */
|
|
139
|
+
FileTooLarge: "file_too_large",
|
|
140
|
+
/** More files than the plan allows. */
|
|
141
|
+
TooManyFiles: "too_many_files",
|
|
142
|
+
/**
|
|
143
|
+
* A zip claims to expand to more than the plan allows.
|
|
144
|
+
*
|
|
145
|
+
* Distinct from {@link TooLarge}, which is about a drop whose files have already been read. This one
|
|
146
|
+
* is raised from the archive's own directory before anything is decompressed, so the numbers in it
|
|
147
|
+
* are what the zip declared rather than what was measured.
|
|
148
|
+
*/
|
|
149
|
+
ZipTooLarge: "zip_too_large",
|
|
150
|
+
/** The user cancelled. */
|
|
151
|
+
Cancelled: "cancelled",
|
|
152
|
+
/** An upload kept failing after every retry. */
|
|
153
|
+
UploadFailed: "upload_failed",
|
|
154
|
+
/**
|
|
155
|
+
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
156
|
+
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
157
|
+
* names memory.
|
|
158
|
+
*/
|
|
159
|
+
OutOfMemory: "out_of_memory",
|
|
160
|
+
/**
|
|
161
|
+
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
162
|
+
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
163
|
+
*/
|
|
164
|
+
ReadFailed: "read_failed",
|
|
165
|
+
/**
|
|
166
|
+
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
167
|
+
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
168
|
+
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
169
|
+
*/
|
|
170
|
+
NetworkFailed: "network_failed"
|
|
171
|
+
};
|
|
172
|
+
function describeError(error) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
let current = error;
|
|
175
|
+
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
176
|
+
if (current instanceof Error) {
|
|
177
|
+
const code = current.code;
|
|
178
|
+
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
179
|
+
current = current.cause;
|
|
180
|
+
} else {
|
|
181
|
+
parts.push(String(current));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
124
184
|
}
|
|
125
|
-
return
|
|
185
|
+
return parts.join(" ← ");
|
|
126
186
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
188
|
+
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
189
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
190
|
+
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
191
|
+
async function prepareDeploy(siteId, files, options, signal) {
|
|
192
|
+
const manifest = files.map((file) => ({
|
|
193
|
+
path: file.path,
|
|
194
|
+
sha256: file.sha256,
|
|
195
|
+
size: sizeOf(file.bytes)
|
|
196
|
+
}));
|
|
197
|
+
const prepared = await request(
|
|
198
|
+
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
199
|
+
// `batches` asks for batch permits; an API that predates them ignores the field, and one that
|
|
200
|
+
// knows them answers with single permits until they are switched on.
|
|
201
|
+
{ files: manifest, batches: true },
|
|
202
|
+
options,
|
|
203
|
+
signal
|
|
141
204
|
);
|
|
142
|
-
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
const problem = await response.json().catch(() => null);
|
|
145
|
-
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
146
|
-
}
|
|
147
|
-
const detail = await response.json();
|
|
148
|
-
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
205
|
+
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
149
206
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
207
|
+
function absolute(uploadUrl, baseUrl) {
|
|
208
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(uploadUrl)) return uploadUrl;
|
|
209
|
+
if (baseUrl === void 0 || !/^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) return uploadUrl;
|
|
210
|
+
return new URL(uploadUrl, baseUrl).toString();
|
|
211
|
+
}
|
|
212
|
+
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
213
|
+
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
214
|
+
const body = name === null || name === void 0 ? void 0 : { name };
|
|
215
|
+
const delay = options.delay ?? wait;
|
|
216
|
+
for (let attempt = 0; ; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
return await request(path, body, options, signal);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
221
|
+
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
222
|
+
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
223
|
+
}
|
|
158
224
|
}
|
|
159
225
|
}
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
credentials,
|
|
163
|
-
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
164
|
-
{ method: "POST" }
|
|
165
|
-
);
|
|
226
|
+
function wait(ms) {
|
|
227
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
166
228
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
229
|
+
async function request(path, body, options, signal) {
|
|
230
|
+
const doFetch = options.fetch ?? globalFetch$1();
|
|
231
|
+
const base = options.baseUrl ?? "/api";
|
|
232
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
233
|
+
const headers = { ...options.headers };
|
|
234
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
235
|
+
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
236
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
237
|
+
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
let timedOut = false;
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
timedOut = true;
|
|
242
|
+
controller.abort();
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
try {
|
|
245
|
+
const response = await doFetch(`${base}/${path}`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers,
|
|
248
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
249
|
+
signal: controller.signal
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) throw await problemToError(response);
|
|
252
|
+
return await response.json();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
255
|
+
if (timedOut) {
|
|
256
|
+
throw new DeployError(
|
|
257
|
+
ClientErrorCode.NetworkFailed,
|
|
258
|
+
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
259
|
+
{ path, cause: describeError(error) }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (error instanceof TypeError) {
|
|
263
|
+
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
264
|
+
path,
|
|
265
|
+
cause: describeError(error)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
throw error;
|
|
269
|
+
} finally {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
}
|
|
169
272
|
}
|
|
170
|
-
function
|
|
171
|
-
return
|
|
273
|
+
function globalFetch$1() {
|
|
274
|
+
return globalThis.fetch.bind(globalThis);
|
|
172
275
|
}
|
|
173
|
-
async function
|
|
276
|
+
async function problemToError(response) {
|
|
277
|
+
let problem = {};
|
|
174
278
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
279
|
+
problem = await response.json();
|
|
280
|
+
} catch {
|
|
281
|
+
return new DeployError(
|
|
282
|
+
`http_${response.status}`,
|
|
283
|
+
`The server responded with ${response.status}.`
|
|
178
284
|
);
|
|
179
|
-
|
|
285
|
+
}
|
|
286
|
+
const code = typeof problem.type === "string" ? problem.type : `http_${response.status}`;
|
|
287
|
+
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
288
|
+
return new DeployError(code, message, problem);
|
|
289
|
+
}
|
|
290
|
+
function batchLayout(members) {
|
|
291
|
+
return members.map((member) => `${member.sha256} ${member.size}
|
|
292
|
+
`).join("");
|
|
293
|
+
}
|
|
294
|
+
function isBatch(target) {
|
|
295
|
+
return "files" in target;
|
|
296
|
+
}
|
|
297
|
+
function parseBatchResult(body) {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(body);
|
|
300
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
301
|
+
const { stored, present, failed } = parsed;
|
|
302
|
+
if (typeof stored !== "number" || typeof present !== "number" || !Array.isArray(failed))
|
|
303
|
+
return null;
|
|
304
|
+
return { stored, present, failed };
|
|
180
305
|
} catch {
|
|
181
306
|
return null;
|
|
182
307
|
}
|
|
183
308
|
}
|
|
184
|
-
async function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
spaMode: site.spaMode,
|
|
206
|
-
docsMode: site.docsMode,
|
|
207
|
-
modeIsManual: site.modeIsManual,
|
|
208
|
-
passwordProtected: site.passwordProtected,
|
|
209
|
-
passwordProtectionAvailable: site.passwordProtectionAvailable,
|
|
210
|
-
formsEnabled: site.formsEnabled,
|
|
211
|
-
formsAvailable: site.formsAvailable,
|
|
212
|
-
expiresAt: site.expiresAt,
|
|
213
|
-
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
214
|
-
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
215
|
-
comments: audienceOf(site.commentAudience),
|
|
216
|
-
commentsAvailable: site.feedbackAvailable === true,
|
|
217
|
-
folderId,
|
|
218
|
-
deploys: site.deploys.map((deploy2) => ({
|
|
219
|
-
deployId: deploy2.deployId,
|
|
220
|
-
status: deploy2.status,
|
|
221
|
-
fileCount: deploy2.fileCount,
|
|
222
|
-
totalBytes: deploy2.totalBytes,
|
|
223
|
-
createdAt: deploy2.createdAt,
|
|
224
|
-
completedAt: deploy2.completedAt,
|
|
225
|
-
filesKept: deploy2.filesKept
|
|
226
|
-
}))
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
async function updateSiteSettings(credentials, siteId, change) {
|
|
230
|
-
const body = {};
|
|
231
|
-
if (change.name !== void 0) body.name = change.name;
|
|
232
|
-
if (change.mode !== void 0) {
|
|
233
|
-
body.spaMode = change.mode === "spa";
|
|
234
|
-
body.docsMode = change.mode === "docs";
|
|
235
|
-
}
|
|
236
|
-
if (change.password !== void 0) body.password = change.password;
|
|
237
|
-
if (change.expiresAt !== void 0) body.expiresAt = change.expiresAt;
|
|
238
|
-
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
239
|
-
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
240
|
-
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
241
|
-
if (change.invitedOnly !== void 0) body.invitedOnly = change.invitedOnly;
|
|
242
|
-
if (change.comments !== void 0) body.commentAudience = API_AUDIENCE[change.comments];
|
|
243
|
-
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
244
|
-
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
245
|
-
const { commentAudience, ...settings } = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
246
|
-
method: "PATCH",
|
|
247
|
-
body: JSON.stringify(body)
|
|
248
|
-
});
|
|
249
|
-
const invitedOnly = typeof settings.invitedOnly === "boolean" ? settings.invitedOnly : null;
|
|
250
|
-
return {
|
|
251
|
-
...settings,
|
|
252
|
-
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
253
|
-
invitedOnly,
|
|
254
|
-
comments: audienceOf(commentAudience),
|
|
255
|
-
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
const API_AUDIENCE = {
|
|
259
|
-
off: "Off",
|
|
260
|
-
invited: "Invited",
|
|
261
|
-
anyone: "Anyone"
|
|
262
|
-
};
|
|
263
|
-
function audienceOf(value) {
|
|
264
|
-
const lower = value?.toLowerCase();
|
|
265
|
-
return lower === "off" || lower === "invited" || lower === "anyone" ? lower : null;
|
|
266
|
-
}
|
|
267
|
-
async function setSitePaused(credentials, siteId, action) {
|
|
268
|
-
const state = await call(
|
|
269
|
-
credentials,
|
|
270
|
-
`sites/${encodeURIComponent(siteId)}/${action}`,
|
|
271
|
-
{ method: "POST" }
|
|
272
|
-
);
|
|
273
|
-
return {
|
|
274
|
-
siteId: state.siteId,
|
|
275
|
-
subdomain: state.subdomain,
|
|
276
|
-
status: enumName(state.status),
|
|
277
|
-
ownerPaused: state.ownerPaused
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
async function listTokens(credentials) {
|
|
281
|
-
const body = await call(credentials, "tokens");
|
|
282
|
-
return body.tokens;
|
|
283
|
-
}
|
|
284
|
-
const SITES_PER_REQUEST = 100;
|
|
285
|
-
async function listSites(credentials) {
|
|
286
|
-
const body = await call(
|
|
287
|
-
credentials,
|
|
288
|
-
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
289
|
-
);
|
|
290
|
-
return { sites: body.sites, total: body.total };
|
|
291
|
-
}
|
|
292
|
-
async function createSite(credentials, subdomain, folderId) {
|
|
293
|
-
const created = await call(
|
|
294
|
-
credentials,
|
|
295
|
-
"sites",
|
|
296
|
-
{
|
|
297
|
-
method: "POST",
|
|
298
|
-
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
299
|
-
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
300
|
-
body: JSON.stringify({
|
|
301
|
-
...subdomain === void 0 ? {} : { subdomain },
|
|
302
|
-
...folderId === void 0 ? {} : { folderId }
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
);
|
|
306
|
-
return { ...created, name: null };
|
|
307
|
-
}
|
|
308
|
-
async function listFolders(credentials) {
|
|
309
|
-
const body = await call(credentials, "site-folders");
|
|
310
|
-
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
311
|
-
const pathOf = (folderId) => {
|
|
312
|
-
const names = [];
|
|
313
|
-
let cursor = byId.get(folderId);
|
|
314
|
-
while (cursor !== void 0 && names.length <= byId.size) {
|
|
315
|
-
names.unshift(cursor.name);
|
|
316
|
-
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
317
|
-
}
|
|
318
|
-
return names.join("/");
|
|
319
|
-
};
|
|
320
|
-
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
321
|
-
}
|
|
322
|
-
function comparablePath(path) {
|
|
323
|
-
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
324
|
-
}
|
|
325
|
-
const FOLDERS_LISTED_ON_MISS = 20;
|
|
326
|
-
async function resolveFolder(credentials, folder) {
|
|
327
|
-
const wanted = comparablePath(folder);
|
|
328
|
-
if (wanted === "") {
|
|
329
|
-
throw new Error(
|
|
330
|
-
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
const folders2 = await listFolders(credentials);
|
|
334
|
-
const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
335
|
-
if (byId !== void 0) return byId.folderId;
|
|
336
|
-
const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
337
|
-
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
338
|
-
if (matches.length > 1) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
if (folders2.length === 0) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
349
|
-
const more = folders2.length - listed.length;
|
|
350
|
-
throw new Error(
|
|
351
|
-
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
const ROOT_FOLDER = "root";
|
|
355
|
-
async function resolveFolderTarget(credentials, folder) {
|
|
356
|
-
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
357
|
-
}
|
|
358
|
-
async function listComments(credentials, siteId, query = {}) {
|
|
359
|
-
const search = new URLSearchParams();
|
|
360
|
-
if (query.status !== void 0) search.set("status", query.status);
|
|
361
|
-
if (query.path !== void 0) search.set("path", query.path);
|
|
362
|
-
if (query.since !== void 0) search.set("since", String(query.since));
|
|
363
|
-
if (query.page !== void 0) search.set("page", String(query.page));
|
|
364
|
-
const encoded = search.toString();
|
|
365
|
-
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
366
|
-
return await call(
|
|
367
|
-
credentials,
|
|
368
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
372
|
-
return await call(
|
|
373
|
-
credentials,
|
|
374
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
375
|
-
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
379
|
-
return await call(
|
|
380
|
-
credentials,
|
|
381
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
382
|
-
{ method: "POST" }
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const PEOPLE_PER_REQUEST = 50;
|
|
386
|
-
async function listPeople(credentials, siteId) {
|
|
387
|
-
const page = await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers?pageSize=${PEOPLE_PER_REQUEST}`);
|
|
388
|
-
return {
|
|
389
|
-
invitedOnly: page.invitedOnly,
|
|
390
|
-
total: page.total,
|
|
391
|
-
limit: page.limit,
|
|
392
|
-
people: page.viewers.map((row) => ({
|
|
393
|
-
email: row.email,
|
|
394
|
-
role: enumName(row.role),
|
|
395
|
-
owner: row.owner,
|
|
396
|
-
status: enumName(row.status)
|
|
397
|
-
}))
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
async function invitePeople(credentials, siteId, emails, role) {
|
|
401
|
-
return await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers`, {
|
|
402
|
-
method: "POST",
|
|
403
|
-
body: JSON.stringify({ emails, role: role === "comment" ? "Comment" : "View", notify: true })
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
async function removePerson(credentials, siteId, email) {
|
|
407
|
-
await call(
|
|
408
|
-
credentials,
|
|
409
|
-
`sites/${encodeURIComponent(siteId)}/viewers?email=${encodeURIComponent(email)}`,
|
|
410
|
-
{ method: "DELETE" }
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
const VERSIONS_LISTED = 10;
|
|
414
|
-
function servingModeOf$1(site) {
|
|
415
|
-
if (site.spaMode) return "spa";
|
|
416
|
-
if (site.docsMode) return "docs";
|
|
417
|
-
return "static";
|
|
418
|
-
}
|
|
419
|
-
function accessOf(site) {
|
|
420
|
-
if (site.passwordProtected) return "password";
|
|
421
|
-
if (site.invitedOnly === true) return "invite-only";
|
|
422
|
-
if (site.invitedOnly === false) return "public";
|
|
423
|
-
return "no password (whether it is invite-only could not be read)";
|
|
424
|
-
}
|
|
425
|
-
function describeSite(site, folderPath) {
|
|
426
|
-
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
427
|
-
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
428
|
-
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
429
|
-
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
430
|
-
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
431
|
-
});
|
|
432
|
-
const more = site.deploys.length - versions.length;
|
|
433
|
-
return [
|
|
434
|
-
`${site.subdomain} — ${site.url}`,
|
|
435
|
-
` name ${site.name ?? "(none)"}`,
|
|
436
|
-
` status ${site.status}`,
|
|
437
|
-
` access ${accessOf(site)}`,
|
|
438
|
-
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
439
|
-
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
440
|
-
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
441
|
-
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
442
|
-
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
443
|
-
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
444
|
-
"",
|
|
445
|
-
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
446
|
-
...versions,
|
|
447
|
-
...more > 0 ? [` … and ${more} older`] : []
|
|
448
|
-
].join("\n");
|
|
449
|
-
}
|
|
450
|
-
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
451
|
-
function newAttempt() {
|
|
452
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
453
|
-
return {
|
|
454
|
-
verifier,
|
|
455
|
-
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
456
|
-
state: randomBytes(16).toString("base64url")
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function clientName(surface) {
|
|
460
|
-
const clean2 = (value) => value.trim().replace(/[^A-Za-z0-9._-]/g, "-");
|
|
461
|
-
const label = "";
|
|
462
|
-
const raw = clean2(hostname()).slice(0, 64 - label.length);
|
|
463
|
-
return `${raw === "" ? "drop2run" : raw}${label}`;
|
|
464
|
-
}
|
|
465
|
-
function openBrowser(url) {
|
|
466
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
467
|
-
try {
|
|
468
|
-
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
469
|
-
child.unref();
|
|
470
|
-
child.on("error", () => {
|
|
471
|
-
});
|
|
472
|
-
return true;
|
|
473
|
-
} catch {
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function listen(state) {
|
|
478
|
-
return new Promise((resolveListener, rejectListener) => {
|
|
479
|
-
let settle = null;
|
|
480
|
-
let fail = null;
|
|
481
|
-
const received = new Promise((resolveCode, rejectCode) => {
|
|
482
|
-
settle = resolveCode;
|
|
483
|
-
fail = rejectCode;
|
|
484
|
-
});
|
|
485
|
-
const server = createServer((request2, response) => {
|
|
486
|
-
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
487
|
-
if (url.pathname !== "/cb") {
|
|
488
|
-
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
489
|
-
response.end("Not found.\n");
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const code = url.searchParams.get("code") ?? "";
|
|
493
|
-
const returned = url.searchParams.get("state") ?? "";
|
|
494
|
-
if (returned !== state || code === "") {
|
|
495
|
-
respond(
|
|
496
|
-
response,
|
|
497
|
-
400,
|
|
498
|
-
"Sign-in could not be completed",
|
|
499
|
-
"This did not come from the sign-in that was started here. Nothing has been changed."
|
|
500
|
-
);
|
|
501
|
-
fail?.(new Error("The browser returned a response that did not match this sign-in."));
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
respond(response, 200, "Signed in", "You can close this tab now.");
|
|
505
|
-
settle?.({ code });
|
|
506
|
-
});
|
|
507
|
-
server.on("error", (error) => {
|
|
508
|
-
rejectListener(error);
|
|
509
|
-
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
510
|
-
});
|
|
511
|
-
server.listen(0, "127.0.0.1", () => {
|
|
512
|
-
const address = server.address();
|
|
513
|
-
if (address === null) {
|
|
514
|
-
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
resolveListener({
|
|
518
|
-
port: address.port,
|
|
519
|
-
received,
|
|
520
|
-
close: () => server.close()
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
function respond(response, status, title, detail) {
|
|
526
|
-
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
527
|
-
response.end(
|
|
528
|
-
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
529
|
-
`
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
533
|
-
let timer;
|
|
534
|
-
try {
|
|
535
|
-
return await Promise.race([
|
|
536
|
-
received,
|
|
537
|
-
new Promise((_, reject) => {
|
|
538
|
-
timer = setTimeout(
|
|
539
|
-
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
540
|
-
timeoutMs
|
|
541
|
-
);
|
|
542
|
-
})
|
|
543
|
-
]);
|
|
544
|
-
} finally {
|
|
545
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function consentUrl(dashboardUrl, attempt, port, name) {
|
|
549
|
-
const url = new URL("/cli-auth", dashboardUrl);
|
|
550
|
-
url.searchParams.set("challenge", attempt.challenge);
|
|
551
|
-
url.searchParams.set("state", attempt.state);
|
|
552
|
-
url.searchParams.set("port", String(port));
|
|
553
|
-
url.searchParams.set("name", name);
|
|
554
|
-
return url.toString();
|
|
555
|
-
}
|
|
556
|
-
async function startDevice(apiBaseUrl, clientName2) {
|
|
557
|
-
const response = await fetch(`${apiBaseUrl}/auth/cli/device`, {
|
|
558
|
-
method: "POST",
|
|
559
|
-
headers: { "Content-Type": "application/json" },
|
|
560
|
-
body: JSON.stringify({ clientName: clientName2 })
|
|
561
|
-
});
|
|
562
|
-
if (!response.ok) throw new Error(await problemDetail(response));
|
|
563
|
-
return await response.json();
|
|
564
|
-
}
|
|
565
|
-
async function pollDevice(apiBaseUrl, deviceCode) {
|
|
566
|
-
let response;
|
|
567
|
-
try {
|
|
568
|
-
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
569
|
-
method: "POST",
|
|
570
|
-
headers: { "Content-Type": "application/json" },
|
|
571
|
-
body: JSON.stringify({ deviceCode })
|
|
572
|
-
});
|
|
573
|
-
} catch {
|
|
574
|
-
return { state: "pending" };
|
|
575
|
-
}
|
|
576
|
-
if (response.ok) {
|
|
577
|
-
const body = await response.json();
|
|
578
|
-
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
579
|
-
}
|
|
580
|
-
const problem = await response.json().catch(() => null);
|
|
581
|
-
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
582
|
-
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
583
|
-
return {
|
|
584
|
-
state: "dead",
|
|
585
|
-
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
async function problemDetail(response) {
|
|
589
|
-
const problem = await response.json().catch(() => null);
|
|
590
|
-
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
591
|
-
}
|
|
592
|
-
async function exchange(apiBaseUrl, code, verifier) {
|
|
593
|
-
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
594
|
-
method: "POST",
|
|
595
|
-
headers: { "Content-Type": "application/json" },
|
|
596
|
-
body: JSON.stringify({ code, verifier })
|
|
597
|
-
});
|
|
598
|
-
if (!response.ok) throw new Error(await problemDetail(response));
|
|
599
|
-
const body = await response.json();
|
|
600
|
-
return { token: body.token, name: body.name, email: body.email };
|
|
601
|
-
}
|
|
602
|
-
const PROJECT_FILE = "drop2run.json";
|
|
603
|
-
function projectPath(directory = process.cwd()) {
|
|
604
|
-
return join(directory, PROJECT_FILE);
|
|
605
|
-
}
|
|
606
|
-
function readProject(directory = process.cwd(), read = (path) => readFileSync(path, "utf8")) {
|
|
607
|
-
let parsed;
|
|
608
|
-
try {
|
|
609
|
-
parsed = JSON.parse(read(projectPath(directory)));
|
|
610
|
-
} catch {
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
if (typeof parsed.siteId !== "string" || parsed.siteId === "") return null;
|
|
614
|
-
return {
|
|
615
|
-
siteId: parsed.siteId,
|
|
616
|
-
subdomain: typeof parsed.subdomain === "string" ? parsed.subdomain : "",
|
|
617
|
-
dir: typeof parsed.dir === "string" && parsed.dir !== "" ? parsed.dir : "."
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
function writeProject(project, directory = process.cwd()) {
|
|
621
|
-
const path = projectPath(directory);
|
|
622
|
-
writeFileSync(path, `${JSON.stringify(project, null, " ")}
|
|
623
|
-
`);
|
|
624
|
-
return path;
|
|
625
|
-
}
|
|
626
|
-
function sizeOf(content) {
|
|
627
|
-
return ArrayBuffer.isView(content) ? content.byteLength : content.size;
|
|
628
|
-
}
|
|
629
|
-
async function bytesOf(content) {
|
|
630
|
-
return ArrayBuffer.isView(content) ? content : new Uint8Array(await content.arrayBuffer());
|
|
631
|
-
}
|
|
632
|
-
class DeployError extends Error {
|
|
633
|
-
/**
|
|
634
|
-
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
635
|
-
* the client-side codes in {@link ClientErrorCode}.
|
|
636
|
-
* @param message Text safe to show the user.
|
|
637
|
-
* @param detail Anything extra worth logging, such as the RFC 9457 body from the API.
|
|
638
|
-
*/
|
|
639
|
-
constructor(code, message, detail) {
|
|
640
|
-
super(message);
|
|
641
|
-
this.code = code;
|
|
642
|
-
this.detail = detail;
|
|
643
|
-
this.name = "DeployError";
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const ClientErrorCode = {
|
|
647
|
-
/** The drop held no files once junk was filtered out. */
|
|
648
|
-
Empty: "empty_drop",
|
|
649
|
-
/** No `index.html` at the root of the drop. */
|
|
650
|
-
MissingIndex: "missing_index",
|
|
651
|
-
/** The drop is larger than the plan allows; the server enforces this too. */
|
|
652
|
-
TooLarge: "too_large",
|
|
653
|
-
/** A single file is larger than the plan allows. */
|
|
654
|
-
FileTooLarge: "file_too_large",
|
|
655
|
-
/** More files than the plan allows. */
|
|
656
|
-
TooManyFiles: "too_many_files",
|
|
657
|
-
/**
|
|
658
|
-
* A zip claims to expand to more than the plan allows.
|
|
659
|
-
*
|
|
660
|
-
* Distinct from {@link TooLarge}, which is about a drop whose files have already been read. This one
|
|
661
|
-
* is raised from the archive's own directory before anything is decompressed, so the numbers in it
|
|
662
|
-
* are what the zip declared rather than what was measured.
|
|
663
|
-
*/
|
|
664
|
-
ZipTooLarge: "zip_too_large",
|
|
665
|
-
/** The user cancelled. */
|
|
666
|
-
Cancelled: "cancelled",
|
|
667
|
-
/** An upload kept failing after every retry. */
|
|
668
|
-
UploadFailed: "upload_failed",
|
|
669
|
-
/**
|
|
670
|
-
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
671
|
-
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
672
|
-
* names memory.
|
|
673
|
-
*/
|
|
674
|
-
OutOfMemory: "out_of_memory",
|
|
675
|
-
/**
|
|
676
|
-
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
677
|
-
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
678
|
-
*/
|
|
679
|
-
ReadFailed: "read_failed",
|
|
680
|
-
/**
|
|
681
|
-
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
682
|
-
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
683
|
-
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
684
|
-
*/
|
|
685
|
-
NetworkFailed: "network_failed"
|
|
686
|
-
};
|
|
687
|
-
function describeError(error) {
|
|
688
|
-
const parts = [];
|
|
689
|
-
let current = error;
|
|
690
|
-
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
691
|
-
if (current instanceof Error) {
|
|
692
|
-
const code = current.code;
|
|
693
|
-
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
694
|
-
current = current.cause;
|
|
695
|
-
} else {
|
|
696
|
-
parts.push(String(current));
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return parts.join(" ← ");
|
|
701
|
-
}
|
|
702
|
-
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
703
|
-
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
704
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
705
|
-
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
706
|
-
async function prepareDeploy(siteId, files, options, signal) {
|
|
707
|
-
const manifest = files.map((file) => ({
|
|
708
|
-
path: file.path,
|
|
709
|
-
sha256: file.sha256,
|
|
710
|
-
size: sizeOf(file.bytes)
|
|
711
|
-
}));
|
|
712
|
-
const prepared = await request(
|
|
713
|
-
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
714
|
-
{ files: manifest },
|
|
715
|
-
options,
|
|
716
|
-
signal
|
|
717
|
-
);
|
|
718
|
-
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
719
|
-
}
|
|
720
|
-
function absolute(uploadUrl, baseUrl) {
|
|
721
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(uploadUrl)) return uploadUrl;
|
|
722
|
-
if (baseUrl === void 0 || !/^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) return uploadUrl;
|
|
723
|
-
return new URL(uploadUrl, baseUrl).toString();
|
|
724
|
-
}
|
|
725
|
-
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
726
|
-
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
727
|
-
const body = name === null || name === void 0 ? void 0 : { name };
|
|
728
|
-
const delay = options.delay ?? wait;
|
|
729
|
-
for (let attempt = 0; ; attempt++) {
|
|
730
|
-
try {
|
|
731
|
-
return await request(path, body, options, signal);
|
|
732
|
-
} catch (error) {
|
|
733
|
-
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
734
|
-
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
735
|
-
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
function wait(ms) {
|
|
740
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
741
|
-
}
|
|
742
|
-
async function request(path, body, options, signal) {
|
|
743
|
-
const doFetch = options.fetch ?? globalFetch$1();
|
|
744
|
-
const base = options.baseUrl ?? "/api";
|
|
745
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
746
|
-
const headers = {};
|
|
747
|
-
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
748
|
-
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
749
|
-
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
750
|
-
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
751
|
-
const controller = new AbortController();
|
|
752
|
-
let timedOut = false;
|
|
753
|
-
const timer = setTimeout(() => {
|
|
754
|
-
timedOut = true;
|
|
755
|
-
controller.abort();
|
|
756
|
-
}, timeoutMs);
|
|
757
|
-
try {
|
|
758
|
-
const response = await doFetch(`${base}/${path}`, {
|
|
759
|
-
method: "POST",
|
|
760
|
-
headers,
|
|
761
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
762
|
-
signal: controller.signal
|
|
763
|
-
});
|
|
764
|
-
if (!response.ok) throw await problemToError(response);
|
|
765
|
-
return await response.json();
|
|
766
|
-
} catch (error) {
|
|
767
|
-
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
768
|
-
if (timedOut) {
|
|
769
|
-
throw new DeployError(
|
|
770
|
-
ClientErrorCode.NetworkFailed,
|
|
771
|
-
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
772
|
-
{ path, cause: describeError(error) }
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
if (error instanceof TypeError) {
|
|
776
|
-
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
777
|
-
path,
|
|
778
|
-
cause: describeError(error)
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
throw error;
|
|
782
|
-
} finally {
|
|
783
|
-
clearTimeout(timer);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
function globalFetch$1() {
|
|
787
|
-
return globalThis.fetch.bind(globalThis);
|
|
788
|
-
}
|
|
789
|
-
async function problemToError(response) {
|
|
790
|
-
let problem = {};
|
|
791
|
-
try {
|
|
792
|
-
problem = await response.json();
|
|
793
|
-
} catch {
|
|
794
|
-
return new DeployError(
|
|
795
|
-
`http_${response.status}`,
|
|
796
|
-
`The server responded with ${response.status}.`
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
const code = typeof problem.type === "string" ? problem.type : `http_${response.status}`;
|
|
800
|
-
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
801
|
-
return new DeployError(code, message, problem);
|
|
309
|
+
async function mapInOrder(items, limit, fn, signal) {
|
|
310
|
+
const results = new Array(items.length);
|
|
311
|
+
let next = 0;
|
|
312
|
+
let failure2 = null;
|
|
313
|
+
const lane = async () => {
|
|
314
|
+
for (; ; ) {
|
|
315
|
+
if (failure2 !== null) return;
|
|
316
|
+
const index = next++;
|
|
317
|
+
if (index >= items.length) return;
|
|
318
|
+
try {
|
|
319
|
+
results[index] = await fn(items[index], index);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
failure2 ??= { error };
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const lanes = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, lane);
|
|
327
|
+
await Promise.all(lanes);
|
|
328
|
+
if (failure2 !== null) throw failure2.error;
|
|
329
|
+
return results;
|
|
802
330
|
}
|
|
803
331
|
const FILE_TYPES = [
|
|
804
332
|
// Documents. The three extensions that make a folder a documents site.
|
|
@@ -873,7 +401,7 @@ const FILE_TYPES = [
|
|
|
873
401
|
{
|
|
874
402
|
ext: ".csv",
|
|
875
403
|
mime: "text/csv; charset=utf-8",
|
|
876
|
-
kind: "
|
|
404
|
+
kind: "csv",
|
|
877
405
|
document: false,
|
|
878
406
|
reader: true,
|
|
879
407
|
editable: true
|
|
@@ -1250,807 +778,1561 @@ const FILE_TYPES = [
|
|
|
1250
778
|
reader: false,
|
|
1251
779
|
editable: false
|
|
1252
780
|
}
|
|
1253
|
-
];
|
|
1254
|
-
function extensionsWhere(include) {
|
|
1255
|
-
return FILE_TYPES.filter(include).map((type) => type.ext);
|
|
781
|
+
];
|
|
782
|
+
function extensionsWhere(include) {
|
|
783
|
+
return FILE_TYPES.filter(include).map((type) => type.ext);
|
|
784
|
+
}
|
|
785
|
+
const DOCUMENT_EXTENSIONS$1 = extensionsWhere((type) => type.document);
|
|
786
|
+
const VIEWABLE_EXTENSIONS$1 = extensionsWhere(
|
|
787
|
+
(type) => type.kind !== "binary"
|
|
788
|
+
);
|
|
789
|
+
extensionsWhere((type) => type.reader);
|
|
790
|
+
extensionsWhere((type) => type.editable);
|
|
791
|
+
const WEB_ASSET_EXTENSIONS = [
|
|
792
|
+
".html",
|
|
793
|
+
".htm",
|
|
794
|
+
".js",
|
|
795
|
+
".mjs",
|
|
796
|
+
".cjs",
|
|
797
|
+
".css"
|
|
798
|
+
];
|
|
799
|
+
const GALLERY_EXTENSIONS = extensionsWhere(
|
|
800
|
+
(type) => type.kind === "image"
|
|
801
|
+
);
|
|
802
|
+
Object.fromEntries(
|
|
803
|
+
FILE_TYPES.filter((type) => type.mime !== null).map((type) => [type.ext, type.mime])
|
|
804
|
+
);
|
|
805
|
+
function extensionOf(path) {
|
|
806
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
807
|
+
const dot = name.lastIndexOf(".");
|
|
808
|
+
return dot <= 0 ? "" : name.slice(dot).toLowerCase();
|
|
809
|
+
}
|
|
810
|
+
async function sha256Hex(bytes) {
|
|
811
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
812
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
813
|
+
}
|
|
814
|
+
const HASH_CONCURRENCY = 4;
|
|
815
|
+
const HASH_BYTES_IN_FLIGHT = 16 * 1024 * 1024;
|
|
816
|
+
async function hashAll(files, onProgress, signal) {
|
|
817
|
+
const budget = byteBudget(HASH_BYTES_IN_FLIGHT);
|
|
818
|
+
let done = 0;
|
|
819
|
+
return mapInOrder(
|
|
820
|
+
files,
|
|
821
|
+
HASH_CONCURRENCY,
|
|
822
|
+
async (file) => {
|
|
823
|
+
const size = sizeOf(file.bytes);
|
|
824
|
+
await budget.acquire(size);
|
|
825
|
+
try {
|
|
826
|
+
signal?.throwIfAborted();
|
|
827
|
+
const hashed = { ...file, sha256: await sha256Hex(await readForHash(file)) };
|
|
828
|
+
done += 1;
|
|
829
|
+
onProgress?.(done, files.length);
|
|
830
|
+
return hashed;
|
|
831
|
+
} finally {
|
|
832
|
+
budget.release(size);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function byteBudget(limit) {
|
|
838
|
+
let held = 0;
|
|
839
|
+
let waiting = [];
|
|
840
|
+
return {
|
|
841
|
+
async acquire(size) {
|
|
842
|
+
while (held > 0 && held + size > limit) {
|
|
843
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
844
|
+
}
|
|
845
|
+
held += size;
|
|
846
|
+
},
|
|
847
|
+
release(size) {
|
|
848
|
+
held -= size;
|
|
849
|
+
const woken = waiting;
|
|
850
|
+
waiting = [];
|
|
851
|
+
for (const wake of woken) wake();
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
async function readForHash(file) {
|
|
856
|
+
try {
|
|
857
|
+
return await bytesOf(file.bytes);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (error instanceof RangeError) throw error;
|
|
860
|
+
throw new DeployError(
|
|
861
|
+
ClientErrorCode.ReadFailed,
|
|
862
|
+
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
863
|
+
{ path: file.path, cause: describeError(error) }
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const IGNORED_EXACT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".gitignore", ".gitkeep"]);
|
|
868
|
+
const IGNORED_DIRECTORIES = [
|
|
869
|
+
"__MACOSX",
|
|
870
|
+
".git",
|
|
871
|
+
"node_modules",
|
|
872
|
+
".svn",
|
|
873
|
+
".hg",
|
|
874
|
+
".idea",
|
|
875
|
+
".vscode",
|
|
876
|
+
".claude",
|
|
877
|
+
".agents",
|
|
878
|
+
".cursor",
|
|
879
|
+
".codex",
|
|
880
|
+
".gemini"
|
|
881
|
+
];
|
|
882
|
+
function isEnvironmentFile(name) {
|
|
883
|
+
return name === ".env" || name === ".envrc" || name.startsWith(".env.");
|
|
884
|
+
}
|
|
885
|
+
function shouldIgnore(path) {
|
|
886
|
+
const segments = path.split("/");
|
|
887
|
+
const name = segments.at(-1) ?? "";
|
|
888
|
+
if (name === "" || IGNORED_EXACT.has(name) || isEnvironmentFile(name)) return true;
|
|
889
|
+
if (name.startsWith("._")) return true;
|
|
890
|
+
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
891
|
+
}
|
|
892
|
+
const REQUIRED_INDEX = "index.html";
|
|
893
|
+
const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
|
|
894
|
+
function isDocumentPath(path) {
|
|
895
|
+
const lower = path.toLowerCase();
|
|
896
|
+
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
897
|
+
}
|
|
898
|
+
const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
|
|
899
|
+
function isViewablePath(path) {
|
|
900
|
+
const lower = path.toLowerCase();
|
|
901
|
+
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
902
|
+
}
|
|
903
|
+
function isImageFolder(paths) {
|
|
904
|
+
let pictures = false;
|
|
905
|
+
for (const path of paths) {
|
|
906
|
+
const ext = extensionOf(path);
|
|
907
|
+
if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
|
|
908
|
+
if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
|
|
909
|
+
}
|
|
910
|
+
return pictures;
|
|
911
|
+
}
|
|
912
|
+
function canPublish(paths) {
|
|
913
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
914
|
+
if (paths.some(isDocumentPath)) return true;
|
|
915
|
+
if (isImageFolder(paths)) return true;
|
|
916
|
+
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
917
|
+
}
|
|
918
|
+
function servingModeOf$1(flags) {
|
|
919
|
+
if (flags.docsMode) return "docs";
|
|
920
|
+
if (flags.spaMode) return "spa";
|
|
921
|
+
return "static";
|
|
922
|
+
}
|
|
923
|
+
function predictedModeOf(paths) {
|
|
924
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
|
|
925
|
+
if (paths.some(isDocumentPath)) return "docs";
|
|
926
|
+
if (isImageFolder(paths)) return "docs";
|
|
927
|
+
if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
|
|
928
|
+
return "static";
|
|
929
|
+
}
|
|
930
|
+
function nestedIndexOf(paths) {
|
|
931
|
+
if (predictedModeOf(paths) !== "docs") return null;
|
|
932
|
+
const suffix = `/${REQUIRED_INDEX}`;
|
|
933
|
+
const folders2 = paths.filter((path) => path.endsWith(suffix)).map((path) => path.slice(0, -suffix.length)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
|
|
934
|
+
return folders2[0] ?? null;
|
|
935
|
+
}
|
|
936
|
+
function renameOfLoneHtmlPage(paths) {
|
|
937
|
+
if (paths.length !== 1) return null;
|
|
938
|
+
const [only] = paths;
|
|
939
|
+
if (only === void 0 || only === REQUIRED_INDEX) return null;
|
|
940
|
+
const lower = only.toLowerCase();
|
|
941
|
+
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
942
|
+
}
|
|
943
|
+
function formatBytes$1(bytes) {
|
|
944
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
945
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
946
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
947
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
948
|
+
}
|
|
949
|
+
function checkLimits(files, limits) {
|
|
950
|
+
if (files.length === 0) {
|
|
951
|
+
throw new DeployError(
|
|
952
|
+
ClientErrorCode.Empty,
|
|
953
|
+
"That drop contained no files to deploy. Folders like .git and node_modules are skipped."
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (!canPublish(files.map((file) => file.path))) {
|
|
957
|
+
throw new DeployError(
|
|
958
|
+
ClientErrorCode.MissingIndex,
|
|
959
|
+
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const refusal = sizeRefusal(
|
|
963
|
+
files.map((file) => ({ path: file.path, size: sizeOf(file.bytes) })),
|
|
964
|
+
limits
|
|
965
|
+
);
|
|
966
|
+
if (refusal) throw refusal;
|
|
967
|
+
}
|
|
968
|
+
function sizeRefusal(files, limits) {
|
|
969
|
+
if (!limits) return null;
|
|
970
|
+
if (files.length > limits.maxFiles) {
|
|
971
|
+
return new DeployError(
|
|
972
|
+
ClientErrorCode.TooManyFiles,
|
|
973
|
+
`That is ${files.length} files, and this plan allows ${limits.maxFiles}.`,
|
|
974
|
+
{ limit: limits.maxFiles, actual: files.length }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const oversized = files.find((file) => file.size > limits.maxFileBytes);
|
|
978
|
+
if (oversized) {
|
|
979
|
+
return new DeployError(
|
|
980
|
+
ClientErrorCode.FileTooLarge,
|
|
981
|
+
`${oversized.path} is ${formatBytes$1(oversized.size)}, and this plan allows ${formatBytes$1(limits.maxFileBytes)} per file.`,
|
|
982
|
+
{
|
|
983
|
+
path: oversized.path,
|
|
984
|
+
limit: limits.maxFileBytes,
|
|
985
|
+
actual: oversized.size
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
990
|
+
if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
|
|
991
|
+
return new DeployError(
|
|
992
|
+
ClientErrorCode.TooLarge,
|
|
993
|
+
`That drop is ${formatBytes$1(total)}, and this plan allows ${formatBytes$1(limits.maxSiteBytes)}.`,
|
|
994
|
+
{ limit: limits.maxSiteBytes, actual: total }
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
1256
998
|
}
|
|
1257
|
-
const
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
)
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
(
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
999
|
+
const MAX_NAME_LENGTH = 60;
|
|
1000
|
+
const HEAD_BYTES = 64 * 1024;
|
|
1001
|
+
const TITLE_SEPARATORS = ["|", "·", "—", "–", " - "];
|
|
1002
|
+
function suggestSiteName(files) {
|
|
1003
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1004
|
+
if (index === void 0 || !ArrayBuffer.isView(index.bytes)) return null;
|
|
1005
|
+
try {
|
|
1006
|
+
return nameFromHtml(decodeHead(index.bytes));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function readSiteName(files) {
|
|
1012
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1013
|
+
if (index === void 0) return null;
|
|
1014
|
+
if (ArrayBuffer.isView(index.bytes)) return suggestSiteName(files);
|
|
1015
|
+
try {
|
|
1016
|
+
const head = new Uint8Array(await index.bytes.slice(0, HEAD_BYTES).arrayBuffer());
|
|
1017
|
+
return suggestSiteName([{ path: index.path, bytes: head }]);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function decodeHead(bytes) {
|
|
1023
|
+
return new TextDecoder("utf-8", { fatal: false, ignoreBOM: false }).decode(
|
|
1024
|
+
bytes.subarray(0, HEAD_BYTES)
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
function nameFromHtml(html) {
|
|
1028
|
+
const document = parse(html);
|
|
1029
|
+
const candidates = document ? [
|
|
1030
|
+
document.querySelector('meta[property="og:site_name"]')?.getAttribute("content"),
|
|
1031
|
+
document.querySelector("title")?.textContent,
|
|
1032
|
+
document.querySelector("h1")?.textContent
|
|
1033
|
+
] : [matchOgSiteName(html), matchTag(html, "title"), matchTag(html, "h1")];
|
|
1034
|
+
for (const candidate of candidates) {
|
|
1035
|
+
const name = clean(candidate);
|
|
1036
|
+
if (name !== null) return name;
|
|
1037
|
+
}
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
function parse(html) {
|
|
1041
|
+
const parser = globalThis.DOMParser;
|
|
1042
|
+
if (parser === void 0) return null;
|
|
1043
|
+
try {
|
|
1044
|
+
return new parser().parseFromString(html, "text/html");
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function matchTag(html, tag) {
|
|
1050
|
+
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(html);
|
|
1051
|
+
return match?.[1] === void 0 ? null : decodeEntities(match[1]);
|
|
1052
|
+
}
|
|
1053
|
+
function matchOgSiteName(html) {
|
|
1054
|
+
const patterns = [
|
|
1055
|
+
/<meta[^>]*property\s*=\s*["']og:site_name["'][^>]*content\s*=\s*["']([^"']*)["']/i,
|
|
1056
|
+
/<meta[^>]*content\s*=\s*["']([^"']*)["'][^>]*property\s*=\s*["']og:site_name["']/i
|
|
1057
|
+
];
|
|
1058
|
+
for (const pattern of patterns) {
|
|
1059
|
+
const match = pattern.exec(html);
|
|
1060
|
+
if (match?.[1] !== void 0) return decodeEntities(match[1]);
|
|
1061
|
+
}
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
const LATIN1_ENTITIES = "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml".split(" ");
|
|
1065
|
+
const NAMED_ENTITIES = new Map([
|
|
1066
|
+
...LATIN1_ENTITIES.map((name, index) => [name, 160 + index]),
|
|
1067
|
+
["amp", 38],
|
|
1068
|
+
["lt", 60],
|
|
1069
|
+
["gt", 62],
|
|
1070
|
+
["quot", 34],
|
|
1071
|
+
["apos", 39],
|
|
1072
|
+
["ensp", 8194],
|
|
1073
|
+
["emsp", 8195],
|
|
1074
|
+
["thinsp", 8201],
|
|
1075
|
+
["ndash", 8211],
|
|
1076
|
+
["mdash", 8212],
|
|
1077
|
+
["lsquo", 8216],
|
|
1078
|
+
["rsquo", 8217],
|
|
1079
|
+
["sbquo", 8218],
|
|
1080
|
+
["ldquo", 8220],
|
|
1081
|
+
["rdquo", 8221],
|
|
1082
|
+
["bdquo", 8222],
|
|
1083
|
+
["dagger", 8224],
|
|
1084
|
+
["Dagger", 8225],
|
|
1085
|
+
["bull", 8226],
|
|
1086
|
+
["hellip", 8230],
|
|
1087
|
+
["prime", 8242],
|
|
1088
|
+
["lsaquo", 8249],
|
|
1089
|
+
["rsaquo", 8250],
|
|
1090
|
+
["euro", 8364],
|
|
1091
|
+
["trade", 8482],
|
|
1092
|
+
["larr", 8592],
|
|
1093
|
+
["rarr", 8594],
|
|
1094
|
+
["hearts", 9829]
|
|
1095
|
+
]);
|
|
1096
|
+
function decodeEntities(text) {
|
|
1097
|
+
return text.replace(
|
|
1098
|
+
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1099
|
+
(whole, decimal, hex, name) => {
|
|
1100
|
+
if (name !== void 0) {
|
|
1101
|
+
const code2 = NAMED_ENTITIES.get(name);
|
|
1102
|
+
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1103
|
+
}
|
|
1104
|
+
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1105
|
+
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1106
|
+
return String.fromCodePoint(valid ? code : 65533);
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const INVISIBLE = new RegExp(
|
|
1111
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}${String.fromCharCode(8203)}-${String.fromCharCode(8207)}${String.fromCharCode(8232)}-${String.fromCharCode(8238)}${String.fromCharCode(8288)}-${String.fromCharCode(8292)}${String.fromCharCode(8294)}-${String.fromCharCode(8303)}${String.fromCharCode(65279)}]`,
|
|
1112
|
+
"g"
|
|
1276
1113
|
);
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1114
|
+
function clean(raw) {
|
|
1115
|
+
if (raw === null || raw === void 0) return null;
|
|
1116
|
+
if (raw.includes("<") || raw.includes(">")) return null;
|
|
1117
|
+
const collapsed = raw.replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
|
|
1118
|
+
const name = trimSuffix(collapsed);
|
|
1119
|
+
if (name.length === 0) return null;
|
|
1120
|
+
if (PLACEHOLDERS.has(name.toLowerCase())) return null;
|
|
1121
|
+
return name.length > MAX_NAME_LENGTH ? name.slice(0, MAX_NAME_LENGTH).trimEnd() : name;
|
|
1281
1122
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1123
|
+
const PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
1124
|
+
"document",
|
|
1125
|
+
"untitled",
|
|
1126
|
+
"untitled document",
|
|
1127
|
+
"index",
|
|
1128
|
+
"home",
|
|
1129
|
+
"new page",
|
|
1130
|
+
"my site",
|
|
1131
|
+
"vite app",
|
|
1132
|
+
"vite + react",
|
|
1133
|
+
"react app",
|
|
1134
|
+
"create next app",
|
|
1135
|
+
"svelte app",
|
|
1136
|
+
"webpack app",
|
|
1137
|
+
"hello world",
|
|
1138
|
+
"title"
|
|
1139
|
+
]);
|
|
1140
|
+
function trimSuffix(title) {
|
|
1141
|
+
for (const separator of TITLE_SEPARATORS) {
|
|
1142
|
+
const at = title.lastIndexOf(separator);
|
|
1143
|
+
if (at <= 0) continue;
|
|
1144
|
+
const before = title.slice(0, at).trim();
|
|
1145
|
+
const after = title.slice(at + separator.length).trim();
|
|
1146
|
+
if (before.length > 0 && after.length > 0) return after;
|
|
1147
|
+
}
|
|
1148
|
+
return title;
|
|
1285
1149
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1150
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
1151
|
+
const MAX_BATCH_CONCURRENCY = 64;
|
|
1152
|
+
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1153
|
+
const DEFAULT_RETRIES = 3;
|
|
1154
|
+
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1155
|
+
const DEFAULT_STALL_MS = 3e4;
|
|
1156
|
+
const PROGRESS_INTERVAL_MS = 200;
|
|
1157
|
+
class StalledError extends Error {
|
|
1158
|
+
/** @param seconds How long nothing moved. */
|
|
1159
|
+
constructor(seconds) {
|
|
1160
|
+
super(`No data moved for ${seconds}s.`);
|
|
1161
|
+
this.name = "StalledError";
|
|
1291
1162
|
}
|
|
1292
|
-
return hashed;
|
|
1293
1163
|
}
|
|
1294
|
-
async function
|
|
1164
|
+
async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {}, onRetry) {
|
|
1165
|
+
const put = deps.put ?? defaultTransport(deps.fetch);
|
|
1166
|
+
const delay = deps.delay ?? defaultDelay;
|
|
1167
|
+
const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1168
|
+
const batchConcurrency = Math.max(
|
|
1169
|
+
1,
|
|
1170
|
+
Math.min(MAX_BATCH_CONCURRENCY, Math.floor(deps.batchConcurrency ?? concurrency))
|
|
1171
|
+
);
|
|
1172
|
+
const retries = deps.retries ?? DEFAULT_RETRIES;
|
|
1173
|
+
const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
|
|
1174
|
+
const now = deps.now ?? Date.now;
|
|
1175
|
+
const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
|
|
1176
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1177
|
+
const fileCount = targets.reduce(
|
|
1178
|
+
(sum, target) => sum + (isBatch(target) ? target.files.length : 1),
|
|
1179
|
+
0
|
|
1180
|
+
);
|
|
1181
|
+
let nextIndex = 0;
|
|
1182
|
+
let done = 0;
|
|
1183
|
+
let failed = false;
|
|
1184
|
+
let firstFailure;
|
|
1185
|
+
const local = new AbortController();
|
|
1186
|
+
const inner = local.signal;
|
|
1187
|
+
const fail = (error) => {
|
|
1188
|
+
if (failed) return;
|
|
1189
|
+
failed = true;
|
|
1190
|
+
firstFailure = error;
|
|
1191
|
+
local.abort();
|
|
1192
|
+
};
|
|
1193
|
+
let reserved = 0;
|
|
1194
|
+
let waiting = [];
|
|
1195
|
+
const acquire = async (size) => {
|
|
1196
|
+
while (reserved > 0 && reserved + size > maxBytesInFlight) {
|
|
1197
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
1198
|
+
}
|
|
1199
|
+
reserved += size;
|
|
1200
|
+
};
|
|
1201
|
+
const release = (size) => {
|
|
1202
|
+
reserved -= size;
|
|
1203
|
+
const woken = waiting;
|
|
1204
|
+
waiting = [];
|
|
1205
|
+
for (const wake of woken) wake();
|
|
1206
|
+
};
|
|
1207
|
+
const running = { single: 0, batch: 0 };
|
|
1208
|
+
const limit = { single: concurrency, batch: batchConcurrency };
|
|
1209
|
+
let slotWaiting = [];
|
|
1210
|
+
const takeSlot = async (kind) => {
|
|
1211
|
+
while (running[kind] >= limit[kind]) {
|
|
1212
|
+
await new Promise((resolve2) => slotWaiting.push(resolve2));
|
|
1213
|
+
}
|
|
1214
|
+
running[kind] += 1;
|
|
1215
|
+
};
|
|
1216
|
+
const freeSlot = (kind) => {
|
|
1217
|
+
running[kind] -= 1;
|
|
1218
|
+
const woken = slotWaiting;
|
|
1219
|
+
slotWaiting = [];
|
|
1220
|
+
for (const wake of woken) wake();
|
|
1221
|
+
};
|
|
1222
|
+
let landedBytes = 0;
|
|
1223
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1224
|
+
let lastReport = Number.NEGATIVE_INFINITY;
|
|
1225
|
+
const report = (path) => {
|
|
1226
|
+
if (onProgress === void 0 || failed) return;
|
|
1227
|
+
const at = now();
|
|
1228
|
+
if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
|
|
1229
|
+
lastReport = at;
|
|
1230
|
+
let bytes = landedBytes;
|
|
1231
|
+
for (const sent of inFlight.values()) bytes += sent;
|
|
1232
|
+
onProgress(done, fileCount, bytes, path);
|
|
1233
|
+
};
|
|
1234
|
+
const worker = async () => {
|
|
1235
|
+
for (; ; ) {
|
|
1236
|
+
if (failed) return;
|
|
1237
|
+
const index = nextIndex++;
|
|
1238
|
+
const target = targets[index];
|
|
1239
|
+
if (target === void 0) return;
|
|
1240
|
+
inner.throwIfAborted();
|
|
1241
|
+
const refs = isBatch(target) ? target.files : [target];
|
|
1242
|
+
const members = [];
|
|
1243
|
+
for (const ref of refs) {
|
|
1244
|
+
const file = byPath.get(ref.path);
|
|
1245
|
+
if (file === void 0) {
|
|
1246
|
+
const missing = new DeployError(
|
|
1247
|
+
ClientErrorCode.UploadFailed,
|
|
1248
|
+
`The server asked for ${ref.path}, which is not in the manifest.`,
|
|
1249
|
+
{ path: ref.path }
|
|
1250
|
+
);
|
|
1251
|
+
fail(missing);
|
|
1252
|
+
throw missing;
|
|
1253
|
+
}
|
|
1254
|
+
members.push(file);
|
|
1255
|
+
}
|
|
1256
|
+
const layout = isBatch(target) ? new TextEncoder().encode(
|
|
1257
|
+
batchLayout(
|
|
1258
|
+
target.files.map((ref, i) => ({
|
|
1259
|
+
sha256: ref.sha256,
|
|
1260
|
+
size: sizeOf(members[i].bytes)
|
|
1261
|
+
}))
|
|
1262
|
+
)
|
|
1263
|
+
) : null;
|
|
1264
|
+
const size = members.reduce((sum, file) => sum + sizeOf(file.bytes), layout?.length ?? 0);
|
|
1265
|
+
const copied = isBatch(target) ? members.reduce(
|
|
1266
|
+
(sum, file) => sum + (file.bytes instanceof Blob ? 0 : sizeOf(file.bytes)),
|
|
1267
|
+
0
|
|
1268
|
+
) : 0;
|
|
1269
|
+
const reservation = size + copied;
|
|
1270
|
+
const first = members[0].path;
|
|
1271
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1272
|
+
const kind = isBatch(target) ? "batch" : "single";
|
|
1273
|
+
await takeSlot(kind);
|
|
1274
|
+
await acquire(reservation);
|
|
1275
|
+
try {
|
|
1276
|
+
if (failed) return;
|
|
1277
|
+
inner.throwIfAborted();
|
|
1278
|
+
const context = {
|
|
1279
|
+
put,
|
|
1280
|
+
delay,
|
|
1281
|
+
retries,
|
|
1282
|
+
signal: inner,
|
|
1283
|
+
uploadUrl,
|
|
1284
|
+
stallMs,
|
|
1285
|
+
onSent: (sent) => {
|
|
1286
|
+
inFlight.set(index, sent);
|
|
1287
|
+
report();
|
|
1288
|
+
},
|
|
1289
|
+
onRetry: (attempt, stalled) => {
|
|
1290
|
+
inFlight.set(index, 0);
|
|
1291
|
+
onRetry?.(label, attempt, retries + 1, stalled);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
if (isBatch(target) && layout !== null)
|
|
1295
|
+
await uploadBatchOne(target, members, layout, context);
|
|
1296
|
+
else await uploadOne(target, members[0], context);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
fail(error);
|
|
1299
|
+
throw error;
|
|
1300
|
+
} finally {
|
|
1301
|
+
release(reservation);
|
|
1302
|
+
freeSlot(kind);
|
|
1303
|
+
}
|
|
1304
|
+
inFlight.delete(index);
|
|
1305
|
+
done += members.length;
|
|
1306
|
+
landedBytes += size;
|
|
1307
|
+
report(members.at(-1).path);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
const workers = Array.from(
|
|
1311
|
+
{ length: Math.min(Math.max(concurrency, batchConcurrency), targets.length) },
|
|
1312
|
+
worker
|
|
1313
|
+
);
|
|
1295
1314
|
try {
|
|
1296
|
-
|
|
1315
|
+
await Promise.all(workers);
|
|
1297
1316
|
} catch (error) {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
ClientErrorCode.ReadFailed,
|
|
1301
|
-
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
1302
|
-
{ path: file.path, cause: describeError(error) }
|
|
1303
|
-
);
|
|
1317
|
+
throw asDeployError(firstFailure ?? error);
|
|
1318
|
+
} finally {
|
|
1304
1319
|
}
|
|
1305
1320
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1321
|
+
async function uploadOne(target, file, context) {
|
|
1322
|
+
let lastError;
|
|
1323
|
+
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1324
|
+
context.signal?.throwIfAborted();
|
|
1325
|
+
if (attempt > 0) {
|
|
1326
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1327
|
+
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1328
|
+
context.signal?.throwIfAborted();
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
const answer = await context.put({
|
|
1332
|
+
url: context.uploadUrl,
|
|
1333
|
+
token: target.token,
|
|
1334
|
+
body: untyped(file.bytes),
|
|
1335
|
+
signal: context.signal,
|
|
1336
|
+
onSent: context.onSent,
|
|
1337
|
+
stallMs: context.stallMs
|
|
1338
|
+
});
|
|
1339
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1340
|
+
if (status >= 200 && status < 300) return;
|
|
1341
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
1342
|
+
throw new DeployError(
|
|
1343
|
+
ClientErrorCode.UploadFailed,
|
|
1344
|
+
`Upload of ${target.path} was refused. The upload window may have expired — try deploying again.`,
|
|
1345
|
+
{ path: target.path, status }
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
if (status === 413) {
|
|
1349
|
+
throw new DeployError(
|
|
1350
|
+
ClientErrorCode.UploadFailed,
|
|
1351
|
+
`${target.path} is too large to upload in one request. Make it smaller, or leave it out of this publish.`,
|
|
1352
|
+
{ path: target.path, status }
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
if (error instanceof DeployError) throw error;
|
|
1358
|
+
if (isAbort(error)) throw cancelled();
|
|
1359
|
+
lastError = error;
|
|
1360
|
+
}
|
|
1345
1361
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (isImageFolder(paths)) return true;
|
|
1352
|
-
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
1353
|
-
}
|
|
1354
|
-
function servingModeOf(flags) {
|
|
1355
|
-
if (flags.docsMode) return "docs";
|
|
1356
|
-
if (flags.spaMode) return "spa";
|
|
1357
|
-
return "static";
|
|
1362
|
+
const message = lastError instanceof StalledError ? `Upload of ${target.path} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${target.path} after ${context.retries + 1} attempts.`;
|
|
1363
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1364
|
+
path: target.path,
|
|
1365
|
+
cause: describeError(lastError)
|
|
1366
|
+
});
|
|
1358
1367
|
}
|
|
1359
|
-
function
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1368
|
+
async function uploadBatchOne(batch, members, layout, context) {
|
|
1369
|
+
const first = members[0].path;
|
|
1370
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1371
|
+
let lastError;
|
|
1372
|
+
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1373
|
+
context.signal?.throwIfAborted();
|
|
1374
|
+
if (attempt > 0) {
|
|
1375
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1376
|
+
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1377
|
+
context.signal?.throwIfAborted();
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const body = new Blob([layout, ...members.map((file) => file.bytes)]);
|
|
1381
|
+
const answer = await context.put({
|
|
1382
|
+
url: context.uploadUrl,
|
|
1383
|
+
token: batch.token,
|
|
1384
|
+
body,
|
|
1385
|
+
signal: context.signal,
|
|
1386
|
+
onSent: context.onSent,
|
|
1387
|
+
stallMs: context.stallMs,
|
|
1388
|
+
wantsBody: true
|
|
1389
|
+
});
|
|
1390
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1391
|
+
if (status >= 200 && status < 300) {
|
|
1392
|
+
const result = typeof answer === "number" ? null : parseBatchResult(answer.body);
|
|
1393
|
+
if (result === null) {
|
|
1394
|
+
lastError = new Error(
|
|
1395
|
+
"The upload service answered with something that is not a batch result."
|
|
1396
|
+
);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (result.failed.length === 0) return;
|
|
1400
|
+
const changed = result.failed.find((failure2) => failure2.reason === "hash_mismatch");
|
|
1401
|
+
if (changed !== void 0) {
|
|
1402
|
+
const path = members[changed.i]?.path ?? first;
|
|
1403
|
+
throw new DeployError(
|
|
1404
|
+
ClientErrorCode.ReadFailed,
|
|
1405
|
+
`${path} changed after it was read, so it no longer matches what was checked. Drop the folder again.`,
|
|
1406
|
+
{ path }
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
lastError = new Error(
|
|
1410
|
+
`${result.failed.length} of ${members.length} files were not stored.`
|
|
1411
|
+
);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if ([400, 401, 403, 411, 413, 429].includes(status)) {
|
|
1415
|
+
throw new DeployError(ClientErrorCode.UploadFailed, refusalMessage(status, label), {
|
|
1416
|
+
path: first,
|
|
1417
|
+
status
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (error instanceof DeployError) throw error;
|
|
1423
|
+
if (isAbort(error)) throw cancelled();
|
|
1424
|
+
lastError = error;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const message = lastError instanceof StalledError ? `Upload of ${label} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${label} after ${context.retries + 1} attempts.`;
|
|
1428
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1429
|
+
path: first,
|
|
1430
|
+
cause: describeError(lastError)
|
|
1431
|
+
});
|
|
1365
1432
|
}
|
|
1366
|
-
function
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1433
|
+
function refusalMessage(status, label) {
|
|
1434
|
+
switch (status) {
|
|
1435
|
+
case 401:
|
|
1436
|
+
return `Upload of ${label} was refused because its upload window expired. Try deploying again.`;
|
|
1437
|
+
case 429:
|
|
1438
|
+
return `Upload of ${label} was refused: this deploy has used every upload it was given. Try deploying again.`;
|
|
1439
|
+
case 400:
|
|
1440
|
+
case 411:
|
|
1441
|
+
case 413:
|
|
1442
|
+
return `Upload of ${label} was refused because it no longer matches what was checked — a file may have changed since you dropped it. Drop the folder again.`;
|
|
1443
|
+
default:
|
|
1444
|
+
return `Upload of ${label} was refused by the upload service. Try deploying again.`;
|
|
1445
|
+
}
|
|
1371
1446
|
}
|
|
1372
|
-
function
|
|
1373
|
-
if (
|
|
1374
|
-
|
|
1375
|
-
if (only === void 0 || only === REQUIRED_INDEX) return null;
|
|
1376
|
-
const lower = only.toLowerCase();
|
|
1377
|
-
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
1447
|
+
function untyped(content) {
|
|
1448
|
+
if (ArrayBuffer.isView(content) || content.type === "") return content;
|
|
1449
|
+
return content.slice(0, content.size);
|
|
1378
1450
|
}
|
|
1379
|
-
function
|
|
1380
|
-
if (
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1451
|
+
function defaultTransport(doFetch) {
|
|
1452
|
+
if (doFetch !== void 0) return fetchTransport(doFetch);
|
|
1453
|
+
const Xhr = globalThis.XMLHttpRequest;
|
|
1454
|
+
return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
|
|
1384
1455
|
}
|
|
1385
|
-
function
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1456
|
+
function fetchTransport(doFetch) {
|
|
1457
|
+
return async (request2) => {
|
|
1458
|
+
const { url, token, body, signal } = request2;
|
|
1459
|
+
const response = await doFetch(url, {
|
|
1460
|
+
method: "PUT",
|
|
1461
|
+
// The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
|
|
1462
|
+
// full-size copies of every file in flight — eight at once, so up to 400 MB on a drop of
|
|
1463
|
+
// 50 MB files. `fetch` sends exactly the range a view covers. The cast is TypeScript's
|
|
1464
|
+
// SharedArrayBuffer distinction again; see `sha256Hex`. A `Blob` goes as it is, and the runtime
|
|
1465
|
+
// streams it from disk.
|
|
1466
|
+
body,
|
|
1467
|
+
// The permit is the whole request. `Content-Length` is required by the Worker and cannot be
|
|
1468
|
+
// set here — it is a forbidden header name — but the runtime derives it from the body, which
|
|
1469
|
+
// is exactly the length the token names.
|
|
1470
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1471
|
+
...signal ? { signal } : {}
|
|
1472
|
+
});
|
|
1473
|
+
return request2.wantsBody === true ? { status: response.status, body: await response.text() } : response.status;
|
|
1474
|
+
};
|
|
1403
1475
|
}
|
|
1404
|
-
function
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
{
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
)
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1476
|
+
function xhrTransport(Xhr) {
|
|
1477
|
+
return ({ url, token, body, signal, onSent, stallMs, wantsBody }) => new Promise((resolve2, reject) => {
|
|
1478
|
+
const xhr = new Xhr();
|
|
1479
|
+
let settled = false;
|
|
1480
|
+
let timer;
|
|
1481
|
+
const settle = (finish) => {
|
|
1482
|
+
if (settled) return;
|
|
1483
|
+
settled = true;
|
|
1484
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1485
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1486
|
+
finish();
|
|
1487
|
+
};
|
|
1488
|
+
const arm = () => {
|
|
1489
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1490
|
+
timer = setTimeout(() => {
|
|
1491
|
+
xhr.abort();
|
|
1492
|
+
settle(() => reject(new StalledError(Math.round(stallMs / 1e3))));
|
|
1493
|
+
}, stallMs);
|
|
1494
|
+
};
|
|
1495
|
+
function onAbort() {
|
|
1496
|
+
xhr.abort();
|
|
1497
|
+
settle(() => reject(abortReason(signal)));
|
|
1498
|
+
}
|
|
1499
|
+
if (signal?.aborted) {
|
|
1500
|
+
reject(abortReason(signal));
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1504
|
+
xhr.open("PUT", url);
|
|
1505
|
+
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
|
|
1506
|
+
xhr.upload.onprogress = (event) => {
|
|
1507
|
+
arm();
|
|
1508
|
+
onSent(event.loaded);
|
|
1509
|
+
};
|
|
1510
|
+
xhr.onprogress = arm;
|
|
1511
|
+
xhr.onload = () => settle(
|
|
1512
|
+
() => resolve2(
|
|
1513
|
+
wantsBody === true ? { status: xhr.status, body: xhr.responseText ?? "" } : xhr.status
|
|
1514
|
+
)
|
|
1431
1515
|
);
|
|
1432
|
-
|
|
1433
|
-
|
|
1516
|
+
xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
|
|
1517
|
+
arm();
|
|
1518
|
+
xhr.send(body);
|
|
1519
|
+
});
|
|
1434
1520
|
}
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
try {
|
|
1442
|
-
return nameFromHtml(decodeHead(index.bytes));
|
|
1443
|
-
} catch {
|
|
1444
|
-
return null;
|
|
1445
|
-
}
|
|
1521
|
+
function abortReason(signal) {
|
|
1522
|
+
const reason = signal?.reason;
|
|
1523
|
+
if (reason instanceof Error) return reason;
|
|
1524
|
+
const error = new Error("Aborted.");
|
|
1525
|
+
error.name = "AbortError";
|
|
1526
|
+
return error;
|
|
1446
1527
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
if (index === void 0) return null;
|
|
1450
|
-
if (ArrayBuffer.isView(index.bytes)) return suggestSiteName(files);
|
|
1451
|
-
try {
|
|
1452
|
-
const head = new Uint8Array(await index.bytes.slice(0, HEAD_BYTES).arrayBuffer());
|
|
1453
|
-
return suggestSiteName([{ path: index.path, bytes: head }]);
|
|
1454
|
-
} catch {
|
|
1455
|
-
return null;
|
|
1456
|
-
}
|
|
1528
|
+
function globalFetch() {
|
|
1529
|
+
return globalThis.fetch.bind(globalThis);
|
|
1457
1530
|
}
|
|
1458
|
-
function
|
|
1459
|
-
return new
|
|
1460
|
-
|
|
1531
|
+
function defaultDelay(ms) {
|
|
1532
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1533
|
+
}
|
|
1534
|
+
function isAbort(error) {
|
|
1535
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
1536
|
+
}
|
|
1537
|
+
function cancelled() {
|
|
1538
|
+
return new DeployError(ClientErrorCode.Cancelled, "Deploy cancelled.");
|
|
1539
|
+
}
|
|
1540
|
+
function outOfMemory(detail) {
|
|
1541
|
+
return new DeployError(
|
|
1542
|
+
ClientErrorCode.OutOfMemory,
|
|
1543
|
+
"This browser ran out of memory reading what you dropped. Drop the unzipped folder instead, or try again in a desktop browser with fewer tabs open.",
|
|
1544
|
+
detail
|
|
1461
1545
|
);
|
|
1462
1546
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
if (name !== null) return name;
|
|
1473
|
-
}
|
|
1474
|
-
return null;
|
|
1547
|
+
function asDeployError(error) {
|
|
1548
|
+
if (error instanceof DeployError) return error;
|
|
1549
|
+
if (isAbort(error)) return cancelled();
|
|
1550
|
+
if (error instanceof RangeError) return outOfMemory(error.message);
|
|
1551
|
+
return new DeployError(
|
|
1552
|
+
ClientErrorCode.UploadFailed,
|
|
1553
|
+
error instanceof Error ? error.message : "Upload failed.",
|
|
1554
|
+
error
|
|
1555
|
+
);
|
|
1475
1556
|
}
|
|
1476
|
-
function
|
|
1477
|
-
const parser = globalThis.DOMParser;
|
|
1478
|
-
if (parser === void 0) return null;
|
|
1557
|
+
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
1479
1558
|
try {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1559
|
+
onProgress({ type: "reading", bytes: 0, total: 0 });
|
|
1560
|
+
const collected = await source(
|
|
1561
|
+
signal,
|
|
1562
|
+
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
1563
|
+
);
|
|
1564
|
+
onProgress({ type: "collecting", files: collected.length });
|
|
1565
|
+
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
1566
|
+
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
1567
|
+
const hashed = await hashAll(
|
|
1568
|
+
files,
|
|
1569
|
+
(done, total) => onProgress({ type: "hashing", done, total }),
|
|
1570
|
+
signal
|
|
1571
|
+
);
|
|
1572
|
+
checkLimits(hashed, options.limits ?? null);
|
|
1573
|
+
onProgress({ type: "preparing" });
|
|
1574
|
+
const prepared = await prepareDeploy(siteId, hashed, options, signal);
|
|
1575
|
+
if (prepared.unchanged) {
|
|
1576
|
+
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1580
|
+
const batches = prepared.batches ?? [];
|
|
1581
|
+
const targets = [...prepared.upload, ...batches];
|
|
1582
|
+
const totalBytes = prepared.upload.reduce((sum, target) => sum + (sizes.get(target.path) ?? 0), 0) + batches.reduce((sum, batch) => {
|
|
1583
|
+
const members = batch.files.map((ref) => ({
|
|
1584
|
+
sha256: ref.sha256,
|
|
1585
|
+
size: sizes.get(ref.path) ?? 0
|
|
1586
|
+
}));
|
|
1587
|
+
return sum + batchLayout(members).length + members.reduce((n, member) => n + member.size, 0);
|
|
1588
|
+
}, 0);
|
|
1589
|
+
if (targets.length > 0) {
|
|
1590
|
+
onProgress({
|
|
1591
|
+
type: "uploading",
|
|
1592
|
+
done: prepared.reused,
|
|
1593
|
+
total: prepared.total,
|
|
1594
|
+
bytes: 0,
|
|
1595
|
+
totalBytes,
|
|
1596
|
+
reused: prepared.reused
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
await uploadAll(
|
|
1600
|
+
targets,
|
|
1601
|
+
prepared.uploadUrl,
|
|
1602
|
+
hashed,
|
|
1603
|
+
(done, _total, bytes, path) => onProgress({
|
|
1604
|
+
type: "uploading",
|
|
1605
|
+
done: prepared.reused + done,
|
|
1606
|
+
total: prepared.total,
|
|
1607
|
+
bytes,
|
|
1608
|
+
totalBytes,
|
|
1609
|
+
reused: prepared.reused,
|
|
1610
|
+
...path === void 0 ? {} : { path }
|
|
1611
|
+
}),
|
|
1612
|
+
signal,
|
|
1613
|
+
// The server's number unless the caller set one: it is tuned on the server so a deploy of many
|
|
1614
|
+
// small files speeds up without a CLI or MCP release.
|
|
1615
|
+
typeof prepared.batchConcurrency === "number" && options.upload?.batchConcurrency === void 0 ? { ...options.upload, batchConcurrency: prepared.batchConcurrency } : options.upload,
|
|
1616
|
+
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1617
|
+
);
|
|
1618
|
+
onProgress({ type: "completing", reused: prepared.reused });
|
|
1619
|
+
const completed = await completeDeploy(
|
|
1620
|
+
siteId,
|
|
1621
|
+
prepared.deployId,
|
|
1622
|
+
options,
|
|
1623
|
+
signal,
|
|
1624
|
+
await readSiteName(files)
|
|
1625
|
+
);
|
|
1626
|
+
onProgress({
|
|
1627
|
+
type: "done",
|
|
1628
|
+
url: completed.url,
|
|
1629
|
+
name: completed.name,
|
|
1630
|
+
mode: servingModeOf$1(completed),
|
|
1631
|
+
modeIsManual: completed.modeIsManual
|
|
1632
|
+
});
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
const failure2 = asDeployError(error);
|
|
1635
|
+
onProgress({
|
|
1636
|
+
type: "error",
|
|
1637
|
+
code: failure2.code,
|
|
1638
|
+
message: failure2.message,
|
|
1639
|
+
...failure2.detail === void 0 ? {} : { detail: failure2.detail }
|
|
1640
|
+
});
|
|
1641
|
+
throw failure2;
|
|
1483
1642
|
}
|
|
1484
1643
|
}
|
|
1485
|
-
function
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1644
|
+
async function call(credentials, path, init2 = {}) {
|
|
1645
|
+
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
1646
|
+
...init2,
|
|
1647
|
+
headers: {
|
|
1648
|
+
...credentials.headers,
|
|
1649
|
+
...init2.headers,
|
|
1650
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
1651
|
+
...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
if (!response.ok) throw await failureOf(response);
|
|
1655
|
+
return await response.json();
|
|
1490
1656
|
}
|
|
1491
|
-
function
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1657
|
+
async function failureOf(response) {
|
|
1658
|
+
const problem = await response.json().catch(() => null);
|
|
1659
|
+
return Object.assign(new Error(problem?.detail ?? `The API answered ${response.status}.`), {
|
|
1660
|
+
status: response.status
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
async function findSite(credentials, site) {
|
|
1664
|
+
const found = await lookUpSite(credentials, site);
|
|
1665
|
+
if (found === void 0) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`No site of yours is called "${site}". \`drop2run ls\` or the list_sites tool shows what exists.`
|
|
1668
|
+
);
|
|
1499
1669
|
}
|
|
1500
|
-
return
|
|
1670
|
+
return found;
|
|
1501
1671
|
}
|
|
1502
|
-
const
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
["hellip", 8230],
|
|
1525
|
-
["prime", 8242],
|
|
1526
|
-
["lsaquo", 8249],
|
|
1527
|
-
["rsaquo", 8250],
|
|
1528
|
-
["euro", 8364],
|
|
1529
|
-
["trade", 8482],
|
|
1530
|
-
["larr", 8592],
|
|
1531
|
-
["rarr", 8594],
|
|
1532
|
-
["hearts", 9829]
|
|
1533
|
-
]);
|
|
1534
|
-
function decodeEntities(text) {
|
|
1535
|
-
return text.replace(
|
|
1536
|
-
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1537
|
-
(whole, decimal, hex, name) => {
|
|
1538
|
-
if (name !== void 0) {
|
|
1539
|
-
const code2 = NAMED_ENTITIES.get(name);
|
|
1540
|
-
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1541
|
-
}
|
|
1542
|
-
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1543
|
-
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1544
|
-
return String.fromCodePoint(valid ? code : 65533);
|
|
1545
|
-
}
|
|
1672
|
+
const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
|
|
1673
|
+
const FORMER_NAME_CANDIDATES = 3;
|
|
1674
|
+
async function lookUpSite(credentials, site) {
|
|
1675
|
+
const wanted = site.trim().toLowerCase();
|
|
1676
|
+
if (wanted === "") return void 0;
|
|
1677
|
+
const { sites } = await call(
|
|
1678
|
+
credentials,
|
|
1679
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(wanted)}`
|
|
1680
|
+
);
|
|
1681
|
+
const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
|
|
1682
|
+
if (bySubdomain !== void 0) return bySubdomain;
|
|
1683
|
+
for (const candidate of sites.slice(0, FORMER_NAME_CANDIDATES)) {
|
|
1684
|
+
const detail2 = await call(
|
|
1685
|
+
credentials,
|
|
1686
|
+
`sites/${encodeURIComponent(candidate.siteId)}`
|
|
1687
|
+
);
|
|
1688
|
+
if (detail2.formerSubdomains?.some((former) => former.subdomain === wanted)) return candidate;
|
|
1689
|
+
}
|
|
1690
|
+
if (!SITE_ID_SHAPE.test(wanted)) return void 0;
|
|
1691
|
+
const response = await fetch(
|
|
1692
|
+
`${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
|
|
1693
|
+
{ headers: { ...credentials.headers, Authorization: `Bearer ${credentials.token}` } }
|
|
1546
1694
|
);
|
|
1695
|
+
if (response.status === 404) return void 0;
|
|
1696
|
+
if (!response.ok) throw await failureOf(response);
|
|
1697
|
+
const detail = await response.json();
|
|
1698
|
+
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
1547
1699
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (
|
|
1554
|
-
if (raw.includes("<") || raw.includes(">")) return null;
|
|
1555
|
-
const collapsed = raw.replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
|
|
1556
|
-
const name = trimSuffix(collapsed);
|
|
1557
|
-
if (name.length === 0) return null;
|
|
1558
|
-
if (PLACEHOLDERS.has(name.toLowerCase())) return null;
|
|
1559
|
-
return name.length > MAX_NAME_LENGTH ? name.slice(0, MAX_NAME_LENGTH).trimEnd() : name;
|
|
1700
|
+
async function deleteSite(credentials, siteId) {
|
|
1701
|
+
const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
|
|
1702
|
+
method: "DELETE",
|
|
1703
|
+
headers: { ...credentials.headers, Authorization: `Bearer ${credentials.token}` }
|
|
1704
|
+
});
|
|
1705
|
+
if (!response.ok) throw await failureOf(response);
|
|
1560
1706
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
"new page",
|
|
1568
|
-
"my site",
|
|
1569
|
-
"vite app",
|
|
1570
|
-
"vite + react",
|
|
1571
|
-
"react app",
|
|
1572
|
-
"create next app",
|
|
1573
|
-
"svelte app",
|
|
1574
|
-
"webpack app",
|
|
1575
|
-
"hello world",
|
|
1576
|
-
"title"
|
|
1577
|
-
]);
|
|
1578
|
-
function trimSuffix(title) {
|
|
1579
|
-
for (const separator of TITLE_SEPARATORS) {
|
|
1580
|
-
const at = title.lastIndexOf(separator);
|
|
1581
|
-
if (at <= 0) continue;
|
|
1582
|
-
const before = title.slice(0, at).trim();
|
|
1583
|
-
const after = title.slice(at + separator.length).trim();
|
|
1584
|
-
if (before.length > 0 && after.length > 0) return after;
|
|
1585
|
-
}
|
|
1586
|
-
return title;
|
|
1707
|
+
async function promoteDeploy(credentials, siteId, deployId) {
|
|
1708
|
+
return await call(
|
|
1709
|
+
credentials,
|
|
1710
|
+
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
1711
|
+
{ method: "POST" }
|
|
1712
|
+
);
|
|
1587
1713
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1714
|
+
function enumName(value) {
|
|
1715
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1716
|
+
}
|
|
1717
|
+
function actionOf(expiresAt, action) {
|
|
1718
|
+
return expiresAt === null ? null : enumName(action);
|
|
1719
|
+
}
|
|
1720
|
+
async function readInvitedOnly(credentials, siteId) {
|
|
1721
|
+
try {
|
|
1722
|
+
const page = await call(
|
|
1723
|
+
credentials,
|
|
1724
|
+
`sites/${encodeURIComponent(siteId)}/viewers?pageSize=1`
|
|
1725
|
+
);
|
|
1726
|
+
return typeof page.invitedOnly === "boolean" ? page.invitedOnly : null;
|
|
1727
|
+
} catch {
|
|
1728
|
+
return null;
|
|
1599
1729
|
}
|
|
1600
1730
|
}
|
|
1601
|
-
async function
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1731
|
+
async function folderFromListing(credentials, siteId, subdomain) {
|
|
1732
|
+
const { sites } = await call(
|
|
1733
|
+
credentials,
|
|
1734
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(subdomain)}`
|
|
1735
|
+
);
|
|
1736
|
+
return sites.find((candidate) => candidate.siteId === siteId)?.folderId ?? null;
|
|
1737
|
+
}
|
|
1738
|
+
async function getSite(credentials, siteId) {
|
|
1739
|
+
const [site, invitedOnly] = await Promise.all([
|
|
1740
|
+
call(credentials, `sites/${encodeURIComponent(siteId)}`),
|
|
1741
|
+
readInvitedOnly(credentials, siteId)
|
|
1742
|
+
]);
|
|
1743
|
+
const folderId = site.folderId !== void 0 ? site.folderId : await folderFromListing(credentials, site.siteId, site.subdomain);
|
|
1744
|
+
return {
|
|
1745
|
+
siteId: site.siteId,
|
|
1746
|
+
subdomain: site.subdomain,
|
|
1747
|
+
url: site.url,
|
|
1748
|
+
name: site.name,
|
|
1749
|
+
status: enumName(site.status),
|
|
1750
|
+
invitedOnly,
|
|
1751
|
+
liveDeployId: site.liveDeployId,
|
|
1752
|
+
spaMode: site.spaMode,
|
|
1753
|
+
docsMode: site.docsMode,
|
|
1754
|
+
modeIsManual: site.modeIsManual,
|
|
1755
|
+
passwordProtected: site.passwordProtected,
|
|
1756
|
+
passwordProtectionAvailable: site.passwordProtectionAvailable,
|
|
1757
|
+
formsEnabled: site.formsEnabled,
|
|
1758
|
+
formsAvailable: site.formsAvailable,
|
|
1759
|
+
expiresAt: site.expiresAt,
|
|
1760
|
+
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
1761
|
+
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
1762
|
+
comments: audienceOf(site.commentAudience),
|
|
1763
|
+
commentsAvailable: site.feedbackAvailable === true,
|
|
1764
|
+
folderId,
|
|
1765
|
+
deploys: site.deploys.map((deploy2) => ({
|
|
1766
|
+
deployId: deploy2.deployId,
|
|
1767
|
+
status: deploy2.status,
|
|
1768
|
+
fileCount: deploy2.fileCount,
|
|
1769
|
+
totalBytes: deploy2.totalBytes,
|
|
1770
|
+
createdAt: deploy2.createdAt,
|
|
1771
|
+
completedAt: deploy2.completedAt,
|
|
1772
|
+
filesKept: deploy2.filesKept
|
|
1773
|
+
})),
|
|
1774
|
+
subdomainChangesLeft: site.subdomainChangesLeft ?? null,
|
|
1775
|
+
formerSubdomains: (site.formerSubdomains ?? []).map((former) => former.subdomain)
|
|
1638
1776
|
};
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
throw new DeployError(
|
|
1649
|
-
ClientErrorCode.UploadFailed,
|
|
1650
|
-
`The server asked for ${target.path}, which is not in the manifest.`,
|
|
1651
|
-
{ path: target.path }
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
const size = sizeOf(file.bytes);
|
|
1655
|
-
await acquire(size);
|
|
1656
|
-
try {
|
|
1657
|
-
if (failed) return;
|
|
1658
|
-
signal?.throwIfAborted();
|
|
1659
|
-
await uploadOne(target, file, {
|
|
1660
|
-
put,
|
|
1661
|
-
delay,
|
|
1662
|
-
retries,
|
|
1663
|
-
signal,
|
|
1664
|
-
uploadUrl,
|
|
1665
|
-
stallMs,
|
|
1666
|
-
onSent: (sent) => {
|
|
1667
|
-
inFlight.set(index, sent);
|
|
1668
|
-
report();
|
|
1669
|
-
},
|
|
1670
|
-
onRetry: (attempt, stalled) => {
|
|
1671
|
-
inFlight.set(index, 0);
|
|
1672
|
-
onRetry?.(target.path, attempt, retries + 1, stalled);
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
} catch (error) {
|
|
1676
|
-
failed = true;
|
|
1677
|
-
throw error;
|
|
1678
|
-
} finally {
|
|
1679
|
-
release(size);
|
|
1680
|
-
}
|
|
1681
|
-
inFlight.delete(index);
|
|
1682
|
-
done += 1;
|
|
1683
|
-
landedBytes += size;
|
|
1684
|
-
report(file.path);
|
|
1685
|
-
}
|
|
1777
|
+
}
|
|
1778
|
+
async function changeSubdomain(credentials, siteId, subdomain) {
|
|
1779
|
+
const changed = await call(credentials, `sites/${encodeURIComponent(siteId)}/subdomain`, {
|
|
1780
|
+
method: "POST",
|
|
1781
|
+
body: JSON.stringify({ subdomain })
|
|
1782
|
+
});
|
|
1783
|
+
return {
|
|
1784
|
+
...changed,
|
|
1785
|
+
formerSubdomains: changed.formerSubdomains.map((former) => former.subdomain)
|
|
1686
1786
|
};
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1787
|
+
}
|
|
1788
|
+
async function updateSiteSettings(credentials, siteId, change) {
|
|
1789
|
+
const body = {};
|
|
1790
|
+
if (change.name !== void 0) body.name = change.name;
|
|
1791
|
+
if (change.mode !== void 0) {
|
|
1792
|
+
body.spaMode = change.mode === "spa";
|
|
1793
|
+
body.docsMode = change.mode === "docs";
|
|
1692
1794
|
}
|
|
1795
|
+
if (change.password !== void 0) body.password = change.password;
|
|
1796
|
+
if (change.expiresAt !== void 0) body.expiresAt = change.expiresAt;
|
|
1797
|
+
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
1798
|
+
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
1799
|
+
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
1800
|
+
if (change.invitedOnly !== void 0) body.invitedOnly = change.invitedOnly;
|
|
1801
|
+
if (change.comments !== void 0) body.commentAudience = API_AUDIENCE[change.comments];
|
|
1802
|
+
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
1803
|
+
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
1804
|
+
const { commentAudience, ...settings } = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
1805
|
+
method: "PATCH",
|
|
1806
|
+
body: JSON.stringify(body)
|
|
1807
|
+
});
|
|
1808
|
+
const invitedOnly = typeof settings.invitedOnly === "boolean" ? settings.invitedOnly : null;
|
|
1809
|
+
return {
|
|
1810
|
+
...settings,
|
|
1811
|
+
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
1812
|
+
invitedOnly,
|
|
1813
|
+
comments: audienceOf(commentAudience),
|
|
1814
|
+
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
const API_AUDIENCE = {
|
|
1818
|
+
off: "Off",
|
|
1819
|
+
invited: "Invited",
|
|
1820
|
+
anyone: "Anyone"
|
|
1821
|
+
};
|
|
1822
|
+
function audienceOf(value) {
|
|
1823
|
+
const lower = value?.toLowerCase();
|
|
1824
|
+
return lower === "off" || lower === "invited" || lower === "anyone" ? lower : null;
|
|
1825
|
+
}
|
|
1826
|
+
async function setSitePaused(credentials, siteId, action) {
|
|
1827
|
+
const state = await call(
|
|
1828
|
+
credentials,
|
|
1829
|
+
`sites/${encodeURIComponent(siteId)}/${action}`,
|
|
1830
|
+
{ method: "POST" }
|
|
1831
|
+
);
|
|
1832
|
+
return {
|
|
1833
|
+
siteId: state.siteId,
|
|
1834
|
+
subdomain: state.subdomain,
|
|
1835
|
+
status: enumName(state.status),
|
|
1836
|
+
ownerPaused: state.ownerPaused
|
|
1837
|
+
};
|
|
1693
1838
|
}
|
|
1694
|
-
async function
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1839
|
+
async function listTokens(credentials) {
|
|
1840
|
+
const body = await call(credentials, "tokens");
|
|
1841
|
+
return body.tokens;
|
|
1842
|
+
}
|
|
1843
|
+
const SITES_PER_REQUEST = 100;
|
|
1844
|
+
async function listSites(credentials) {
|
|
1845
|
+
const body = await call(
|
|
1846
|
+
credentials,
|
|
1847
|
+
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
1848
|
+
);
|
|
1849
|
+
return { sites: body.sites, total: body.total };
|
|
1850
|
+
}
|
|
1851
|
+
async function createSite(credentials, subdomain, folderId) {
|
|
1852
|
+
const created = await call(
|
|
1853
|
+
credentials,
|
|
1854
|
+
"sites",
|
|
1855
|
+
{
|
|
1856
|
+
method: "POST",
|
|
1857
|
+
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
1858
|
+
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
1859
|
+
body: JSON.stringify({
|
|
1860
|
+
...subdomain === void 0 ? {} : { subdomain },
|
|
1861
|
+
...folderId === void 0 ? {} : { folderId }
|
|
1862
|
+
})
|
|
1702
1863
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
ClientErrorCode.UploadFailed,
|
|
1716
|
-
`Upload of ${target.path} was refused. The upload window may have expired — try deploying again.`,
|
|
1717
|
-
{ path: target.path, status }
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
if (status === 413) {
|
|
1721
|
-
throw new DeployError(
|
|
1722
|
-
ClientErrorCode.UploadFailed,
|
|
1723
|
-
`${target.path} is too large to upload in one request. Make it smaller, or leave it out of this publish.`,
|
|
1724
|
-
{ path: target.path, status }
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
lastError = new Error(`HTTP ${status}`);
|
|
1728
|
-
} catch (error) {
|
|
1729
|
-
if (error instanceof DeployError) throw error;
|
|
1730
|
-
if (isAbort(error)) throw cancelled();
|
|
1731
|
-
lastError = error;
|
|
1864
|
+
);
|
|
1865
|
+
return { ...created, name: null };
|
|
1866
|
+
}
|
|
1867
|
+
async function listFolders(credentials) {
|
|
1868
|
+
const body = await call(credentials, "site-folders");
|
|
1869
|
+
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
1870
|
+
const pathOf = (folderId) => {
|
|
1871
|
+
const names = [];
|
|
1872
|
+
let cursor = byId.get(folderId);
|
|
1873
|
+
while (cursor !== void 0 && names.length <= byId.size) {
|
|
1874
|
+
names.unshift(cursor.name);
|
|
1875
|
+
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
1732
1876
|
}
|
|
1877
|
+
return names.join("/");
|
|
1878
|
+
};
|
|
1879
|
+
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
1880
|
+
}
|
|
1881
|
+
function comparablePath(path) {
|
|
1882
|
+
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
1883
|
+
}
|
|
1884
|
+
const FOLDERS_LISTED_ON_MISS = 20;
|
|
1885
|
+
async function resolveFolder(credentials, folder) {
|
|
1886
|
+
const wanted = comparablePath(folder);
|
|
1887
|
+
if (wanted === "") {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
1890
|
+
);
|
|
1733
1891
|
}
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1892
|
+
const folders2 = await listFolders(credentials);
|
|
1893
|
+
const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
1894
|
+
if (byId !== void 0) return byId.folderId;
|
|
1895
|
+
const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
1896
|
+
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
1897
|
+
if (matches.length > 1) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
if (folders2.length === 0) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
1908
|
+
const more = folders2.length - listed.length;
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
1911
|
+
);
|
|
1739
1912
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
return
|
|
1913
|
+
const ROOT_FOLDER = "root";
|
|
1914
|
+
async function resolveFolderTarget(credentials, folder) {
|
|
1915
|
+
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
1743
1916
|
}
|
|
1744
|
-
function
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1917
|
+
async function listComments(credentials, siteId, query = {}) {
|
|
1918
|
+
const search = new URLSearchParams();
|
|
1919
|
+
if (query.status !== void 0) search.set("status", query.status);
|
|
1920
|
+
if (query.path !== void 0) search.set("path", query.path);
|
|
1921
|
+
if (query.since !== void 0) search.set("since", String(query.since));
|
|
1922
|
+
if (query.page !== void 0) search.set("page", String(query.page));
|
|
1923
|
+
const encoded = search.toString();
|
|
1924
|
+
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
1925
|
+
return await call(
|
|
1926
|
+
credentials,
|
|
1927
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
1928
|
+
);
|
|
1748
1929
|
}
|
|
1749
|
-
function
|
|
1750
|
-
return
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1930
|
+
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
1931
|
+
return await call(
|
|
1932
|
+
credentials,
|
|
1933
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
1934
|
+
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
1938
|
+
return await call(
|
|
1939
|
+
credentials,
|
|
1940
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
1941
|
+
{ method: "POST" }
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
const PEOPLE_PER_REQUEST = 50;
|
|
1945
|
+
async function listPeople(credentials, siteId) {
|
|
1946
|
+
const page = await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers?pageSize=${PEOPLE_PER_REQUEST}`);
|
|
1947
|
+
return {
|
|
1948
|
+
invitedOnly: page.invitedOnly,
|
|
1949
|
+
total: page.total,
|
|
1950
|
+
limit: page.limit,
|
|
1951
|
+
people: page.viewers.map((row) => ({
|
|
1952
|
+
email: row.email,
|
|
1953
|
+
role: enumName(row.role),
|
|
1954
|
+
owner: row.owner,
|
|
1955
|
+
status: enumName(row.status)
|
|
1956
|
+
}))
|
|
1766
1957
|
};
|
|
1767
1958
|
}
|
|
1768
|
-
function
|
|
1769
|
-
return (
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1959
|
+
async function invitePeople(credentials, siteId, emails, role) {
|
|
1960
|
+
return await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers`, {
|
|
1961
|
+
method: "POST",
|
|
1962
|
+
body: JSON.stringify({ emails, role: role === "comment" ? "Comment" : "View", notify: true })
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
async function removePerson(credentials, siteId, email) {
|
|
1966
|
+
await call(
|
|
1967
|
+
credentials,
|
|
1968
|
+
`sites/${encodeURIComponent(siteId)}/viewers?email=${encodeURIComponent(email)}`,
|
|
1969
|
+
{ method: "DELETE" }
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
function describeMode(result) {
|
|
1973
|
+
if (result.mode === null) return null;
|
|
1974
|
+
if (result.nestedIndex !== null) {
|
|
1975
|
+
return `Served as documents: there is no index.html at the top, but ${result.nestedIndex}/index.html is there. To publish the website, publish the ${result.nestedIndex} folder instead.`;
|
|
1976
|
+
}
|
|
1977
|
+
if (result.mode === "docs") return "Served as documents, in the documents reader.";
|
|
1978
|
+
if (result.mode === "spa") return "Served as a single-page app.";
|
|
1979
|
+
return "Served as a static site.";
|
|
1980
|
+
}
|
|
1981
|
+
async function ownSiteAt(credentials, subdomain) {
|
|
1982
|
+
const found = await lookUpSite(credentials, subdomain);
|
|
1983
|
+
return found?.subdomain.toLowerCase() === subdomain.trim().toLowerCase() ? found : void 0;
|
|
1984
|
+
}
|
|
1985
|
+
async function resolveSite(credentials, site, newSite) {
|
|
1986
|
+
const { subdomain, folder } = newSite;
|
|
1987
|
+
if (site !== void 0 && subdomain !== void 0) {
|
|
1988
|
+
throw new Error(
|
|
1989
|
+
"Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
if (site !== void 0 && folder !== void 0) {
|
|
1993
|
+
throw new Error(
|
|
1994
|
+
"A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site with `drop2run set folder` or the `update_site` tool's `folder`."
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (site === void 0) {
|
|
1998
|
+
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
1999
|
+
const held = subdomain === void 0 ? void 0 : await ownSiteAt(credentials, subdomain);
|
|
2000
|
+
if (held !== void 0) {
|
|
2001
|
+
if (held.liveDeployId === null) return held;
|
|
2002
|
+
throw new Error(
|
|
2003
|
+
`You already have a site at ${held.subdomain}, and it is live. To replace what it serves, publish to it as an existing site (the \`site\` argument, or \`--site\` on the command line); to make a new site, choose another subdomain.`
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
return await createSite(credentials, subdomain, folderId);
|
|
2007
|
+
}
|
|
2008
|
+
const found = await lookUpSite(credentials, site);
|
|
2009
|
+
if (found === void 0) {
|
|
2010
|
+
throw new Error(
|
|
2011
|
+
`No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
return found;
|
|
2015
|
+
}
|
|
2016
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
2017
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
2018
|
+
let files = 0;
|
|
2019
|
+
let url = target.url;
|
|
2020
|
+
let unchanged = false;
|
|
2021
|
+
let mode = null;
|
|
2022
|
+
let paths = [];
|
|
2023
|
+
let failure2 = null;
|
|
2024
|
+
let failureCode;
|
|
2025
|
+
let failureDetail;
|
|
2026
|
+
const record = (event) => {
|
|
2027
|
+
if (event.type === "collecting") files = event.files;
|
|
2028
|
+
if (event.type === "unchanged") {
|
|
2029
|
+
unchanged = true;
|
|
2030
|
+
url = event.url || target.url;
|
|
1790
2031
|
}
|
|
1791
|
-
if (
|
|
1792
|
-
|
|
1793
|
-
|
|
2032
|
+
if (event.type === "done") {
|
|
2033
|
+
url = event.url;
|
|
2034
|
+
mode = event.mode;
|
|
1794
2035
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2036
|
+
if (event.type === "error") {
|
|
2037
|
+
failure2 = event.message;
|
|
2038
|
+
failureCode = event.code;
|
|
2039
|
+
failureDetail = event.detail;
|
|
2040
|
+
}
|
|
2041
|
+
onProgress?.(event);
|
|
2042
|
+
};
|
|
2043
|
+
const watched = async (signal, onRead) => {
|
|
2044
|
+
const collected = await source(signal, onRead);
|
|
2045
|
+
paths = collected.map((file) => file.path);
|
|
2046
|
+
return collected;
|
|
2047
|
+
};
|
|
2048
|
+
await deploy(watched, target.siteId, record, void 0, {
|
|
2049
|
+
baseUrl: credentials.apiBaseUrl,
|
|
2050
|
+
token: credentials.token,
|
|
2051
|
+
...credentials.headers === void 0 ? {} : { headers: credentials.headers }
|
|
2052
|
+
}).catch((error) => {
|
|
2053
|
+
const thrown = new Error(
|
|
2054
|
+
failure2 ?? (error instanceof Error ? error.message : String(error))
|
|
2055
|
+
);
|
|
2056
|
+
thrown.detail = failureDetail ?? error?.detail;
|
|
2057
|
+
thrown.code = failureCode ?? error?.code;
|
|
2058
|
+
throw thrown;
|
|
1807
2059
|
});
|
|
2060
|
+
return {
|
|
2061
|
+
url,
|
|
2062
|
+
siteId: target.siteId,
|
|
2063
|
+
subdomain: target.subdomain,
|
|
2064
|
+
files,
|
|
2065
|
+
unchanged,
|
|
2066
|
+
mode,
|
|
2067
|
+
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
2068
|
+
};
|
|
1808
2069
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
return globalThis.fetch.bind(globalThis);
|
|
2070
|
+
const MAX_FILES = 2e4;
|
|
2071
|
+
function directorySource(root) {
|
|
2072
|
+
return async () => {
|
|
2073
|
+
const base = await realpath(root);
|
|
2074
|
+
const files = [];
|
|
2075
|
+
await walk(base, base, files);
|
|
2076
|
+
return files;
|
|
2077
|
+
};
|
|
1818
2078
|
}
|
|
1819
|
-
function
|
|
1820
|
-
|
|
2079
|
+
async function walk(root, directory, into) {
|
|
2080
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
2081
|
+
const full = join(directory, entry.name);
|
|
2082
|
+
const path = relative(root, full).split(sep).join("/");
|
|
2083
|
+
if (shouldIgnore(path)) continue;
|
|
2084
|
+
if (entry.isSymbolicLink()) {
|
|
2085
|
+
const target = await realpath(full).catch(() => null);
|
|
2086
|
+
if (target === null || escapesRoot(root, target)) continue;
|
|
2087
|
+
const targeted = await stat(target);
|
|
2088
|
+
if (targeted.isDirectory()) {
|
|
2089
|
+
await walk(root, target, into);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (!targeted.isFile()) continue;
|
|
2093
|
+
} else if (entry.isDirectory()) {
|
|
2094
|
+
await walk(root, full, into);
|
|
2095
|
+
continue;
|
|
2096
|
+
} else if (!entry.isFile()) {
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (into.length >= MAX_FILES) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`This folder holds more than ${MAX_FILES.toLocaleString()} files. Publish a build output directory rather than a whole project.`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
into.push({ path, bytes: new Uint8Array(await readFile(full)) });
|
|
2105
|
+
}
|
|
1821
2106
|
}
|
|
1822
|
-
function
|
|
1823
|
-
|
|
2107
|
+
function escapesRoot(root, candidate) {
|
|
2108
|
+
const inside = relative(root, candidate);
|
|
2109
|
+
return inside === "" || inside.startsWith("..");
|
|
1824
2110
|
}
|
|
1825
|
-
function
|
|
1826
|
-
|
|
2111
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
2112
|
+
const found = await stat(directory).catch(() => null);
|
|
2113
|
+
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
2114
|
+
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
2115
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
1827
2116
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2117
|
+
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2118
|
+
function newAttempt() {
|
|
2119
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
2120
|
+
return {
|
|
2121
|
+
verifier,
|
|
2122
|
+
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
2123
|
+
state: randomBytes(16).toString("base64url")
|
|
2124
|
+
};
|
|
1834
2125
|
}
|
|
1835
|
-
function
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
return
|
|
1840
|
-
ClientErrorCode.UploadFailed,
|
|
1841
|
-
error instanceof Error ? error.message : "Upload failed.",
|
|
1842
|
-
error
|
|
1843
|
-
);
|
|
2126
|
+
function clientName(surface) {
|
|
2127
|
+
const clean2 = (value) => value.trim().replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2128
|
+
const label = "";
|
|
2129
|
+
const raw = clean2(hostname()).slice(0, 64 - label.length);
|
|
2130
|
+
return `${raw === "" ? "drop2run" : raw}${label}`;
|
|
1844
2131
|
}
|
|
1845
|
-
|
|
2132
|
+
function openBrowser(url) {
|
|
2133
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1846
2134
|
try {
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
1851
|
-
);
|
|
1852
|
-
onProgress({ type: "collecting", files: collected.length });
|
|
1853
|
-
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
1854
|
-
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
1855
|
-
const hashed = await hashAll(
|
|
1856
|
-
files,
|
|
1857
|
-
(done, total) => onProgress({ type: "hashing", done, total }),
|
|
1858
|
-
signal
|
|
1859
|
-
);
|
|
1860
|
-
checkLimits(hashed, options.limits ?? null);
|
|
1861
|
-
onProgress({ type: "preparing" });
|
|
1862
|
-
const prepared = await prepareDeploy(siteId, hashed, options, signal);
|
|
1863
|
-
if (prepared.unchanged) {
|
|
1864
|
-
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1868
|
-
const totalBytes = prepared.upload.reduce(
|
|
1869
|
-
(sum, target) => sum + (sizes.get(target.path) ?? 0),
|
|
1870
|
-
0
|
|
1871
|
-
);
|
|
1872
|
-
if (prepared.upload.length > 0) {
|
|
1873
|
-
onProgress({
|
|
1874
|
-
type: "uploading",
|
|
1875
|
-
done: prepared.reused,
|
|
1876
|
-
total: prepared.total,
|
|
1877
|
-
bytes: 0,
|
|
1878
|
-
totalBytes,
|
|
1879
|
-
reused: prepared.reused
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
await uploadAll(
|
|
1883
|
-
prepared.upload,
|
|
1884
|
-
prepared.uploadUrl,
|
|
1885
|
-
hashed,
|
|
1886
|
-
(done, _total, bytes, path) => onProgress({
|
|
1887
|
-
type: "uploading",
|
|
1888
|
-
done: prepared.reused + done,
|
|
1889
|
-
total: prepared.total,
|
|
1890
|
-
bytes,
|
|
1891
|
-
totalBytes,
|
|
1892
|
-
reused: prepared.reused,
|
|
1893
|
-
...path === void 0 ? {} : { path }
|
|
1894
|
-
}),
|
|
1895
|
-
signal,
|
|
1896
|
-
options.upload,
|
|
1897
|
-
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1898
|
-
);
|
|
1899
|
-
onProgress({ type: "completing", reused: prepared.reused });
|
|
1900
|
-
const completed = await completeDeploy(
|
|
1901
|
-
siteId,
|
|
1902
|
-
prepared.deployId,
|
|
1903
|
-
options,
|
|
1904
|
-
signal,
|
|
1905
|
-
await readSiteName(files)
|
|
1906
|
-
);
|
|
1907
|
-
onProgress({
|
|
1908
|
-
type: "done",
|
|
1909
|
-
url: completed.url,
|
|
1910
|
-
name: completed.name,
|
|
1911
|
-
mode: servingModeOf(completed),
|
|
1912
|
-
modeIsManual: completed.modeIsManual
|
|
2135
|
+
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
2136
|
+
child.unref();
|
|
2137
|
+
child.on("error", () => {
|
|
1913
2138
|
});
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2139
|
+
return true;
|
|
2140
|
+
} catch {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function listen(state) {
|
|
2145
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
2146
|
+
let settle = null;
|
|
2147
|
+
let fail = null;
|
|
2148
|
+
const received = new Promise((resolveCode, rejectCode) => {
|
|
2149
|
+
settle = resolveCode;
|
|
2150
|
+
fail = rejectCode;
|
|
2151
|
+
});
|
|
2152
|
+
const server = createServer((request2, response) => {
|
|
2153
|
+
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
2154
|
+
if (url.pathname !== "/cb") {
|
|
2155
|
+
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
2156
|
+
response.end("Not found.\n");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const code = url.searchParams.get("code") ?? "";
|
|
2160
|
+
const returned = url.searchParams.get("state") ?? "";
|
|
2161
|
+
if (returned !== state || code === "") {
|
|
2162
|
+
respond(
|
|
2163
|
+
response,
|
|
2164
|
+
400,
|
|
2165
|
+
"Sign-in could not be completed",
|
|
2166
|
+
"This did not come from the sign-in that was started here. Nothing has been changed."
|
|
2167
|
+
);
|
|
2168
|
+
fail?.(new Error("The browser returned a response that did not match this sign-in."));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
respond(response, 200, "Signed in", "You can close this tab now.");
|
|
2172
|
+
settle?.({ code });
|
|
2173
|
+
});
|
|
2174
|
+
server.on("error", (error) => {
|
|
2175
|
+
rejectListener(error);
|
|
2176
|
+
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
2177
|
+
});
|
|
2178
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2179
|
+
const address = server.address();
|
|
2180
|
+
if (address === null) {
|
|
2181
|
+
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
resolveListener({
|
|
2185
|
+
port: address.port,
|
|
2186
|
+
received,
|
|
2187
|
+
close: () => server.close()
|
|
2188
|
+
});
|
|
1921
2189
|
});
|
|
1922
|
-
|
|
1923
|
-
}
|
|
2190
|
+
});
|
|
1924
2191
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
return files;
|
|
1932
|
-
};
|
|
2192
|
+
function respond(response, status, title, detail) {
|
|
2193
|
+
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
2194
|
+
response.end(
|
|
2195
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
2196
|
+
`
|
|
2197
|
+
);
|
|
1933
2198
|
}
|
|
1934
|
-
async function
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
} else if (entry.isDirectory()) {
|
|
1949
|
-
await walk(root, full, into);
|
|
1950
|
-
continue;
|
|
1951
|
-
} else if (!entry.isFile()) {
|
|
1952
|
-
continue;
|
|
1953
|
-
}
|
|
1954
|
-
if (into.length >= MAX_FILES) {
|
|
1955
|
-
throw new Error(
|
|
1956
|
-
`This folder holds more than ${MAX_FILES.toLocaleString()} files. Publish a build output directory rather than a whole project.`
|
|
1957
|
-
);
|
|
1958
|
-
}
|
|
1959
|
-
into.push({ path, bytes: new Uint8Array(await readFile(full)) });
|
|
2199
|
+
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
2200
|
+
let timer;
|
|
2201
|
+
try {
|
|
2202
|
+
return await Promise.race([
|
|
2203
|
+
received,
|
|
2204
|
+
new Promise((_, reject) => {
|
|
2205
|
+
timer = setTimeout(
|
|
2206
|
+
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
2207
|
+
timeoutMs
|
|
2208
|
+
);
|
|
2209
|
+
})
|
|
2210
|
+
]);
|
|
2211
|
+
} finally {
|
|
2212
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1960
2213
|
}
|
|
1961
2214
|
}
|
|
1962
|
-
function
|
|
1963
|
-
const
|
|
1964
|
-
|
|
2215
|
+
function consentUrl(dashboardUrl, attempt, port, name) {
|
|
2216
|
+
const url = new URL("/cli-auth", dashboardUrl);
|
|
2217
|
+
url.searchParams.set("challenge", attempt.challenge);
|
|
2218
|
+
url.searchParams.set("state", attempt.state);
|
|
2219
|
+
url.searchParams.set("port", String(port));
|
|
2220
|
+
url.searchParams.set("name", name);
|
|
2221
|
+
return url.toString();
|
|
1965
2222
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
if (
|
|
1973
|
-
return
|
|
2223
|
+
async function startDevice(apiBaseUrl, clientName2) {
|
|
2224
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/device`, {
|
|
2225
|
+
method: "POST",
|
|
2226
|
+
headers: { "Content-Type": "application/json" },
|
|
2227
|
+
body: JSON.stringify({ clientName: clientName2 })
|
|
2228
|
+
});
|
|
2229
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
2230
|
+
return await response.json();
|
|
1974
2231
|
}
|
|
1975
|
-
async function
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
);
|
|
1986
|
-
}
|
|
1987
|
-
if (site === void 0) {
|
|
1988
|
-
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
1989
|
-
return await createSite(credentials, subdomain, folderId);
|
|
2232
|
+
async function pollDevice(apiBaseUrl, deviceCode) {
|
|
2233
|
+
let response;
|
|
2234
|
+
try {
|
|
2235
|
+
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
2236
|
+
method: "POST",
|
|
2237
|
+
headers: { "Content-Type": "application/json" },
|
|
2238
|
+
body: JSON.stringify({ deviceCode })
|
|
2239
|
+
});
|
|
2240
|
+
} catch {
|
|
2241
|
+
return { state: "pending" };
|
|
1990
2242
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
`No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
|
|
1995
|
-
);
|
|
2243
|
+
if (response.ok) {
|
|
2244
|
+
const body = await response.json();
|
|
2245
|
+
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
1996
2246
|
}
|
|
1997
|
-
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
let unchanged = false;
|
|
2004
|
-
let mode = null;
|
|
2005
|
-
let paths = [];
|
|
2006
|
-
let failure2 = null;
|
|
2007
|
-
let failureDetail;
|
|
2008
|
-
const record = (event) => {
|
|
2009
|
-
if (event.type === "collecting") files = event.files;
|
|
2010
|
-
if (event.type === "unchanged") {
|
|
2011
|
-
unchanged = true;
|
|
2012
|
-
url = event.url || target.url;
|
|
2013
|
-
}
|
|
2014
|
-
if (event.type === "done") {
|
|
2015
|
-
url = event.url;
|
|
2016
|
-
mode = event.mode;
|
|
2017
|
-
}
|
|
2018
|
-
if (event.type === "error") {
|
|
2019
|
-
failure2 = event.message;
|
|
2020
|
-
failureDetail = event.detail;
|
|
2021
|
-
}
|
|
2022
|
-
onProgress?.(event);
|
|
2023
|
-
};
|
|
2024
|
-
const watched = async (signal, onRead) => {
|
|
2025
|
-
const collected = await source(signal, onRead);
|
|
2026
|
-
paths = collected.map((file) => file.path);
|
|
2027
|
-
return collected;
|
|
2247
|
+
const problem = await response.json().catch(() => null);
|
|
2248
|
+
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
2249
|
+
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
2250
|
+
return {
|
|
2251
|
+
state: "dead",
|
|
2252
|
+
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
2028
2253
|
};
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2254
|
+
}
|
|
2255
|
+
async function problemDetail(response) {
|
|
2256
|
+
const problem = await response.json().catch(() => null);
|
|
2257
|
+
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
2258
|
+
}
|
|
2259
|
+
async function exchange(apiBaseUrl, code, verifier) {
|
|
2260
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
2261
|
+
method: "POST",
|
|
2262
|
+
headers: { "Content-Type": "application/json" },
|
|
2263
|
+
body: JSON.stringify({ code, verifier })
|
|
2264
|
+
});
|
|
2265
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
2266
|
+
const body = await response.json();
|
|
2267
|
+
return { token: body.token, name: body.name, email: body.email };
|
|
2268
|
+
}
|
|
2269
|
+
const VERSIONS_LISTED = 10;
|
|
2270
|
+
function servingModeOf(site) {
|
|
2271
|
+
if (site.spaMode) return "spa";
|
|
2272
|
+
if (site.docsMode) return "docs";
|
|
2273
|
+
return "static";
|
|
2274
|
+
}
|
|
2275
|
+
function accessOf(site) {
|
|
2276
|
+
if (site.passwordProtected) return "password";
|
|
2277
|
+
if (site.invitedOnly === true) return "invite-only";
|
|
2278
|
+
if (site.invitedOnly === false) return "public";
|
|
2279
|
+
return "no password (whether it is invite-only could not be read)";
|
|
2280
|
+
}
|
|
2281
|
+
function addressLines(site) {
|
|
2282
|
+
return [
|
|
2283
|
+
...site.formerSubdomains.length > 0 ? [` formerly ${site.formerSubdomains.join(", ")} (redirect here)`] : [],
|
|
2284
|
+
...site.subdomainChangesLeft === null ? [] : [` address ${site.subdomainChangesLeft} change(s) left`]
|
|
2285
|
+
];
|
|
2286
|
+
}
|
|
2287
|
+
function describeSite(site, folderPath) {
|
|
2288
|
+
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
2289
|
+
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
2290
|
+
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
2291
|
+
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
2292
|
+
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
2038
2293
|
});
|
|
2294
|
+
const more = site.deploys.length - versions.length;
|
|
2295
|
+
return [
|
|
2296
|
+
`${site.subdomain} — ${site.url}`,
|
|
2297
|
+
` name ${site.name ?? "(none)"}`,
|
|
2298
|
+
` status ${site.status}`,
|
|
2299
|
+
` access ${accessOf(site)}`,
|
|
2300
|
+
` mode ${servingModeOf(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
2301
|
+
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
2302
|
+
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
2303
|
+
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
2304
|
+
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
2305
|
+
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
2306
|
+
...addressLines(site),
|
|
2307
|
+
"",
|
|
2308
|
+
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
2309
|
+
...versions,
|
|
2310
|
+
...more > 0 ? [` … and ${more} older`] : []
|
|
2311
|
+
].join("\n");
|
|
2312
|
+
}
|
|
2313
|
+
const PROJECT_FILE = "drop2run.json";
|
|
2314
|
+
function projectPath(directory = process.cwd()) {
|
|
2315
|
+
return join(directory, PROJECT_FILE);
|
|
2316
|
+
}
|
|
2317
|
+
function readProject(directory = process.cwd(), read = (path) => readFileSync(path, "utf8")) {
|
|
2318
|
+
let parsed;
|
|
2319
|
+
try {
|
|
2320
|
+
parsed = JSON.parse(read(projectPath(directory)));
|
|
2321
|
+
} catch {
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId === "") return null;
|
|
2039
2325
|
return {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
files,
|
|
2044
|
-
unchanged,
|
|
2045
|
-
mode,
|
|
2046
|
-
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
2326
|
+
siteId: parsed.siteId,
|
|
2327
|
+
subdomain: typeof parsed.subdomain === "string" ? parsed.subdomain : "",
|
|
2328
|
+
dir: typeof parsed.dir === "string" && parsed.dir !== "" ? parsed.dir : "."
|
|
2047
2329
|
};
|
|
2048
2330
|
}
|
|
2049
|
-
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
return
|
|
2331
|
+
function writeProject(project, directory = process.cwd()) {
|
|
2332
|
+
const path = projectPath(directory);
|
|
2333
|
+
writeFileSync(path, `${JSON.stringify(project, null, " ")}
|
|
2334
|
+
`);
|
|
2335
|
+
return path;
|
|
2054
2336
|
}
|
|
2055
2337
|
const silentProgress = Object.assign(() => {
|
|
2056
2338
|
}, { done: () => {
|
|
@@ -2530,7 +2812,7 @@ async function info(site) {
|
|
|
2530
2812
|
)?.path;
|
|
2531
2813
|
return {
|
|
2532
2814
|
text: describeSite(detail, folderPath),
|
|
2533
|
-
json: { ...detail, mode: servingModeOf
|
|
2815
|
+
json: { ...detail, mode: servingModeOf(detail), folderPath: folderPath ?? null },
|
|
2534
2816
|
code: 0
|
|
2535
2817
|
};
|
|
2536
2818
|
} catch (error) {
|
|
@@ -2567,7 +2849,8 @@ const SETTINGS_USAGE = [
|
|
|
2567
2849
|
" expires <ISO 8601 instant> When it comes down; add --then delete to delete rather than pause",
|
|
2568
2850
|
" folder <path|id|root> Which folder it is filed in",
|
|
2569
2851
|
" invite-only on|off Whether only the people on its list can open it",
|
|
2570
|
-
" comments off|invited|anyone Who may comment on its pages"
|
|
2852
|
+
" comments off|invited|anyone Who may comment on its pages",
|
|
2853
|
+
" subdomain <name> Its address; needs --yes, the old one keeps redirecting"
|
|
2571
2854
|
].join("\n");
|
|
2572
2855
|
function settingChange(setting, values, then) {
|
|
2573
2856
|
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
@@ -2615,6 +2898,8 @@ function settingChange(setting, values, then) {
|
|
|
2615
2898
|
}
|
|
2616
2899
|
case "folder":
|
|
2617
2900
|
return value ? { folder: value } : needs("Clients/Acme");
|
|
2901
|
+
case "subdomain":
|
|
2902
|
+
return value ? { subdomain: value } : needs("tidy-otter --yes");
|
|
2618
2903
|
case "invite-only":
|
|
2619
2904
|
if (value === "on" || value === "off") return { change: { invitedOnly: value === "on" } };
|
|
2620
2905
|
return { refusal: "`set invite-only` takes `on` or `off`." };
|
|
@@ -2640,6 +2925,15 @@ async function set(site, setting, values, options, readPassword = () => readSecr
|
|
|
2640
2925
|
try {
|
|
2641
2926
|
const summary = await findSite(found.credentials, target.named);
|
|
2642
2927
|
let change;
|
|
2928
|
+
if ("subdomain" in parsed) {
|
|
2929
|
+
return await moved(
|
|
2930
|
+
found.credentials,
|
|
2931
|
+
summary.siteId,
|
|
2932
|
+
summary.subdomain,
|
|
2933
|
+
parsed.subdomain,
|
|
2934
|
+
options.confirmed
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2643
2937
|
if ("folder" in parsed) {
|
|
2644
2938
|
change = { folderId: await resolveFolderTarget(found.credentials, parsed.folder) };
|
|
2645
2939
|
} else if ("password" in parsed) {
|
|
@@ -2662,6 +2956,29 @@ Add --yes if that is what you want, or leave out --then to pause it instead.`
|
|
|
2662
2956
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2663
2957
|
}
|
|
2664
2958
|
}
|
|
2959
|
+
async function moved(credentials, siteId, current, wanted, confirmed) {
|
|
2960
|
+
if (!confirmed) {
|
|
2961
|
+
const site = await getSite(credentials, siteId);
|
|
2962
|
+
const left2 = site.subdomainChangesLeft === null ? "Your plan does not limit how often it changes." : `It spends one of the ${site.subdomainChangesLeft} address change(s) this site has left, a change back to an old name included.`;
|
|
2963
|
+
return failure(
|
|
2964
|
+
`This moves ${current} to ${wanted}. ${current} keeps working and redirects there, and nobody else can take it. ${left2}
|
|
2965
|
+
Add --yes if that is what you want.`
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
const change = await changeSubdomain(credentials, siteId, wanted);
|
|
2969
|
+
const project = readProject();
|
|
2970
|
+
if (project?.siteId === siteId && project.subdomain !== change.subdomain) {
|
|
2971
|
+
writeProject({ ...project, subdomain: change.subdomain });
|
|
2972
|
+
}
|
|
2973
|
+
const left = change.subdomainChangesLeft === null ? "" : `
|
|
2974
|
+
${change.subdomainChangesLeft} address change(s) left.`;
|
|
2975
|
+
return {
|
|
2976
|
+
text: `${current} is now ${change.url}
|
|
2977
|
+
The old address redirects there.${left}`,
|
|
2978
|
+
json: change,
|
|
2979
|
+
code: 0
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2665
2982
|
async function unset(site, setting, extra = []) {
|
|
2666
2983
|
if (extra.length > 0) {
|
|
2667
2984
|
return failure(
|
|
@@ -2698,7 +3015,7 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2698
3015
|
`Updated ${subdomain}.`,
|
|
2699
3016
|
` name ${settings.name ?? "(none)"}`,
|
|
2700
3017
|
` access ${accessOf(settings)}`,
|
|
2701
|
-
` mode ${servingModeOf
|
|
3018
|
+
` mode ${servingModeOf(settings)}`,
|
|
2702
3019
|
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2703
3020
|
` comments ${settings.comments ?? "unknown"}`,
|
|
2704
3021
|
` takedown ${takedown}`,
|
|
@@ -2711,7 +3028,7 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2711
3028
|
] : [],
|
|
2712
3029
|
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2713
3030
|
].join("\n"),
|
|
2714
|
-
json: { ...settings, mode: servingModeOf
|
|
3031
|
+
json: { ...settings, mode: servingModeOf(settings) },
|
|
2715
3032
|
code: 0
|
|
2716
3033
|
};
|
|
2717
3034
|
}
|
|
@@ -3058,7 +3375,7 @@ async function run(commandLine) {
|
|
|
3058
3375
|
const positional = [...positionals(argv), ...literal];
|
|
3059
3376
|
const [command, ...rest] = positional;
|
|
3060
3377
|
if (argv.includes("--version")) {
|
|
3061
|
-
const { version } = await import("./package-
|
|
3378
|
+
const { version } = await import("./package-DgeW4aj9.js").then(
|
|
3062
3379
|
(module) => module.default
|
|
3063
3380
|
);
|
|
3064
3381
|
return { text: version, json: { version }, code: 0 };
|