From 545db16f01f5f06d34be4586d0696dd9034740eb Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 03:16:00 +0000 Subject: [PATCH] fix(plugins): rewrite relative README image URLs to source-host raw URLs --- src/__tests__/plugins-publish-route.test.tsx | 37 +++++++++++++ src/lib/detectRelativeReadmeAssets.test.ts | 19 +++++++ src/lib/detectRelativeReadmeAssets.ts | 58 +++++++++++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/__tests__/plugins-publish-route.test.tsx b/src/__tests__/plugins-publish-route.test.tsx index e5314f66..49472b4e 100644 --- a/src/__tests__/plugins-publish-route.test.tsx +++ b/src/__tests__/plugins-publish-route.test.tsx @@ -680,6 +680,43 @@ describe("plugins publish route", () => { expect(screen.getByText(/\.\/images\/bar\.png/)).toBeTruthy(); }); + it("warns when README picture source srcset references relative image paths", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File( + [ + '# Demo Plugin\n\nx', + ], + "README.md", + { type: "text/markdown" }, + ), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/2 package-relative image paths/i)).toBeTruthy(); + }); + expect(screen.getByText(/\.\/images\/dark\.png/)).toBeTruthy(); + expect(screen.getByText(/\.\/images\/dark@2x\.png/)).toBeTruthy(); + }); + it("swaps the missing-source warning for a Package-path reminder once Source repo and a valid 40-hex Source commit are filled", async () => { renderPublishRoute(); diff --git a/src/lib/detectRelativeReadmeAssets.test.ts b/src/lib/detectRelativeReadmeAssets.test.ts index 65802744..41779240 100644 --- a/src/lib/detectRelativeReadmeAssets.test.ts +++ b/src/lib/detectRelativeReadmeAssets.test.ts @@ -30,6 +30,25 @@ describe("detectRelativeReadmeAssets", () => { expect(report.unresolvableSamples).toEqual([]); }); + it("flags relative candidates in raw HTML", () => { + const report = detectRelativeReadmeAssets( + ``, + ); + expect(report.samples).toEqual(["./dark.png", "./dark@2x.png"]); + expect(report.total).toBe(2); + expect(report.unresolvableSamples).toEqual([]); + }); + + it("flags root-absolute candidates separately", () => { + const report = detectRelativeReadmeAssets( + ``, + ); + expect(report.samples).toEqual(["/dark.png", "./light.png"]); + expect(report.total).toBe(2); + expect(report.unresolvableSamples).toEqual(["/dark.png"]); + expect(report.unresolvableTotal).toBe(1); + }); + it("flags root-absolute paths separately as unresolvable", () => { const report = detectRelativeReadmeAssets("![logo](/static/logo.png)"); expect(report.samples).toEqual(["/static/logo.png"]); diff --git a/src/lib/detectRelativeReadmeAssets.ts b/src/lib/detectRelativeReadmeAssets.ts index fb27b61f..005a75a0 100644 --- a/src/lib/detectRelativeReadmeAssets.ts +++ b/src/lib/detectRelativeReadmeAssets.ts @@ -1,7 +1,8 @@ /** * Scans README markdown text for relative image references — both Markdown - * `![alt](./path)` syntax and raw HTML `` tags — and returns - * the unique set of relative paths it finds (capped to keep UI warnings short). + * `![alt](./path)` syntax, raw HTML `` tags, and + * `` candidates — and returns the unique set of + * relative paths it finds (capped to keep UI warnings short). * * Why: ClawHub does not host package binary assets. When a publisher uploads * a zip/tgz whose README references local images via relative paths, those @@ -30,12 +31,17 @@ const MARKDOWN_IMAGE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g; const HTML_IMG_SRC = /]*?\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi; +const HTML_SOURCE_SRCSET = /]*?\bsrcset\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi; const ABSOLUTE_URL = /^[a-z][a-z0-9+\-.]*:/i; const PROTOCOL_RELATIVE = /^\/\//; const MAX_REPORTED = 5; +function isAsciiWhitespace(char: string): boolean { + return char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f"; +} + function classifyRelativeAsset(rawSrc: string): "package-relative" | "root-absolute" | null { const src = rawSrc.trim(); if (!src) return null; @@ -94,6 +100,45 @@ export function detectRelativeReadmeAssets(readmeText: string): RelativeReadmeAs } }; + const recordSrcset = (srcset: string | undefined) => { + if (!srcset) return; + let index = 0; + while (index < srcset.length) { + while (index < srcset.length) { + const char = srcset[index]; + if (isAsciiWhitespace(char) || char === ",") { + index += 1; + continue; + } + break; + } + if (index >= srcset.length) break; + + const urlStart = index; + while (index < srcset.length && !isAsciiWhitespace(srcset[index])) { + index += 1; + } + let url = srcset.slice(urlStart, index); + const endedWithComma = url.endsWith(","); + if (endedWithComma) { + url = url.slice(0, -1); + } + record(url); + + if (!endedWithComma) { + while (index < srcset.length && isAsciiWhitespace(srcset[index])) { + index += 1; + } + while (index < srcset.length && srcset[index] !== ",") { + index += 1; + } + } + if (srcset[index] === ",") { + index += 1; + } + } + }; + MARKDOWN_IMAGE.lastIndex = 0; for ( let match = MARKDOWN_IMAGE.exec(readmeText); @@ -108,5 +153,14 @@ export function detectRelativeReadmeAssets(readmeText: string): RelativeReadmeAs record(match[1] ?? match[2]); } + HTML_SOURCE_SRCSET.lastIndex = 0; + for ( + let match = HTML_SOURCE_SRCSET.exec(readmeText); + match; + match = HTML_SOURCE_SRCSET.exec(readmeText) + ) { + recordSrcset(match[1] ?? match[2]); + } + return { samples, total, unresolvableSamples, unresolvableTotal }; }