hostedge 0.0.0-stage → 0.1.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 +28 -2
- package/dist/hostedge.js +2461 -0
- package/package.json +47 -4
package/dist/hostedge.js
ADDED
|
@@ -0,0 +1,2461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
|
|
9
|
+
// src/commands/auth.ts
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { hostname } from "node:os";
|
|
13
|
+
|
|
14
|
+
// src/config.ts
|
|
15
|
+
import { mkdir, readFile, writeFile, chmod, rm } from "node:fs/promises";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
var DEFAULT_API_URL = "https://hostedge.app";
|
|
19
|
+
function configPath(io2) {
|
|
20
|
+
const base = io2.env.HOSTEDGE_CONFIG_DIR ?? join(io2.env.XDG_CONFIG_HOME ?? join(io2.env.HOME ?? homedir(), ".config"), "hostedge");
|
|
21
|
+
return join(base, "config.json");
|
|
22
|
+
}
|
|
23
|
+
async function readJson(path) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function readConfig(io2) {
|
|
31
|
+
return await readJson(configPath(io2)) ?? {};
|
|
32
|
+
}
|
|
33
|
+
async function writeConfig(io2, config) {
|
|
34
|
+
const path = configPath(io2);
|
|
35
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
36
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
37
|
+
`, { mode: 384 });
|
|
38
|
+
await chmod(path, 384);
|
|
39
|
+
}
|
|
40
|
+
async function clearConfig(io2) {
|
|
41
|
+
await rm(configPath(io2), { force: true });
|
|
42
|
+
}
|
|
43
|
+
async function credentials(io2) {
|
|
44
|
+
const saved = await readConfig(io2);
|
|
45
|
+
return {
|
|
46
|
+
apiUrl: (io2.env.HOSTEDGE_API_URL ?? saved.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, ""),
|
|
47
|
+
token: io2.env.HOSTEDGE_TOKEN ?? saved.token ?? null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
var linkPath = (dir) => join(dir, ".hostedge", "project.json");
|
|
51
|
+
async function readLink(dir) {
|
|
52
|
+
const link = await readJson(linkPath(dir));
|
|
53
|
+
return link && typeof link.projectId === "string" ? link : null;
|
|
54
|
+
}
|
|
55
|
+
async function writeLink(dir, link) {
|
|
56
|
+
await mkdir(join(dir, ".hostedge"), { recursive: true });
|
|
57
|
+
await writeFile(linkPath(dir), `${JSON.stringify(link, null, 2)}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
async function ensureGitignored(dir, entry) {
|
|
61
|
+
const path = join(dir, ".gitignore");
|
|
62
|
+
let text = "";
|
|
63
|
+
try {
|
|
64
|
+
text = await readFile(path, "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim());
|
|
68
|
+
if (lines.includes(entry) || lines.includes(`/${entry}`)) return false;
|
|
69
|
+
await writeFile(path, `${text}${text && !text.endsWith("\n") ? "\n" : ""}${entry}
|
|
70
|
+
`);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/io.ts
|
|
75
|
+
var CliError = class extends Error {
|
|
76
|
+
constructor(message, exitCode = 1) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.exitCode = exitCode;
|
|
79
|
+
}
|
|
80
|
+
exitCode;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/api.ts
|
|
84
|
+
var ApiError = class extends CliError {
|
|
85
|
+
constructor(message, status, code) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.status = status;
|
|
88
|
+
this.code = code;
|
|
89
|
+
}
|
|
90
|
+
status;
|
|
91
|
+
code;
|
|
92
|
+
};
|
|
93
|
+
function createApi(io2, apiUrl, token) {
|
|
94
|
+
async function raw(path, init2 = {}) {
|
|
95
|
+
const headers = new Headers(init2.headers);
|
|
96
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
97
|
+
headers.set("user-agent", "hostedge-cli/0.1.0");
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await io2.fetch(`${apiUrl}${path}`, { ...init2, headers });
|
|
101
|
+
} catch {
|
|
102
|
+
throw new CliError(`Could not reach ${apiUrl}. Check your connection, or HOSTEDGE_API_URL.`);
|
|
103
|
+
}
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const body = await response.json().catch(() => ({}));
|
|
106
|
+
const fields = body.error?.fields ? ` ${Object.entries(body.error.fields).map(([k, v]) => `${k}: ${v}`).join("; ")}` : "";
|
|
107
|
+
const message = response.status === 401 ? `${body.error?.message ?? "Not signed in."} Run \`hostedge login\`.` : `${body.error?.message ?? `HostEdge answered ${response.status}.`}${fields}`;
|
|
108
|
+
throw new ApiError(message, response.status, body.error?.code ?? null);
|
|
109
|
+
}
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
apiUrl,
|
|
114
|
+
raw,
|
|
115
|
+
async request(path, init2 = {}) {
|
|
116
|
+
const { json, ...rest } = init2;
|
|
117
|
+
let request = rest;
|
|
118
|
+
if (json !== void 0) {
|
|
119
|
+
const headers = new Headers(rest.headers);
|
|
120
|
+
headers.set("content-type", "application/json");
|
|
121
|
+
request = { ...rest, method: rest.method ?? "POST", headers, body: JSON.stringify(json) };
|
|
122
|
+
}
|
|
123
|
+
const response = await raw(path, request);
|
|
124
|
+
return await response.json();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function connect(io2) {
|
|
129
|
+
const { apiUrl, token } = await credentials(io2);
|
|
130
|
+
if (!token) throw new CliError("Not signed in. Run `hostedge login` (or set HOSTEDGE_TOKEN).");
|
|
131
|
+
return createApi(io2, apiUrl, token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/output.ts
|
|
135
|
+
function styles(io2) {
|
|
136
|
+
const on = io2.isTty && !io2.env.NO_COLOR;
|
|
137
|
+
const wrap = (open, close) => (text) => on ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
138
|
+
return {
|
|
139
|
+
bold: wrap(1, 22),
|
|
140
|
+
dim: wrap(2, 22),
|
|
141
|
+
red: wrap(31, 39),
|
|
142
|
+
green: wrap(32, 39),
|
|
143
|
+
yellow: wrap(33, 39),
|
|
144
|
+
cyan: wrap(36, 39)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function formatBytes(bytes) {
|
|
148
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
149
|
+
const units = ["KB", "MB", "GB"];
|
|
150
|
+
let value = bytes / 1024;
|
|
151
|
+
let unit = 0;
|
|
152
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
153
|
+
value /= 1024;
|
|
154
|
+
unit += 1;
|
|
155
|
+
}
|
|
156
|
+
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/commands/auth.ts
|
|
160
|
+
var LOGIN_TIMEOUT_MS = 5 * 6e4;
|
|
161
|
+
var DONE_PAGE = `<!doctype html><meta charset="utf-8"><title>hostedge CLI</title>
|
|
162
|
+
<body style="font:16px system-ui;margin:4rem auto;max-width:28rem;color:#16161a;background:#f7f6f2">
|
|
163
|
+
<p><strong>The hostedge CLI is signed in.</strong></p><p>You can close this tab and return to your terminal.</p>`;
|
|
164
|
+
async function browserLogin(io2, apiUrl) {
|
|
165
|
+
const state = randomBytes(24).toString("base64url");
|
|
166
|
+
let resolveToken = () => void 0;
|
|
167
|
+
const received = new Promise((resolve5) => {
|
|
168
|
+
resolveToken = resolve5;
|
|
169
|
+
});
|
|
170
|
+
const server = createServer((req, res) => {
|
|
171
|
+
const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
172
|
+
const token = url2.searchParams.get("token");
|
|
173
|
+
if (url2.pathname !== "/callback" || url2.searchParams.get("state") !== state || !token) {
|
|
174
|
+
res.writeHead(400, { "content-type": "text/plain" }).end("Unexpected request.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DONE_PAGE);
|
|
178
|
+
resolveToken(token);
|
|
179
|
+
});
|
|
180
|
+
await new Promise((resolve5) => server.listen(0, "127.0.0.1", resolve5));
|
|
181
|
+
const address = server.address();
|
|
182
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
183
|
+
const url = `${apiUrl}/cli/authorize?${new URLSearchParams({ port: String(port), state, host: hostname() })}`;
|
|
184
|
+
const s = styles(io2);
|
|
185
|
+
const opened = await io2.openUrl(url);
|
|
186
|
+
io2.stdout(
|
|
187
|
+
opened ? `Opened your browser to authorize the CLI. If nothing happened, visit:
|
|
188
|
+
${s.bold(url)}
|
|
189
|
+
` : `Open this URL in a browser to authorize the CLI:
|
|
190
|
+
${s.bold(url)}
|
|
191
|
+
`
|
|
192
|
+
);
|
|
193
|
+
let timer;
|
|
194
|
+
const timeout = new Promise((_, reject) => {
|
|
195
|
+
timer = setTimeout(
|
|
196
|
+
() => reject(new CliError("Timed out waiting for the browser. Run `hostedge login` again.")),
|
|
197
|
+
LOGIN_TIMEOUT_MS
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
return await Promise.race([received, timeout]);
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
server.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function login(io2, options) {
|
|
208
|
+
const saved = await readConfig(io2);
|
|
209
|
+
const apiUrl = (options.api ?? io2.env.HOSTEDGE_API_URL ?? saved.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
210
|
+
const token = options.token ?? await browserLogin(io2, apiUrl);
|
|
211
|
+
const identity = await createApi(io2, apiUrl, token).request("/api/v1/me");
|
|
212
|
+
await writeConfig(io2, { apiUrl, token, workspace: identity.workspace.slug });
|
|
213
|
+
const s = styles(io2);
|
|
214
|
+
io2.stdout(
|
|
215
|
+
`${s.green("\u2713")} Signed in as ${s.bold(identity.user.email)} in ${s.bold(identity.workspace.name)}.
|
|
216
|
+
`
|
|
217
|
+
);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
async function logout(io2) {
|
|
221
|
+
await clearConfig(io2);
|
|
222
|
+
io2.stdout(
|
|
223
|
+
"Signed out on this computer. The token still exists until you revoke it in Settings \u2192 API tokens.\n"
|
|
224
|
+
);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
async function whoami(io2) {
|
|
228
|
+
const api = await connect(io2);
|
|
229
|
+
const me = await api.request("/api/v1/me");
|
|
230
|
+
const { apiUrl } = await credentials(io2);
|
|
231
|
+
io2.stdout(
|
|
232
|
+
`${me.user.email} in ${me.workspace.name} (${me.workspace.slug}) on ${apiUrl}
|
|
233
|
+
Scopes: ${me.scopes.join(", ")}
|
|
234
|
+
`
|
|
235
|
+
);
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ../../packages/core/src/plans.ts
|
|
240
|
+
var MB = 1024 * 1024;
|
|
241
|
+
var GB = 1024 * MB;
|
|
242
|
+
var PLANS = {
|
|
243
|
+
free: {
|
|
244
|
+
id: "free",
|
|
245
|
+
name: "Free",
|
|
246
|
+
priceMonthlyUsd: 0,
|
|
247
|
+
limits: {
|
|
248
|
+
projects: 3,
|
|
249
|
+
customDomains: 1,
|
|
250
|
+
requestsPerMonth: 1e5,
|
|
251
|
+
bandwidthBytesPerMonth: 10 * GB,
|
|
252
|
+
members: 1,
|
|
253
|
+
cpuMs: 10,
|
|
254
|
+
subrequests: 50,
|
|
255
|
+
maxDeploymentBytes: 50 * MB,
|
|
256
|
+
maxFiles: 2e3,
|
|
257
|
+
runtimeLogsPerDay: 2e4,
|
|
258
|
+
analyticsEventsPerMonth: 2e4,
|
|
259
|
+
analyticsRetentionDays: 30,
|
|
260
|
+
storageResourcesPerProject: 3,
|
|
261
|
+
storageBytes: 1 * GB,
|
|
262
|
+
kvOperationsPerMonth: 1e6,
|
|
263
|
+
cronJobsPerProject: 2,
|
|
264
|
+
cronMinIntervalMinutes: 60,
|
|
265
|
+
formSubmissionsPerMonth: 100,
|
|
266
|
+
formRetentionDays: 90,
|
|
267
|
+
monitorsPerProject: 2,
|
|
268
|
+
monitorMinIntervalSeconds: 300
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
starter: {
|
|
272
|
+
id: "starter",
|
|
273
|
+
name: "Starter",
|
|
274
|
+
priceMonthlyUsd: 9,
|
|
275
|
+
limits: {
|
|
276
|
+
projects: 10,
|
|
277
|
+
customDomains: 5,
|
|
278
|
+
requestsPerMonth: 1e6,
|
|
279
|
+
bandwidthBytesPerMonth: 100 * GB,
|
|
280
|
+
members: 3,
|
|
281
|
+
cpuMs: 30,
|
|
282
|
+
subrequests: 100,
|
|
283
|
+
maxDeploymentBytes: 250 * MB,
|
|
284
|
+
maxFiles: 2e4,
|
|
285
|
+
runtimeLogsPerDay: 2e5,
|
|
286
|
+
analyticsEventsPerMonth: 2e5,
|
|
287
|
+
analyticsRetentionDays: 365,
|
|
288
|
+
storageResourcesPerProject: 10,
|
|
289
|
+
storageBytes: 10 * GB,
|
|
290
|
+
kvOperationsPerMonth: 1e7,
|
|
291
|
+
cronJobsPerProject: 10,
|
|
292
|
+
cronMinIntervalMinutes: 5,
|
|
293
|
+
formSubmissionsPerMonth: 2e3,
|
|
294
|
+
formRetentionDays: 365,
|
|
295
|
+
monitorsPerProject: 10,
|
|
296
|
+
monitorMinIntervalSeconds: 60
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
pro: {
|
|
300
|
+
id: "pro",
|
|
301
|
+
name: "Pro",
|
|
302
|
+
priceMonthlyUsd: 29,
|
|
303
|
+
limits: {
|
|
304
|
+
projects: 50,
|
|
305
|
+
customDomains: 25,
|
|
306
|
+
requestsPerMonth: 1e7,
|
|
307
|
+
bandwidthBytesPerMonth: 500 * GB,
|
|
308
|
+
members: 10,
|
|
309
|
+
cpuMs: 50,
|
|
310
|
+
subrequests: 500,
|
|
311
|
+
maxDeploymentBytes: 250 * MB,
|
|
312
|
+
maxFiles: 2e4,
|
|
313
|
+
runtimeLogsPerDay: 2e6,
|
|
314
|
+
analyticsEventsPerMonth: 2e6,
|
|
315
|
+
analyticsRetentionDays: 730,
|
|
316
|
+
storageResourcesPerProject: 25,
|
|
317
|
+
storageBytes: 100 * GB,
|
|
318
|
+
kvOperationsPerMonth: 5e7,
|
|
319
|
+
cronJobsPerProject: 50,
|
|
320
|
+
cronMinIntervalMinutes: 1,
|
|
321
|
+
formSubmissionsPerMonth: 2e4,
|
|
322
|
+
formRetentionDays: 730,
|
|
323
|
+
monitorsPerProject: 50,
|
|
324
|
+
monitorMinIntervalSeconds: 60
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
agency: {
|
|
328
|
+
id: "agency",
|
|
329
|
+
name: "Agency",
|
|
330
|
+
priceMonthlyUsd: 79,
|
|
331
|
+
limits: {
|
|
332
|
+
projects: null,
|
|
333
|
+
customDomains: 100,
|
|
334
|
+
requestsPerMonth: 5e7,
|
|
335
|
+
bandwidthBytesPerMonth: 2e3 * GB,
|
|
336
|
+
members: 25,
|
|
337
|
+
cpuMs: 50,
|
|
338
|
+
subrequests: 1e3,
|
|
339
|
+
maxDeploymentBytes: 250 * MB,
|
|
340
|
+
maxFiles: 2e4,
|
|
341
|
+
runtimeLogsPerDay: 1e7,
|
|
342
|
+
analyticsEventsPerMonth: 1e7,
|
|
343
|
+
analyticsRetentionDays: 1095,
|
|
344
|
+
storageResourcesPerProject: 50,
|
|
345
|
+
storageBytes: 500 * GB,
|
|
346
|
+
kvOperationsPerMonth: 2e8,
|
|
347
|
+
cronJobsPerProject: 100,
|
|
348
|
+
cronMinIntervalMinutes: 1,
|
|
349
|
+
formSubmissionsPerMonth: 1e5,
|
|
350
|
+
formRetentionDays: 1095,
|
|
351
|
+
monitorsPerProject: 100,
|
|
352
|
+
monitorMinIntervalSeconds: 60
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var MAX_FILE_BYTES = 25 * MB;
|
|
357
|
+
|
|
358
|
+
// ../../packages/core/src/projects/upload.ts
|
|
359
|
+
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
360
|
+
"html",
|
|
361
|
+
"htm",
|
|
362
|
+
"css",
|
|
363
|
+
"js",
|
|
364
|
+
"mjs",
|
|
365
|
+
"cjs",
|
|
366
|
+
"jsx",
|
|
367
|
+
"ts",
|
|
368
|
+
"tsx",
|
|
369
|
+
"json",
|
|
370
|
+
"map",
|
|
371
|
+
"webmanifest",
|
|
372
|
+
"txt",
|
|
373
|
+
"md",
|
|
374
|
+
"mdx",
|
|
375
|
+
"xml",
|
|
376
|
+
"csv",
|
|
377
|
+
"svg",
|
|
378
|
+
"png",
|
|
379
|
+
"jpg",
|
|
380
|
+
"jpeg",
|
|
381
|
+
"gif",
|
|
382
|
+
"webp",
|
|
383
|
+
"avif",
|
|
384
|
+
"ico",
|
|
385
|
+
"bmp",
|
|
386
|
+
"woff",
|
|
387
|
+
"woff2",
|
|
388
|
+
"ttf",
|
|
389
|
+
"otf",
|
|
390
|
+
"eot",
|
|
391
|
+
"wasm",
|
|
392
|
+
"pdf",
|
|
393
|
+
"mp4",
|
|
394
|
+
"webm",
|
|
395
|
+
"ogg",
|
|
396
|
+
"mp3",
|
|
397
|
+
"wav",
|
|
398
|
+
"m4a",
|
|
399
|
+
"vtt",
|
|
400
|
+
"astro",
|
|
401
|
+
"vue",
|
|
402
|
+
"svelte",
|
|
403
|
+
// Template languages that static site generators build from (Eleventy and friends).
|
|
404
|
+
"njk",
|
|
405
|
+
"liquid",
|
|
406
|
+
"hbs",
|
|
407
|
+
"handlebars",
|
|
408
|
+
"mustache",
|
|
409
|
+
"ejs",
|
|
410
|
+
"pug",
|
|
411
|
+
"webc",
|
|
412
|
+
"scss",
|
|
413
|
+
"sass",
|
|
414
|
+
"less",
|
|
415
|
+
"yaml",
|
|
416
|
+
"yml",
|
|
417
|
+
"toml",
|
|
418
|
+
"lock",
|
|
419
|
+
"gitignore",
|
|
420
|
+
"npmrc",
|
|
421
|
+
"nvmrc",
|
|
422
|
+
"editorconfig",
|
|
423
|
+
"prettierrc",
|
|
424
|
+
"eslintrc",
|
|
425
|
+
"browserslistrc",
|
|
426
|
+
"gltf",
|
|
427
|
+
"glb",
|
|
428
|
+
"zip"
|
|
429
|
+
]);
|
|
430
|
+
var ALLOWED_BARE_NAMES = /* @__PURE__ */ new Set([
|
|
431
|
+
"license",
|
|
432
|
+
"readme",
|
|
433
|
+
"cname",
|
|
434
|
+
"_headers",
|
|
435
|
+
"_redirects",
|
|
436
|
+
"dockerfile",
|
|
437
|
+
"procfile",
|
|
438
|
+
"makefile",
|
|
439
|
+
"notice",
|
|
440
|
+
"authors",
|
|
441
|
+
"changelog"
|
|
442
|
+
]);
|
|
443
|
+
var BLOCKED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
444
|
+
".git",
|
|
445
|
+
".svn",
|
|
446
|
+
".hg",
|
|
447
|
+
"node_modules",
|
|
448
|
+
".DS_Store",
|
|
449
|
+
"__MACOSX",
|
|
450
|
+
".wrangler",
|
|
451
|
+
".vercel",
|
|
452
|
+
".netlify"
|
|
453
|
+
]);
|
|
454
|
+
var SECRET_NAMES = /^(\.env(\..*)?|.*\.pem|.*\.key|id_rsa.*|\.npmrc\.local|\.dev\.vars.*)$/i;
|
|
455
|
+
var HASH_PATTERN = /^[0-9a-f]{32}$/;
|
|
456
|
+
function normalisePath(raw) {
|
|
457
|
+
if (typeof raw !== "string" || raw.length === 0) return { ok: false, reason: "Empty file name." };
|
|
458
|
+
if (raw.length > 512) return { ok: false, reason: "Path is longer than 512 characters." };
|
|
459
|
+
if (/[\u0000-\u001f\u007f]/.test(raw))
|
|
460
|
+
return { ok: false, reason: "Path contains control characters." };
|
|
461
|
+
const path = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
462
|
+
if (path.startsWith("/") || /^[a-zA-Z]:/.test(path))
|
|
463
|
+
return { ok: false, reason: "Absolute paths are not allowed." };
|
|
464
|
+
const segments = path.split("/");
|
|
465
|
+
if (segments.some((s) => s === "..")) return { ok: false, reason: "Paths may not contain \u201C..\u201D." };
|
|
466
|
+
if (segments.some((s) => s === "" || s === "."))
|
|
467
|
+
return { ok: false, reason: "Path has an empty or \u201C.\u201D segment." };
|
|
468
|
+
return { ok: true, path };
|
|
469
|
+
}
|
|
470
|
+
function isIgnoredPath(path) {
|
|
471
|
+
return path.split("/").some((segment) => BLOCKED_SEGMENTS.has(segment));
|
|
472
|
+
}
|
|
473
|
+
function fileTypeProblem(path) {
|
|
474
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
475
|
+
if (SECRET_NAMES.test(name))
|
|
476
|
+
return "Looks like a secret (env file or key). Remove it \u2014 set secrets as environment variables instead.";
|
|
477
|
+
const dot = name.lastIndexOf(".");
|
|
478
|
+
const ext = dot > 0 ? name.slice(dot + 1).toLowerCase() : name.startsWith(".") ? name.slice(1).toLowerCase() : "";
|
|
479
|
+
if (!ext || dot <= 0) {
|
|
480
|
+
if (name.startsWith(".") && ALLOWED_EXTENSIONS.has(ext)) return null;
|
|
481
|
+
return ALLOWED_BARE_NAMES.has(name.toLowerCase()) ? null : `\u201C${name}\u201D has no file extension HostEdge can serve.`;
|
|
482
|
+
}
|
|
483
|
+
return ALLOWED_EXTENSIONS.has(ext) ? null : `.${ext} files can\u2019t be deployed.`;
|
|
484
|
+
}
|
|
485
|
+
function checkManifest(entries, limits) {
|
|
486
|
+
const problems = [];
|
|
487
|
+
const ignored = [];
|
|
488
|
+
const files = [];
|
|
489
|
+
const seen = /* @__PURE__ */ new Set();
|
|
490
|
+
if (!Array.isArray(entries))
|
|
491
|
+
return {
|
|
492
|
+
files,
|
|
493
|
+
ignored,
|
|
494
|
+
problems: [{ path: "", reason: "Manifest must be a list of files." }],
|
|
495
|
+
totalBytes: 0
|
|
496
|
+
};
|
|
497
|
+
for (const raw of entries) {
|
|
498
|
+
const entry = raw;
|
|
499
|
+
const rawPath = typeof entry?.path === "string" ? entry.path : "";
|
|
500
|
+
const normal = normalisePath(rawPath);
|
|
501
|
+
if (!normal.ok) {
|
|
502
|
+
problems.push({ path: rawPath, reason: normal.reason });
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const { path } = normal;
|
|
506
|
+
if (isIgnoredPath(path)) {
|
|
507
|
+
ignored.push(path);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const lower = path.toLowerCase();
|
|
511
|
+
if (seen.has(lower)) {
|
|
512
|
+
problems.push({ path, reason: "Two files share this path (names differ only in case)." });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
seen.add(lower);
|
|
516
|
+
const size = entry?.size;
|
|
517
|
+
const hash = entry?.hash;
|
|
518
|
+
if (typeof size !== "number" || !Number.isInteger(size) || size < 0) {
|
|
519
|
+
problems.push({ path, reason: "Invalid file size." });
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (size > MAX_FILE_BYTES) {
|
|
523
|
+
problems.push({
|
|
524
|
+
path,
|
|
525
|
+
reason: `File is ${(size / 1024 / 1024).toFixed(1)} MB; the limit per file is 25 MB.`
|
|
526
|
+
});
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (typeof hash !== "string" || !HASH_PATTERN.test(hash)) {
|
|
530
|
+
problems.push({ path, reason: "Invalid content hash." });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const typeProblem = fileTypeProblem(path);
|
|
534
|
+
if (typeProblem) {
|
|
535
|
+
problems.push({ path, reason: typeProblem });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
files.push({ path, size, hash });
|
|
539
|
+
}
|
|
540
|
+
const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
541
|
+
if (files.length === 0 && problems.length === 0)
|
|
542
|
+
problems.push({ path: "", reason: "No files to deploy." });
|
|
543
|
+
if (files.length > limits.maxFiles)
|
|
544
|
+
problems.push({
|
|
545
|
+
path: "",
|
|
546
|
+
reason: `This deploy has ${files.length.toLocaleString("en-US")} files; your plan allows ${limits.maxFiles.toLocaleString("en-US")}.`
|
|
547
|
+
});
|
|
548
|
+
if (totalBytes > limits.maxDeploymentBytes) {
|
|
549
|
+
problems.push({
|
|
550
|
+
path: "",
|
|
551
|
+
reason: `This deploy is ${(totalBytes / 1024 / 1024).toFixed(1)} MB; your plan allows ${Math.round(limits.maxDeploymentBytes / 1024 / 1024)} MB.`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return { files, ignored, problems, totalBytes };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ../../packages/core/src/projects/frameworks.ts
|
|
558
|
+
var NEXT_CACHE_OUTPUT = "_hostedge/next-cache";
|
|
559
|
+
var OPEN_NEXT_DEFAULT_CONFIG = `// Written by HostEdge because this project had no open-next.config.ts.
|
|
560
|
+
// Commit your own to change it: https://opennext.js.org/cloudflare/caching
|
|
561
|
+
import { defineCloudflareConfig, getCloudflareContext } from '@opennextjs/cloudflare'
|
|
562
|
+
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache'
|
|
563
|
+
import d1NextTagCache from '@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache'
|
|
564
|
+
|
|
565
|
+
const pending = new Set<string>()
|
|
566
|
+
|
|
567
|
+
/** Stale-while-revalidate: answer now, regenerate the page after the response. */
|
|
568
|
+
const queue = {
|
|
569
|
+
name: 'hostedge-queue',
|
|
570
|
+
async send(message: {
|
|
571
|
+
MessageBody: { host: string; url: string }
|
|
572
|
+
MessageDeduplicationId: string
|
|
573
|
+
}) {
|
|
574
|
+
const { host, url } = message.MessageBody
|
|
575
|
+
const id = message.MessageDeduplicationId
|
|
576
|
+
if (pending.has(id)) return
|
|
577
|
+
pending.add(id)
|
|
578
|
+
const protocol = host.includes('localhost') ? 'http' : 'https'
|
|
579
|
+
const work = fetch(\`\${protocol}://\${host}\${url}\`, {
|
|
580
|
+
method: 'HEAD',
|
|
581
|
+
headers: {
|
|
582
|
+
'x-prerender-revalidate': process.env.NEXT_PREVIEW_MODE_ID ?? '',
|
|
583
|
+
'x-isr': '1',
|
|
584
|
+
},
|
|
585
|
+
signal: AbortSignal.timeout(10_000),
|
|
586
|
+
})
|
|
587
|
+
.then((response) => response.body?.cancel())
|
|
588
|
+
.catch(() => undefined)
|
|
589
|
+
.finally(() => pending.delete(id))
|
|
590
|
+
getCloudflareContext().ctx.waitUntil(work)
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export default defineCloudflareConfig({
|
|
595
|
+
incrementalCache: r2IncrementalCache,
|
|
596
|
+
tagCache: d1NextTagCache,
|
|
597
|
+
queue: () => Promise.resolve(queue),
|
|
598
|
+
})
|
|
599
|
+
`;
|
|
600
|
+
var OPEN_NEXT_DEFAULT_WRANGLER = `// Written by HostEdge because this project had no wrangler config.
|
|
601
|
+
{
|
|
602
|
+
"name": "hostedge-app",
|
|
603
|
+
"main": ".open-next/worker.js",
|
|
604
|
+
"compatibility_date": "{{compatibilityDate}}",
|
|
605
|
+
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
|
606
|
+
"assets": { "directory": ".open-next/assets", "binding": "ASSETS" },
|
|
607
|
+
"r2_buckets": [{ "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "hostedge-next-cache" }],
|
|
608
|
+
"d1_databases": [
|
|
609
|
+
{
|
|
610
|
+
"binding": "NEXT_TAG_CACHE_D1",
|
|
611
|
+
"database_name": "hostedge-next-tags",
|
|
612
|
+
"database_id": "00000000-0000-0000-0000-000000000000"
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
`;
|
|
617
|
+
var npm = (outputDirectory, buildCommand = "npm run build") => ({
|
|
618
|
+
installCommand: "npm install",
|
|
619
|
+
buildCommand,
|
|
620
|
+
outputDirectory
|
|
621
|
+
});
|
|
622
|
+
var FRAMEWORK_INFO = {
|
|
623
|
+
static: {
|
|
624
|
+
id: "static",
|
|
625
|
+
name: "Static HTML",
|
|
626
|
+
description: "No build step. Your files are deployed as they are.",
|
|
627
|
+
group: "static",
|
|
628
|
+
defaults: { installCommand: null, buildCommand: null, outputDirectory: null },
|
|
629
|
+
notFound: "404-page"
|
|
630
|
+
},
|
|
631
|
+
astro: {
|
|
632
|
+
id: "astro",
|
|
633
|
+
name: "Astro",
|
|
634
|
+
description: "Static build to dist/. Your 404 page answers unknown paths.",
|
|
635
|
+
group: "static",
|
|
636
|
+
defaults: npm("dist"),
|
|
637
|
+
notFound: "404-page",
|
|
638
|
+
outputHint: "Astro writes dist/ for static output. With @astrojs/cloudflare, pick Astro (server)."
|
|
639
|
+
},
|
|
640
|
+
next: {
|
|
641
|
+
id: "next",
|
|
642
|
+
name: "Next.js (static export)",
|
|
643
|
+
description: 'Static export to out/. Needs output: "export" in next.config.',
|
|
644
|
+
group: "app",
|
|
645
|
+
defaults: npm("out"),
|
|
646
|
+
notFound: "404-page",
|
|
647
|
+
outputHint: 'Next.js writes out/ only when next.config sets output: "export". For server features, pick Next.js (full, OpenNext).'
|
|
648
|
+
},
|
|
649
|
+
nuxt: {
|
|
650
|
+
id: "nuxt",
|
|
651
|
+
name: "Nuxt (static)",
|
|
652
|
+
description: "Prerendered with nuxi generate to .output/public.",
|
|
653
|
+
group: "app",
|
|
654
|
+
defaults: npm(".output/public", "npx nuxi generate"),
|
|
655
|
+
notFound: "404-page",
|
|
656
|
+
outputHint: "Nuxt writes .output/public with `nuxi generate`. For a server, pick Nuxt (server)."
|
|
657
|
+
},
|
|
658
|
+
sveltekit: {
|
|
659
|
+
id: "sveltekit",
|
|
660
|
+
name: "SvelteKit (static)",
|
|
661
|
+
description: "adapter-static build to build/.",
|
|
662
|
+
group: "app",
|
|
663
|
+
defaults: npm("build"),
|
|
664
|
+
notFound: "404-page",
|
|
665
|
+
outputHint: "SvelteKit writes build/ with @sveltejs/adapter-static. With adapter-cloudflare, pick SvelteKit (server)."
|
|
666
|
+
},
|
|
667
|
+
"react-router": {
|
|
668
|
+
id: "react-router",
|
|
669
|
+
name: "React Router (SPA mode)",
|
|
670
|
+
description: "ssr: false build to build/client, with client-side routing.",
|
|
671
|
+
group: "app",
|
|
672
|
+
defaults: npm("build/client"),
|
|
673
|
+
notFound: "single-page-application",
|
|
674
|
+
outputHint: "React Router writes build/client when react-router.config sets ssr: false."
|
|
675
|
+
},
|
|
676
|
+
"next-server": {
|
|
677
|
+
id: "next-server",
|
|
678
|
+
name: "Next.js (full, OpenNext)",
|
|
679
|
+
description: "Server rendering, API routes, server actions and middleware, through @opennextjs/cloudflare.",
|
|
680
|
+
group: "server",
|
|
681
|
+
defaults: npm(".open-next/assets", "npx opennextjs-cloudflare build"),
|
|
682
|
+
notFound: "none",
|
|
683
|
+
server: {
|
|
684
|
+
entry: ".open-next/worker.js",
|
|
685
|
+
packages: ["@opennextjs/cloudflare@^1", "wrangler@^4"],
|
|
686
|
+
files: [
|
|
687
|
+
{
|
|
688
|
+
path: "open-next.config.ts",
|
|
689
|
+
content: OPEN_NEXT_DEFAULT_CONFIG,
|
|
690
|
+
unless: ["open-next.config.ts"]
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
path: "wrangler.jsonc",
|
|
694
|
+
content: OPEN_NEXT_DEFAULT_WRANGLER,
|
|
695
|
+
unless: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
extraOutput: { from: ".open-next/cache", to: NEXT_CACHE_OUTPUT }
|
|
699
|
+
},
|
|
700
|
+
outputHint: "OpenNext writes .open-next/. HostEdge adds @opennextjs/cloudflare and its config when they are missing."
|
|
701
|
+
},
|
|
702
|
+
"astro-server": {
|
|
703
|
+
id: "astro-server",
|
|
704
|
+
name: "Astro (server)",
|
|
705
|
+
description: "On-demand rendering and endpoints with the @astrojs/cloudflare adapter.",
|
|
706
|
+
group: "server",
|
|
707
|
+
defaults: npm("dist/client"),
|
|
708
|
+
notFound: "none",
|
|
709
|
+
server: { entry: null },
|
|
710
|
+
outputHint: "Astro with @astrojs/cloudflare writes dist/client and dist/server."
|
|
711
|
+
},
|
|
712
|
+
"sveltekit-server": {
|
|
713
|
+
id: "sveltekit-server",
|
|
714
|
+
name: "SvelteKit (server)",
|
|
715
|
+
description: "Server load functions and form actions with @sveltejs/adapter-cloudflare.",
|
|
716
|
+
group: "server",
|
|
717
|
+
defaults: npm(".svelte-kit/cloudflare"),
|
|
718
|
+
notFound: "none",
|
|
719
|
+
server: { entry: ".svelte-kit/cloudflare/_worker.js" },
|
|
720
|
+
outputHint: "SvelteKit with @sveltejs/adapter-cloudflare writes .svelte-kit/cloudflare."
|
|
721
|
+
},
|
|
722
|
+
"nuxt-server": {
|
|
723
|
+
id: "nuxt-server",
|
|
724
|
+
name: "Nuxt (server)",
|
|
725
|
+
description: "Server routes and SSR with the Nitro cloudflare_module preset.",
|
|
726
|
+
group: "server",
|
|
727
|
+
defaults: npm(".output/public", "npx nuxi build --preset=cloudflare_module"),
|
|
728
|
+
notFound: "none",
|
|
729
|
+
server: { entry: ".output/server/index.mjs" },
|
|
730
|
+
outputHint: "Nuxt writes .output/public and .output/server with the cloudflare_module preset."
|
|
731
|
+
},
|
|
732
|
+
"react-router-server": {
|
|
733
|
+
id: "react-router-server",
|
|
734
|
+
name: "React Router (server)",
|
|
735
|
+
description: "Loaders, actions and SSR with @cloudflare/vite-plugin.",
|
|
736
|
+
group: "server",
|
|
737
|
+
defaults: npm("build/client"),
|
|
738
|
+
notFound: "none",
|
|
739
|
+
server: { entry: null },
|
|
740
|
+
outputHint: "React Router with @cloudflare/vite-plugin writes build/client and build/server."
|
|
741
|
+
},
|
|
742
|
+
"vite-react": {
|
|
743
|
+
id: "vite-react",
|
|
744
|
+
name: "Vite + React",
|
|
745
|
+
description: "Single-page app in dist/. Unknown paths serve index.html.",
|
|
746
|
+
group: "app",
|
|
747
|
+
defaults: npm("dist"),
|
|
748
|
+
notFound: "single-page-application"
|
|
749
|
+
},
|
|
750
|
+
"vite-vue": {
|
|
751
|
+
id: "vite-vue",
|
|
752
|
+
name: "Vite + Vue",
|
|
753
|
+
description: "Single-page app in dist/. Unknown paths serve index.html.",
|
|
754
|
+
group: "app",
|
|
755
|
+
defaults: npm("dist"),
|
|
756
|
+
notFound: "single-page-application"
|
|
757
|
+
},
|
|
758
|
+
"vite-svelte": {
|
|
759
|
+
id: "vite-svelte",
|
|
760
|
+
name: "Vite + Svelte",
|
|
761
|
+
description: "Single-page app in dist/. Unknown paths serve index.html.",
|
|
762
|
+
group: "app",
|
|
763
|
+
defaults: npm("dist"),
|
|
764
|
+
notFound: "single-page-application"
|
|
765
|
+
},
|
|
766
|
+
vite: {
|
|
767
|
+
id: "vite",
|
|
768
|
+
name: "Vite",
|
|
769
|
+
description: "Solid, Preact, Lit or plain Vite in dist/, with client-side routing.",
|
|
770
|
+
group: "app",
|
|
771
|
+
defaults: npm("dist"),
|
|
772
|
+
notFound: "single-page-application"
|
|
773
|
+
},
|
|
774
|
+
angular: {
|
|
775
|
+
id: "angular",
|
|
776
|
+
name: "Angular",
|
|
777
|
+
description: "Browser build in dist/<project>/browser, with client-side routing.",
|
|
778
|
+
group: "app",
|
|
779
|
+
defaults: npm("dist/app/browser"),
|
|
780
|
+
notFound: "single-page-application",
|
|
781
|
+
outputHint: "Angular writes dist/<project name>/browser. Set the output directory to match angular.json."
|
|
782
|
+
},
|
|
783
|
+
"create-react-app": {
|
|
784
|
+
id: "create-react-app",
|
|
785
|
+
name: "Create React App",
|
|
786
|
+
description: "Single-page app in build/. Unknown paths serve index.html.",
|
|
787
|
+
group: "app",
|
|
788
|
+
defaults: npm("build"),
|
|
789
|
+
notFound: "single-page-application"
|
|
790
|
+
},
|
|
791
|
+
gatsby: {
|
|
792
|
+
id: "gatsby",
|
|
793
|
+
name: "Gatsby",
|
|
794
|
+
description: "Static build to public/.",
|
|
795
|
+
group: "static",
|
|
796
|
+
defaults: npm("public"),
|
|
797
|
+
notFound: "404-page"
|
|
798
|
+
},
|
|
799
|
+
docusaurus: {
|
|
800
|
+
id: "docusaurus",
|
|
801
|
+
name: "Docusaurus",
|
|
802
|
+
description: "Documentation site built to build/.",
|
|
803
|
+
group: "docs",
|
|
804
|
+
defaults: npm("build"),
|
|
805
|
+
notFound: "404-page"
|
|
806
|
+
},
|
|
807
|
+
vitepress: {
|
|
808
|
+
id: "vitepress",
|
|
809
|
+
name: "VitePress",
|
|
810
|
+
description: "Documentation site built to .vitepress/dist.",
|
|
811
|
+
group: "docs",
|
|
812
|
+
defaults: npm("docs/.vitepress/dist", "npm run docs:build"),
|
|
813
|
+
notFound: "404-page",
|
|
814
|
+
outputHint: "VitePress writes <docs folder>/.vitepress/dist."
|
|
815
|
+
},
|
|
816
|
+
eleventy: {
|
|
817
|
+
id: "eleventy",
|
|
818
|
+
name: "Eleventy",
|
|
819
|
+
description: "Static build to _site/.",
|
|
820
|
+
group: "static",
|
|
821
|
+
defaults: npm("_site", "npx @11ty/eleventy"),
|
|
822
|
+
notFound: "404-page"
|
|
823
|
+
},
|
|
824
|
+
hono: {
|
|
825
|
+
id: "hono",
|
|
826
|
+
name: "Hono",
|
|
827
|
+
description: "A Worker (dist/_worker.js) with static files served alongside.",
|
|
828
|
+
group: "worker",
|
|
829
|
+
defaults: npm("dist"),
|
|
830
|
+
notFound: "none",
|
|
831
|
+
outputHint: "The build must write dist/_worker.js (a single bundled file)."
|
|
832
|
+
},
|
|
833
|
+
other: {
|
|
834
|
+
id: "other",
|
|
835
|
+
name: "Other",
|
|
836
|
+
description: "Any build that writes static files, or a _worker.js, to a folder.",
|
|
837
|
+
group: "static",
|
|
838
|
+
defaults: npm("dist"),
|
|
839
|
+
notFound: "404-page"
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
var LOCKFILES = [
|
|
843
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
844
|
+
["yarn.lock", "yarn"],
|
|
845
|
+
["package-lock.json", "npm"],
|
|
846
|
+
["npm-shrinkwrap.json", "npm"]
|
|
847
|
+
];
|
|
848
|
+
function installCommandFor(pm, locked, yarnBerry) {
|
|
849
|
+
if (pm === "pnpm") return locked ? "pnpm install --frozen-lockfile" : "pnpm install";
|
|
850
|
+
if (pm === "yarn")
|
|
851
|
+
return locked ? yarnBerry ? "yarn install --immutable" : "yarn install --frozen-lockfile" : "yarn install";
|
|
852
|
+
return locked ? "npm ci" : "npm install";
|
|
853
|
+
}
|
|
854
|
+
function withPackageManager(defaults, pm, install) {
|
|
855
|
+
const run2 = (cmd) => {
|
|
856
|
+
if (!cmd || pm === "npm") return cmd;
|
|
857
|
+
const match = /^npm run (\S+)(.*)$/.exec(cmd);
|
|
858
|
+
if (match)
|
|
859
|
+
return pm === "yarn" ? `yarn ${match[1]}${match[2]}` : `pnpm run ${match[1]}${match[2]}`;
|
|
860
|
+
if (cmd.startsWith("npx "))
|
|
861
|
+
return pm === "yarn" ? `yarn dlx ${cmd.slice(4)}` : `pnpm exec ${cmd.slice(4)}`;
|
|
862
|
+
return cmd;
|
|
863
|
+
};
|
|
864
|
+
return {
|
|
865
|
+
installCommand: defaults.installCommand === null ? null : install ?? installCommandFor(pm, false, false),
|
|
866
|
+
buildCommand: run2(defaults.buildCommand),
|
|
867
|
+
outputDirectory: defaults.outputDirectory
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function parsePackageJson(text) {
|
|
871
|
+
if (!text) return null;
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(text);
|
|
874
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
875
|
+
} catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function scoped(paths, root) {
|
|
880
|
+
if (!root) return paths;
|
|
881
|
+
const prefix = `${root.replace(/\/+$/, "")}/`;
|
|
882
|
+
return paths.filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length));
|
|
883
|
+
}
|
|
884
|
+
var has = (...names) => (deps) => names.some((n) => n in deps);
|
|
885
|
+
var RULES = [
|
|
886
|
+
{
|
|
887
|
+
framework: "next-server",
|
|
888
|
+
when: (d) => "next" in d && "@opennextjs/cloudflare" in d,
|
|
889
|
+
found: "next and @opennextjs/cloudflare"
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
framework: "astro-server",
|
|
893
|
+
when: (d) => "astro" in d && "@astrojs/cloudflare" in d,
|
|
894
|
+
found: "astro and @astrojs/cloudflare"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
framework: "sveltekit-server",
|
|
898
|
+
when: (d) => "@sveltejs/kit" in d && "@sveltejs/adapter-cloudflare" in d,
|
|
899
|
+
found: "@sveltejs/kit and @sveltejs/adapter-cloudflare"
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
framework: "nuxt-server",
|
|
903
|
+
when: (d) => ("nuxt" in d || "nuxt3" in d) && ("nitro-cloudflare-dev" in d || "wrangler" in d),
|
|
904
|
+
found: "nuxt with Cloudflare tooling"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
framework: "react-router-server",
|
|
908
|
+
when: (d) => "@react-router/dev" in d && "@cloudflare/vite-plugin" in d,
|
|
909
|
+
found: "@react-router/dev and @cloudflare/vite-plugin"
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
// HostEdge adds OpenNext during the build, so plain Next.js apps get the full runtime.
|
|
913
|
+
framework: "next-server",
|
|
914
|
+
when: has("next"),
|
|
915
|
+
found: "next",
|
|
916
|
+
problem: () => 'HostEdge adds @opennextjs/cloudflare during the build. If next.config sets output: "export", pick Next.js (static export) instead.'
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
framework: "nuxt",
|
|
920
|
+
when: has("nuxt", "nuxt3"),
|
|
921
|
+
found: "nuxt",
|
|
922
|
+
tune: (pkg) => pkg.scripts?.generate ? { buildCommand: "npm run generate" } : {}
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
framework: "sveltekit",
|
|
926
|
+
when: has("@sveltejs/kit"),
|
|
927
|
+
found: "@sveltejs/kit",
|
|
928
|
+
problem: (deps) => "@sveltejs/adapter-static" in deps ? null : "Use @sveltejs/adapter-static for a static site, or @sveltejs/adapter-cloudflare and pick SvelteKit (server)."
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
framework: "react-router",
|
|
932
|
+
when: has("@react-router/dev"),
|
|
933
|
+
found: "@react-router/dev",
|
|
934
|
+
problem: () => "Deployed in SPA mode: set ssr: false in react-router.config. For server rendering, add @cloudflare/vite-plugin and pick React Router (server)."
|
|
935
|
+
},
|
|
936
|
+
{ framework: "astro", when: has("astro"), found: "astro" },
|
|
937
|
+
{ framework: "gatsby", when: has("gatsby"), found: "gatsby" },
|
|
938
|
+
{ framework: "docusaurus", when: has("@docusaurus/core"), found: "@docusaurus/core" },
|
|
939
|
+
{
|
|
940
|
+
framework: "vitepress",
|
|
941
|
+
when: has("vitepress"),
|
|
942
|
+
found: "vitepress",
|
|
943
|
+
tune: (pkg) => {
|
|
944
|
+
const entry = Object.entries(pkg.scripts ?? {}).find(
|
|
945
|
+
([, cmd]) => /\bvitepress build\b/.test(cmd)
|
|
946
|
+
);
|
|
947
|
+
if (!entry) return {};
|
|
948
|
+
const dir = /\bvitepress build\s+([^\s-][^\s]*)/.exec(entry[1])?.[1]?.replace(/^\.\/|\/$/g, "");
|
|
949
|
+
return {
|
|
950
|
+
buildCommand: `npm run ${entry[0]}`,
|
|
951
|
+
outputDirectory: dir && dir !== "." ? `${dir}/.vitepress/dist` : ".vitepress/dist"
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
framework: "eleventy",
|
|
957
|
+
when: has("@11ty/eleventy"),
|
|
958
|
+
found: "@11ty/eleventy",
|
|
959
|
+
tune: (pkg) => pkg.scripts?.build ? { buildCommand: "npm run build" } : {}
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
framework: "angular",
|
|
963
|
+
when: has("@angular/core"),
|
|
964
|
+
found: "@angular/core",
|
|
965
|
+
tune: (pkg) => {
|
|
966
|
+
const name = pkg.name?.replace(/^@[^/]+\//, "");
|
|
967
|
+
return name && /^[\w.-]+$/.test(name) ? { outputDirectory: `dist/${name}/browser` } : {};
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
{ framework: "create-react-app", when: has("react-scripts"), found: "react-scripts" },
|
|
971
|
+
{ framework: "hono", when: has("hono"), found: "hono" },
|
|
972
|
+
{
|
|
973
|
+
framework: "vite-react",
|
|
974
|
+
when: (d) => "vite" in d && ("react" in d || "react-dom" in d),
|
|
975
|
+
found: "vite and react"
|
|
976
|
+
},
|
|
977
|
+
{ framework: "vite-vue", when: (d) => "vite" in d && "vue" in d, found: "vite and vue" },
|
|
978
|
+
{ framework: "vite-svelte", when: (d) => "vite" in d && "svelte" in d, found: "vite and svelte" },
|
|
979
|
+
{ framework: "vite", when: has("vite"), found: "vite" }
|
|
980
|
+
];
|
|
981
|
+
function packageManagerFrom(paths, allPaths, pkg) {
|
|
982
|
+
const lock = LOCKFILES.find(([file]) => paths.includes(file) || allPaths.includes(file));
|
|
983
|
+
const declared = /^(npm|pnpm|yarn)@(\d+)/.exec(pkg?.packageManager ?? "");
|
|
984
|
+
const pm = lock?.[1] ?? declared?.[1] ?? "npm";
|
|
985
|
+
const berry = pm === "yarn" && (paths.includes(".yarnrc.yml") || allPaths.includes(".yarnrc.yml") || Number(declared?.[2] ?? 1) >= 2);
|
|
986
|
+
const bun = ["bun.lockb", "bun.lock"].some((f) => paths.includes(f) || allPaths.includes(f));
|
|
987
|
+
return {
|
|
988
|
+
pm,
|
|
989
|
+
locked: Boolean(lock),
|
|
990
|
+
berry,
|
|
991
|
+
note: bun && !lock ? "Found a Bun lockfile; builds use npm, so bun-only APIs won\u2019t run." : null
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function detectFramework(allPaths, packageJsonText, rootDirectory = "") {
|
|
995
|
+
const normalised = allPaths.map((p) => p.replace(/^\/+/, ""));
|
|
996
|
+
const paths = scoped(normalised, rootDirectory);
|
|
997
|
+
const pkg = parsePackageJson(packageJsonText);
|
|
998
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
999
|
+
const manager = packageManagerFrom(paths, normalised, pkg);
|
|
1000
|
+
const install = installCommandFor(manager.pm, manager.locked, manager.berry);
|
|
1001
|
+
const settingsFor = (framework, overrides = {}) => ({
|
|
1002
|
+
framework,
|
|
1003
|
+
rootDirectory,
|
|
1004
|
+
...withPackageManager(
|
|
1005
|
+
{ ...FRAMEWORK_INFO[framework].defaults, ...overrides },
|
|
1006
|
+
manager.pm,
|
|
1007
|
+
install
|
|
1008
|
+
)
|
|
1009
|
+
});
|
|
1010
|
+
const managerNote = [
|
|
1011
|
+
manager.locked ? `Installs with ${manager.pm} from the lockfile.` : null,
|
|
1012
|
+
manager.note
|
|
1013
|
+
].filter(Boolean).join(" ");
|
|
1014
|
+
const withNote = (reason) => managerNote ? `${reason} ${managerNote}` : reason;
|
|
1015
|
+
if (pkg) {
|
|
1016
|
+
const rule = RULES.find((r) => r.when(deps, pkg));
|
|
1017
|
+
if (rule)
|
|
1018
|
+
return {
|
|
1019
|
+
settings: settingsFor(rule.framework, rule.tune?.(pkg, deps)),
|
|
1020
|
+
reason: withNote(`Found ${rule.found} in package.json.`),
|
|
1021
|
+
problem: rule.problem?.(deps) ?? null
|
|
1022
|
+
};
|
|
1023
|
+
if (pkg.scripts?.build)
|
|
1024
|
+
return {
|
|
1025
|
+
settings: settingsFor("other"),
|
|
1026
|
+
reason: withNote("Found a build script in package.json."),
|
|
1027
|
+
problem: null
|
|
1028
|
+
};
|
|
1029
|
+
if (paths.includes("index.html"))
|
|
1030
|
+
return {
|
|
1031
|
+
settings: settingsFor("static"),
|
|
1032
|
+
reason: "Found index.html and no build script.",
|
|
1033
|
+
problem: null
|
|
1034
|
+
};
|
|
1035
|
+
return {
|
|
1036
|
+
settings: settingsFor("other"),
|
|
1037
|
+
reason: "Found package.json without a build script.",
|
|
1038
|
+
problem: 'Add a "build" script to package.json, or deploy the folder that contains index.html.'
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
if (paths.includes("index.html"))
|
|
1042
|
+
return { settings: settingsFor("static"), reason: "Found index.html.", problem: null };
|
|
1043
|
+
const nested = paths.find((p) => /^[^/]+\/index\.html$/.test(p));
|
|
1044
|
+
if (nested) {
|
|
1045
|
+
const dir = nested.split("/")[0] ?? "";
|
|
1046
|
+
return {
|
|
1047
|
+
settings: settingsFor("static", { outputDirectory: dir }),
|
|
1048
|
+
reason: `Found ${nested}.`,
|
|
1049
|
+
problem: null
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const anyHtml = paths.some((p) => p.endsWith(".html"));
|
|
1053
|
+
return {
|
|
1054
|
+
settings: settingsFor("static"),
|
|
1055
|
+
reason: anyHtml ? "Found HTML files but no index.html." : "No HTML files found.",
|
|
1056
|
+
problem: anyHtml ? "Add an index.html so the site has a home page." : "This folder has no index.html. Drop the folder that contains your site\u2019s index.html."
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ../../packages/core/src/client.ts
|
|
1061
|
+
async function contentHash(content) {
|
|
1062
|
+
const buffer = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
1063
|
+
const digest = new Uint8Array(
|
|
1064
|
+
await crypto.subtle.digest("SHA-256", buffer)
|
|
1065
|
+
);
|
|
1066
|
+
let hex = "";
|
|
1067
|
+
for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
|
|
1068
|
+
return hex.slice(0, 32);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/commands/deploy.ts
|
|
1072
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1073
|
+
import { basename, join as join3, resolve } from "node:path";
|
|
1074
|
+
|
|
1075
|
+
// src/files.ts
|
|
1076
|
+
import { lstat, readdir, readFile as readFile2 } from "node:fs/promises";
|
|
1077
|
+
import { join as join2, relative, sep } from "node:path";
|
|
1078
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".hostedge", ".next", ".turbo", ".wrangler"]);
|
|
1079
|
+
async function collectFiles(root, limits = { maxFiles: 1e5 }) {
|
|
1080
|
+
const files = [];
|
|
1081
|
+
const skipped = [];
|
|
1082
|
+
async function walk(dir) {
|
|
1083
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1084
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1085
|
+
for (const entry of entries) {
|
|
1086
|
+
const absolute = join2(dir, entry.name);
|
|
1087
|
+
const rel = relative(root, absolute).split(sep).join("/");
|
|
1088
|
+
if (entry.isSymbolicLink())
|
|
1089
|
+
throw new CliError(
|
|
1090
|
+
`${rel} is a symbolic link. HostEdge uploads real files only \u2014 replace the link with the file it points to.`
|
|
1091
|
+
);
|
|
1092
|
+
if (entry.isDirectory()) {
|
|
1093
|
+
if (SKIP_DIRS.has(entry.name)) skipped.push(`${rel}/`);
|
|
1094
|
+
else await walk(absolute);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (!entry.isFile()) continue;
|
|
1098
|
+
const normal = normalisePath(rel);
|
|
1099
|
+
if (!normal.ok) throw new CliError(`${rel}: ${normal.reason}`);
|
|
1100
|
+
if (isIgnoredPath(normal.path)) {
|
|
1101
|
+
skipped.push(normal.path);
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (files.length >= limits.maxFiles)
|
|
1105
|
+
throw new CliError(`More than ${limits.maxFiles.toLocaleString("en-US")} files to upload.`);
|
|
1106
|
+
const stat2 = await lstat(absolute);
|
|
1107
|
+
files.push({ path: normal.path, size: stat2.size, hash: "", absolute });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await walk(root);
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (error instanceof CliError) throw error;
|
|
1114
|
+
throw new CliError(`Could not read ${root}: ${error.message}`);
|
|
1115
|
+
}
|
|
1116
|
+
const check = checkManifest(
|
|
1117
|
+
files.map((f) => ({ path: f.path, size: f.size, hash: "0".repeat(32) })),
|
|
1118
|
+
{ maxDeploymentBytes: Number.MAX_SAFE_INTEGER, maxFiles: Number.MAX_SAFE_INTEGER }
|
|
1119
|
+
);
|
|
1120
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
1121
|
+
for (const problem of check.problems) {
|
|
1122
|
+
if (problem.reason.startsWith("Looks like a secret")) secrets.add(problem.path);
|
|
1123
|
+
else throw new CliError(`${problem.path}: ${problem.reason}`);
|
|
1124
|
+
}
|
|
1125
|
+
const upload = files.filter((f) => !secrets.has(f.path));
|
|
1126
|
+
if (limits.hash !== false)
|
|
1127
|
+
for (const file of upload) file.hash = await contentHash(await readFile2(file.absolute));
|
|
1128
|
+
return { files: upload, skipped, secrets: [...secrets] };
|
|
1129
|
+
}
|
|
1130
|
+
async function pool(items, limit, work) {
|
|
1131
|
+
let next = 0;
|
|
1132
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1133
|
+
while (next < items.length) {
|
|
1134
|
+
const item = items[next++];
|
|
1135
|
+
await work(item);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
await Promise.all(runners);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/sse.ts
|
|
1142
|
+
async function* readSse(body) {
|
|
1143
|
+
const decoder = new TextDecoder();
|
|
1144
|
+
let buffer = "";
|
|
1145
|
+
let event = "message";
|
|
1146
|
+
let data = [];
|
|
1147
|
+
let id = null;
|
|
1148
|
+
for await (const chunk of body) {
|
|
1149
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1150
|
+
let newline = buffer.search(/\r?\n/);
|
|
1151
|
+
while (newline !== -1) {
|
|
1152
|
+
const line = buffer.slice(0, newline);
|
|
1153
|
+
buffer = buffer.slice(buffer[newline] === "\r" ? newline + 2 : newline + 1);
|
|
1154
|
+
if (line === "") {
|
|
1155
|
+
if (data.length > 0) yield { event, data: data.join("\n"), id };
|
|
1156
|
+
event = "message";
|
|
1157
|
+
data = [];
|
|
1158
|
+
} else if (!line.startsWith(":")) {
|
|
1159
|
+
const colon = line.indexOf(":");
|
|
1160
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
1161
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
1162
|
+
if (field === "event") event = value;
|
|
1163
|
+
else if (field === "data") data.push(value);
|
|
1164
|
+
else if (field === "id") id = value;
|
|
1165
|
+
}
|
|
1166
|
+
newline = buffer.search(/\r?\n/);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/commands/deploy.ts
|
|
1172
|
+
var FINISHED = /* @__PURE__ */ new Set(["ready", "failed", "canceled"]);
|
|
1173
|
+
var UPLOAD_CONCURRENCY = 8;
|
|
1174
|
+
async function follow(io2, api, deploymentId) {
|
|
1175
|
+
const s = styles(io2);
|
|
1176
|
+
let cursor = null;
|
|
1177
|
+
let last = { status: "queued", failureReason: null };
|
|
1178
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
1179
|
+
const response = await api.raw(`/api/v1/deployments/${deploymentId}/events`, {
|
|
1180
|
+
headers: cursor ? { "last-event-id": cursor, accept: "text/event-stream" } : {}
|
|
1181
|
+
});
|
|
1182
|
+
if (!response.body) break;
|
|
1183
|
+
for await (const event of readSse(response.body)) {
|
|
1184
|
+
if (event.id) cursor = event.id;
|
|
1185
|
+
if (event.event === "log") {
|
|
1186
|
+
for (const line of JSON.parse(event.data)) {
|
|
1187
|
+
const text = line.level === "error" ? s.red(line.msg) : line.level === "warn" ? s.yellow(line.msg) : line.level === "success" ? s.green(line.msg) : line.msg;
|
|
1188
|
+
io2.stdout(`${s.dim("\u2502")} ${text}
|
|
1189
|
+
`);
|
|
1190
|
+
}
|
|
1191
|
+
} else if (event.event === "status") {
|
|
1192
|
+
const data = JSON.parse(event.data);
|
|
1193
|
+
last = { status: data.status, failureReason: data.failureReason };
|
|
1194
|
+
} else if (event.event === "end") {
|
|
1195
|
+
return last;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (FINISHED.has(last.status)) return last;
|
|
1199
|
+
await io2.sleep(1e3);
|
|
1200
|
+
}
|
|
1201
|
+
return last;
|
|
1202
|
+
}
|
|
1203
|
+
async function deploy(io2, options) {
|
|
1204
|
+
const started = Date.now();
|
|
1205
|
+
const dir = resolve(io2.cwd, options.dir ?? ".");
|
|
1206
|
+
const s = styles(io2);
|
|
1207
|
+
const api = await connect(io2);
|
|
1208
|
+
const link = await readLink(dir);
|
|
1209
|
+
const { files, skipped, secrets } = await collectFiles(dir);
|
|
1210
|
+
if (files.length === 0) throw new CliError(`No files to deploy in ${dir}.`);
|
|
1211
|
+
for (const secret of secrets)
|
|
1212
|
+
io2.stdout(
|
|
1213
|
+
`${s.dim("\u2013")} Left out ${secret}: it looks like a secret. Set it with environment variables instead.
|
|
1214
|
+
`
|
|
1215
|
+
);
|
|
1216
|
+
if (skipped.length > 0)
|
|
1217
|
+
io2.stdout(
|
|
1218
|
+
s.dim(
|
|
1219
|
+
`Skipped ${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ` and ${skipped.length - 5} more` : ""}
|
|
1220
|
+
`
|
|
1221
|
+
)
|
|
1222
|
+
);
|
|
1223
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
1224
|
+
const { missing } = await api.request("/api/v1/uploads", {
|
|
1225
|
+
json: { hashes: [...new Set(files.map((f) => f.hash))] }
|
|
1226
|
+
});
|
|
1227
|
+
const needed = new Set(missing);
|
|
1228
|
+
const toUpload = files.filter((f) => {
|
|
1229
|
+
if (!needed.has(f.hash)) return false;
|
|
1230
|
+
needed.delete(f.hash);
|
|
1231
|
+
return true;
|
|
1232
|
+
});
|
|
1233
|
+
const uploadBytes = toUpload.reduce((sum, f) => sum + f.size, 0);
|
|
1234
|
+
io2.stdout(
|
|
1235
|
+
`Uploading ${toUpload.length} of ${files.length} files (${formatBytes(uploadBytes)} of ${formatBytes(total)}; the rest are already on HostEdge)
|
|
1236
|
+
`
|
|
1237
|
+
);
|
|
1238
|
+
await pool(toUpload, UPLOAD_CONCURRENCY, async (file) => {
|
|
1239
|
+
const bytes = await readFile3(file.absolute);
|
|
1240
|
+
await api.raw(`/api/v1/uploads/${file.hash}`, {
|
|
1241
|
+
method: "PUT",
|
|
1242
|
+
headers: { "content-type": "application/octet-stream" },
|
|
1243
|
+
body: bytes
|
|
1244
|
+
});
|
|
1245
|
+
});
|
|
1246
|
+
let project;
|
|
1247
|
+
if (!link) {
|
|
1248
|
+
let pkg = null;
|
|
1249
|
+
try {
|
|
1250
|
+
pkg = await readFile3(join3(dir, "package.json"), "utf8");
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
project = {
|
|
1254
|
+
name: basename(dir),
|
|
1255
|
+
settings: detectFramework(
|
|
1256
|
+
files.map((f) => f.path),
|
|
1257
|
+
pkg
|
|
1258
|
+
).settings
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const created = await api.request("/api/v1/deployments", {
|
|
1262
|
+
json: {
|
|
1263
|
+
projectId: link?.projectId,
|
|
1264
|
+
project,
|
|
1265
|
+
files: files.map((f) => ({ path: f.path, hash: f.hash, size: f.size })),
|
|
1266
|
+
environment: options.preview ? "preview" : "production",
|
|
1267
|
+
commitMessage: options.message ?? null
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
if (!link) {
|
|
1271
|
+
const me = await api.request("/api/v1/me");
|
|
1272
|
+
await writeLink(dir, {
|
|
1273
|
+
projectId: created.projectId,
|
|
1274
|
+
slug: created.projectSlug ?? created.projectId,
|
|
1275
|
+
workspace: me.workspace.slug
|
|
1276
|
+
});
|
|
1277
|
+
await ensureGitignored(dir, ".hostedge");
|
|
1278
|
+
io2.stdout(
|
|
1279
|
+
`${s.green("\u2713")} Created project ${s.bold(created.projectSlug ?? created.projectId)}
|
|
1280
|
+
`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
io2.stdout(`Deployment ${s.bold(created.deploymentId)}
|
|
1284
|
+
`);
|
|
1285
|
+
if (options.wait === false) {
|
|
1286
|
+
io2.stdout(`Queued. Follow it at ${created.url}
|
|
1287
|
+
`);
|
|
1288
|
+
return 0;
|
|
1289
|
+
}
|
|
1290
|
+
const result = await follow(io2, api, created.deploymentId);
|
|
1291
|
+
const seconds = ((Date.now() - started) / 1e3).toFixed(1);
|
|
1292
|
+
if (result.status === "ready") {
|
|
1293
|
+
io2.stdout(`${s.green("\u2713")} Live in ${seconds}s: ${s.bold(created.url)}
|
|
1294
|
+
`);
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
if (result.status === "canceled") {
|
|
1298
|
+
io2.stderr(`Deployment canceled.
|
|
1299
|
+
`);
|
|
1300
|
+
return 1;
|
|
1301
|
+
}
|
|
1302
|
+
io2.stderr(
|
|
1303
|
+
`${s.red("\u2717")} Deployment ${result.status === "failed" ? "failed" : `stopped while ${result.status}`}${result.failureReason ? `: ${result.failureReason}` : ""}
|
|
1304
|
+
`
|
|
1305
|
+
);
|
|
1306
|
+
return 1;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/commands/env.ts
|
|
1310
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
1311
|
+
import { resolve as resolve3 } from "node:path";
|
|
1312
|
+
|
|
1313
|
+
// src/commands/logs.ts
|
|
1314
|
+
import { resolve as resolve2 } from "node:path";
|
|
1315
|
+
var POLL_MS = 2e3;
|
|
1316
|
+
async function requireLink(io2, dir = ".") {
|
|
1317
|
+
const link = await readLink(resolve2(io2.cwd, dir));
|
|
1318
|
+
if (!link)
|
|
1319
|
+
throw new CliError(
|
|
1320
|
+
"This folder isn\u2019t linked to a project. Run `hostedge init` or `hostedge deploy`."
|
|
1321
|
+
);
|
|
1322
|
+
return link;
|
|
1323
|
+
}
|
|
1324
|
+
var newer = (a, b) => a.ts > b.ts || a.ts === b.ts && a.id > b.id;
|
|
1325
|
+
async function logs(io2, options) {
|
|
1326
|
+
const link = await requireLink(io2);
|
|
1327
|
+
const api = await connect(io2);
|
|
1328
|
+
const s = styles(io2);
|
|
1329
|
+
const query = new URLSearchParams();
|
|
1330
|
+
if (options.level) query.set("level", options.level);
|
|
1331
|
+
if (options.search) query.set("q", options.search);
|
|
1332
|
+
query.set("range", options.since ?? (options.follow ? "1h" : "24h"));
|
|
1333
|
+
const print = (row) => {
|
|
1334
|
+
const time = new Date(row.ts).toISOString().replace("T", " ").slice(0, 19);
|
|
1335
|
+
const level = row.level.toUpperCase().padEnd(5);
|
|
1336
|
+
const colored = row.level === "error" ? s.red(level) : row.level === "warn" ? s.yellow(level) : s.dim(level);
|
|
1337
|
+
io2.stdout(`${s.dim(time)} ${colored} ${row.message}
|
|
1338
|
+
`);
|
|
1339
|
+
};
|
|
1340
|
+
const fetchRows = async () => (await api.request(
|
|
1341
|
+
`/api/v1/projects/${link.projectId}/logs?${query.toString()}`
|
|
1342
|
+
)).rows;
|
|
1343
|
+
const first = await fetchRows();
|
|
1344
|
+
for (const row of [...first].reverse()) print(row);
|
|
1345
|
+
if (!options.follow) {
|
|
1346
|
+
if (first.length === 0) io2.stdout(s.dim("No logs in this range.\n"));
|
|
1347
|
+
return 0;
|
|
1348
|
+
}
|
|
1349
|
+
let cursor = first[0] ? { ts: first[0].ts, id: first[0].id } : { ts: 0, id: "" };
|
|
1350
|
+
while (!options.signal?.aborted) {
|
|
1351
|
+
await io2.sleep(POLL_MS);
|
|
1352
|
+
if (options.signal?.aborted) break;
|
|
1353
|
+
const rows = (await fetchRows()).filter((row) => newer(row, cursor)).reverse();
|
|
1354
|
+
for (const row of rows) print(row);
|
|
1355
|
+
const last = rows.at(-1);
|
|
1356
|
+
if (last) cursor = { ts: last.ts, id: last.id };
|
|
1357
|
+
}
|
|
1358
|
+
return 0;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/commands/env.ts
|
|
1362
|
+
async function envPull(io2, options) {
|
|
1363
|
+
const link = await requireLink(io2);
|
|
1364
|
+
const api = await connect(io2);
|
|
1365
|
+
const environment = options.environment ?? "development";
|
|
1366
|
+
const file = options.file ?? ".env.local";
|
|
1367
|
+
const response = await api.raw(
|
|
1368
|
+
`/api/v1/projects/${link.projectId}/env?${new URLSearchParams({ environment, format: "dotenv" })}`
|
|
1369
|
+
);
|
|
1370
|
+
const text = await response.text();
|
|
1371
|
+
const path = resolve3(io2.cwd, file);
|
|
1372
|
+
await writeFile2(path, text, { mode: 384 });
|
|
1373
|
+
const count = text.split("\n").filter(Boolean).length;
|
|
1374
|
+
const ignored = await ensureGitignored(io2.cwd, file);
|
|
1375
|
+
const s = styles(io2);
|
|
1376
|
+
io2.stdout(
|
|
1377
|
+
`${s.green("\u2713")} Wrote ${count} ${environment} variable${count === 1 ? "" : "s"} to ${file}${ignored ? ` (added to .gitignore)` : ""}.
|
|
1378
|
+
`
|
|
1379
|
+
);
|
|
1380
|
+
return 0;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/commands/init.ts
|
|
1384
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1385
|
+
import { basename as basename2, join as join4, resolve as resolve4 } from "node:path";
|
|
1386
|
+
async function init(io2, options) {
|
|
1387
|
+
const dir = resolve4(io2.cwd, options.dir ?? ".");
|
|
1388
|
+
const s = styles(io2);
|
|
1389
|
+
const existing = await readLink(dir);
|
|
1390
|
+
if (existing && !options.force) {
|
|
1391
|
+
io2.stdout(
|
|
1392
|
+
`Already linked to ${s.bold(existing.slug)}. Use --force to link a different project.
|
|
1393
|
+
`
|
|
1394
|
+
);
|
|
1395
|
+
return 0;
|
|
1396
|
+
}
|
|
1397
|
+
const api = await connect(io2);
|
|
1398
|
+
const me = await api.request("/api/v1/me");
|
|
1399
|
+
let project;
|
|
1400
|
+
if (options.project) {
|
|
1401
|
+
const { projects } = await api.request("/api/v1/projects");
|
|
1402
|
+
const found = projects.find((p) => p.id === options.project || p.slug === options.project);
|
|
1403
|
+
if (!found)
|
|
1404
|
+
throw new CliError(
|
|
1405
|
+
`No project \u201C${options.project}\u201D in ${me.workspace.slug}. Run \`hostedge init\` without --project to create one.`
|
|
1406
|
+
);
|
|
1407
|
+
project = found;
|
|
1408
|
+
} else {
|
|
1409
|
+
const { files } = await collectFiles(dir, { maxFiles: 1e5, hash: false });
|
|
1410
|
+
let pkg = null;
|
|
1411
|
+
try {
|
|
1412
|
+
pkg = await readFile4(join4(dir, "package.json"), "utf8");
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
const detection = detectFramework(
|
|
1416
|
+
files.map((f) => f.path),
|
|
1417
|
+
pkg
|
|
1418
|
+
);
|
|
1419
|
+
const settings = detection.settings;
|
|
1420
|
+
io2.stdout(`Detected ${s.bold(FRAMEWORK_INFO[settings.framework].name)}: ${detection.reason}
|
|
1421
|
+
`);
|
|
1422
|
+
if (detection.problem) io2.stdout(`${s.yellow("!")} ${detection.problem}
|
|
1423
|
+
`);
|
|
1424
|
+
project = await api.request("/api/v1/projects", {
|
|
1425
|
+
json: { name: options.name ?? basename2(dir), settings }
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
await writeLink(dir, { projectId: project.id, slug: project.slug, workspace: me.workspace.slug });
|
|
1429
|
+
await ensureGitignored(dir, ".hostedge");
|
|
1430
|
+
io2.stdout(
|
|
1431
|
+
`${s.green("\u2713")} Linked ${s.bold(project.slug)}. Run ${s.bold("hostedge deploy")} to publish it.
|
|
1432
|
+
`
|
|
1433
|
+
);
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/commands/share.ts
|
|
1438
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
1439
|
+
import { chmod as chmod2, mkdir as mkdir2, rename, rm as rm2, stat, writeFile as writeFile3 } from "node:fs/promises";
|
|
1440
|
+
import { connect as netConnect } from "node:net";
|
|
1441
|
+
import { homedir as homedir2 } from "node:os";
|
|
1442
|
+
import { delimiter, join as join5 } from "node:path";
|
|
1443
|
+
|
|
1444
|
+
// ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
|
|
1445
|
+
var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
|
|
1446
|
+
QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
|
|
1447
|
+
QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
|
|
1448
|
+
QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
|
|
1449
|
+
QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
|
|
1450
|
+
QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
|
|
1451
|
+
QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
|
|
1452
|
+
return QrCodeDataType2;
|
|
1453
|
+
})(QrCodeDataType || {});
|
|
1454
|
+
var LOW = [0, 1];
|
|
1455
|
+
var MEDIUM = [1, 0];
|
|
1456
|
+
var QUARTILE = [2, 3];
|
|
1457
|
+
var HIGH = [3, 2];
|
|
1458
|
+
var EccMap = {
|
|
1459
|
+
L: LOW,
|
|
1460
|
+
M: MEDIUM,
|
|
1461
|
+
Q: QUARTILE,
|
|
1462
|
+
H: HIGH
|
|
1463
|
+
};
|
|
1464
|
+
var NUMERIC_REGEX = /^\d*$/;
|
|
1465
|
+
var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
|
|
1466
|
+
var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
1467
|
+
var MIN_VERSION = 1;
|
|
1468
|
+
var MAX_VERSION = 40;
|
|
1469
|
+
var PENALTY_N1 = 3;
|
|
1470
|
+
var PENALTY_N2 = 3;
|
|
1471
|
+
var PENALTY_N3 = 40;
|
|
1472
|
+
var PENALTY_N4 = 10;
|
|
1473
|
+
var ECC_CODEWORDS_PER_BLOCK = [
|
|
1474
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
1475
|
+
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
|
1476
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
1477
|
+
// Low
|
|
1478
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
1479
|
+
// Medium
|
|
1480
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
1481
|
+
// Quartile
|
|
1482
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
|
|
1483
|
+
// High
|
|
1484
|
+
];
|
|
1485
|
+
var NUM_ERROR_CORRECTION_BLOCKS = [
|
|
1486
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
1487
|
+
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
|
|
1488
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
1489
|
+
// Low
|
|
1490
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
1491
|
+
// Medium
|
|
1492
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
1493
|
+
// Quartile
|
|
1494
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
|
|
1495
|
+
// High
|
|
1496
|
+
];
|
|
1497
|
+
var QrCode = class {
|
|
1498
|
+
/* -- Constructor (low level) and fields -- */
|
|
1499
|
+
// Creates a new QR Code with the given version number,
|
|
1500
|
+
// error correction level, data codeword bytes, and mask number.
|
|
1501
|
+
// This is a low-level API that most users should not use directly.
|
|
1502
|
+
// A mid-level API is the encodeSegments() function.
|
|
1503
|
+
constructor(version, ecc, dataCodewords, msk) {
|
|
1504
|
+
this.version = version;
|
|
1505
|
+
this.ecc = ecc;
|
|
1506
|
+
if (version < MIN_VERSION || version > MAX_VERSION)
|
|
1507
|
+
throw new RangeError("Version value out of range");
|
|
1508
|
+
if (msk < -1 || msk > 7)
|
|
1509
|
+
throw new RangeError("Mask value out of range");
|
|
1510
|
+
this.size = version * 4 + 17;
|
|
1511
|
+
const row = Array.from({ length: this.size }).fill(false);
|
|
1512
|
+
for (let i = 0; i < this.size; i++) {
|
|
1513
|
+
this.modules.push(row.slice());
|
|
1514
|
+
this.types.push(row.map(() => 0));
|
|
1515
|
+
}
|
|
1516
|
+
this.drawFunctionPatterns();
|
|
1517
|
+
const allCodewords = this.addEccAndInterleave(dataCodewords);
|
|
1518
|
+
this.drawCodewords(allCodewords);
|
|
1519
|
+
if (msk === -1) {
|
|
1520
|
+
let minPenalty = 1e9;
|
|
1521
|
+
for (let i = 0; i < 8; i++) {
|
|
1522
|
+
this.applyMask(i);
|
|
1523
|
+
this.drawFormatBits(i);
|
|
1524
|
+
const penalty = this.getPenaltyScore();
|
|
1525
|
+
if (penalty < minPenalty) {
|
|
1526
|
+
msk = i;
|
|
1527
|
+
minPenalty = penalty;
|
|
1528
|
+
}
|
|
1529
|
+
this.applyMask(i);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
this.mask = msk;
|
|
1533
|
+
this.applyMask(msk);
|
|
1534
|
+
this.drawFormatBits(msk);
|
|
1535
|
+
}
|
|
1536
|
+
/* -- Fields -- */
|
|
1537
|
+
// The width and height of this QR Code, measured in modules, between
|
|
1538
|
+
// 21 and 177 (inclusive). This is equal to version * 4 + 17.
|
|
1539
|
+
size;
|
|
1540
|
+
// The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
|
|
1541
|
+
// Even if a QR Code is created with automatic masking requested (mask = -1),
|
|
1542
|
+
// the resulting object still has a mask value between 0 and 7.
|
|
1543
|
+
mask;
|
|
1544
|
+
// The modules of this QR Code (false = light, true = dark).
|
|
1545
|
+
// Immutable after constructor finishes. Accessed through getModule().
|
|
1546
|
+
modules = [];
|
|
1547
|
+
types = [];
|
|
1548
|
+
/* -- Accessor methods -- */
|
|
1549
|
+
// Returns the color of the module (pixel) at the given coordinates, which is false
|
|
1550
|
+
// for light or true for dark. The top left corner has the coordinates (x=0, y=0).
|
|
1551
|
+
// If the given coordinates are out of bounds, then false (light) is returned.
|
|
1552
|
+
getModule(x, y) {
|
|
1553
|
+
return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
|
|
1554
|
+
}
|
|
1555
|
+
/* -- Private helper methods for constructor: Drawing function modules -- */
|
|
1556
|
+
// Reads this object's version field, and draws and marks all function modules.
|
|
1557
|
+
drawFunctionPatterns() {
|
|
1558
|
+
for (let i = 0; i < this.size; i++) {
|
|
1559
|
+
this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
|
|
1560
|
+
this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
|
|
1561
|
+
}
|
|
1562
|
+
this.drawFinderPattern(3, 3);
|
|
1563
|
+
this.drawFinderPattern(this.size - 4, 3);
|
|
1564
|
+
this.drawFinderPattern(3, this.size - 4);
|
|
1565
|
+
const alignPatPos = this.getAlignmentPatternPositions();
|
|
1566
|
+
const numAlign = alignPatPos.length;
|
|
1567
|
+
for (let i = 0; i < numAlign; i++) {
|
|
1568
|
+
for (let j = 0; j < numAlign; j++) {
|
|
1569
|
+
if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
|
|
1570
|
+
this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
this.drawFormatBits(0);
|
|
1574
|
+
this.drawVersion();
|
|
1575
|
+
}
|
|
1576
|
+
// Draws two copies of the format bits (with its own error correction code)
|
|
1577
|
+
// based on the given mask and this object's error correction level field.
|
|
1578
|
+
drawFormatBits(mask) {
|
|
1579
|
+
const data = this.ecc[1] << 3 | mask;
|
|
1580
|
+
let rem = data;
|
|
1581
|
+
for (let i = 0; i < 10; i++)
|
|
1582
|
+
rem = rem << 1 ^ (rem >>> 9) * 1335;
|
|
1583
|
+
const bits = (data << 10 | rem) ^ 21522;
|
|
1584
|
+
for (let i = 0; i <= 5; i++)
|
|
1585
|
+
this.setFunctionModule(8, i, getBit(bits, i));
|
|
1586
|
+
this.setFunctionModule(8, 7, getBit(bits, 6));
|
|
1587
|
+
this.setFunctionModule(8, 8, getBit(bits, 7));
|
|
1588
|
+
this.setFunctionModule(7, 8, getBit(bits, 8));
|
|
1589
|
+
for (let i = 9; i < 15; i++)
|
|
1590
|
+
this.setFunctionModule(14 - i, 8, getBit(bits, i));
|
|
1591
|
+
for (let i = 0; i < 8; i++)
|
|
1592
|
+
this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
|
|
1593
|
+
for (let i = 8; i < 15; i++)
|
|
1594
|
+
this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
|
|
1595
|
+
this.setFunctionModule(8, this.size - 8, true);
|
|
1596
|
+
}
|
|
1597
|
+
// Draws two copies of the version bits (with its own error correction code),
|
|
1598
|
+
// based on this object's version field, iff 7 <= version <= 40.
|
|
1599
|
+
drawVersion() {
|
|
1600
|
+
if (this.version < 7)
|
|
1601
|
+
return;
|
|
1602
|
+
let rem = this.version;
|
|
1603
|
+
for (let i = 0; i < 12; i++)
|
|
1604
|
+
rem = rem << 1 ^ (rem >>> 11) * 7973;
|
|
1605
|
+
const bits = this.version << 12 | rem;
|
|
1606
|
+
for (let i = 0; i < 18; i++) {
|
|
1607
|
+
const color = getBit(bits, i);
|
|
1608
|
+
const a = this.size - 11 + i % 3;
|
|
1609
|
+
const b = Math.floor(i / 3);
|
|
1610
|
+
this.setFunctionModule(a, b, color);
|
|
1611
|
+
this.setFunctionModule(b, a, color);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
// Draws a 9*9 finder pattern including the border separator,
|
|
1615
|
+
// with the center module at (x, y). Modules can be out of bounds.
|
|
1616
|
+
drawFinderPattern(x, y) {
|
|
1617
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
1618
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
1619
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
1620
|
+
const xx = x + dx;
|
|
1621
|
+
const yy = y + dy;
|
|
1622
|
+
if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
|
|
1623
|
+
this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Draws a 5*5 alignment pattern, with the center module
|
|
1628
|
+
// at (x, y). All modules must be in bounds.
|
|
1629
|
+
drawAlignmentPattern(x, y) {
|
|
1630
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
1631
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
1632
|
+
this.setFunctionModule(
|
|
1633
|
+
x + dx,
|
|
1634
|
+
y + dy,
|
|
1635
|
+
Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
|
|
1636
|
+
QrCodeDataType.Alignment
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// Sets the color of a module and marks it as a function module.
|
|
1642
|
+
// Only used by the constructor. Coordinates must be in bounds.
|
|
1643
|
+
setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
|
|
1644
|
+
this.modules[y][x] = isDark;
|
|
1645
|
+
this.types[y][x] = type;
|
|
1646
|
+
}
|
|
1647
|
+
/* -- Private helper methods for constructor: Codewords and masking -- */
|
|
1648
|
+
// Returns a new byte string representing the given data with the appropriate error correction
|
|
1649
|
+
// codewords appended to it, based on this object's version and error correction level.
|
|
1650
|
+
addEccAndInterleave(data) {
|
|
1651
|
+
const ver = this.version;
|
|
1652
|
+
const ecl = this.ecc;
|
|
1653
|
+
if (data.length !== getNumDataCodewords(ver, ecl))
|
|
1654
|
+
throw new RangeError("Invalid argument");
|
|
1655
|
+
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
1656
|
+
const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
|
|
1657
|
+
const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
|
|
1658
|
+
const numShortBlocks = numBlocks - rawCodewords % numBlocks;
|
|
1659
|
+
const shortBlockLen = Math.floor(rawCodewords / numBlocks);
|
|
1660
|
+
const blocks = [];
|
|
1661
|
+
const rsDiv = reedSolomonComputeDivisor(blockEccLen);
|
|
1662
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
1663
|
+
const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
|
|
1664
|
+
k += dat.length;
|
|
1665
|
+
const ecc = reedSolomonComputeRemainder(dat, rsDiv);
|
|
1666
|
+
if (i < numShortBlocks)
|
|
1667
|
+
dat.push(0);
|
|
1668
|
+
blocks.push(dat.concat(ecc));
|
|
1669
|
+
}
|
|
1670
|
+
const result = [];
|
|
1671
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
1672
|
+
blocks.forEach((block, j) => {
|
|
1673
|
+
if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
|
|
1674
|
+
result.push(block[i]);
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
return result;
|
|
1678
|
+
}
|
|
1679
|
+
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
|
|
1680
|
+
// data area of this QR Code. Function modules need to be marked off before this is called.
|
|
1681
|
+
drawCodewords(data) {
|
|
1682
|
+
if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
|
|
1683
|
+
throw new RangeError("Invalid argument");
|
|
1684
|
+
let i = 0;
|
|
1685
|
+
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
1686
|
+
if (right === 6)
|
|
1687
|
+
right = 5;
|
|
1688
|
+
for (let vert = 0; vert < this.size; vert++) {
|
|
1689
|
+
for (let j = 0; j < 2; j++) {
|
|
1690
|
+
const x = right - j;
|
|
1691
|
+
const upward = (right + 1 & 2) === 0;
|
|
1692
|
+
const y = upward ? this.size - 1 - vert : vert;
|
|
1693
|
+
if (!this.types[y][x] && i < data.length * 8) {
|
|
1694
|
+
this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
|
|
1695
|
+
i++;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
// XORs the codeword modules in this QR Code with the given mask pattern.
|
|
1702
|
+
// The function modules must be marked and the codeword bits must be drawn
|
|
1703
|
+
// before masking. Due to the arithmetic of XOR, calling applyMask() with
|
|
1704
|
+
// the same mask value a second time will undo the mask. A final well-formed
|
|
1705
|
+
// QR Code needs exactly one (not zero, two, etc.) mask applied.
|
|
1706
|
+
applyMask(mask) {
|
|
1707
|
+
if (mask < 0 || mask > 7)
|
|
1708
|
+
throw new RangeError("Mask value out of range");
|
|
1709
|
+
for (let y = 0; y < this.size; y++) {
|
|
1710
|
+
for (let x = 0; x < this.size; x++) {
|
|
1711
|
+
let invert;
|
|
1712
|
+
switch (mask) {
|
|
1713
|
+
case 0:
|
|
1714
|
+
invert = (x + y) % 2 === 0;
|
|
1715
|
+
break;
|
|
1716
|
+
case 1:
|
|
1717
|
+
invert = y % 2 === 0;
|
|
1718
|
+
break;
|
|
1719
|
+
case 2:
|
|
1720
|
+
invert = x % 3 === 0;
|
|
1721
|
+
break;
|
|
1722
|
+
case 3:
|
|
1723
|
+
invert = (x + y) % 3 === 0;
|
|
1724
|
+
break;
|
|
1725
|
+
case 4:
|
|
1726
|
+
invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
|
|
1727
|
+
break;
|
|
1728
|
+
case 5:
|
|
1729
|
+
invert = x * y % 2 + x * y % 3 === 0;
|
|
1730
|
+
break;
|
|
1731
|
+
case 6:
|
|
1732
|
+
invert = (x * y % 2 + x * y % 3) % 2 === 0;
|
|
1733
|
+
break;
|
|
1734
|
+
case 7:
|
|
1735
|
+
invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
|
|
1736
|
+
break;
|
|
1737
|
+
default:
|
|
1738
|
+
throw new Error("Unreachable");
|
|
1739
|
+
}
|
|
1740
|
+
if (!this.types[y][x] && invert)
|
|
1741
|
+
this.modules[y][x] = !this.modules[y][x];
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// Calculates and returns the penalty score based on state of this QR Code's current modules.
|
|
1746
|
+
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
|
|
1747
|
+
getPenaltyScore() {
|
|
1748
|
+
let result = 0;
|
|
1749
|
+
for (let y = 0; y < this.size; y++) {
|
|
1750
|
+
let runColor = false;
|
|
1751
|
+
let runX = 0;
|
|
1752
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
1753
|
+
for (let x = 0; x < this.size; x++) {
|
|
1754
|
+
if (this.modules[y][x] === runColor) {
|
|
1755
|
+
runX++;
|
|
1756
|
+
if (runX === 5)
|
|
1757
|
+
result += PENALTY_N1;
|
|
1758
|
+
else if (runX > 5)
|
|
1759
|
+
result++;
|
|
1760
|
+
} else {
|
|
1761
|
+
this.finderPenaltyAddHistory(runX, runHistory);
|
|
1762
|
+
if (!runColor)
|
|
1763
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
1764
|
+
runColor = this.modules[y][x];
|
|
1765
|
+
runX = 1;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
|
|
1769
|
+
}
|
|
1770
|
+
for (let x = 0; x < this.size; x++) {
|
|
1771
|
+
let runColor = false;
|
|
1772
|
+
let runY = 0;
|
|
1773
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
1774
|
+
for (let y = 0; y < this.size; y++) {
|
|
1775
|
+
if (this.modules[y][x] === runColor) {
|
|
1776
|
+
runY++;
|
|
1777
|
+
if (runY === 5)
|
|
1778
|
+
result += PENALTY_N1;
|
|
1779
|
+
else if (runY > 5)
|
|
1780
|
+
result++;
|
|
1781
|
+
} else {
|
|
1782
|
+
this.finderPenaltyAddHistory(runY, runHistory);
|
|
1783
|
+
if (!runColor)
|
|
1784
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
1785
|
+
runColor = this.modules[y][x];
|
|
1786
|
+
runY = 1;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
|
|
1790
|
+
}
|
|
1791
|
+
for (let y = 0; y < this.size - 1; y++) {
|
|
1792
|
+
for (let x = 0; x < this.size - 1; x++) {
|
|
1793
|
+
const color = this.modules[y][x];
|
|
1794
|
+
if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
|
|
1795
|
+
result += PENALTY_N2;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
let dark = 0;
|
|
1800
|
+
for (const row of this.modules)
|
|
1801
|
+
dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
|
|
1802
|
+
const total = this.size * this.size;
|
|
1803
|
+
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
|
1804
|
+
result += k * PENALTY_N4;
|
|
1805
|
+
return result;
|
|
1806
|
+
}
|
|
1807
|
+
/* -- Private helper functions -- */
|
|
1808
|
+
// Returns an ascending list of positions of alignment patterns for this version number.
|
|
1809
|
+
// Each position is in the range [0,177), and are used on both the x and y axes.
|
|
1810
|
+
// This could be implemented as lookup table of 40 variable-length lists of integers.
|
|
1811
|
+
getAlignmentPatternPositions() {
|
|
1812
|
+
if (this.version === 1) {
|
|
1813
|
+
return [];
|
|
1814
|
+
} else {
|
|
1815
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
1816
|
+
const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
|
|
1817
|
+
const result = [6];
|
|
1818
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
|
|
1819
|
+
result.splice(1, 0, pos);
|
|
1820
|
+
return result;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
// Can only be called immediately after a light run is added, and
|
|
1824
|
+
// returns either 0, 1, or 2. A helper function for getPenaltyScore().
|
|
1825
|
+
finderPenaltyCountPatterns(runHistory) {
|
|
1826
|
+
const n = runHistory[1];
|
|
1827
|
+
const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
|
|
1828
|
+
return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
|
|
1829
|
+
}
|
|
1830
|
+
// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
|
|
1831
|
+
finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
|
|
1832
|
+
if (currentRunColor) {
|
|
1833
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
1834
|
+
currentRunLength = 0;
|
|
1835
|
+
}
|
|
1836
|
+
currentRunLength += this.size;
|
|
1837
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
1838
|
+
return this.finderPenaltyCountPatterns(runHistory);
|
|
1839
|
+
}
|
|
1840
|
+
// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
|
|
1841
|
+
finderPenaltyAddHistory(currentRunLength, runHistory) {
|
|
1842
|
+
if (runHistory[0] === 0)
|
|
1843
|
+
currentRunLength += this.size;
|
|
1844
|
+
runHistory.pop();
|
|
1845
|
+
runHistory.unshift(currentRunLength);
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
function appendBits(val, len, bb) {
|
|
1849
|
+
if (len < 0 || len > 31 || val >>> len !== 0)
|
|
1850
|
+
throw new RangeError("Value out of range");
|
|
1851
|
+
for (let i = len - 1; i >= 0; i--)
|
|
1852
|
+
bb.push(val >>> i & 1);
|
|
1853
|
+
}
|
|
1854
|
+
function getBit(x, i) {
|
|
1855
|
+
return (x >>> i & 1) !== 0;
|
|
1856
|
+
}
|
|
1857
|
+
var QrSegment = class {
|
|
1858
|
+
// Creates a new QR Code segment with the given attributes and data.
|
|
1859
|
+
// The character count (numChars) must agree with the mode and the bit buffer length,
|
|
1860
|
+
// but the constraint isn't checked. The given bit buffer is cloned and stored.
|
|
1861
|
+
constructor(mode, numChars, bitData) {
|
|
1862
|
+
this.mode = mode;
|
|
1863
|
+
this.numChars = numChars;
|
|
1864
|
+
this.bitData = bitData;
|
|
1865
|
+
if (numChars < 0)
|
|
1866
|
+
throw new RangeError("Invalid argument");
|
|
1867
|
+
this.bitData = bitData.slice();
|
|
1868
|
+
}
|
|
1869
|
+
/* -- Methods -- */
|
|
1870
|
+
// Returns a new copy of the data bits of this segment.
|
|
1871
|
+
getData() {
|
|
1872
|
+
return this.bitData.slice();
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
var MODE_NUMERIC = [1, 10, 12, 14];
|
|
1876
|
+
var MODE_ALPHANUMERIC = [2, 9, 11, 13];
|
|
1877
|
+
var MODE_BYTE = [4, 8, 16, 16];
|
|
1878
|
+
function numCharCountBits(mode, ver) {
|
|
1879
|
+
return mode[Math.floor((ver + 7) / 17) + 1];
|
|
1880
|
+
}
|
|
1881
|
+
function makeBytes(data) {
|
|
1882
|
+
const bb = [];
|
|
1883
|
+
for (const b of data)
|
|
1884
|
+
appendBits(b, 8, bb);
|
|
1885
|
+
return new QrSegment(MODE_BYTE, data.length, bb);
|
|
1886
|
+
}
|
|
1887
|
+
function makeNumeric(digits) {
|
|
1888
|
+
if (!isNumeric(digits))
|
|
1889
|
+
throw new RangeError("String contains non-numeric characters");
|
|
1890
|
+
const bb = [];
|
|
1891
|
+
for (let i = 0; i < digits.length; ) {
|
|
1892
|
+
const n = Math.min(digits.length - i, 3);
|
|
1893
|
+
appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
|
|
1894
|
+
i += n;
|
|
1895
|
+
}
|
|
1896
|
+
return new QrSegment(MODE_NUMERIC, digits.length, bb);
|
|
1897
|
+
}
|
|
1898
|
+
function makeAlphanumeric(text) {
|
|
1899
|
+
if (!isAlphanumeric(text))
|
|
1900
|
+
throw new RangeError("String contains unencodable characters in alphanumeric mode");
|
|
1901
|
+
const bb = [];
|
|
1902
|
+
let i;
|
|
1903
|
+
for (i = 0; i + 2 <= text.length; i += 2) {
|
|
1904
|
+
let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
|
|
1905
|
+
temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
|
|
1906
|
+
appendBits(temp, 11, bb);
|
|
1907
|
+
}
|
|
1908
|
+
if (i < text.length)
|
|
1909
|
+
appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
|
|
1910
|
+
return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
|
|
1911
|
+
}
|
|
1912
|
+
function makeSegments(text) {
|
|
1913
|
+
if (text === "")
|
|
1914
|
+
return [];
|
|
1915
|
+
else if (isNumeric(text))
|
|
1916
|
+
return [makeNumeric(text)];
|
|
1917
|
+
else if (isAlphanumeric(text))
|
|
1918
|
+
return [makeAlphanumeric(text)];
|
|
1919
|
+
else
|
|
1920
|
+
return [makeBytes(toUtf8ByteArray(text))];
|
|
1921
|
+
}
|
|
1922
|
+
function isNumeric(text) {
|
|
1923
|
+
return NUMERIC_REGEX.test(text);
|
|
1924
|
+
}
|
|
1925
|
+
function isAlphanumeric(text) {
|
|
1926
|
+
return ALPHANUMERIC_REGEX.test(text);
|
|
1927
|
+
}
|
|
1928
|
+
function getTotalBits(segs, version) {
|
|
1929
|
+
let result = 0;
|
|
1930
|
+
for (const seg of segs) {
|
|
1931
|
+
const ccbits = numCharCountBits(seg.mode, version);
|
|
1932
|
+
if (seg.numChars >= 1 << ccbits)
|
|
1933
|
+
return Number.POSITIVE_INFINITY;
|
|
1934
|
+
result += 4 + ccbits + seg.bitData.length;
|
|
1935
|
+
}
|
|
1936
|
+
return result;
|
|
1937
|
+
}
|
|
1938
|
+
function toUtf8ByteArray(str2) {
|
|
1939
|
+
str2 = encodeURI(str2);
|
|
1940
|
+
const result = [];
|
|
1941
|
+
for (let i = 0; i < str2.length; i++) {
|
|
1942
|
+
if (str2.charAt(i) !== "%") {
|
|
1943
|
+
result.push(str2.charCodeAt(i));
|
|
1944
|
+
} else {
|
|
1945
|
+
result.push(Number.parseInt(str2.substring(i + 1, i + 3), 16));
|
|
1946
|
+
i += 2;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return result;
|
|
1950
|
+
}
|
|
1951
|
+
function getNumRawDataModules(ver) {
|
|
1952
|
+
if (ver < MIN_VERSION || ver > MAX_VERSION)
|
|
1953
|
+
throw new RangeError("Version number out of range");
|
|
1954
|
+
let result = (16 * ver + 128) * ver + 64;
|
|
1955
|
+
if (ver >= 2) {
|
|
1956
|
+
const numAlign = Math.floor(ver / 7) + 2;
|
|
1957
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
1958
|
+
if (ver >= 7)
|
|
1959
|
+
result -= 36;
|
|
1960
|
+
}
|
|
1961
|
+
return result;
|
|
1962
|
+
}
|
|
1963
|
+
function getNumDataCodewords(ver, ecl) {
|
|
1964
|
+
return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
1965
|
+
}
|
|
1966
|
+
function reedSolomonComputeDivisor(degree) {
|
|
1967
|
+
if (degree < 1 || degree > 255)
|
|
1968
|
+
throw new RangeError("Degree out of range");
|
|
1969
|
+
const result = [];
|
|
1970
|
+
for (let i = 0; i < degree - 1; i++)
|
|
1971
|
+
result.push(0);
|
|
1972
|
+
result.push(1);
|
|
1973
|
+
let root = 1;
|
|
1974
|
+
for (let i = 0; i < degree; i++) {
|
|
1975
|
+
for (let j = 0; j < result.length; j++) {
|
|
1976
|
+
result[j] = reedSolomonMultiply(result[j], root);
|
|
1977
|
+
if (j + 1 < result.length)
|
|
1978
|
+
result[j] ^= result[j + 1];
|
|
1979
|
+
}
|
|
1980
|
+
root = reedSolomonMultiply(root, 2);
|
|
1981
|
+
}
|
|
1982
|
+
return result;
|
|
1983
|
+
}
|
|
1984
|
+
function reedSolomonComputeRemainder(data, divisor) {
|
|
1985
|
+
const result = divisor.map((_) => 0);
|
|
1986
|
+
for (const b of data) {
|
|
1987
|
+
const factor = b ^ result.shift();
|
|
1988
|
+
result.push(0);
|
|
1989
|
+
divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
|
|
1990
|
+
}
|
|
1991
|
+
return result;
|
|
1992
|
+
}
|
|
1993
|
+
function reedSolomonMultiply(x, y) {
|
|
1994
|
+
if (x >>> 8 !== 0 || y >>> 8 !== 0)
|
|
1995
|
+
throw new RangeError("Byte out of range");
|
|
1996
|
+
let z = 0;
|
|
1997
|
+
for (let i = 7; i >= 0; i--) {
|
|
1998
|
+
z = z << 1 ^ (z >>> 7) * 285;
|
|
1999
|
+
z ^= (y >>> i & 1) * x;
|
|
2000
|
+
}
|
|
2001
|
+
return z;
|
|
2002
|
+
}
|
|
2003
|
+
function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
|
|
2004
|
+
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
|
|
2005
|
+
throw new RangeError("Invalid value");
|
|
2006
|
+
}
|
|
2007
|
+
let version;
|
|
2008
|
+
let dataUsedBits;
|
|
2009
|
+
for (version = minVersion; ; version++) {
|
|
2010
|
+
const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
|
|
2011
|
+
const usedBits = getTotalBits(segs, version);
|
|
2012
|
+
if (usedBits <= dataCapacityBits2) {
|
|
2013
|
+
dataUsedBits = usedBits;
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
if (version >= maxVersion)
|
|
2017
|
+
throw new RangeError("Data too long");
|
|
2018
|
+
}
|
|
2019
|
+
for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
|
|
2020
|
+
if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
|
|
2021
|
+
ecl = newEcl;
|
|
2022
|
+
}
|
|
2023
|
+
const bb = [];
|
|
2024
|
+
for (const seg of segs) {
|
|
2025
|
+
appendBits(seg.mode[0], 4, bb);
|
|
2026
|
+
appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
|
|
2027
|
+
for (const b of seg.getData())
|
|
2028
|
+
bb.push(b);
|
|
2029
|
+
}
|
|
2030
|
+
const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
|
|
2031
|
+
appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
|
|
2032
|
+
appendBits(0, (8 - bb.length % 8) % 8, bb);
|
|
2033
|
+
for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
|
|
2034
|
+
appendBits(padByte, 8, bb);
|
|
2035
|
+
const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
|
|
2036
|
+
bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
|
|
2037
|
+
return new QrCode(version, ecl, dataCodewords, mask);
|
|
2038
|
+
}
|
|
2039
|
+
function encode(data, options) {
|
|
2040
|
+
const {
|
|
2041
|
+
ecc = "L",
|
|
2042
|
+
boostEcc = false,
|
|
2043
|
+
minVersion = 1,
|
|
2044
|
+
maxVersion = 40,
|
|
2045
|
+
maskPattern = -1,
|
|
2046
|
+
border = 1
|
|
2047
|
+
} = options || {};
|
|
2048
|
+
const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
|
|
2049
|
+
if (!segment)
|
|
2050
|
+
throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
|
|
2051
|
+
const qr = encodeSegments(
|
|
2052
|
+
segment,
|
|
2053
|
+
EccMap[ecc],
|
|
2054
|
+
minVersion,
|
|
2055
|
+
maxVersion,
|
|
2056
|
+
maskPattern,
|
|
2057
|
+
boostEcc
|
|
2058
|
+
);
|
|
2059
|
+
const result = addBorder({
|
|
2060
|
+
version: qr.version,
|
|
2061
|
+
maskPattern: qr.mask,
|
|
2062
|
+
size: qr.size,
|
|
2063
|
+
data: qr.modules,
|
|
2064
|
+
types: qr.types
|
|
2065
|
+
}, border);
|
|
2066
|
+
if (options?.invert)
|
|
2067
|
+
result.data = result.data.map((row) => row.map((mod) => !mod));
|
|
2068
|
+
options?.onEncoded?.(result);
|
|
2069
|
+
return result;
|
|
2070
|
+
}
|
|
2071
|
+
function addBorder(input, border = 1) {
|
|
2072
|
+
if (!border)
|
|
2073
|
+
return input;
|
|
2074
|
+
const { size } = input;
|
|
2075
|
+
const newSize = size + border * 2;
|
|
2076
|
+
input.size = newSize;
|
|
2077
|
+
input.data.forEach((row) => {
|
|
2078
|
+
for (let i = 0; i < border; i++) {
|
|
2079
|
+
row.unshift(false);
|
|
2080
|
+
row.push(false);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
for (let i = 0; i < border; i++) {
|
|
2084
|
+
input.data.unshift(Array.from({ length: newSize }, (_) => false));
|
|
2085
|
+
input.data.push(Array.from({ length: newSize }, (_) => false));
|
|
2086
|
+
}
|
|
2087
|
+
const b = QrCodeDataType.Border;
|
|
2088
|
+
input.types.forEach((row) => {
|
|
2089
|
+
for (let i = 0; i < border; i++) {
|
|
2090
|
+
row.unshift(b);
|
|
2091
|
+
row.push(b);
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
for (let i = 0; i < border; i++) {
|
|
2095
|
+
input.types.unshift(Array.from({ length: newSize }, (_) => b));
|
|
2096
|
+
input.types.push(Array.from({ length: newSize }, (_) => b));
|
|
2097
|
+
}
|
|
2098
|
+
return input;
|
|
2099
|
+
}
|
|
2100
|
+
function getDataAt(data, x, y, defaults = false) {
|
|
2101
|
+
if (x < 0 || y < 0 || x >= data.length || y >= data.length)
|
|
2102
|
+
return defaults;
|
|
2103
|
+
return data[y][x];
|
|
2104
|
+
}
|
|
2105
|
+
function renderUnicodeCompact(data, options = {}) {
|
|
2106
|
+
const palette = {
|
|
2107
|
+
WHITE_ALL: "\u2588",
|
|
2108
|
+
WHITE_BLACK: "\u2580",
|
|
2109
|
+
BLACK_WHITE: "\u2584",
|
|
2110
|
+
BLACK_ALL: " "
|
|
2111
|
+
};
|
|
2112
|
+
const result = encode(data, options);
|
|
2113
|
+
const WHITE = false;
|
|
2114
|
+
const BLACK = true;
|
|
2115
|
+
const at = (x, y) => getDataAt(result.data, x, y, true);
|
|
2116
|
+
const lines = [];
|
|
2117
|
+
let line = "";
|
|
2118
|
+
for (let row = 0; row < result.size; row += 2) {
|
|
2119
|
+
for (let col = 0; col < result.size; col++) {
|
|
2120
|
+
if (at(col, row) === WHITE && at(col, row + 1) === WHITE)
|
|
2121
|
+
line += palette.WHITE_ALL;
|
|
2122
|
+
else if (at(col, row) === WHITE && at(col, row + 1) === BLACK)
|
|
2123
|
+
line += palette.WHITE_BLACK;
|
|
2124
|
+
else if (at(col, row) === BLACK && at(col, row + 1) === WHITE)
|
|
2125
|
+
line += palette.BLACK_WHITE;
|
|
2126
|
+
else
|
|
2127
|
+
line += palette.BLACK_ALL;
|
|
2128
|
+
}
|
|
2129
|
+
lines.push(line);
|
|
2130
|
+
line = "";
|
|
2131
|
+
}
|
|
2132
|
+
return lines.join("\n");
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/commands/share.ts
|
|
2136
|
+
var RELEASES = "https://github.com/cloudflare/cloudflared/releases/latest/download";
|
|
2137
|
+
function cloudflaredAsset(platform, arch) {
|
|
2138
|
+
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
|
|
2139
|
+
if (!cpu) return null;
|
|
2140
|
+
if (platform === "linux") return `cloudflared-linux-${cpu}`;
|
|
2141
|
+
if (platform === "darwin") return `cloudflared-darwin-${cpu}.tgz`;
|
|
2142
|
+
if (platform === "win32" && cpu === "amd64") return "cloudflared-windows-amd64.exe";
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
var exists = async (path) => (await stat(path).catch(() => null))?.isFile() ?? false;
|
|
2146
|
+
async function findCloudflared(io2) {
|
|
2147
|
+
if (io2.env.HOSTEDGE_CLOUDFLARED) return io2.env.HOSTEDGE_CLOUDFLARED;
|
|
2148
|
+
const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
2149
|
+
for (const dir of (io2.env.PATH ?? "").split(delimiter).filter(Boolean))
|
|
2150
|
+
if (await exists(join5(dir, exe))) return join5(dir, exe);
|
|
2151
|
+
const binDir = join5(io2.env.HOME ?? homedir2(), ".hostedge", "bin");
|
|
2152
|
+
const own = join5(binDir, exe);
|
|
2153
|
+
if (await exists(own)) return own;
|
|
2154
|
+
const asset = cloudflaredAsset(process.platform, process.arch);
|
|
2155
|
+
if (!asset)
|
|
2156
|
+
throw new CliError(
|
|
2157
|
+
"Install cloudflared (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and run this again."
|
|
2158
|
+
);
|
|
2159
|
+
io2.stderr("Downloading cloudflared (one time)\u2026\n");
|
|
2160
|
+
const response = await io2.fetch(`${RELEASES}/${asset}`);
|
|
2161
|
+
if (!response.ok) throw new CliError(`Could not download cloudflared (${response.status}).`);
|
|
2162
|
+
await mkdir2(binDir, { recursive: true });
|
|
2163
|
+
const tmp = join5(binDir, `${asset}.download`);
|
|
2164
|
+
await writeFile3(tmp, new Uint8Array(await response.arrayBuffer()));
|
|
2165
|
+
if (asset.endsWith(".tgz")) {
|
|
2166
|
+
await new Promise((resolve5, reject) => {
|
|
2167
|
+
const tar = nodeSpawn("tar", ["-xzf", tmp, "-C", binDir], { stdio: "ignore" });
|
|
2168
|
+
tar.on("error", reject);
|
|
2169
|
+
tar.on("exit", (code) => code === 0 ? resolve5() : reject(new Error(`tar exited ${code}`)));
|
|
2170
|
+
});
|
|
2171
|
+
await rm2(tmp, { force: true });
|
|
2172
|
+
} else {
|
|
2173
|
+
await rename(tmp, own);
|
|
2174
|
+
}
|
|
2175
|
+
await chmod2(own, 493);
|
|
2176
|
+
return own;
|
|
2177
|
+
}
|
|
2178
|
+
function listening(port) {
|
|
2179
|
+
return new Promise((resolve5) => {
|
|
2180
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
2181
|
+
socket.once("connect", () => {
|
|
2182
|
+
socket.destroy();
|
|
2183
|
+
resolve5(true);
|
|
2184
|
+
});
|
|
2185
|
+
socket.once("error", () => resolve5(false));
|
|
2186
|
+
socket.setTimeout(1e3, () => {
|
|
2187
|
+
socket.destroy();
|
|
2188
|
+
resolve5(false);
|
|
2189
|
+
});
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
var QUICK_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2193
|
+
async function share(io2, options) {
|
|
2194
|
+
const s = styles(io2);
|
|
2195
|
+
const port = Number(options.port ?? "3000");
|
|
2196
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
2197
|
+
throw new CliError("Give the port your dev server runs on, e.g. `hostedge share 5173`.", 2);
|
|
2198
|
+
if (!await listening(port))
|
|
2199
|
+
io2.stderr(
|
|
2200
|
+
`${s.yellow("Note:")} nothing answers on localhost:${port} yet. Start your dev server; the link works as soon as it does.
|
|
2201
|
+
`
|
|
2202
|
+
);
|
|
2203
|
+
const { apiUrl, token } = await credentials(io2);
|
|
2204
|
+
let opened = { mode: "quick" };
|
|
2205
|
+
const api = token && !options.quick ? createApi(io2, apiUrl, token) : null;
|
|
2206
|
+
if (api) opened = await api.request("/api/v1/shares", { json: { port } });
|
|
2207
|
+
const binary = await findCloudflared(io2);
|
|
2208
|
+
const hostHeader = options.hostHeader ?? "localhost";
|
|
2209
|
+
const args = opened.mode === "tunnel" && opened.token ? ["tunnel", "--no-autoupdate", "run", "--token", opened.token] : [
|
|
2210
|
+
"tunnel",
|
|
2211
|
+
"--no-autoupdate",
|
|
2212
|
+
"--url",
|
|
2213
|
+
`http://localhost:${port}`,
|
|
2214
|
+
"--http-host-header",
|
|
2215
|
+
hostHeader
|
|
2216
|
+
];
|
|
2217
|
+
const spawn2 = options.spawn ?? nodeSpawn;
|
|
2218
|
+
const child = spawn2(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2219
|
+
const progress = { announced: false };
|
|
2220
|
+
const tail = [];
|
|
2221
|
+
const remember = (chunk) => {
|
|
2222
|
+
for (const line of chunk.toString().split("\n").filter((l) => l.trim())) {
|
|
2223
|
+
tail.push(line.trim());
|
|
2224
|
+
if (tail.length > 4) tail.shift();
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
child.stdout?.on("data", remember);
|
|
2228
|
+
child.stderr?.on("data", remember);
|
|
2229
|
+
const announce = (url) => {
|
|
2230
|
+
if (progress.announced) return;
|
|
2231
|
+
progress.announced = true;
|
|
2232
|
+
io2.stdout(`
|
|
2233
|
+
${s.bold("Your localhost is live:")} ${s.cyan(url)}
|
|
2234
|
+
|
|
2235
|
+
`);
|
|
2236
|
+
io2.stdout(`${renderUnicodeCompact(url, { border: 1 })}
|
|
2237
|
+
|
|
2238
|
+
`);
|
|
2239
|
+
io2.stdout(
|
|
2240
|
+
`Scan with your phone, or share the link.${opened.share ? ` It closes when you press Ctrl+C, or at ${new Date(opened.share.expiresAt).toLocaleTimeString()}.` : " Quick links are public and anonymous; they close when you press Ctrl+C."}
|
|
2241
|
+
`
|
|
2242
|
+
);
|
|
2243
|
+
};
|
|
2244
|
+
if (opened.mode === "tunnel" && opened.share) {
|
|
2245
|
+
const url = opened.share.url;
|
|
2246
|
+
const watch = (chunk) => {
|
|
2247
|
+
if (/Registered tunnel connection|Connection [0-9a-f-]+ registered/i.test(chunk.toString()))
|
|
2248
|
+
announce(url);
|
|
2249
|
+
};
|
|
2250
|
+
child.stdout?.on("data", watch);
|
|
2251
|
+
child.stderr?.on("data", watch);
|
|
2252
|
+
} else {
|
|
2253
|
+
if (!api && !options.quick)
|
|
2254
|
+
io2.stderr(
|
|
2255
|
+
`${s.dim("Not signed in: using a quick tunnel. `hostedge login` for a hostedge.app link.")}
|
|
2256
|
+
`
|
|
2257
|
+
);
|
|
2258
|
+
const watch = (chunk) => {
|
|
2259
|
+
const match = QUICK_URL.exec(chunk.toString());
|
|
2260
|
+
if (match) announce(match[0]);
|
|
2261
|
+
};
|
|
2262
|
+
child.stdout?.on("data", watch);
|
|
2263
|
+
child.stderr?.on("data", watch);
|
|
2264
|
+
}
|
|
2265
|
+
const exitCode = await new Promise((resolve5) => {
|
|
2266
|
+
const stop = () => {
|
|
2267
|
+
child.kill("SIGTERM");
|
|
2268
|
+
};
|
|
2269
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
2270
|
+
child.on("error", () => resolve5(1));
|
|
2271
|
+
child.on("exit", (code) => {
|
|
2272
|
+
options.signal?.removeEventListener("abort", stop);
|
|
2273
|
+
resolve5(options.signal?.aborted ? 0 : code ?? 1);
|
|
2274
|
+
});
|
|
2275
|
+
});
|
|
2276
|
+
if (api && opened.share) {
|
|
2277
|
+
await api.request(`/api/v1/shares/${encodeURIComponent(opened.share.id)}`, { method: "DELETE" }).catch(() => void 0);
|
|
2278
|
+
io2.stdout(`${s.dim("Share closed.")}
|
|
2279
|
+
`);
|
|
2280
|
+
}
|
|
2281
|
+
if (!progress.announced && exitCode !== 0)
|
|
2282
|
+
throw new CliError(
|
|
2283
|
+
`cloudflared stopped before the link was ready. Check your connection.${tail.length ? `
|
|
2284
|
+
${tail.join("\n")}` : ""}`
|
|
2285
|
+
);
|
|
2286
|
+
return exitCode;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/cli.ts
|
|
2290
|
+
var VERSION = "0.1.0";
|
|
2291
|
+
var HELP = `hostedge \u2014 deploy to HostEdge from your terminal
|
|
2292
|
+
|
|
2293
|
+
Usage
|
|
2294
|
+
hostedge login [--token <token>] [--api <url>] Sign in (opens your browser)
|
|
2295
|
+
hostedge logout Forget the saved token
|
|
2296
|
+
hostedge whoami Show who and where you are signed in
|
|
2297
|
+
hostedge init [dir] [--name <name>] [--project <id|slug>] [--force]
|
|
2298
|
+
Link a folder to a project
|
|
2299
|
+
hostedge deploy [dir] [--preview] [--message <text>] [--no-wait]
|
|
2300
|
+
Upload and deploy a folder
|
|
2301
|
+
hostedge logs [--follow] [--level <level>] [--search <text>] [--since 1h|24h|7d]
|
|
2302
|
+
Show runtime logs
|
|
2303
|
+
hostedge env pull [--environment <name>] [--file <path>]
|
|
2304
|
+
Write variables to a .env file
|
|
2305
|
+
hostedge share [port] [--quick] [--host-header <host>]
|
|
2306
|
+
Public HTTPS link to a local dev server
|
|
2307
|
+
(try it on your phone before deploying)
|
|
2308
|
+
|
|
2309
|
+
Environment
|
|
2310
|
+
HOSTEDGE_TOKEN API token (CI); overrides the saved login
|
|
2311
|
+
HOSTEDGE_API_URL HostEdge URL (default https://hostedge.app)
|
|
2312
|
+
HOSTEDGE_CLOUDFLARED Path to cloudflared (otherwise PATH, then a one-time download)
|
|
2313
|
+
NO_COLOR Disable colors
|
|
2314
|
+
`;
|
|
2315
|
+
var str = (value) => typeof value === "string" ? value : void 0;
|
|
2316
|
+
async function run(argv, io2, signal) {
|
|
2317
|
+
const s = styles(io2);
|
|
2318
|
+
let parsed;
|
|
2319
|
+
try {
|
|
2320
|
+
parsed = parseArgs({
|
|
2321
|
+
args: argv,
|
|
2322
|
+
allowPositionals: true,
|
|
2323
|
+
strict: true,
|
|
2324
|
+
options: {
|
|
2325
|
+
help: { type: "boolean", short: "h" },
|
|
2326
|
+
version: { type: "boolean", short: "v" },
|
|
2327
|
+
token: { type: "string" },
|
|
2328
|
+
api: { type: "string" },
|
|
2329
|
+
name: { type: "string" },
|
|
2330
|
+
project: { type: "string" },
|
|
2331
|
+
force: { type: "boolean" },
|
|
2332
|
+
preview: { type: "boolean" },
|
|
2333
|
+
message: { type: "string", short: "m" },
|
|
2334
|
+
"no-wait": { type: "boolean" },
|
|
2335
|
+
follow: { type: "boolean", short: "f" },
|
|
2336
|
+
level: { type: "string" },
|
|
2337
|
+
search: { type: "string" },
|
|
2338
|
+
since: { type: "string" },
|
|
2339
|
+
environment: { type: "string", short: "e" },
|
|
2340
|
+
file: { type: "string" },
|
|
2341
|
+
quick: { type: "boolean" },
|
|
2342
|
+
"host-header": { type: "string" }
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
io2.stderr(`${error.message}
|
|
2347
|
+
Run \`hostedge --help\` for usage.
|
|
2348
|
+
`);
|
|
2349
|
+
return 2;
|
|
2350
|
+
}
|
|
2351
|
+
const { values, positionals } = parsed;
|
|
2352
|
+
const [command, ...rest] = positionals;
|
|
2353
|
+
if (values.version) {
|
|
2354
|
+
io2.stdout(`${VERSION}
|
|
2355
|
+
`);
|
|
2356
|
+
return 0;
|
|
2357
|
+
}
|
|
2358
|
+
if (values.help || !command) {
|
|
2359
|
+
io2.stdout(HELP);
|
|
2360
|
+
return command || values.help ? 0 : 2;
|
|
2361
|
+
}
|
|
2362
|
+
try {
|
|
2363
|
+
switch (command) {
|
|
2364
|
+
case "login":
|
|
2365
|
+
return await login(io2, { token: str(values.token), api: str(values.api) });
|
|
2366
|
+
case "logout":
|
|
2367
|
+
return await logout(io2);
|
|
2368
|
+
case "whoami":
|
|
2369
|
+
return await whoami(io2);
|
|
2370
|
+
case "init":
|
|
2371
|
+
return await init(io2, {
|
|
2372
|
+
dir: rest[0],
|
|
2373
|
+
name: str(values.name),
|
|
2374
|
+
project: str(values.project),
|
|
2375
|
+
force: values.force === true
|
|
2376
|
+
});
|
|
2377
|
+
case "deploy":
|
|
2378
|
+
return await deploy(io2, {
|
|
2379
|
+
dir: rest[0],
|
|
2380
|
+
preview: values.preview === true,
|
|
2381
|
+
message: str(values.message),
|
|
2382
|
+
wait: values["no-wait"] !== true
|
|
2383
|
+
});
|
|
2384
|
+
case "logs":
|
|
2385
|
+
return await logs(io2, {
|
|
2386
|
+
follow: values.follow === true,
|
|
2387
|
+
level: str(values.level),
|
|
2388
|
+
search: str(values.search),
|
|
2389
|
+
since: str(values.since),
|
|
2390
|
+
signal
|
|
2391
|
+
});
|
|
2392
|
+
case "env":
|
|
2393
|
+
if (rest[0] !== "pull") {
|
|
2394
|
+
io2.stderr("Usage: hostedge env pull [--environment <name>] [--file <path>]\n");
|
|
2395
|
+
return 2;
|
|
2396
|
+
}
|
|
2397
|
+
return await envPull(io2, { environment: str(values.environment), file: str(values.file) });
|
|
2398
|
+
case "share":
|
|
2399
|
+
return await share(io2, {
|
|
2400
|
+
port: rest[0],
|
|
2401
|
+
quick: values.quick === true,
|
|
2402
|
+
hostHeader: str(values["host-header"]),
|
|
2403
|
+
signal
|
|
2404
|
+
});
|
|
2405
|
+
default:
|
|
2406
|
+
io2.stderr(`Unknown command \u201C${command}\u201D. Run \`hostedge --help\` for usage.
|
|
2407
|
+
`);
|
|
2408
|
+
return 2;
|
|
2409
|
+
}
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
if (error instanceof CliError) {
|
|
2412
|
+
io2.stderr(`${s.red("Error:")} ${error.message}
|
|
2413
|
+
`);
|
|
2414
|
+
return error.exitCode;
|
|
2415
|
+
}
|
|
2416
|
+
throw error;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/main.ts
|
|
2421
|
+
function openUrl(url) {
|
|
2422
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2423
|
+
return new Promise((resolve5) => {
|
|
2424
|
+
try {
|
|
2425
|
+
const child = spawn(command, [url], { stdio: "ignore", detached: true });
|
|
2426
|
+
child.on("error", () => resolve5(false));
|
|
2427
|
+
child.on("spawn", () => {
|
|
2428
|
+
child.unref();
|
|
2429
|
+
resolve5(true);
|
|
2430
|
+
});
|
|
2431
|
+
} catch {
|
|
2432
|
+
resolve5(false);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
var io = {
|
|
2437
|
+
cwd: process.cwd(),
|
|
2438
|
+
env: process.env,
|
|
2439
|
+
stdout: (text) => process.stdout.write(text),
|
|
2440
|
+
stderr: (text) => process.stderr.write(text),
|
|
2441
|
+
fetch: globalThis.fetch,
|
|
2442
|
+
openUrl,
|
|
2443
|
+
// Undefined (not false) when stdout is a pipe.
|
|
2444
|
+
isTty: process.stdout.isTTY,
|
|
2445
|
+
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
|
|
2446
|
+
};
|
|
2447
|
+
var controller = new AbortController();
|
|
2448
|
+
process.on("SIGINT", () => {
|
|
2449
|
+
if (controller.signal.aborted) process.exit(130);
|
|
2450
|
+
controller.abort();
|
|
2451
|
+
});
|
|
2452
|
+
run(process.argv.slice(2), io, controller.signal).then(
|
|
2453
|
+
(code) => process.exit(code),
|
|
2454
|
+
(error) => {
|
|
2455
|
+
process.stderr.write(
|
|
2456
|
+
`Unexpected error: ${error instanceof Error ? error.stack : String(error)}
|
|
2457
|
+
`
|
|
2458
|
+
);
|
|
2459
|
+
process.exit(1);
|
|
2460
|
+
}
|
|
2461
|
+
);
|