Skip to content

Commit d343e9c

Browse files
Merge pull request #923 from heygen-com/fix/css-import-review-fixes
fix(core): posix paths, diamond imports, comment safety
2 parents f44d53e + 72506f3 commit d343e9c

2 files changed

Lines changed: 75 additions & 8 deletions

File tree

packages/core/src/compiler/htmlBundler.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,4 +898,45 @@ describe("bundleToSingleHtml", () => {
898898

899899
expect(bundled).toContain("url('styles/sprite.png?v=2#section')");
900900
});
901+
902+
it("deduplicates diamond @import (same file imported by two parents)", async () => {
903+
const dir = makeTempProject({
904+
"index.html": `<!doctype html>
905+
<html><body>
906+
<link rel="stylesheet" href="styles/main.css">
907+
<div data-composition-id="root" data-width="320" data-height="180"></div>
908+
<script>window.__timelines = window.__timelines || {}; window.__timelines.root = {}</script>
909+
</body></html>`,
910+
"styles/main.css": `@import url('./a.css');\n@import url('./b.css');`,
911+
"styles/a.css": `@import url('./shared.css');\n.a { color: red; }`,
912+
"styles/b.css": `@import url('./shared.css');\n.b { color: blue; }`,
913+
"styles/shared.css": `:root { --shared: 1; }`,
914+
});
915+
916+
const bundled = await bundleToSingleHtml(dir);
917+
918+
const sharedCount = (bundled.match(/--shared: 1/g) || []).length;
919+
expect(sharedCount).toBe(1);
920+
expect(bundled).toContain(".a { color: red; }");
921+
expect(bundled).toContain(".b { color: blue; }");
922+
expect(bundled).not.toContain("@import");
923+
});
924+
925+
it("does not resolve @import inside CSS comments", async () => {
926+
const dir = makeTempProject({
927+
"index.html": `<!doctype html>
928+
<html><body>
929+
<link rel="stylesheet" href="app.css">
930+
<div data-composition-id="root" data-width="320" data-height="180"></div>
931+
<script>window.__timelines = window.__timelines || {}; window.__timelines.root = {}</script>
932+
</body></html>`,
933+
"app.css": `/* @import url('./old.css'); */\nbody { margin: 0; }`,
934+
"old.css": `.old { display: none; }`,
935+
});
936+
937+
const bundled = await bundleToSingleHtml(dir);
938+
939+
expect(bundled).toContain("/* @import url('./old.css'); */");
940+
expect(bundled).not.toContain(".old { display: none; }");
941+
});
901942
});

packages/core/src/compiler/htmlBundler.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ const CSS_IMPORT_RE =
9292

9393
const REBASE_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
9494

95+
const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
96+
97+
function withCommentsStripped<T>(
98+
css: string,
99+
fn: (stripped: string) => T,
100+
): { result: T; restore: (s: string) => string } {
101+
const comments: string[] = [];
102+
const stripped = css.replace(CSS_COMMENT_RE, (m) => {
103+
const idx = comments.length;
104+
comments.push(m);
105+
return `/*__hf_c${idx}__*/`;
106+
});
107+
const result = fn(stripped);
108+
const restore = (s: string) => {
109+
let out = s;
110+
for (let i = 0; i < comments.length; i++) {
111+
out = out.replace(`/*__hf_c${i}__*/`, comments[i]!);
112+
}
113+
return out;
114+
};
115+
return { result, restore };
116+
}
117+
95118
function rebaseCssUrls(css: string, cssFileDir: string, projectDir: string): string {
96119
const resolvedRoot = resolve(projectDir);
97120
const resolvedDir = resolve(cssFileDir);
@@ -101,7 +124,7 @@ function rebaseCssUrls(css: string, cssFileDir: string, projectDir: string): str
101124
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
102125
if (!basePath) return full;
103126
const absolutePath = resolve(resolvedDir, basePath);
104-
const rebased = relative(resolvedRoot, absolutePath);
127+
const rebased = relative(resolvedRoot, absolutePath).split(sep).join("/");
105128
if (rebased === basePath) return full;
106129
return `url(${quote || ""}${rebased}${suffix}${quote || ""})`;
107130
});
@@ -113,29 +136,32 @@ function inlineCssFile(
113136
projectDir: string,
114137
visited: Set<string> = new Set(),
115138
): string {
116-
const placeholders: string[] = [];
117-
const withPlaceholders = css.replace(
139+
const { result: strippedCss, restore: restoreComments } = withCommentsStripped(css, (s) => s);
140+
const importPlaceholders: string[] = [];
141+
const withPlaceholders = strippedCss.replace(
118142
CSS_IMPORT_RE,
119143
(full, _q1, urlPath, _q2, barePath, mediaQuery) => {
120144
const importPath = urlPath ?? barePath;
121145
if (!importPath || !isRelativeUrl(importPath)) return full;
122146
const resolved = resolve(cssFileDir, importPath);
123147
const normalizedBase = resolve(projectDir) + sep;
124-
if (!resolved.startsWith(normalizedBase) || visited.has(resolved)) return full;
148+
if (!resolved.startsWith(normalizedBase)) return full;
149+
if (visited.has(resolved)) return "";
125150
const content = safeReadFile(resolved);
126151
if (content == null) return full;
127152
visited.add(resolved);
128153
const inlined = inlineCssFile(content, dirname(resolved), projectDir, visited);
129154
const trimmedMedia = (mediaQuery || "").trim();
130155
const block = trimmedMedia ? `@media ${trimmedMedia} {\n${inlined}\n}\n` : inlined + "\n";
131-
const idx = placeholders.length;
132-
placeholders.push(block);
156+
const idx = importPlaceholders.length;
157+
importPlaceholders.push(block);
133158
return `/*__hf_import_${idx}__*/`;
134159
},
135160
);
136161
let rebased = rebaseCssUrls(withPlaceholders, cssFileDir, projectDir);
137-
for (let i = 0; i < placeholders.length; i++) {
138-
rebased = rebased.replace(`/*__hf_import_${i}__*/`, placeholders[i]!);
162+
rebased = restoreComments(rebased);
163+
for (let i = 0; i < importPlaceholders.length; i++) {
164+
rebased = rebased.replace(`/*__hf_import_${i}__*/`, importPlaceholders[i]!);
139165
}
140166
return rebased;
141167
}

0 commit comments

Comments
 (0)