From 5a28b4e7ebc82e5e7d77c1ee2646e79f2f2a92b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 27 Oct 2026 01:10:49 +0000 Subject: [PATCH] fix(lint): a media src whose file name holds # or ? says to write it percent-encoded (#6495) * fix(lint): a media src whose file name holds # or ? says to write it percent-encoded * refactor(lint): the url-safe hint encodes with the shared encodeUrlPath --- packages/lint/src/project.test.ts | 37 ++++++++++++++++++++++++ packages/lint/src/project.ts | 47 +++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/lint/src/project.test.ts b/packages/lint/src/project.test.ts index 71885aba0f..ef57766efa 100644 --- a/packages/lint/src/project.test.ts +++ b/packages/lint/src/project.test.ts @@ -1236,3 +1236,40 @@ describe("sub-composition files", () => { ); }); }); + +describe("a media file whose own name holds # or ?", () => { + const name = "track #1.mp3"; + + async function findingFor(tag: string, code: string) { + const project = makeProject(` +
+ ${tag} +`); + mkdirSync(join(project, "assets"), { recursive: true }); + writeFileSync(join(project, "assets", name), ""); + writeFileSync(join(project, "assets", "pic #2.png"), ""); + const { results } = await lintProject(project); + return results.flatMap((entry) => entry.result.findings).find((f) => f.code === code); + } + + it("tells an audio src the name is cut at the # and how to write it", async () => { + const finding = await findingFor( + ``, + "audio_src_not_found", + ); + expect(finding?.fixHint).toContain('"assets/track%20%231.mp3"'); + }); + + it("tells an image src the same", async () => { + const finding = await findingFor(``, "missing_local_asset"); + expect(finding?.fixHint).toContain('"assets/pic%20%232.png"'); + }); + + it("keeps the add-the-file hint when no file by that name exists", async () => { + const finding = await findingFor( + ``, + "audio_src_not_found", + ); + expect(finding?.fixHint).toContain("Add the file"); + }); +}); diff --git a/packages/lint/src/project.ts b/packages/lint/src/project.ts index 1337f9c58e..da71259d1e 100644 --- a/packages/lint/src/project.ts +++ b/packages/lint/src/project.ts @@ -33,6 +33,7 @@ import { resolveLocalAssetCandidates, resolveProjectRelativeSrc, } from "@hyperframes/parsers/asset-resolution"; +import { encodeUrlPath } from "@hyperframes/parsers"; import { collectLocalVideoCandidates, lintHevcPreviewCodec, @@ -385,6 +386,18 @@ function lintProjectAudioFiles( return findings; } +/** The src that loads a project file whose own name holds `#` or `?`, which a URL cuts the name at; null when the raw + * src names no such file. */ +function urlSafeSrcOf(projectDir: string, rootRelative: string): string | null { + if (!/[?#]/.test(rootRelative)) return null; + const literal = resolve(projectDir, rootRelative.replace(/^\//, "")); + if (!existsSync(literal)) return null; + return encodeUrlPath(rootRelative); +} + +const urlSafeHint = (safe: string) => + `The file exists, but a "#" or "?" in a src ends the file name there. Write the src as "${safe}".`; + function lintAudioSrcNotFound( projectDir: string, htmlSources: HtmlSource[], @@ -394,6 +407,7 @@ function lintAudioSrcNotFound( const audioSrcRe = mediaSrcTagRe("audio"); const missingByFile = new Map>(); + const urlSafe = new Map(); for (const { html, compSrcPath } of htmlSources) { const scannable = maskNonScannableRanges(html); let match: RegExpExecArray | null; @@ -405,6 +419,8 @@ function lintAudioSrcNotFound( ? rewriteAssetPath(compSrcPath, src, (path) => existsSync(join(projectDir, path))) : src; if (!existsSync(resolveProjectRelativeSrc(rootRelative, projectDir))) { + const safe = urlSafeSrcOf(projectDir, rootRelative); + if (safe) urlSafe.set(src, safe); const file = sourceFile(projectDir, compSrcPath); missingByFile.set(file, (missingByFile.get(file) ?? new Set()).add(src)); } @@ -419,9 +435,11 @@ function lintAudioSrcNotFound( file, message: `