diff --git a/packages/cli/src/utils/lintProject.test.ts b/packages/cli/src/utils/lintProject.test.ts
index 25bd371bf..65def2039 100644
--- a/packages/cli/src/utils/lintProject.test.ts
+++ b/packages/cli/src/utils/lintProject.test.ts
@@ -119,6 +119,29 @@ describe("lintProject", () => {
expect(finding?.selector).toBe('[data-composition-id="scene"] .title');
});
+ it("lints percent-encoded linked CSS filenames that exist decoded on disk", () => {
+ const encodedFilename = "%E6%97%A5%E6%9C%AC%E8%AA%9E.css";
+ const project = makeProject(validHtml(), {
+ "scene.html": `
+
+
+`,
+ });
+ writeFileSync(
+ join(project.dir, "compositions", decodeURIComponent(encodedFilename)),
+ '[data-composition-id="scene"] .title { opacity: 0; }',
+ );
+
+ const { results } = lintProject(project);
+ const subResult = results.find((result) => result.file === "compositions/scene.html");
+ const finding = subResult?.result.findings.find(
+ (item) => item.code === "composition_self_attribute_selector",
+ );
+
+ expect(finding).toBeDefined();
+ expect(finding?.selector).toBe('[data-composition-id="scene"] .title');
+ });
+
it("aggregates errors across index.html and sub-compositions", () => {
const project = makeProject(htmlWithMissingMediaId(), {
"overlay.html": htmlWithMissingMediaId(),
@@ -528,6 +551,30 @@ describe("texture_mask_asset_not_found", () => {
expect(finding).toBeUndefined();
});
+ it("checks mask-image URLs inside percent-encoded linked CSS filenames", () => {
+ const encodedFilename = "%E6%97%A5%E6%9C%AC%E8%AA%9E.css";
+ const project = makeProject(validHtml(), {
+ "scene.html": `
+
+
+`,
+ });
+ writeFileSync(
+ join(project.dir, "compositions", decodeURIComponent(encodedFilename)),
+ '.hf-texture-lava { mask-image: url("masks/missing.png"); }',
+ );
+
+ const { results } = lintProject(project);
+ const finding = results[0]?.result.findings.find(
+ (item) => item.code === "texture_mask_asset_not_found",
+ );
+
+ expect(finding).toBeDefined();
+ expect(finding?.message).toContain("masks/missing.png");
+ });
+
it("resolves root-absolute mask-image URLs from the project root", () => {
const html = `
diff --git a/packages/cli/src/utils/lintProject.ts b/packages/cli/src/utils/lintProject.ts
index 3d1af293c..a3a8f8d5a 100644
--- a/packages/cli/src/utils/lintProject.ts
+++ b/packages/cli/src/utils/lintProject.ts
@@ -62,9 +62,9 @@ function collectExternalStyles(
const href = tag.match(/\bhref\s*=\s*["']([^"']+)["']/i)?.[1] ?? "";
if (!isLocalStylesheetHref(href)) continue;
const rootRelative = compSrcPath ? join(dirname(compSrcPath), href) : href;
- const resolved = resolve(projectDir, rootRelative);
- if (!existsSync(resolved)) continue;
- styles.push({ href, content: readFileSync(resolved, "utf-8") });
+ const stylesheet = resolveExistingLocalAsset(projectDir, rootRelative);
+ if (!stylesheet) continue;
+ styles.push({ href, content: readFileSync(stylesheet.resolved, "utf-8") });
}
return styles;
}
@@ -88,9 +88,12 @@ function collectCssSources(projectDir: string, html: string, compSrcPath?: strin
if (!isLocalStylesheetHref(href)) continue;
const rootRelativePath = compSrcPath ? join(dirname(compSrcPath), href) : href;
- const resolved = resolve(projectDir, rootRelativePath);
- if (!existsSync(resolved)) continue;
- sources.push({ content: readFileSync(resolved, "utf-8"), rootRelativePath });
+ const stylesheet = resolveExistingLocalAsset(projectDir, rootRelativePath);
+ if (!stylesheet) continue;
+ sources.push({
+ content: readFileSync(stylesheet.resolved, "utf-8"),
+ rootRelativePath: stylesheet.rootRelativePath,
+ });
}
let tagMatch: RegExpExecArray | null;
@@ -146,6 +149,16 @@ function resolveLocalAssetCandidates(projectDir: string, url: string): string[]
return candidates;
}
+function resolveExistingLocalAsset(
+ projectDir: string,
+ url: string,
+): { resolved: string; rootRelativePath: string } | null {
+ const projectRoot = resolve(projectDir);
+ const resolved = resolveLocalAssetCandidates(projectRoot, url).find(existsSync);
+ if (!resolved) return null;
+ return { resolved, rootRelativePath: relative(projectRoot, resolved) };
+}
+
function resolveCssAssetCandidates(
projectDir: string,
url: string,