@x12i/docify-export 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assemble-web.d.ts +21 -0
- package/dist/assemble-web.d.ts.map +1 -0
- package/dist/assemble-web.js +29 -0
- package/dist/assemble-web.js.map +1 -0
- package/dist/check-package.d.ts +11 -0
- package/dist/check-package.d.ts.map +1 -0
- package/dist/check-package.js +42 -0
- package/dist/check-package.js.map +1 -0
- package/dist/harden-web.d.ts +2 -0
- package/dist/harden-web.d.ts.map +1 -1
- package/dist/harden-web.js +7 -1
- package/dist/harden-web.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/publish.d.ts +30 -5
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +474 -56
- package/dist/publish.js.map +1 -1
- package/dist/release.d.ts +63 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +155 -0
- package/dist/release.js.map +1 -0
- package/dist/serve-site.d.ts +61 -0
- package/dist/serve-site.d.ts.map +1 -0
- package/dist/serve-site.js +134 -0
- package/dist/serve-site.js.map +1 -0
- package/package.json +3 -3
package/dist/publish.js
CHANGED
|
@@ -4,16 +4,206 @@ import { join } from "node:path";
|
|
|
4
4
|
import { DocifyConfigSchema, resolvePublishHostname, } from "@x12i/docify-core";
|
|
5
5
|
const PAGES_MAX_FILES = 20_000;
|
|
6
6
|
const PAGES_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
7
|
-
|
|
7
|
+
const ACCOUNT_ID_RE = /\b([a-f0-9]{32})\b/gi;
|
|
8
|
+
/** Never return an empty failure message when status ends with -failed. */
|
|
9
|
+
export function failedMessage(r, fallback) {
|
|
10
|
+
const text = [r.stderr, r.stdout, r.error?.message]
|
|
11
|
+
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
12
|
+
.find((s) => s.length > 0);
|
|
13
|
+
if (text)
|
|
14
|
+
return text;
|
|
15
|
+
if (r.status === null && r.error)
|
|
16
|
+
return r.error.message;
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
export function defaultExec(command, args, opts) {
|
|
20
|
+
// Interactive commands (OAuth consent) must reach the terminal, otherwise the
|
|
21
|
+
// login URL and prompt are captured and the user sees nothing to approve.
|
|
22
|
+
// On Windows, npx is typically a .cmd shim — spawn without shell yields empty pipes.
|
|
23
|
+
const useShell = process.platform === "win32" || command === "npx";
|
|
8
24
|
const r = spawnSync(command, args, {
|
|
9
25
|
encoding: "utf8",
|
|
10
26
|
env: { ...process.env, ...opts?.env },
|
|
27
|
+
stdio: opts?.interactive ? "inherit" : "pipe",
|
|
28
|
+
shell: useShell,
|
|
11
29
|
});
|
|
12
30
|
return {
|
|
13
31
|
status: r.status,
|
|
14
32
|
stdout: r.stdout ?? "",
|
|
15
33
|
stderr: r.stderr ?? "",
|
|
34
|
+
error: r.error,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Parse account ids (and optional names) from `wrangler whoami` text or JSON. */
|
|
38
|
+
export function parseWhoamiAccounts(output) {
|
|
39
|
+
const trimmed = (output || "").trim();
|
|
40
|
+
if (!trimmed)
|
|
41
|
+
return [];
|
|
42
|
+
// JSON forms: { accounts: [{ id, name }] } or array of accounts
|
|
43
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(trimmed);
|
|
46
|
+
const list = Array.isArray(data)
|
|
47
|
+
? data
|
|
48
|
+
: data &&
|
|
49
|
+
typeof data === "object" &&
|
|
50
|
+
Array.isArray(data.accounts)
|
|
51
|
+
? data.accounts
|
|
52
|
+
: null;
|
|
53
|
+
if (list) {
|
|
54
|
+
const fromJson = [];
|
|
55
|
+
for (const item of list) {
|
|
56
|
+
if (!item || typeof item !== "object")
|
|
57
|
+
continue;
|
|
58
|
+
const id = typeof item.id === "string"
|
|
59
|
+
? item.id
|
|
60
|
+
: typeof item.account_id === "string"
|
|
61
|
+
? item.account_id
|
|
62
|
+
: "";
|
|
63
|
+
if (!/^[a-f0-9]{32}$/i.test(id))
|
|
64
|
+
continue;
|
|
65
|
+
const name = typeof item.name === "string"
|
|
66
|
+
? item.name
|
|
67
|
+
: undefined;
|
|
68
|
+
fromJson.push({ id: id.toLowerCase(), name });
|
|
69
|
+
}
|
|
70
|
+
if (fromJson.length)
|
|
71
|
+
return uniqueAccounts(fromJson);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// fall through to text parse
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const accounts = [];
|
|
79
|
+
const lines = trimmed.split(/\r?\n/);
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const ids = [...line.matchAll(ACCOUNT_ID_RE)].map((m) => m[1].toLowerCase());
|
|
82
|
+
if (!ids.length)
|
|
83
|
+
continue;
|
|
84
|
+
// Prefer last hex token on the line (typical whoami table: name … id)
|
|
85
|
+
const id = ids[ids.length - 1];
|
|
86
|
+
const namePart = line
|
|
87
|
+
.replace(ACCOUNT_ID_RE, "")
|
|
88
|
+
.replace(/[|\u2502]/g, " ")
|
|
89
|
+
.replace(/\s+/g, " ")
|
|
90
|
+
.trim();
|
|
91
|
+
accounts.push({
|
|
92
|
+
id,
|
|
93
|
+
name: namePart || undefined,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return uniqueAccounts(accounts);
|
|
97
|
+
}
|
|
98
|
+
function uniqueAccounts(list) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const a of list) {
|
|
102
|
+
if (seen.has(a.id))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(a.id);
|
|
105
|
+
out.push(a);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** Parse Pages projects + custom domains from list --json or text. */
|
|
110
|
+
export function parsePagesProjectList(output) {
|
|
111
|
+
const trimmed = (output || "").trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
return [];
|
|
114
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(trimmed);
|
|
117
|
+
const list = Array.isArray(data)
|
|
118
|
+
? data
|
|
119
|
+
: data &&
|
|
120
|
+
typeof data === "object" &&
|
|
121
|
+
Array.isArray(data.projects)
|
|
122
|
+
? data.projects
|
|
123
|
+
: data &&
|
|
124
|
+
typeof data === "object" &&
|
|
125
|
+
Array.isArray(data.result)
|
|
126
|
+
? data.result
|
|
127
|
+
: null;
|
|
128
|
+
if (list) {
|
|
129
|
+
const projects = [];
|
|
130
|
+
for (const item of list) {
|
|
131
|
+
if (!item || typeof item !== "object")
|
|
132
|
+
continue;
|
|
133
|
+
const name = typeof item.name === "string"
|
|
134
|
+
? item.name
|
|
135
|
+
: typeof item.project_name ===
|
|
136
|
+
"string"
|
|
137
|
+
? item.project_name
|
|
138
|
+
: "";
|
|
139
|
+
if (!name)
|
|
140
|
+
continue;
|
|
141
|
+
const domains = collectDomains(item);
|
|
142
|
+
projects.push({ name, domains });
|
|
143
|
+
}
|
|
144
|
+
if (projects.length)
|
|
145
|
+
return projects;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// fall through
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Text fallback: lines that look like "name domain1, domain2" or include *.pages.dev
|
|
153
|
+
const projects = [];
|
|
154
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
155
|
+
const domainMatches = [
|
|
156
|
+
...line.matchAll(/\b([a-z0-9][a-z0-9.-]*\.(?:pages\.dev|[a-z0-9.-]+\.[a-z]{2,}))\b/gi),
|
|
157
|
+
].map((m) => m[1].toLowerCase());
|
|
158
|
+
const nameMatch = /^[\s|]*([a-z0-9][a-z0-9_-]*)/i.exec(line);
|
|
159
|
+
if (!nameMatch)
|
|
160
|
+
continue;
|
|
161
|
+
const name = nameMatch[1];
|
|
162
|
+
if (/^(name|project|id|created|account)$/i.test(name) ||
|
|
163
|
+
name.length < 2) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
projects.push({ name, domains: domainMatches });
|
|
167
|
+
}
|
|
168
|
+
return projects;
|
|
169
|
+
}
|
|
170
|
+
function collectDomains(item) {
|
|
171
|
+
const domains = [];
|
|
172
|
+
const push = (v) => {
|
|
173
|
+
if (typeof v === "string" && v.trim())
|
|
174
|
+
domains.push(v.trim().toLowerCase());
|
|
16
175
|
};
|
|
176
|
+
const rec = item;
|
|
177
|
+
push(rec.subdomain);
|
|
178
|
+
push(rec.domain);
|
|
179
|
+
if (Array.isArray(rec.domains)) {
|
|
180
|
+
for (const d of rec.domains) {
|
|
181
|
+
if (typeof d === "string")
|
|
182
|
+
push(d);
|
|
183
|
+
else if (d && typeof d === "object") {
|
|
184
|
+
push(d.name);
|
|
185
|
+
push(d.domain);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(rec.aliases)) {
|
|
190
|
+
for (const a of rec.aliases)
|
|
191
|
+
push(a);
|
|
192
|
+
}
|
|
193
|
+
if (rec.canonical_deployment && typeof rec.canonical_deployment === "object") {
|
|
194
|
+
const cd = rec.canonical_deployment;
|
|
195
|
+
if (Array.isArray(cd.aliases))
|
|
196
|
+
for (const a of cd.aliases)
|
|
197
|
+
push(a);
|
|
198
|
+
}
|
|
199
|
+
return [...new Set(domains)];
|
|
200
|
+
}
|
|
201
|
+
function findProjectByHostname(projects, hostname) {
|
|
202
|
+
const host = hostname.toLowerCase().replace(/^https?:\/\//, "");
|
|
203
|
+
return projects.find((p) => p.domains.some((d) => {
|
|
204
|
+
const domain = d.toLowerCase().replace(/^https?:\/\//, "");
|
|
205
|
+
return domain === host;
|
|
206
|
+
}));
|
|
17
207
|
}
|
|
18
208
|
function countFiles(dir) {
|
|
19
209
|
let count = 0;
|
|
@@ -35,6 +225,11 @@ function countFiles(dir) {
|
|
|
35
225
|
walk(dir);
|
|
36
226
|
return { count, tooLarge };
|
|
37
227
|
}
|
|
228
|
+
function formatAccountTable(accounts) {
|
|
229
|
+
return accounts
|
|
230
|
+
.map((a) => (a.name ? ` - ${a.name} (${a.id})` : ` - ${a.id}`))
|
|
231
|
+
.join("\n");
|
|
232
|
+
}
|
|
38
233
|
/**
|
|
39
234
|
* Provider-neutral Docify publisher. Cloudflare Pages is the first adapter,
|
|
40
235
|
* using Wrangler Direct Upload (no credentials written to the repo).
|
|
@@ -51,16 +246,82 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
51
246
|
const publishCfg = publish;
|
|
52
247
|
const hostname = resolvePublishHostname(publishCfg);
|
|
53
248
|
const customDomainUrl = `https://${hostname}`;
|
|
54
|
-
|
|
249
|
+
let project = publishCfg.projectName;
|
|
55
250
|
const outputDir = config.build.outputDir;
|
|
251
|
+
const configuredAccountId = publishCfg.accountId ||
|
|
252
|
+
process.env.CLOUDFLARE_ACCOUNT_ID ||
|
|
253
|
+
process.env.CF_ACCOUNT_ID ||
|
|
254
|
+
"";
|
|
255
|
+
let effectiveAccountId = configuredAccountId;
|
|
256
|
+
const adoptExisting = deps.adoptExistingProject === true ||
|
|
257
|
+
publishCfg.adoptExistingProject === true;
|
|
258
|
+
const wranglerEnv = () => effectiveAccountId ? { CLOUDFLARE_ACCOUNT_ID: effectiveAccountId } : {};
|
|
259
|
+
const wrangler = (args, opts) => exec("npx", ["--yes", "wrangler", ...args], {
|
|
260
|
+
env: wranglerEnv(),
|
|
261
|
+
interactive: opts?.interactive,
|
|
262
|
+
});
|
|
263
|
+
const dashboardFor = (proj) => effectiveAccountId
|
|
264
|
+
? `https://dash.cloudflare.com/${effectiveAccountId}/pages/view/${proj}`
|
|
265
|
+
: `https://dash.cloudflare.com/?to=/:account/pages/view/${proj}`;
|
|
56
266
|
const baseResult = (environment, extra = {}) => ({
|
|
57
267
|
provider: publishCfg.provider,
|
|
58
268
|
project,
|
|
59
269
|
environment,
|
|
60
270
|
customDomainUrl,
|
|
61
271
|
status: "ok",
|
|
272
|
+
dashboardUrl: dashboardFor(project),
|
|
62
273
|
...extra,
|
|
63
274
|
});
|
|
275
|
+
function resolveAccount() {
|
|
276
|
+
if (configuredAccountId) {
|
|
277
|
+
effectiveAccountId = configuredAccountId;
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
accountId: configuredAccountId,
|
|
281
|
+
accounts: [{ id: configuredAccountId }],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Prefer JSON when Wrangler supports it; fall back to text.
|
|
285
|
+
let who = wrangler(["whoami", "--json"]);
|
|
286
|
+
let combined = `${who.stdout || ""}\n${who.stderr || ""}`;
|
|
287
|
+
let accounts = parseWhoamiAccounts(combined);
|
|
288
|
+
if (who.status !== 0 || accounts.length === 0) {
|
|
289
|
+
who = wrangler(["whoami"]);
|
|
290
|
+
combined = `${who.stdout || ""}\n${who.stderr || ""}`;
|
|
291
|
+
accounts = parseWhoamiAccounts(combined);
|
|
292
|
+
}
|
|
293
|
+
if (who.status !== 0 && accounts.length === 0) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
status: "preflight-failed",
|
|
297
|
+
message: failedMessage(who, "Not logged in to Cloudflare. Run `docify publish connect`, then retry."),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (accounts.length === 0) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
status: "preflight-failed",
|
|
304
|
+
message: "No Cloudflare accounts visible. Run `docify publish connect`, then retry.",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (accounts.length >= 2) {
|
|
308
|
+
const first = accounts[0].id;
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
status: "preflight-failed",
|
|
312
|
+
message: [
|
|
313
|
+
"More than one Cloudflare account is available; set publish.accountId (or CLOUDFLARE_ACCOUNT_ID) for non-interactive deploy.",
|
|
314
|
+
"Accounts:",
|
|
315
|
+
formatAccountTable(accounts),
|
|
316
|
+
"Add to docify.config.json:",
|
|
317
|
+
JSON.stringify({ publish: { accountId: first } }, null, 2),
|
|
318
|
+
"(Pick the correct account id from the list above.)",
|
|
319
|
+
].join("\n"),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
effectiveAccountId = accounts[0].id;
|
|
323
|
+
return { ok: true, accountId: effectiveAccountId, accounts };
|
|
324
|
+
}
|
|
64
325
|
async function verifyDeployment(baseUrl) {
|
|
65
326
|
const checked = [];
|
|
66
327
|
const failed = [];
|
|
@@ -82,7 +343,7 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
82
343
|
providerUrl: baseUrl,
|
|
83
344
|
});
|
|
84
345
|
}
|
|
85
|
-
function
|
|
346
|
+
function preflightBuildLimits() {
|
|
86
347
|
if (!existsSync(outputDir)) {
|
|
87
348
|
throw new Error(`Build output missing: ${outputDir}. Run build first.`);
|
|
88
349
|
}
|
|
@@ -94,68 +355,182 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
94
355
|
throw new Error(`Preflight failed: files exceed size limit:\n${tooLarge.slice(0, 10).join("\n")}`);
|
|
95
356
|
}
|
|
96
357
|
}
|
|
358
|
+
function runPreflight() {
|
|
359
|
+
try {
|
|
360
|
+
preflightBuildLimits();
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
return baseResult("production", {
|
|
364
|
+
status: "preflight-failed",
|
|
365
|
+
message: e instanceof Error ? e.message : String(e),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const account = resolveAccount();
|
|
369
|
+
if (!account.ok) {
|
|
370
|
+
return baseResult("production", {
|
|
371
|
+
status: account.status,
|
|
372
|
+
message: account.message,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const { count } = countFiles(outputDir);
|
|
376
|
+
return baseResult("production", {
|
|
377
|
+
status: "preflight-ok",
|
|
378
|
+
message: `${count} files under ${PAGES_MAX_FILES}; account=${account.accountId}`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function ensureAccountOrFail(environment = "production") {
|
|
382
|
+
const account = resolveAccount();
|
|
383
|
+
if (!account.ok) {
|
|
384
|
+
return baseResult(environment, {
|
|
385
|
+
status: account.status,
|
|
386
|
+
message: account.message,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
function listProjects() {
|
|
392
|
+
let list = wrangler(["pages", "project", "list", "--json"]);
|
|
393
|
+
let projects = parsePagesProjectList(list.stdout || list.stderr || "");
|
|
394
|
+
if (list.status !== 0 || projects.length === 0) {
|
|
395
|
+
list = wrangler(["pages", "project", "list"]);
|
|
396
|
+
if (list.status !== 0) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
result: baseResult("production", {
|
|
400
|
+
status: "setup-failed",
|
|
401
|
+
message: failedMessage(list, "Failed to list Cloudflare Pages projects"),
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
projects = parsePagesProjectList(list.stdout || list.stderr || "");
|
|
406
|
+
}
|
|
407
|
+
return { ok: true, projects };
|
|
408
|
+
}
|
|
97
409
|
return {
|
|
98
410
|
validateConfig() {
|
|
99
411
|
return config;
|
|
100
412
|
},
|
|
101
413
|
async connect() {
|
|
102
414
|
log("Connecting via Wrangler login (credentials stay outside the repo)…");
|
|
103
|
-
|
|
415
|
+
log("Approve the Cloudflare consent screen in the browser window that opens.");
|
|
416
|
+
const r = wrangler(["login"], { interactive: true });
|
|
104
417
|
if (r.status !== 0) {
|
|
105
418
|
return baseResult("production", {
|
|
106
419
|
status: "connect-failed",
|
|
107
|
-
message: r
|
|
420
|
+
message: failedMessage(r, "wrangler login failed"),
|
|
108
421
|
});
|
|
109
422
|
}
|
|
110
|
-
const who =
|
|
423
|
+
const who = wrangler(["whoami"]);
|
|
111
424
|
return baseResult("production", {
|
|
112
425
|
status: "connected",
|
|
113
|
-
message: (who.stdout || "Connected").trim(),
|
|
426
|
+
message: (who.stdout || who.stderr || "Connected").trim() || "Connected",
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
async whoami() {
|
|
430
|
+
const who = wrangler(["whoami"]);
|
|
431
|
+
return baseResult("production", {
|
|
432
|
+
status: who.status === 0 ? "ok" : "whoami-failed",
|
|
433
|
+
message: who.status === 0
|
|
434
|
+
? (who.stdout || who.stderr || "").trim() || "ok"
|
|
435
|
+
: failedMessage(who, "wrangler whoami failed"),
|
|
436
|
+
dashboardUrl: dashboardFor(project),
|
|
114
437
|
});
|
|
115
438
|
},
|
|
439
|
+
preflight: runPreflight,
|
|
116
440
|
async setup() {
|
|
441
|
+
const accountFail = ensureAccountOrFail();
|
|
442
|
+
if (accountFail) {
|
|
443
|
+
return { ...accountFail, status: "setup-failed", message: accountFail.message };
|
|
444
|
+
}
|
|
117
445
|
log(`Setting up Cloudflare Pages project "${project}" for ${hostname}…`);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
446
|
+
const listed = listProjects();
|
|
447
|
+
if (!listed.ok)
|
|
448
|
+
return listed.result;
|
|
449
|
+
const { projects } = listed;
|
|
450
|
+
const byHostname = findProjectByHostname(projects, hostname);
|
|
451
|
+
const nameExists = projects.some((p) => p.name === project);
|
|
452
|
+
if (byHostname) {
|
|
453
|
+
if (byHostname.name === project) {
|
|
125
454
|
return baseResult("production", {
|
|
126
|
-
status: "
|
|
127
|
-
message:
|
|
455
|
+
status: "adopted-existing-project",
|
|
456
|
+
message: `Hostname ${hostname} is already on Pages project "${project}". Associate custom domain in the dashboard if needed. Docify will not delete other domains.`,
|
|
457
|
+
dashboardUrl: dashboardFor(project),
|
|
128
458
|
});
|
|
129
459
|
}
|
|
130
|
-
|
|
131
|
-
"--yes",
|
|
132
|
-
"wrangler",
|
|
133
|
-
"pages",
|
|
134
|
-
"project",
|
|
135
|
-
"create",
|
|
136
|
-
project,
|
|
137
|
-
"--production-branch",
|
|
138
|
-
publishCfg.productionBranch,
|
|
139
|
-
]);
|
|
140
|
-
if (created.status !== 0) {
|
|
460
|
+
if (!adoptExisting) {
|
|
141
461
|
return baseResult("production", {
|
|
142
462
|
status: "setup-failed",
|
|
143
|
-
message:
|
|
463
|
+
message: [
|
|
464
|
+
`Hostname ${hostname} is already bound to Pages project "${byHostname.name}", but docify.config.json has projectName "${project}".`,
|
|
465
|
+
`Set "projectName": "${byHostname.name}" (or pass --adopt / publish.adoptExistingProject: true) and retry.`,
|
|
466
|
+
].join("\n"),
|
|
467
|
+
project: byHostname.name,
|
|
468
|
+
dashboardUrl: dashboardFor(byHostname.name),
|
|
144
469
|
});
|
|
145
470
|
}
|
|
471
|
+
project = byHostname.name;
|
|
472
|
+
return baseResult("production", {
|
|
473
|
+
status: "adopted-existing-project",
|
|
474
|
+
message: `Adopted existing Pages project "${project}" that already serves ${hostname} (config projectName differed).`,
|
|
475
|
+
dashboardUrl: dashboardFor(project),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (nameExists) {
|
|
479
|
+
return baseResult("production", {
|
|
480
|
+
status: "adopted-existing-project",
|
|
481
|
+
message: `Associate custom domain ${hostname} in Cloudflare Pages if not already linked. Docify will not delete other domains.`,
|
|
482
|
+
dashboardUrl: dashboardFor(project),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
const confirmed = deps.confirm == null
|
|
486
|
+
? true
|
|
487
|
+
: await deps.confirm(`Create Cloudflare Pages project "${project}"? Direct Upload projects cannot later switch to Git integration.`);
|
|
488
|
+
if (!confirmed) {
|
|
489
|
+
return baseResult("production", {
|
|
490
|
+
status: "setup-cancelled",
|
|
491
|
+
message: "User cancelled project creation",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const created = wrangler([
|
|
495
|
+
"pages",
|
|
496
|
+
"project",
|
|
497
|
+
"create",
|
|
498
|
+
project,
|
|
499
|
+
"--production-branch",
|
|
500
|
+
publishCfg.productionBranch,
|
|
501
|
+
]);
|
|
502
|
+
if (created.status !== 0) {
|
|
503
|
+
return baseResult("production", {
|
|
504
|
+
status: "setup-failed",
|
|
505
|
+
message: failedMessage(created, "pages project create failed"),
|
|
506
|
+
});
|
|
146
507
|
}
|
|
147
508
|
return baseResult("production", {
|
|
148
|
-
status:
|
|
509
|
+
status: "project-created",
|
|
149
510
|
message: `Associate custom domain ${hostname} in Cloudflare Pages if not already linked. Docify will not delete other domains.`,
|
|
150
|
-
dashboardUrl:
|
|
511
|
+
dashboardUrl: dashboardFor(project),
|
|
151
512
|
});
|
|
152
513
|
},
|
|
153
514
|
async deployProduction() {
|
|
154
|
-
|
|
515
|
+
try {
|
|
516
|
+
preflightBuildLimits();
|
|
517
|
+
}
|
|
518
|
+
catch (e) {
|
|
519
|
+
return baseResult("production", {
|
|
520
|
+
status: "deploy-failed",
|
|
521
|
+
message: e instanceof Error ? e.message : String(e),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const accountFail = ensureAccountOrFail();
|
|
525
|
+
if (accountFail) {
|
|
526
|
+
return {
|
|
527
|
+
...accountFail,
|
|
528
|
+
status: "deploy-failed",
|
|
529
|
+
message: accountFail.message,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
155
532
|
log(`Deploying ${outputDir} → Pages project ${project} (production)…`);
|
|
156
|
-
const r =
|
|
157
|
-
"--yes",
|
|
158
|
-
"wrangler",
|
|
533
|
+
const r = wrangler([
|
|
159
534
|
"pages",
|
|
160
535
|
"deploy",
|
|
161
536
|
outputDir,
|
|
@@ -163,11 +538,12 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
163
538
|
project,
|
|
164
539
|
"--branch",
|
|
165
540
|
publishCfg.productionBranch,
|
|
541
|
+
"--commit-dirty=true",
|
|
166
542
|
]);
|
|
167
543
|
if (r.status !== 0) {
|
|
168
544
|
return baseResult("production", {
|
|
169
545
|
status: "deploy-failed",
|
|
170
|
-
message: r
|
|
546
|
+
message: failedMessage(r, "pages deploy failed"),
|
|
171
547
|
});
|
|
172
548
|
}
|
|
173
549
|
const urlMatch = /https:\/\/[^\s]+/.exec(r.stdout);
|
|
@@ -182,14 +558,29 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
182
558
|
return {
|
|
183
559
|
...verified,
|
|
184
560
|
providerUrl: providerUrl ?? verified.providerUrl,
|
|
185
|
-
message: r.stdout.trim(),
|
|
561
|
+
message: r.stdout.trim() || verified.message,
|
|
562
|
+
dashboardUrl: dashboardFor(project),
|
|
186
563
|
};
|
|
187
564
|
},
|
|
188
565
|
async deployPreview(branch = "preview") {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
566
|
+
try {
|
|
567
|
+
preflightBuildLimits();
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
return baseResult("preview", {
|
|
571
|
+
status: "deploy-failed",
|
|
572
|
+
message: e instanceof Error ? e.message : String(e),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
const accountFail = ensureAccountOrFail("preview");
|
|
576
|
+
if (accountFail) {
|
|
577
|
+
return {
|
|
578
|
+
...accountFail,
|
|
579
|
+
status: "deploy-failed",
|
|
580
|
+
message: accountFail.message,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const r = wrangler([
|
|
193
584
|
"pages",
|
|
194
585
|
"deploy",
|
|
195
586
|
outputDir,
|
|
@@ -197,11 +588,12 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
197
588
|
project,
|
|
198
589
|
"--branch",
|
|
199
590
|
branch,
|
|
591
|
+
"--commit-dirty=true",
|
|
200
592
|
]);
|
|
201
593
|
if (r.status !== 0) {
|
|
202
594
|
return baseResult("preview", {
|
|
203
595
|
status: "deploy-failed",
|
|
204
|
-
message: r
|
|
596
|
+
message: failedMessage(r, "pages deploy failed"),
|
|
205
597
|
});
|
|
206
598
|
}
|
|
207
599
|
const urlMatch = /https:\/\/[^\s]+/.exec(r.stdout);
|
|
@@ -210,12 +602,19 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
210
602
|
status: "deployed",
|
|
211
603
|
providerUrl,
|
|
212
604
|
message: r.stdout.trim(),
|
|
605
|
+
dashboardUrl: dashboardFor(project),
|
|
213
606
|
});
|
|
214
607
|
},
|
|
215
608
|
async getStatus() {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
609
|
+
const accountFail = ensureAccountOrFail();
|
|
610
|
+
if (accountFail) {
|
|
611
|
+
return {
|
|
612
|
+
...accountFail,
|
|
613
|
+
status: "status-failed",
|
|
614
|
+
message: accountFail.message,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const r = wrangler([
|
|
219
618
|
"pages",
|
|
220
619
|
"deployment",
|
|
221
620
|
"list",
|
|
@@ -224,13 +623,23 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
224
623
|
]);
|
|
225
624
|
return baseResult("production", {
|
|
226
625
|
status: r.status === 0 ? "ok" : "status-failed",
|
|
227
|
-
message:
|
|
626
|
+
message: r.status === 0
|
|
627
|
+
? (r.stdout || r.stderr).trim() || "ok"
|
|
628
|
+
: failedMessage(r, "deployment list failed"),
|
|
629
|
+
dashboardUrl: dashboardFor(project),
|
|
228
630
|
});
|
|
229
631
|
},
|
|
230
632
|
async listDeployments() {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
633
|
+
const accountFail = ensureAccountOrFail();
|
|
634
|
+
if (accountFail) {
|
|
635
|
+
return {
|
|
636
|
+
...accountFail,
|
|
637
|
+
status: "list-failed",
|
|
638
|
+
message: accountFail.message,
|
|
639
|
+
deployments: [],
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const r = wrangler([
|
|
234
643
|
"pages",
|
|
235
644
|
"deployment",
|
|
236
645
|
"list",
|
|
@@ -240,7 +649,10 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
240
649
|
return {
|
|
241
650
|
...baseResult("production", {
|
|
242
651
|
status: r.status === 0 ? "ok" : "list-failed",
|
|
243
|
-
message:
|
|
652
|
+
message: r.status === 0
|
|
653
|
+
? (r.stdout || r.stderr).trim() || "ok"
|
|
654
|
+
: failedMessage(r, "deployment list failed"),
|
|
655
|
+
dashboardUrl: dashboardFor(project),
|
|
244
656
|
}),
|
|
245
657
|
deployments: [],
|
|
246
658
|
};
|
|
@@ -250,14 +662,21 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
250
662
|
deploymentId: id,
|
|
251
663
|
status: "ok",
|
|
252
664
|
message: `Inspect deployment ${id} in the Cloudflare dashboard`,
|
|
253
|
-
dashboardUrl:
|
|
665
|
+
dashboardUrl: dashboardFor(project),
|
|
254
666
|
});
|
|
255
667
|
},
|
|
256
668
|
verifyDeployment,
|
|
257
669
|
async rollbackDeployment(id) {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
670
|
+
const accountFail = ensureAccountOrFail();
|
|
671
|
+
if (accountFail) {
|
|
672
|
+
return {
|
|
673
|
+
...accountFail,
|
|
674
|
+
deploymentId: id,
|
|
675
|
+
status: "rollback-failed",
|
|
676
|
+
message: accountFail.message,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const r = wrangler([
|
|
261
680
|
"pages",
|
|
262
681
|
"deployment",
|
|
263
682
|
"rollback",
|
|
@@ -269,14 +688,13 @@ export function createDocifyPublisher(configInput, deps = {}) {
|
|
|
269
688
|
return baseResult("production", {
|
|
270
689
|
deploymentId: id,
|
|
271
690
|
status: "rollback-failed",
|
|
272
|
-
message: (r.
|
|
273
|
-
"Rollback failed. Use the Cloudflare Pages Deployments API rollback endpoint with a scoped API token.",
|
|
691
|
+
message: failedMessage(r, "Rollback failed. Use the Cloudflare Pages Deployments API rollback endpoint with a scoped API token."),
|
|
274
692
|
});
|
|
275
693
|
}
|
|
276
694
|
return baseResult("production", {
|
|
277
695
|
deploymentId: id,
|
|
278
696
|
status: "rolled-back",
|
|
279
|
-
message: r.stdout.trim()
|
|
697
|
+
message: r.stdout.trim() || `Rolled back ${id}`,
|
|
280
698
|
});
|
|
281
699
|
},
|
|
282
700
|
async publish(opts = {}) {
|