@buildinternet/uploads 0.24.0 → 0.26.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 +6 -2
- package/dist/commands.js +20 -11
- package/dist/github-gh.d.ts +4 -2
- package/dist/github-gh.js +8 -3
- package/dist/github.d.ts +4 -0
- package/dist/github.js +150 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,8 +91,12 @@ attachments when one opens: automatically via the
|
|
|
91
91
|
[GitHub App](https://uploads.sh/docs/github-app) webhook, or on the first
|
|
92
92
|
`attach` after the PR exists (`--promote` forces it with no new files,
|
|
93
93
|
`--no-promote` opts out). `uploads github link` inspects or claims the
|
|
94
|
-
workspace↔repo binding the webhook path uses.
|
|
95
|
-
|
|
94
|
+
workspace↔repo binding the webhook path uses. Promotion is copy-and-keep —
|
|
95
|
+
the staged original is never deleted, so any URL already embedded keeps
|
|
96
|
+
serving — and staged objects follow only normal per-workspace retention and
|
|
97
|
+
explicit deletes. Promotion (auto or `--promote`) does skip files staged
|
|
98
|
+
more than 30 days before the PR opens, though; they're still there, just no
|
|
99
|
+
longer auto-promoted.
|
|
96
100
|
|
|
97
101
|
**Bare `put` stages too, by default (issue #403):** on a non-default git
|
|
98
102
|
branch, a `put` with none of
|
package/dist/commands.js
CHANGED
|
@@ -527,16 +527,16 @@ export async function syncAttachmentsComment(client, target, run, workspace) {
|
|
|
527
527
|
return gallery;
|
|
528
528
|
}
|
|
529
529
|
}));
|
|
530
|
-
if (items.length === 0 && previewGalleries.length === 0)
|
|
531
|
-
return { action: "skipped", count: 0, via: "gh" };
|
|
532
530
|
const marker = attachmentsMarker(workspace);
|
|
533
531
|
const body = attachmentsCommentBody(items, previewGalleries, marker);
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
532
|
+
const count = items.length + previewGalleries.length;
|
|
533
|
+
// Empty (count 0) renders the neutral empty-state body but must not create a
|
|
534
|
+
// comment — it only rewrites one that already exists (`action: "skipped"`
|
|
535
|
+
// when none does).
|
|
536
|
+
const { action } = upsertAttachmentsComment(target, body, run, marker, {
|
|
537
|
+
createIfMissing: count > 0,
|
|
538
|
+
});
|
|
539
|
+
return { action, count, via: "gh" };
|
|
540
540
|
}
|
|
541
541
|
// --- attach ---
|
|
542
542
|
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
@@ -2247,9 +2247,18 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
2247
2247
|
}
|
|
2248
2248
|
else if (!ctx.quiet) {
|
|
2249
2249
|
const via = commentViaSuffix(result.via);
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2250
|
+
let line;
|
|
2251
|
+
if (result.action === "skipped") {
|
|
2252
|
+
line = `no attachments under ${ghKeyPrefix(target)} — nothing to do\n`;
|
|
2253
|
+
}
|
|
2254
|
+
else if (result.count === 0) {
|
|
2255
|
+
// An existing comment rewritten to the empty state (every file removed).
|
|
2256
|
+
line = `cleared attachments comment on ${target.repo}#${target.num} — no files remaining${via}\n`;
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
line = `${result.action} attachments comment on ${target.repo}#${target.num} (${result.count} file${result.count === 1 ? "" : "s"})${via}\n`;
|
|
2260
|
+
}
|
|
2261
|
+
process.stderr.write(line);
|
|
2253
2262
|
}
|
|
2254
2263
|
return 0;
|
|
2255
2264
|
}
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export declare function ghMetadataFromTargetWithTitle(target: GhTarget, run?: Co
|
|
|
64
64
|
* an adopted legacy comment migrates it to the namespaced marker in place.
|
|
65
65
|
* Defaults to the shared legacy marker for backward compatibility.
|
|
66
66
|
*/
|
|
67
|
-
export declare function upsertAttachmentsComment(target: GhTarget, body: string, run?: CommandRunner, marker?: string
|
|
68
|
-
|
|
67
|
+
export declare function upsertAttachmentsComment(target: GhTarget, body: string, run?: CommandRunner, marker?: string, opts?: {
|
|
68
|
+
createIfMissing?: boolean;
|
|
69
|
+
}): {
|
|
70
|
+
action: "created" | "updated" | "skipped";
|
|
69
71
|
};
|
package/dist/github-gh.js
CHANGED
|
@@ -221,7 +221,8 @@ function findManagedComment(target, run, marker) {
|
|
|
221
221
|
* an adopted legacy comment migrates it to the namespaced marker in place.
|
|
222
222
|
* Defaults to the shared legacy marker for backward compatibility.
|
|
223
223
|
*/
|
|
224
|
-
export function upsertAttachmentsComment(target, body, run = execRunner, marker = ATTACHMENTS_MARKER) {
|
|
224
|
+
export function upsertAttachmentsComment(target, body, run = execRunner, marker = ATTACHMENTS_MARKER, opts = {}) {
|
|
225
|
+
const createIfMissing = opts.createIfMissing ?? true;
|
|
225
226
|
const existing = findManagedComment(target, run, marker);
|
|
226
227
|
if (existing) {
|
|
227
228
|
run("gh", [
|
|
@@ -232,8 +233,12 @@ export function upsertAttachmentsComment(target, body, run = execRunner, marker
|
|
|
232
233
|
"-F",
|
|
233
234
|
"body=@-",
|
|
234
235
|
], body);
|
|
235
|
-
return {
|
|
236
|
+
return { action: "updated" };
|
|
236
237
|
}
|
|
238
|
+
// Patch-only (createIfMissing false, i.e. an empty body) with no existing
|
|
239
|
+
// comment: nothing to do — never create one just to say it's empty.
|
|
240
|
+
if (!createIfMissing)
|
|
241
|
+
return { action: "skipped" };
|
|
237
242
|
run("gh", ["api", `repos/${target.repo}/issues/${target.num}/comments`, "-F", "body=@-"], body);
|
|
238
|
-
return {
|
|
243
|
+
return { action: "created" };
|
|
239
244
|
}
|
package/dist/github.d.ts
CHANGED
|
@@ -123,6 +123,10 @@ export declare const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
|
123
123
|
* practical signal (we don't re-fetch dimensions when rebuilding the comment).
|
|
124
124
|
*/
|
|
125
125
|
export declare function attachmentImageWidth(filename: string): number;
|
|
126
|
+
/** Max display width for one image inside a before/after pair row — smaller
|
|
127
|
+
* than a standalone image so two side by side stay under GitHub's comment
|
|
128
|
+
* column width (and don't overflow on mobile). */
|
|
129
|
+
export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
126
130
|
/**
|
|
127
131
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
128
132
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
package/dist/github.js
CHANGED
|
@@ -255,6 +255,120 @@ function metaCaptionMarkdown(meta) {
|
|
|
255
255
|
return "";
|
|
256
256
|
return ` · ${parts.map((p) => escapeMarkdownText(escapeHtmlText(p))).join(" · ")}`;
|
|
257
257
|
}
|
|
258
|
+
/** Extract the filename stem's before/after token (issue #419 fallback pairing).
|
|
259
|
+
* `base` is the stem lowercased with the token removed; `null` when the stem
|
|
260
|
+
* carries no recognizable before/after token. Requires a separator (`-`, `_`,
|
|
261
|
+
* or `.`) between the token and the rest of the name — except when the token
|
|
262
|
+
* IS the whole stem (`before.png`) — so `beforehand.png` doesn't false-match. */
|
|
263
|
+
// Token bounded by `-`, `_`, `.`, or stem start/end, so `hero-before.webp`
|
|
264
|
+
// and `paired-view-before-desktop.webp` match but `beforehand.webp` does
|
|
265
|
+
// not. Mirrors before-after.ts's TOKEN_RE (file page), applied to the stem.
|
|
266
|
+
const STEM_TOKEN_RE = /(^|[-_.])(before|after)($|[-_.])/i;
|
|
267
|
+
function filenameStemToken(name) {
|
|
268
|
+
const dot = name.lastIndexOf(".");
|
|
269
|
+
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
270
|
+
const m = STEM_TOKEN_RE.exec(stem);
|
|
271
|
+
if (!m)
|
|
272
|
+
return null;
|
|
273
|
+
const state = m[2].toLowerCase();
|
|
274
|
+
const tokenStart = m.index + m[1].length;
|
|
275
|
+
const tokenEnd = tokenStart + m[2].length;
|
|
276
|
+
// Base = stem with the token and one adjoining delimiter removed, so
|
|
277
|
+
// `paired-view-before-desktop` and `paired-view-after-desktop` both
|
|
278
|
+
// collapse to `paired-view-desktop` and group together.
|
|
279
|
+
const base = m[1].length > 0
|
|
280
|
+
? stem.slice(0, m.index) + stem.slice(tokenEnd)
|
|
281
|
+
: stem.slice(tokenEnd + m[3].length);
|
|
282
|
+
return { base: base.toLowerCase(), state };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Pair up attachments for the before/after side-by-side row (issue #419).
|
|
286
|
+
* `isImageAt[i]` mirrors the renderer's own image test — only images pair;
|
|
287
|
+
* videos and non-image links render exactly as before.
|
|
288
|
+
*
|
|
289
|
+
* Priority order, checked independently per candidate item so rule 2 only
|
|
290
|
+
* ever claims items rule 1 left untouched:
|
|
291
|
+
* 1. Same `path` metadata (trimmed, not bare `/`), one item `state=before`
|
|
292
|
+
* and one `state=after`. Ambiguous groups (more than one of a state)
|
|
293
|
+
* don't pair — no way to know which side goes with which.
|
|
294
|
+
* 2. No usable `path` metadata: filename stems that differ only by a
|
|
295
|
+
* before/after token, same extension. Same ambiguity rule.
|
|
296
|
+
*/
|
|
297
|
+
function pairAttachments(items, isImageAt) {
|
|
298
|
+
const partnerOf = new Map();
|
|
299
|
+
const roleOf = new Map();
|
|
300
|
+
const pair = (beforeIdx, afterIdx) => {
|
|
301
|
+
partnerOf.set(beforeIdx, afterIdx);
|
|
302
|
+
partnerOf.set(afterIdx, beforeIdx);
|
|
303
|
+
roleOf.set(beforeIdx, "before");
|
|
304
|
+
roleOf.set(afterIdx, "after");
|
|
305
|
+
};
|
|
306
|
+
// Priority 1: same path metadata, exactly one before + one after.
|
|
307
|
+
const pathGroups = new Map();
|
|
308
|
+
items.forEach((item, i) => {
|
|
309
|
+
if (!isImageAt[i])
|
|
310
|
+
return;
|
|
311
|
+
const path = item.meta?.path?.trim();
|
|
312
|
+
if (!path || path === "/")
|
|
313
|
+
return;
|
|
314
|
+
const state = item.meta?.state?.trim().toLowerCase();
|
|
315
|
+
if (state !== "before" && state !== "after")
|
|
316
|
+
return;
|
|
317
|
+
const g = pathGroups.get(path) ?? { before: [], after: [] };
|
|
318
|
+
g[state].push(i);
|
|
319
|
+
pathGroups.set(path, g);
|
|
320
|
+
});
|
|
321
|
+
for (const g of pathGroups.values()) {
|
|
322
|
+
if (g.before.length === 1 && g.after.length === 1)
|
|
323
|
+
pair(g.before[0], g.after[0]);
|
|
324
|
+
}
|
|
325
|
+
// Priority 2: no usable path metadata — filename stem token, same extension.
|
|
326
|
+
const stemGroups = new Map();
|
|
327
|
+
items.forEach((item, i) => {
|
|
328
|
+
if (!isImageAt[i] || partnerOf.has(i))
|
|
329
|
+
return;
|
|
330
|
+
const path = item.meta?.path?.trim();
|
|
331
|
+
if (path && path !== "/")
|
|
332
|
+
return; // usable path metadata — rule 1 owns this item
|
|
333
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
334
|
+
const tok = filenameStemToken(name);
|
|
335
|
+
if (!tok)
|
|
336
|
+
return;
|
|
337
|
+
const dot = name.lastIndexOf(".");
|
|
338
|
+
const ext = dot > 0 ? name.slice(dot).toLowerCase() : "";
|
|
339
|
+
const key = `${tok.base}${ext}`;
|
|
340
|
+
const g = stemGroups.get(key) ?? { before: [], after: [] };
|
|
341
|
+
g[tok.state].push(i);
|
|
342
|
+
stemGroups.set(key, g);
|
|
343
|
+
});
|
|
344
|
+
for (const g of stemGroups.values()) {
|
|
345
|
+
if (g.before.length === 1 && g.after.length === 1)
|
|
346
|
+
pair(g.before[0], g.after[0]);
|
|
347
|
+
}
|
|
348
|
+
return { partnerOf, roleOf };
|
|
349
|
+
}
|
|
350
|
+
/** Max display width for one image inside a before/after pair row — smaller
|
|
351
|
+
* than a standalone image so two side by side stay under GitHub's comment
|
|
352
|
+
* column width (and don't overflow on mobile). */
|
|
353
|
+
export const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
354
|
+
function renderPairCell(item, label) {
|
|
355
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
356
|
+
const src = item.embedUrl ?? item.url;
|
|
357
|
+
const link = item.pageUrl ?? item.url;
|
|
358
|
+
const w = Math.min(attachmentImageWidth(name), ATTACHMENT_IMAGE_WIDTH_PAIR);
|
|
359
|
+
const alt = escapeHtmlAttr(name);
|
|
360
|
+
const href = escapeHtmlAttr((link ?? src));
|
|
361
|
+
const imgSrc = escapeHtmlAttr(src);
|
|
362
|
+
const caption = metaCaptionHtml(item.meta);
|
|
363
|
+
const captionHtml = caption ? `<br><sub>${caption}</sub>` : "";
|
|
364
|
+
return `<td align="center"><sub><strong>${label}</strong></sub><br><a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>${captionHtml}</td>`;
|
|
365
|
+
}
|
|
366
|
+
/** One side-by-side before/after row (issue #419): a single HTML table so
|
|
367
|
+
* GitHub renders both images on one line, with `Before`/`After` labels and
|
|
368
|
+
* each side's usual path/state caption preserved underneath. */
|
|
369
|
+
function renderPairRow(beforeItem, afterItem) {
|
|
370
|
+
return `<table><tr>${renderPairCell(beforeItem, "Before")}${renderPairCell(afterItem, "After")}</tr></table>`;
|
|
371
|
+
}
|
|
258
372
|
/**
|
|
259
373
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
260
374
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
@@ -279,9 +393,36 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
279
393
|
}
|
|
280
394
|
if (sorted.length > 0 || sortedGalleries.length === 0)
|
|
281
395
|
lines.push("### 📎 Attachments", "");
|
|
396
|
+
const isImageAt = sorted.map((item) => {
|
|
397
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
398
|
+
const src = item.embedUrl ?? item.url;
|
|
399
|
+
return Boolean(src) && inferContentType(name).startsWith("image/");
|
|
400
|
+
});
|
|
401
|
+
const { partnerOf, roleOf } = pairAttachments(sorted, isImageAt);
|
|
402
|
+
const consumedByPair = new Set();
|
|
282
403
|
let inlinedImages = 0;
|
|
283
404
|
const overflowImages = [];
|
|
284
|
-
for (
|
|
405
|
+
for (let idx = 0; idx < sorted.length; idx++) {
|
|
406
|
+
if (consumedByPair.has(idx))
|
|
407
|
+
continue;
|
|
408
|
+
const item = sorted[idx];
|
|
409
|
+
const partnerIdx = partnerOf.get(idx);
|
|
410
|
+
if (partnerIdx !== undefined) {
|
|
411
|
+
const partner = sorted[partnerIdx];
|
|
412
|
+
if (inlinedImages + 2 <= MAX_INLINE_ATTACHMENT_IMAGES) {
|
|
413
|
+
inlinedImages += 2;
|
|
414
|
+
consumedByPair.add(partnerIdx);
|
|
415
|
+
const beforeItem = roleOf.get(idx) === "before" ? item : partner;
|
|
416
|
+
const afterItem = roleOf.get(idx) === "before" ? partner : item;
|
|
417
|
+
lines.push(renderPairRow(beforeItem, afterItem), "");
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
// Cap already full for a two-image row — degrade this pair to two
|
|
421
|
+
// ordinary overflow entries rather than only half-rendering the row.
|
|
422
|
+
overflowImages.push(item, partner);
|
|
423
|
+
consumedByPair.add(partnerIdx);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
285
426
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
286
427
|
const stable = item.url;
|
|
287
428
|
const src = item.embedUrl ?? item.url;
|
|
@@ -341,6 +482,14 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
341
482
|
}
|
|
342
483
|
lines.push("", "</details>", "");
|
|
343
484
|
}
|
|
485
|
+
// Emptied state: a PR/issue whose attachments and galleries were all removed
|
|
486
|
+
// still keeps its managed comment (a later push can repopulate it) — show a
|
|
487
|
+
// neutral resting state under the heading rather than a bare heading. Only
|
|
488
|
+
// the truly-empty case (no attachments, no galleries); a galleries-only
|
|
489
|
+
// comment has no Attachments heading and must not get this line.
|
|
490
|
+
if (sorted.length === 0 && sortedGalleries.length === 0) {
|
|
491
|
+
lines.push("_No attachments are currently associated with this pull request._", "");
|
|
492
|
+
}
|
|
344
493
|
lines.push('<sub>Maintained by <a href="https://uploads.sh">uploads.sh</a> — re-uploading a file with the same name updates it everywhere it is embedded.</sub>');
|
|
345
494
|
lines.push('<sub>Add media: <code>uploads put <file> --pr <N> --comment</code> (or <code>--issue <N></code>) · <a href="https://uploads.sh/docs/github-app">docs</a></sub>');
|
|
346
495
|
return lines.join("\n");
|