@vincentt-xr/cli 0.16.0 → 0.18.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/MIGRATION.md +24 -0
- package/README.md +94 -1
- package/dist/cli/index.js +196 -87
- package/dist/cli/usage-text.js +32 -10
- package/dist/domain/api.d.ts +10 -8
- package/dist/domain/api.js +14 -9
- package/dist/domain/command.js +25 -16
- package/dist/domain/copy.d.ts +6 -3
- package/dist/domain/copy.js +10 -6
- package/dist/dx/command.d.ts +24 -4
- package/dist/dx/command.js +80 -25
- package/dist/dx/consent.js +6 -5
- package/dist/dx/journal.d.ts +17 -2
- package/dist/dx/journal.js +19 -3
- package/dist/dx/report.js +4 -3
- package/dist/dx/reportIo.js +6 -5
- package/dist/dx/send.js +6 -5
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +2 -26
- package/dist/lib/detect.d.ts +87 -0
- package/dist/lib/detect.js +867 -0
- package/dist/lib/diagnostics.js +2 -2
- package/dist/lib/feedback.js +1 -1
- package/dist/lib/feedbackCopy.js +1 -1
- package/dist/lib/initFrom.js +4 -4
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +9 -3
- package/dist/lib/listProjects.js +2 -1
- package/dist/lib/projects.d.ts +37 -2
- package/dist/lib/projects.js +86 -21
- package/dist/lib/settings.d.ts +113 -0
- package/dist/lib/settings.js +284 -0
- package/dist/lib/settingsCopy.d.ts +6 -0
- package/dist/lib/settingsCopy.js +99 -0
- package/dist/lib/settle.d.ts +61 -0
- package/dist/lib/settle.js +338 -0
- package/dist/lib/template.d.ts +19 -0
- package/dist/lib/template.js +73 -0
- package/dist/lib/terminalText.d.ts +3 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.js +5 -11
- package/dist/lib/whoami.js +10 -3
- package/dist/library/assetsCommand.js +1 -1
- package/dist/library/client.js +8 -3
- package/dist/library/render.js +8 -6
- package/dist/login/authBackend.js +3 -2
- package/dist/login/ensure.d.ts +8 -2
- package/dist/login/ensure.js +21 -10
- package/dist/login/guard.js +5 -4
- package/dist/login/loginCommand.d.ts +2 -2
- package/dist/login/loginCommand.js +29 -5
- package/dist/login/pkce.js +1 -1
- package/dist/login/pkceLogin.js +1 -1
- package/dist/platform/outdated.js +2 -2
- package/dist/platform/upgradeReport.js +16 -12
- package/dist/preview/api.d.ts +24 -11
- package/dist/preview/api.js +42 -15
- package/dist/preview/command.d.ts +29 -0
- package/dist/preview/command.js +61 -1
- package/dist/preview/copy.d.ts +35 -3
- package/dist/preview/copy.js +102 -10
- package/dist/preview/proxy.d.ts +38 -1
- package/dist/preview/proxy.js +151 -2
- package/dist/preview/qrPage.js +1 -1
- package/dist/preview/runner.d.ts +25 -13
- package/dist/preview/runner.js +127 -26
- package/dist/preview/viewers.js +1 -1
- package/dist/publish/api.d.ts +39 -6
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +25 -0
- package/dist/publish/command.js +230 -32
- package/dist/publish/copy.d.ts +78 -5
- package/dist/publish/copy.js +271 -37
- package/dist/publish/fit.d.ts +15 -0
- package/dist/publish/fit.js +103 -0
- package/dist/publish/manifest.d.ts +44 -3
- package/dist/publish/manifest.js +172 -3
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +15 -1
- package/dist/publish/upload.d.ts +6 -0
- package/dist/publish/upload.js +11 -1
- package/dist/publish/visibility.d.ts +21 -0
- package/dist/publish/visibility.js +68 -0
- package/dist/relay/annotations.js +9 -4
- package/dist/relay/server.d.ts +21 -0
- package/dist/relay/server.js +35 -1
- package/dist/shared/authError.d.ts +27 -0
- package/dist/shared/authError.js +48 -4
- package/dist/shared/config.d.ts +21 -1
- package/dist/shared/config.js +39 -8
- package/dist/shared/countLimit.d.ts +114 -0
- package/dist/shared/countLimit.js +176 -0
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
package/dist/publish/copy.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// NEVER PRINTED, at any point, in any state: the project id, the org id, the
|
|
13
13
|
// uploadId, the PAT, any presigned URL or fragment of one, the bucket, any S3
|
|
14
14
|
// key, or a per-file upload log.
|
|
15
|
+
import { escapeControl } from "../lib/terminalText.js";
|
|
15
16
|
/** The only horizontal rules in any f4 output. They exist here and nowhere else. */
|
|
16
17
|
const RULE = " ─────────────────────────────────────────────────────────────────────";
|
|
17
18
|
const LABELS = {
|
|
@@ -84,7 +85,7 @@ export function relativeTime(iso, now) {
|
|
|
84
85
|
* A FINDING IS NEVER A COUNT. No "2 issues found", no summary line, and no exit
|
|
85
86
|
* code that varies with findings.
|
|
86
87
|
*/
|
|
87
|
-
export function findingLines(byFile, withSubtitle) {
|
|
88
|
+
export function findingLines(byFile, withSubtitle, folder = "dist/") {
|
|
88
89
|
const out = [];
|
|
89
90
|
out.push(withSubtitle
|
|
90
91
|
? " Link preview (what Slack, iMessage and WhatsApp will show)"
|
|
@@ -99,7 +100,7 @@ export function findingLines(byFile, withSubtitle) {
|
|
|
99
100
|
out.push("");
|
|
100
101
|
}
|
|
101
102
|
for (const finding of file.findings) {
|
|
102
|
-
out.push(...findingLine(finding, file.file, multi));
|
|
103
|
+
out.push(...findingLine(finding, file.file, multi, folder));
|
|
103
104
|
}
|
|
104
105
|
if (multi)
|
|
105
106
|
out.push("");
|
|
@@ -108,7 +109,7 @@ export function findingLines(byFile, withSubtitle) {
|
|
|
108
109
|
out.push("");
|
|
109
110
|
return out;
|
|
110
111
|
}
|
|
111
|
-
function findingLine(f, file, indented) {
|
|
112
|
+
function findingLine(f, file, indented, folder) {
|
|
112
113
|
const pad = indented ? " " : "";
|
|
113
114
|
const label = LABELS[f.field].padEnd(LABEL_WIDTH);
|
|
114
115
|
// The file · tag sits on a continuation line indented to the VALUE column, so
|
|
@@ -128,7 +129,7 @@ function findingLine(f, file, indented) {
|
|
|
128
129
|
case "likely-default":
|
|
129
130
|
return [`${pad} ! ${label} looks unset: "${truncate(f.value ?? "")}"`, detail];
|
|
130
131
|
case "dead-image":
|
|
131
|
-
return [`${pad} ✗ ${label} ${f.value} is not in
|
|
132
|
+
return [`${pad} ✗ ${label} ${f.value} is not in ${folder}`, detail];
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
@@ -140,13 +141,36 @@ function findingLine(f, file, indented) {
|
|
|
140
141
|
export function nothingBlocksYou() {
|
|
141
142
|
return [
|
|
142
143
|
" Scrapers don't run JavaScript, so these have to be in the HTML your",
|
|
143
|
-
" build produces. Fix them in your source and publish again
|
|
144
|
+
" build produces. Fix them in your source and publish again, or publish",
|
|
144
145
|
" now and fix them later. Nothing here blocks you.",
|
|
145
146
|
"",
|
|
146
147
|
];
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const FINDINGS_SHOWN = 10;
|
|
150
|
+
/**
|
|
151
|
+
* The `Paths from the root` section: a second section of the checklist, same
|
|
152
|
+
* geometry as the link preview. Absent when there are no findings, never an
|
|
153
|
+
* empty heading. One explanation for the whole section.
|
|
154
|
+
*/
|
|
155
|
+
export function pathsFromRootLines(findings, folder = "dist/") {
|
|
156
|
+
if (!findings || findings.length === 0)
|
|
157
|
+
return [];
|
|
158
|
+
const out = [" Paths from the root", ""];
|
|
159
|
+
for (const f of findings.slice(0, FINDINGS_SHOWN)) {
|
|
160
|
+
out.push(` ! ${f.ref} is not in ${folder}`);
|
|
161
|
+
out.push(`${" ".repeat(21)}${f.file} · ${f.tag}`);
|
|
162
|
+
}
|
|
163
|
+
if (findings.length > FINDINGS_SHOWN) {
|
|
164
|
+
out.push(` and ${findings.length - FINDINGS_SHOWN} more`);
|
|
165
|
+
}
|
|
166
|
+
out.push("", " Vincentt serves this app at the root of its address. A missing path that", " starts with / usually means the build was made for a subpath, such as a", " GitHub Pages project site. Nothing here blocks you.", "");
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
const DEFAULT_OUTPUT = { folder: "dist/", sourceNote: "" };
|
|
170
|
+
export function readingLine(fileCount, totalBytes, output = DEFAULT_OUTPUT) {
|
|
171
|
+
const note = output.sourceNote ? ` ${output.sourceNote}` : "";
|
|
172
|
+
// The folder is the creator's build.output or --dist, so it is escaped.
|
|
173
|
+
return `→ Reading ${escapeControl(output.folder)}${note} … ${fileCount} files · ${formatBytes(totalBytes)}`;
|
|
150
174
|
}
|
|
151
175
|
export function headerLine(projectName, orgName) {
|
|
152
176
|
return orgName ? `→ ${projectName} · ${orgName}` : `→ ${projectName}`;
|
|
@@ -165,7 +189,7 @@ export function firstPublishChecklist(i) {
|
|
|
165
189
|
return [
|
|
166
190
|
"",
|
|
167
191
|
headerLine(i.projectName, i.orgName),
|
|
168
|
-
readingLine(i.fileCount, i.totalBytes),
|
|
192
|
+
readingLine(i.fileCount, i.totalBytes, i.output),
|
|
169
193
|
"",
|
|
170
194
|
RULE,
|
|
171
195
|
" FIRST PUBLISH",
|
|
@@ -178,14 +202,15 @@ export function firstPublishChecklist(i) {
|
|
|
178
202
|
// visible.
|
|
179
203
|
` ${i.address}`,
|
|
180
204
|
"",
|
|
181
|
-
" After this it is yours forever, and can never be changed or reused
|
|
205
|
+
" After this it is yours forever, and can never be changed or reused,",
|
|
182
206
|
" not by you, not by anyone else. Publishing again replaces what it",
|
|
183
207
|
" serves; it never changes the address.",
|
|
184
208
|
"",
|
|
185
209
|
RULE,
|
|
186
210
|
"",
|
|
187
|
-
...findingLines(i.findings, true),
|
|
211
|
+
...findingLines(i.findings, true, i.output?.folder),
|
|
188
212
|
...nothingBlocksYou(),
|
|
213
|
+
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
189
214
|
` Publishing v${i.nextVersion} · ${i.fileCount} files · ${formatBytes(i.totalBytes)}`,
|
|
190
215
|
"",
|
|
191
216
|
];
|
|
@@ -223,12 +248,13 @@ export function republishChecklist(i) {
|
|
|
223
248
|
return [
|
|
224
249
|
"",
|
|
225
250
|
headerLine(i.projectName, i.orgName),
|
|
226
|
-
readingLine(i.fileCount, i.totalBytes),
|
|
251
|
+
readingLine(i.fileCount, i.totalBytes, i.output),
|
|
227
252
|
"",
|
|
228
253
|
` Live at ${i.address} ${context}`,
|
|
229
254
|
` Publishing v${i.nextVersion} · ${i.fileCount} files · ${formatBytes(i.totalBytes)}`,
|
|
230
255
|
"",
|
|
231
|
-
...findingLines(i.findings, false),
|
|
256
|
+
...findingLines(i.findings, false, i.output?.folder),
|
|
257
|
+
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
232
258
|
];
|
|
233
259
|
}
|
|
234
260
|
/** `[Y/n]` — on a republish an accidental Enter does the thing the creator typed the command to do. */
|
|
@@ -245,18 +271,74 @@ export function offlineChecklist(i) {
|
|
|
245
271
|
return [
|
|
246
272
|
"",
|
|
247
273
|
headerLine(i.projectName, i.orgName),
|
|
248
|
-
readingLine(i.fileCount, i.totalBytes),
|
|
274
|
+
readingLine(i.fileCount, i.totalBytes, i.output),
|
|
249
275
|
"",
|
|
250
276
|
`! ${i.address} is currently offline.`,
|
|
251
277
|
"",
|
|
252
278
|
` Publishing v${i.nextVersion} will put it back online and make v${i.nextVersion} live.`,
|
|
253
279
|
"",
|
|
254
|
-
...findingLines(i.findings, false),
|
|
280
|
+
...findingLines(i.findings, false, i.output?.folder),
|
|
281
|
+
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
255
282
|
];
|
|
256
283
|
}
|
|
257
284
|
export function offlinePrompt(version) {
|
|
258
285
|
return ` Publish v${version} and bring the address back online? [y/N] `;
|
|
259
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* The address has published before but serves nothing now (`not_serving`), or
|
|
289
|
+
* the api reports a state this CLI does not know. Neither may say "Live at":
|
|
290
|
+
* the creator would share an address that answers with nothing.
|
|
291
|
+
*/
|
|
292
|
+
export function notServingChecklist(i, stateKnown) {
|
|
293
|
+
return [
|
|
294
|
+
"",
|
|
295
|
+
headerLine(i.projectName, i.orgName),
|
|
296
|
+
readingLine(i.fileCount, i.totalBytes, i.output),
|
|
297
|
+
"",
|
|
298
|
+
stateKnown
|
|
299
|
+
? `! ${i.address} is not serving anything right now.`
|
|
300
|
+
: `! This CLI can't tell whether ${i.address} is serving right now.`,
|
|
301
|
+
"",
|
|
302
|
+
` Publishing v${i.nextVersion} will make v${i.nextVersion} live there.`,
|
|
303
|
+
"",
|
|
304
|
+
...findingLines(i.findings, false, i.output?.folder),
|
|
305
|
+
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
/** `[y/N]`: the CLI cannot say what this publish changes, so Enter does nothing. */
|
|
309
|
+
export function unknownStatePrompt(version) {
|
|
310
|
+
return ` Publish v${version} and make it live? [y/N] `;
|
|
311
|
+
}
|
|
312
|
+
// ---- f10, the held address ----------------------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* The project is `held_by_platform`: Vincentt took the org's addresses offline
|
|
315
|
+
* over its traffic allowance, and the api refuses every publish until they are
|
|
316
|
+
* restored. Refused BEFORE the upload, because uploading a whole build only to
|
|
317
|
+
* be refused at commit is the retry loop this copy exists to stop.
|
|
318
|
+
*
|
|
319
|
+
* The project read carries no figures, so this names none. It shares
|
|
320
|
+
* planLimitRefused's voice: the platform is the actor, the remedy is outside
|
|
321
|
+
* the CLI, and nothing was lost.
|
|
322
|
+
*/
|
|
323
|
+
export function heldByPlatform(address, org = {}) {
|
|
324
|
+
return [
|
|
325
|
+
"",
|
|
326
|
+
`✗ Vincentt has taken ${address} offline.`,
|
|
327
|
+
"",
|
|
328
|
+
" This wasn't you. The organization went over its monthly traffic allowance,",
|
|
329
|
+
" and Vincentt took its published addresses offline. Publishing stays refused",
|
|
330
|
+
" until they are restored.",
|
|
331
|
+
"",
|
|
332
|
+
" Taking a paid plan restores them automatically, in the console. It cannot be",
|
|
333
|
+
" done from the CLI.",
|
|
334
|
+
"",
|
|
335
|
+
" Every version is still stored and the address is still yours.",
|
|
336
|
+
"",
|
|
337
|
+
...remedyLines(org.orgSlug, org.consoleUrl),
|
|
338
|
+
" Nothing was uploaded.",
|
|
339
|
+
"",
|
|
340
|
+
];
|
|
341
|
+
}
|
|
260
342
|
// ---- Flow L31, not signed in -------------------------------------------------
|
|
261
343
|
/**
|
|
262
344
|
* f1's line verbatim, with f4's reason. Note what is ABSENT, deliberately: f3's
|
|
@@ -350,28 +432,136 @@ function largestBlock(v) {
|
|
|
350
432
|
return v.largest.map((f) => ` ${f.path.padEnd(width)} ${formatBytes(f.size).padStart(sizeWidth)}`);
|
|
351
433
|
}
|
|
352
434
|
/** No dist at all, or no index.html at its root. Zero network calls. */
|
|
353
|
-
|
|
435
|
+
/**
|
|
436
|
+
* The build line. The recorded `build.command` when there is one; the template's
|
|
437
|
+
* `npm run build` when there is no vincentt.json; never a guess for an app whose
|
|
438
|
+
* file records no build command.
|
|
439
|
+
*/
|
|
440
|
+
function buildItLine(buildCommand) {
|
|
441
|
+
if (buildCommand === null)
|
|
442
|
+
return " Run your build, then publish again.";
|
|
443
|
+
return ` Build it, then publish: ${buildCommand ?? "npm run build"}`;
|
|
444
|
+
}
|
|
445
|
+
// The refusals below carry a creator-chosen path or command, so every line is
|
|
446
|
+
// escaped: none of their fixed copy holds a control character.
|
|
447
|
+
export function noDist(distDir, buildCommand) {
|
|
354
448
|
return [
|
|
355
449
|
"",
|
|
356
450
|
`✗ There's no build to publish at ${distDir}.`,
|
|
357
451
|
"",
|
|
358
|
-
" Publishing uploads what your build produced
|
|
452
|
+
" Publishing uploads what your build produced. Did the build run?",
|
|
359
453
|
"",
|
|
360
|
-
|
|
454
|
+
buildItLine(buildCommand),
|
|
361
455
|
"",
|
|
362
456
|
" Nothing was uploaded.",
|
|
363
457
|
"",
|
|
364
|
-
];
|
|
458
|
+
].map(escapeControl);
|
|
365
459
|
}
|
|
366
|
-
export function noIndexHtml(distDir) {
|
|
460
|
+
export function noIndexHtml(distDir, buildCommand) {
|
|
367
461
|
return [
|
|
368
462
|
"",
|
|
369
463
|
"✗ There's no index.html at the root of your build.",
|
|
370
464
|
"",
|
|
371
|
-
` ${distDir} exists, but nothing there answers the address's own URL
|
|
372
|
-
"
|
|
465
|
+
` ${distDir} exists, but nothing there answers the address's own URL.`,
|
|
466
|
+
" Did the build run?",
|
|
467
|
+
"",
|
|
468
|
+
buildItLine(buildCommand),
|
|
469
|
+
"",
|
|
470
|
+
" Nothing was uploaded.",
|
|
471
|
+
"",
|
|
472
|
+
].map(escapeControl);
|
|
473
|
+
}
|
|
474
|
+
/** Flow U1 — the output is the project folder itself. */
|
|
475
|
+
export function outputIsRoot(o) {
|
|
476
|
+
const how = o.source === "vincentt.json"
|
|
477
|
+
? ` vincentt.json sets build.output to "${o.value}", which would publish your source,`
|
|
478
|
+
: o.source === "flag"
|
|
479
|
+
? ` --dist ${o.value} points at it, which would publish your source,`
|
|
480
|
+
: ` ${o.value}/ resolves to it, which would publish your source,`;
|
|
481
|
+
return [
|
|
482
|
+
"",
|
|
483
|
+
"✗ The build output is this project's own folder, not a build.",
|
|
484
|
+
"",
|
|
485
|
+
how,
|
|
486
|
+
" settings and history. Set it to the folder your build writes.",
|
|
487
|
+
"",
|
|
488
|
+
" Nothing was uploaded.",
|
|
489
|
+
"",
|
|
490
|
+
].map(escapeControl);
|
|
491
|
+
}
|
|
492
|
+
/** Flow U1 — the output lies outside the project. `shown` is project-relative. */
|
|
493
|
+
export function outputOutside(shown) {
|
|
494
|
+
return [
|
|
495
|
+
"",
|
|
496
|
+
`✗ The build output ${escapeControl(shown)} is outside this project.`,
|
|
497
|
+
"",
|
|
498
|
+
" Publish uploads a folder inside the project. Set --dist, or build.output in",
|
|
499
|
+
" vincentt.json, to the folder your build writes.",
|
|
373
500
|
"",
|
|
374
|
-
"
|
|
501
|
+
" Nothing was uploaded.",
|
|
502
|
+
"",
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
/** Flow U1 — the output is, or sits inside, `.git` or `.vincentt`. `shown` is project-relative. */
|
|
506
|
+
export function outputInHidden(shown, hidden) {
|
|
507
|
+
return [
|
|
508
|
+
"",
|
|
509
|
+
`✗ The build output ${escapeControl(shown)} is inside ${hidden}/, not a build.`,
|
|
510
|
+
"",
|
|
511
|
+
" Publishing it would put your repository's history or its Vincentt link at a",
|
|
512
|
+
" public address. Set --dist, or build.output in vincentt.json, to the folder",
|
|
513
|
+
" your build writes.",
|
|
514
|
+
"",
|
|
515
|
+
" Nothing was uploaded.",
|
|
516
|
+
"",
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
function offenderList(paths, folder) {
|
|
520
|
+
// File names and the folder prefix come from the build and the creator's
|
|
521
|
+
// settings, and may carry control characters.
|
|
522
|
+
folder = escapeControl(folder);
|
|
523
|
+
const shown = paths.slice(0, FINDINGS_SHOWN).map((p) => ({
|
|
524
|
+
path: escapeControl(p.path),
|
|
525
|
+
target: p.target === undefined ? undefined : escapeControl(p.target),
|
|
526
|
+
}));
|
|
527
|
+
const width = Math.max(...shown.map((p) => folder.length + p.path.length));
|
|
528
|
+
const out = shown.map((p) => p.target !== undefined
|
|
529
|
+
? ` ${(folder + p.path).padEnd(width)} links to ${p.target}`
|
|
530
|
+
: ` ${folder}${p.path}`);
|
|
531
|
+
if (paths.length > FINDINGS_SHOWN)
|
|
532
|
+
out.push(` and ${paths.length - FINDINGS_SHOWN} more`);
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
/** Flow U1 — hidden names or symlinks that would leak. After the header and reading line. */
|
|
536
|
+
export function secretFiles(paths, output = DEFAULT_OUTPUT) {
|
|
537
|
+
return [
|
|
538
|
+
"",
|
|
539
|
+
"✗ This build holds files that must never be published.",
|
|
540
|
+
"",
|
|
541
|
+
...offenderList(paths, output.folder),
|
|
542
|
+
"",
|
|
543
|
+
` Everything in ${escapeControl(output.folder)} becomes public at your address. Remove these from the build`,
|
|
544
|
+
" output, rebuild, then publish.",
|
|
545
|
+
"",
|
|
546
|
+
" Nothing was uploaded.",
|
|
547
|
+
"",
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
551
|
+
export function lfsPointers(paths, output = DEFAULT_OUTPUT, buildCommand) {
|
|
552
|
+
return [
|
|
553
|
+
"",
|
|
554
|
+
"✗ This build holds Git LFS pointer files instead of the assets.",
|
|
555
|
+
"",
|
|
556
|
+
...offenderList(paths, output.folder),
|
|
557
|
+
"",
|
|
558
|
+
" The repo keeps these in Git LFS, and they were not downloaded before the build",
|
|
559
|
+
" ran. Download them, rebuild, then publish:",
|
|
560
|
+
"",
|
|
561
|
+
" git lfs pull",
|
|
562
|
+
buildCommand !== undefined
|
|
563
|
+
? ` ${escapeControl(buildCommand)}`
|
|
564
|
+
: " then run your build again",
|
|
375
565
|
"",
|
|
376
566
|
" Nothing was uploaded.",
|
|
377
567
|
"",
|
|
@@ -391,7 +581,7 @@ export function badPath(reason, offending) {
|
|
|
391
581
|
"",
|
|
392
582
|
`✗ This build ${why[reason] ?? "has a path we can't publish"}.`,
|
|
393
583
|
"",
|
|
394
|
-
` ${offending}`,
|
|
584
|
+
` ${escapeControl(offending)}`,
|
|
395
585
|
"",
|
|
396
586
|
" Rename it in your source, rebuild, then publish again.",
|
|
397
587
|
"",
|
|
@@ -436,6 +626,22 @@ export function liveAt(liveUrl, version) {
|
|
|
436
626
|
"",
|
|
437
627
|
];
|
|
438
628
|
}
|
|
629
|
+
/** Prints once the commit succeeded and the address is still serving the previous bytes. */
|
|
630
|
+
export function propagating(liveUrl, version) {
|
|
631
|
+
return `→ Propagating v${version} to ${liveUrl}…`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The wait ran out. The publish DID commit, so this is a success, and it says so
|
|
635
|
+
* plainly: a creator told "not yet" without that will publish again.
|
|
636
|
+
*/
|
|
637
|
+
export function notYetVisible(liveUrl, version) {
|
|
638
|
+
return [
|
|
639
|
+
`✓ Published v${version}, not yet visible at ${liveUrl}`,
|
|
640
|
+
"",
|
|
641
|
+
" It usually appears within a minute. There is no need to publish again.",
|
|
642
|
+
"",
|
|
643
|
+
];
|
|
644
|
+
}
|
|
439
645
|
/** Prints only if a retry is needed. It says nothing about a failure, because nothing has failed yet. */
|
|
440
646
|
export function takingLonger() {
|
|
441
647
|
return "! Taking a moment longer than usual…";
|
|
@@ -462,7 +668,10 @@ export function planLimitRefused(i) {
|
|
|
462
668
|
if (i.orgName || i.orgSlug)
|
|
463
669
|
rows.push(["Organization", orgLabel(i.orgName, i.orgSlug)]);
|
|
464
670
|
if (i.planName) {
|
|
465
|
-
rows.push([
|
|
671
|
+
rows.push([
|
|
672
|
+
"Plan",
|
|
673
|
+
`${i.planName}, ${formatBytes(i.allowanceBytes)} traffic per month`,
|
|
674
|
+
]);
|
|
466
675
|
}
|
|
467
676
|
else {
|
|
468
677
|
rows.push(["Allowance", `${formatBytes(i.allowanceBytes)} traffic per month`]);
|
|
@@ -488,7 +697,7 @@ export function planLimitRefused(i) {
|
|
|
488
697
|
// minutes, and copy that promises instant is copy that lies on a slow tick.
|
|
489
698
|
` Published addresses were taken offline${takenOn} because the organization`,
|
|
490
699
|
" went over its monthly traffic allowance. Taking a paid plan restores them",
|
|
491
|
-
" automatically, in the console
|
|
700
|
+
" automatically, in the console. It cannot be done from the CLI.",
|
|
492
701
|
"",
|
|
493
702
|
" Every version is still stored and the address is still yours.",
|
|
494
703
|
"",
|
|
@@ -519,15 +728,21 @@ function remedyLines(orgSlug, consoleUrl) {
|
|
|
519
728
|
if (!orgSlug)
|
|
520
729
|
return [` Email ${SUPPORT_EMAIL}.`, ""];
|
|
521
730
|
const base = (consoleUrl ?? "https://console.vincentt.studio").replace(/\/$/, "");
|
|
522
|
-
return [
|
|
523
|
-
` See ${base}/orgs/${orgSlug}/usage, or email`,
|
|
524
|
-
` ${SUPPORT_EMAIL}.`,
|
|
525
|
-
"",
|
|
526
|
-
];
|
|
731
|
+
return [` See ${base}/orgs/${orgSlug}/usage, or email`, ` ${SUPPORT_EMAIL}.`, ""];
|
|
527
732
|
}
|
|
528
733
|
const MONTHS = [
|
|
529
|
-
"Jan",
|
|
530
|
-
"
|
|
734
|
+
"Jan",
|
|
735
|
+
"Feb",
|
|
736
|
+
"Mar",
|
|
737
|
+
"Apr",
|
|
738
|
+
"May",
|
|
739
|
+
"Jun",
|
|
740
|
+
"Jul",
|
|
741
|
+
"Aug",
|
|
742
|
+
"Sep",
|
|
743
|
+
"Oct",
|
|
744
|
+
"Nov",
|
|
745
|
+
"Dec",
|
|
531
746
|
];
|
|
532
747
|
/** `29 Aug` — the date the creator reads, in UTC, matching the marker's own day. */
|
|
533
748
|
function shortDate(iso) {
|
|
@@ -568,6 +783,25 @@ export function flipFailed(address, liveVersion) {
|
|
|
568
783
|
"",
|
|
569
784
|
];
|
|
570
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* The commit's answer never arrived (the connection dropped or the api stopped
|
|
788
|
+
* answering), so whether the address switched is unknown. It says so, rather than
|
|
789
|
+
* claiming either outcome, and names the one check that settles it.
|
|
790
|
+
*/
|
|
791
|
+
export function commitOutcomeUnknown(address, liveVersion) {
|
|
792
|
+
return [
|
|
793
|
+
"",
|
|
794
|
+
"✗ Your files uploaded, but Vincentt's answer to the switch-over never arrived.",
|
|
795
|
+
"",
|
|
796
|
+
` It may or may not be live. Open https://${address} to check.`,
|
|
797
|
+
liveVersion === undefined
|
|
798
|
+
? " If it did not switch, nothing is served there yet."
|
|
799
|
+
: ` If it did not switch, it is still serving v${liveVersion}.`,
|
|
800
|
+
"",
|
|
801
|
+
" Publishing again is safe: vincentt publish",
|
|
802
|
+
"",
|
|
803
|
+
];
|
|
804
|
+
}
|
|
571
805
|
/**
|
|
572
806
|
* Two sentences, no explanation of version allocation. The real case is a
|
|
573
807
|
* teammate and an agent publishing in the same second, and "someone else
|
|
@@ -599,7 +833,7 @@ export function publishIncomplete(address, counts, liveVersion) {
|
|
|
599
833
|
: `${missing} file${missing === 1 ? "" : "s"} didn't arrive`;
|
|
600
834
|
return [
|
|
601
835
|
"",
|
|
602
|
-
`✗ The upload didn't complete
|
|
836
|
+
`✗ The upload didn't complete: ${what}.`,
|
|
603
837
|
"",
|
|
604
838
|
...unchangedLines(address, liveVersion),
|
|
605
839
|
"",
|
|
@@ -611,7 +845,7 @@ export function publishIncomplete(address, counts, liveVersion) {
|
|
|
611
845
|
export function badDigest(filePath, address, liveVersion) {
|
|
612
846
|
return [
|
|
613
847
|
"",
|
|
614
|
-
`✗ ${filePath} changed while uploading
|
|
848
|
+
`✗ ${filePath} changed while uploading. Rebuild and publish again.`,
|
|
615
849
|
"",
|
|
616
850
|
...unchangedLines(address, liveVersion),
|
|
617
851
|
"",
|
|
@@ -625,10 +859,10 @@ export function badDigest(filePath, address, liveVersion) {
|
|
|
625
859
|
*/
|
|
626
860
|
function unchangedLines(address, liveVersion) {
|
|
627
861
|
if (liveVersion === undefined) {
|
|
628
|
-
return [` Your live address is unchanged
|
|
862
|
+
return [` Your live address is unchanged: ${address}.`];
|
|
629
863
|
}
|
|
630
864
|
return [
|
|
631
|
-
` Your live address is unchanged
|
|
865
|
+
` Your live address is unchanged: ${address} is still`,
|
|
632
866
|
` serving v${liveVersion}.`,
|
|
633
867
|
];
|
|
634
868
|
}
|
|
@@ -682,7 +916,7 @@ export function consoleVerb(verb, versionsUrl) {
|
|
|
682
916
|
`✗ There's no \`${verb}\` command.`,
|
|
683
917
|
"",
|
|
684
918
|
" Rolling back, taking an address offline, and putting it back online are",
|
|
685
|
-
" console actions
|
|
919
|
+
" console actions: they change what a permanent address serves, and that",
|
|
686
920
|
" belongs on a screen where you can see every version first.",
|
|
687
921
|
"",
|
|
688
922
|
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HtmlFile {
|
|
2
|
+
/** Output-relative POSIX path, as in the manifest. */
|
|
3
|
+
file: string;
|
|
4
|
+
html: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RootAbsoluteFinding {
|
|
7
|
+
file: string;
|
|
8
|
+
/** The reference as written, query and fragment included. */
|
|
9
|
+
ref: string;
|
|
10
|
+
/** `<script src>`, `<link rel=stylesheet>`, `<img src>`, ... */
|
|
11
|
+
tag: string;
|
|
12
|
+
}
|
|
13
|
+
/** Does a `/path` reference resolve under the host's index rules? */
|
|
14
|
+
export declare function resolvesInManifest(ref: string, manifest: ReadonlySet<string>): boolean;
|
|
15
|
+
export declare function rootAbsoluteFindings(htmlFiles: readonly HtmlFile[], manifestPaths: ReadonlySet<string>): RootAbsoluteFinding[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Root-absolute resource references that do not resolve in the output. The
|
|
2
|
+
// likely cause is a build made for a subpath (a GitHub Pages project site) while
|
|
3
|
+
// Vincentt serves the root. A WARNING only: nothing is rewritten and nothing is
|
|
4
|
+
// blocked, because the creator's build is what fixes it.
|
|
5
|
+
//
|
|
6
|
+
// Resource tags only. `<a href>` is a link to a route, not a missing file, and
|
|
7
|
+
// scanning it would warn on nearly every site's navigation. URLs baked into JS
|
|
8
|
+
// or CSS are not scanned (a stated limit).
|
|
9
|
+
//
|
|
10
|
+
// Regex-based, like classify.ts; every pattern here is linear.
|
|
11
|
+
import { escapeControl } from "../lib/terminalText.js";
|
|
12
|
+
const LINK_RELS = new Set(["stylesheet", "preload", "modulepreload", "icon"]);
|
|
13
|
+
const SRC_TAGS = new Set(["script", "img", "source", "video", "audio"]);
|
|
14
|
+
const TAG_RE = /<(script|link|img|source|video|audio)\b([^>]*)>/gi;
|
|
15
|
+
const ATTR_RE = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
16
|
+
function stripComments(html) {
|
|
17
|
+
let out = "";
|
|
18
|
+
let i = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const start = html.indexOf("<!--", i);
|
|
21
|
+
if (start === -1)
|
|
22
|
+
return out + html.slice(i);
|
|
23
|
+
out += html.slice(i, start);
|
|
24
|
+
const end = html.indexOf("-->", start + 4);
|
|
25
|
+
if (end === -1)
|
|
26
|
+
return out;
|
|
27
|
+
i = end + 3;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function attributes(attrs) {
|
|
31
|
+
const out = new Map();
|
|
32
|
+
for (const m of attrs.matchAll(ATTR_RE)) {
|
|
33
|
+
const name = m[1].toLowerCase();
|
|
34
|
+
if (!out.has(name))
|
|
35
|
+
out.set(name, m[2] ?? m[3] ?? m[4] ?? "");
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Does a `/path` reference resolve under the host's index rules? */
|
|
40
|
+
export function resolvesInManifest(ref, manifest) {
|
|
41
|
+
const bare = ref.split("#")[0].split("?")[0];
|
|
42
|
+
let decoded = bare;
|
|
43
|
+
try {
|
|
44
|
+
decoded = decodeURI(bare);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// An undecodable reference is judged as written.
|
|
48
|
+
}
|
|
49
|
+
const rel = decoded.slice(1);
|
|
50
|
+
if (manifest.has(rel))
|
|
51
|
+
return true;
|
|
52
|
+
if (decoded.endsWith("/"))
|
|
53
|
+
return manifest.has(`${rel}index.html`);
|
|
54
|
+
const last = rel.split("/").pop() ?? "";
|
|
55
|
+
if (!last.includes("."))
|
|
56
|
+
return manifest.has(`${rel}/index.html`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function isRootAbsolute(value) {
|
|
60
|
+
return value.startsWith("/") && !value.startsWith("//");
|
|
61
|
+
}
|
|
62
|
+
export function rootAbsoluteFindings(htmlFiles, manifestPaths) {
|
|
63
|
+
const findings = [];
|
|
64
|
+
for (const { file, html } of htmlFiles) {
|
|
65
|
+
for (const m of stripComments(html).matchAll(TAG_RE)) {
|
|
66
|
+
const tag = m[1].toLowerCase();
|
|
67
|
+
const attrs = attributes(m[2] ?? "");
|
|
68
|
+
let value;
|
|
69
|
+
let label;
|
|
70
|
+
if (tag === "link") {
|
|
71
|
+
const rel = (attrs.get("rel") ?? "")
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.split(/\s+/)
|
|
74
|
+
.find((r) => LINK_RELS.has(r));
|
|
75
|
+
if (rel === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
value = attrs.get("href");
|
|
78
|
+
label = `<link rel=${rel}>`;
|
|
79
|
+
}
|
|
80
|
+
else if (SRC_TAGS.has(tag)) {
|
|
81
|
+
value = attrs.get("src");
|
|
82
|
+
label = `<${tag} src>`;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
if (!isRootAbsolute(trimmed))
|
|
91
|
+
continue;
|
|
92
|
+
if (resolvesInManifest(trimmed, manifestPaths))
|
|
93
|
+
continue;
|
|
94
|
+
// Both come from the creator's build and reach the terminal verbatim.
|
|
95
|
+
findings.push({
|
|
96
|
+
file: escapeControl(file),
|
|
97
|
+
ref: escapeControl(trimmed),
|
|
98
|
+
tag: label,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
@@ -10,11 +10,52 @@ export interface Manifest {
|
|
|
10
10
|
files: ManifestEntry[];
|
|
11
11
|
totalBytes: number;
|
|
12
12
|
}
|
|
13
|
+
export type ManifestReason = PathRejection | "no_dist" | "output_is_repo_root" | "output_outside_repo" | "output_in_hidden" | "secret_path" | "lfs_pointer";
|
|
14
|
+
/** An output-relative path the leak or LFS check refused. */
|
|
15
|
+
export interface Offender {
|
|
16
|
+
/** Output-relative, POSIX. A refused folder carries a trailing `/`. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** For a symlink: where it resolves, repo-relative inside and absolute outside. */
|
|
19
|
+
target?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ManifestErrorDetail {
|
|
22
|
+
paths?: Offender[];
|
|
23
|
+
/** What the walk saw, for the reading line above a refusal. */
|
|
24
|
+
stats?: {
|
|
25
|
+
files: number;
|
|
26
|
+
bytes: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
13
29
|
export declare class ManifestError extends Error {
|
|
14
|
-
readonly reason:
|
|
30
|
+
readonly reason: ManifestReason;
|
|
15
31
|
readonly offendingPath: string;
|
|
16
|
-
|
|
32
|
+
readonly paths: Offender[];
|
|
33
|
+
readonly stats?: {
|
|
34
|
+
files: number;
|
|
35
|
+
bytes: number;
|
|
36
|
+
};
|
|
37
|
+
constructor(reason: ManifestReason, offendingPath: string, message: string, detail?: ManifestErrorDetail);
|
|
38
|
+
}
|
|
39
|
+
export interface BuildManifestOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The directory holding the binding. When given, the output is refused before
|
|
42
|
+
* any hashing if it is the repo root, lies outside it, holds a dotted name
|
|
43
|
+
* outside the two allowed ones, holds a symlink that resolves outside the root
|
|
44
|
+
* or onto a dotted path, or holds a Git LFS pointer. All of it is local.
|
|
45
|
+
*/
|
|
46
|
+
repoRoot?: string;
|
|
17
47
|
}
|
|
48
|
+
/** A Git LFS pointer file: at most 1 KB, the spec's exact first line, and an oid line. */
|
|
49
|
+
export declare function isLfsPointer(absPath: string, size: number): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* The leak and LFS checks, in that order, before anything is hashed or sent.
|
|
52
|
+
* Returns the output-relative file list the checks passed, which is exactly the
|
|
53
|
+
* list the manifest is built from: what is checked is what is uploaded.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkOutput(distDir: string, repoRoot: string): Promise<{
|
|
56
|
+
rel: string;
|
|
57
|
+
size: number;
|
|
58
|
+
}[]>;
|
|
18
59
|
/** Recursively list files under `dir` as dir-relative POSIX paths. Symlinks are not followed. */
|
|
19
60
|
export declare function walkDist(dir: string, base?: string): Promise<string[]>;
|
|
20
61
|
/** sha256 of a file, base64 std — the exact form `x-amz-checksum-sha256` wants. */
|
|
@@ -24,4 +65,4 @@ export declare function hashFile(absPath: string): Promise<string>;
|
|
|
24
65
|
* hashed, so the string this returns is the string the API validates, signs, and
|
|
25
66
|
* later lists — there is no second form anywhere.
|
|
26
67
|
*/
|
|
27
|
-
export declare function buildManifest(distDir: string): Promise<Manifest>;
|
|
68
|
+
export declare function buildManifest(distDir: string, opts?: BuildManifestOptions): Promise<Manifest>;
|