diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index 93945b411..b91258370 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -725,4 +725,80 @@ describe("bundleToSingleHtml", () => { expect(bundled).toContain('url("fonts/brand.woff2")'); expect(bundled).not.toContain('url("../fonts/brand.woff2")'); }); + + it("resolves CSS @import statements when inlining stylesheets", async () => { + const dir = makeTempProject({ + "index.html": ` +
+ + + +`, + "styles/canvas.css": `@import url('./tokens.css');\nbody { margin: 0; }`, + "styles/tokens.css": `:root { --brand: #ff5728; }`, + }); + + const bundled = await bundleToSingleHtml(dir); + + expect(bundled).toContain("--brand: #ff5728"); + expect(bundled).not.toContain("@import"); + expect(bundled).toContain("margin: 0"); + }); + + it("resolves nested CSS @import chains", async () => { + const dir = makeTempProject({ + "index.html": ` + + + + +`, + "styles/main.css": `@import url('./base.css');\n.main { color: red; }`, + "styles/base.css": `@import url('../tokens.css');\n.base { display: flex; }`, + "tokens.css": `:root { --tk-teal: #1a3540; }`, + }); + + const bundled = await bundleToSingleHtml(dir); + + expect(bundled).toContain("--tk-teal: #1a3540"); + expect(bundled).toContain("display: flex"); + expect(bundled).toContain("color: red"); + expect(bundled).not.toContain("@import"); + }); + + it("wraps @import with media query in @media block", async () => { + const dir = makeTempProject({ + "index.html": ` + + + + +`, + "print.css": `@import url('./print-tokens.css') print;\nbody { font-size: 12pt; }`, + "print-tokens.css": `.print-only { display: block; }`, + }); + + const bundled = await bundleToSingleHtml(dir); + + expect(bundled).toContain("@media print"); + expect(bundled).toContain("display: block"); + expect(bundled).not.toContain("@import"); + }); + + it("preserves @import for absolute URLs", async () => { + const dir = makeTempProject({ + "index.html": ` + + + + +`, + "app.css": `@import url('https://fonts.googleapis.com/css2?family=Inter');\nbody { margin: 0; }`, + }); + + const bundled = await bundleToSingleHtml(dir); + + expect(bundled).toContain("@import url('https://fonts.googleapis.com/css2?family=Inter')"); + expect(bundled).toContain("margin: 0"); + }); }); diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index 3e1377d84..f433fed89 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -1,5 +1,5 @@ import { readFileSync, existsSync } from "fs"; -import { join, resolve, isAbsolute, sep } from "path"; +import { join, resolve, dirname, isAbsolute, sep } from "path"; import { transformSync } from "esbuild"; import { compileHtml, type MediaDurationProber } from "./htmlCompiler"; import { @@ -90,6 +90,31 @@ function safeReadFile(filePath: string): string | null { } } +const CSS_IMPORT_RE = + /@import\s+(?:url\(\s*(["']?)([^)"']+)\1\s*\)|(["'])([^"']+)\3)\s*([^;]*);\s*/g; + +function resolveCssImports( + css: string, + cssFileDir: string, + projectDir: string, + visited: Set