Skip to content

Commit a68b4b6

Browse files
committed
fix(cli): decode linked stylesheet paths
1 parent 526709c commit a68b4b6

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

packages/cli/src/utils/lintProject.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,29 @@ describe("lintProject", () => {
119119
expect(finding?.selector).toBe('[data-composition-id="scene"] .title');
120120
});
121121

122+
it("lints percent-encoded linked CSS filenames that exist decoded on disk", () => {
123+
const encodedFilename = "%E6%97%A5%E6%9C%AC%E8%AA%9E.css";
124+
const project = makeProject(validHtml(), {
125+
"scene.html": `<html><head><link rel="stylesheet" href="${encodedFilename}"></head><body>
126+
<div id="scene" data-composition-id="scene" data-width="1920" data-height="1080" data-start="0" data-duration="2"></div>
127+
<script>window.__timelines = window.__timelines || {}; window.__timelines["scene"] = gsap.timeline({ paused: true });</script>
128+
</body></html>`,
129+
});
130+
writeFileSync(
131+
join(project.dir, "compositions", decodeURIComponent(encodedFilename)),
132+
'[data-composition-id="scene"] .title { opacity: 0; }',
133+
);
134+
135+
const { results } = lintProject(project);
136+
const subResult = results.find((result) => result.file === "compositions/scene.html");
137+
const finding = subResult?.result.findings.find(
138+
(item) => item.code === "composition_self_attribute_selector",
139+
);
140+
141+
expect(finding).toBeDefined();
142+
expect(finding?.selector).toBe('[data-composition-id="scene"] .title');
143+
});
144+
122145
it("aggregates errors across index.html and sub-compositions", () => {
123146
const project = makeProject(htmlWithMissingMediaId(), {
124147
"overlay.html": htmlWithMissingMediaId(),
@@ -528,6 +551,30 @@ describe("texture_mask_asset_not_found", () => {
528551
expect(finding).toBeUndefined();
529552
});
530553

554+
it("checks mask-image URLs inside percent-encoded linked CSS filenames", () => {
555+
const encodedFilename = "%E6%97%A5%E6%9C%AC%E8%AA%9E.css";
556+
const project = makeProject(validHtml(), {
557+
"scene.html": `<html><head><link rel="stylesheet" href="${encodedFilename}"></head><body>
558+
<div data-composition-id="scene" data-width="1920" data-height="1080">
559+
<div class="hf-texture-text hf-texture-lava">TEXT</div>
560+
</div>
561+
<script>window.__timelines = window.__timelines || {}; window.__timelines["scene"] = gsap.timeline({ paused: true });</script>
562+
</body></html>`,
563+
});
564+
writeFileSync(
565+
join(project.dir, "compositions", decodeURIComponent(encodedFilename)),
566+
'.hf-texture-lava { mask-image: url("masks/missing.png"); }',
567+
);
568+
569+
const { results } = lintProject(project);
570+
const finding = results[0]?.result.findings.find(
571+
(item) => item.code === "texture_mask_asset_not_found",
572+
);
573+
574+
expect(finding).toBeDefined();
575+
expect(finding?.message).toContain("masks/missing.png");
576+
});
577+
531578
it("resolves root-absolute mask-image URLs from the project root", () => {
532579
const html = `<html><body>
533580
<div data-composition-id="main" data-width="1920" data-height="1080">

packages/cli/src/utils/lintProject.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ function collectExternalStyles(
6262
const href = tag.match(/\bhref\s*=\s*["']([^"']+)["']/i)?.[1] ?? "";
6363
if (!isLocalStylesheetHref(href)) continue;
6464
const rootRelative = compSrcPath ? join(dirname(compSrcPath), href) : href;
65-
const resolved = resolve(projectDir, rootRelative);
66-
if (!existsSync(resolved)) continue;
67-
styles.push({ href, content: readFileSync(resolved, "utf-8") });
65+
const stylesheet = resolveExistingLocalAsset(projectDir, rootRelative);
66+
if (!stylesheet) continue;
67+
styles.push({ href, content: readFileSync(stylesheet.resolved, "utf-8") });
6868
}
6969
return styles;
7070
}
@@ -88,9 +88,12 @@ function collectCssSources(projectDir: string, html: string, compSrcPath?: strin
8888
if (!isLocalStylesheetHref(href)) continue;
8989

9090
const rootRelativePath = compSrcPath ? join(dirname(compSrcPath), href) : href;
91-
const resolved = resolve(projectDir, rootRelativePath);
92-
if (!existsSync(resolved)) continue;
93-
sources.push({ content: readFileSync(resolved, "utf-8"), rootRelativePath });
91+
const stylesheet = resolveExistingLocalAsset(projectDir, rootRelativePath);
92+
if (!stylesheet) continue;
93+
sources.push({
94+
content: readFileSync(stylesheet.resolved, "utf-8"),
95+
rootRelativePath: stylesheet.rootRelativePath,
96+
});
9497
}
9598

9699
let tagMatch: RegExpExecArray | null;
@@ -146,6 +149,16 @@ function resolveLocalAssetCandidates(projectDir: string, url: string): string[]
146149
return candidates;
147150
}
148151

152+
function resolveExistingLocalAsset(
153+
projectDir: string,
154+
url: string,
155+
): { resolved: string; rootRelativePath: string } | null {
156+
const projectRoot = resolve(projectDir);
157+
const resolved = resolveLocalAssetCandidates(projectRoot, url).find(existsSync);
158+
if (!resolved) return null;
159+
return { resolved, rootRelativePath: relative(projectRoot, resolved) };
160+
}
161+
149162
function resolveCssAssetCandidates(
150163
projectDir: string,
151164
url: string,

0 commit comments

Comments
 (0)