diff --git a/.changeset/famous-shoes-joke.md b/.changeset/famous-shoes-joke.md new file mode 100644 index 000000000..b09729357 --- /dev/null +++ b/.changeset/famous-shoes-joke.md @@ -0,0 +1,5 @@ +--- +"@web/rollup-plugin-html": minor +--- + +feat(rollup-plugin-html): resolves assets in styles diff --git a/docs/docs/building/rollup-plugin-html.md b/docs/docs/building/rollup-plugin-html.md index 44ef32773..0796bdc8f 100644 --- a/docs/docs/building/rollup-plugin-html.md +++ b/docs/docs/building/rollup-plugin-html.md @@ -125,6 +125,45 @@ export default { }; ``` +#### Including assets referenced from css + +If your css files reference other assets via `url`, like for example: + +```css +body { + background-image: url('images/star.gif'); +} + +/* or */ +@font-face { + src: url('fonts/font-bold.woff2') format('woff2'); + /* ...etc */ +} +``` + +You can enable the `bundleAssetsFromCss` option: + +```js +rollupPluginHTML({ + bundleAssetsFromCss: true, + // ...etc +}); +``` + +And those assets will get output to the `assets/` dir, and the source css file will get updated with the output locations of those assets, e.g.: + +```css +body { + background-image: url('assets/star-P4TYRBwL.gif'); +} + +/* or */ +@font-face { + src: url('assets/font-bold-f0mNRiTD.woff2') format('woff2'); + /* ...etc */ +} +``` + ### Handling absolute paths If your HTML file contains any absolute paths they will be resolved against the current working directory. You can set a different root directory in the config. Input paths will be resolved relative to this root directory as well. diff --git a/package-lock.json b/package-lock.json index 9637b595e..e6e65a3a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19297,6 +19297,203 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/lightningcss": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.0.tgz", + "integrity": "sha512-y36QEEDVx4IM7/yIZNsZJMRREIu26WzTsauIysf5s76YeCmlSbRZS7aC97IGPuoFRnyZ5Wx43OBsQBFB5Ne7ng==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.24.0", + "lightningcss-darwin-x64": "1.24.0", + "lightningcss-freebsd-x64": "1.24.0", + "lightningcss-linux-arm-gnueabihf": "1.24.0", + "lightningcss-linux-arm64-gnu": "1.24.0", + "lightningcss-linux-arm64-musl": "1.24.0", + "lightningcss-linux-x64-gnu": "1.24.0", + "lightningcss-linux-x64-musl": "1.24.0", + "lightningcss-win32-x64-msvc": "1.24.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.0.tgz", + "integrity": "sha512-rTNPkEiynOu4CfGdd5ZfVOQe2gd2idfQd4EfX1l2ZUUwd+2SwSdbb7cG4rlwfnZckbzCAygm85xkpekRE5/wFw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.0.tgz", + "integrity": "sha512-4KCeF2RJjzp9xdGY8zIH68CUtptEg8uz8PfkHvsIdrP4t9t5CIgfDBhiB8AmuO75N6SofdmZexDZIKdy9vA7Ww==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.0.tgz", + "integrity": "sha512-FJAYlek1wXuVTsncNU0C6YD41q126dXcIUm97KAccMn9C4s/JfLSqGWT2gIzAblavPFkyGG2gIADTWp3uWfN1g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.0.tgz", + "integrity": "sha512-N55K6JqzMx7C0hYUu1YmWqhkHwzEJlkQRMA6phY65noO0I1LOAvP4wBIoFWrzRE+O6zL0RmXJ2xppqyTbk3sYw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.0.tgz", + "integrity": "sha512-MqqUB2TpYtFWeBvvf5KExDdClU3YGLW5bHKs50uKKootcvG9KoS7wYwd5UichS+W3mYLc5yXUPGD1DNWbLiYKw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.0.tgz", + "integrity": "sha512-5wn4d9tFwa5bS1ao9mLexYVJdh3nn09HNIipsII6ZF7z9ZA5J4dOEhMgKoeCl891axTGTUYd8Kxn+Hn3XUSYRQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.0.tgz", + "integrity": "sha512-3j5MdTh+LSDF3o6uDwRjRUgw4J+IfDCVtdkUrJvKxL79qBLUujXY7CTe5X3IQDDLKEe/3wu49r8JKgxr0MfjbQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.0.tgz", + "integrity": "sha512-HI+rNnvaLz0o36z6Ki0gyG5igVGrJmzczxA5fznr6eFTj3cHORoR/j2q8ivMzNFR4UKJDkTWUH5LMhacwOHWBA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.0.tgz", + "integrity": "sha512-oeije/t7OZ5N9vSs6amyW/34wIYoBCpE6HUlsSKcP2SR1CVgx9oKEM00GtQmtqNnYiMIfsSm7+ppMb4NLtD5vg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -35067,7 +35264,7 @@ "@rollup/plugin-replace": "^5.0.5", "@types/parse5": "^6.0.1", "@types/whatwg-url": "^11.0.0", - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-core": "^0.13.0", "chai": "^4.2.0", "mocha": "^10.2.0", @@ -35788,6 +35985,7 @@ "@web/parse5-utils": "^2.1.0", "glob": "^10.0.0", "html-minifier-terser": "^7.1.0", + "lightningcss": "^1.24.0", "parse5": "^6.0.1" }, "devDependencies": { @@ -36216,13 +36414,13 @@ }, "packages/test-runner": { "name": "@web/test-runner", - "version": "0.18.0", + "version": "0.18.1", "license": "MIT", "dependencies": { "@web/browser-logs": "^0.4.0", "@web/config-loader": "^0.3.0", "@web/dev-server": "^0.4.0", - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-commands": "^0.9.0", "@web/test-runner-core": "^0.13.0", "@web/test-runner-mocha": "^0.9.0", @@ -36277,7 +36475,7 @@ }, "packages/test-runner-chrome": { "name": "@web/test-runner-chrome", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0", @@ -36438,7 +36636,7 @@ "mkdirp": "^1.0.4" }, "devDependencies": { - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-playwright": "^0.11.0", "@web/test-runner-webdriver": "^0.8.0", "mocha": "^10.2.0" @@ -36583,10 +36781,10 @@ }, "packages/test-runner-junit-reporter": { "name": "@web/test-runner-junit-reporter", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "dependencies": { - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-core": "^0.13.0", "array-flat-polyfill": "^1.0.1", "xml": "^1.0.1" @@ -36624,7 +36822,7 @@ "es-module-lexer": "^1.3.1" }, "devDependencies": { - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-core": "^0.13.0" }, "engines": { @@ -36655,10 +36853,10 @@ }, "packages/test-runner-puppeteer": { "name": "@web/test-runner-puppeteer", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "dependencies": { - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-core": "^0.13.0", "puppeteer": "^22.0.0" }, @@ -36914,7 +37112,7 @@ "pngjs": "^7.0.0" }, "devDependencies": { - "@web/test-runner-chrome": "^0.15.0", + "@web/test-runner-chrome": "^0.16.0", "@web/test-runner-playwright": "^0.11.0", "@web/test-runner-webdriver": "^0.8.0", "mocha": "^10.2.0" diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 03b931e4b..fab5430e9 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -47,6 +47,7 @@ "@web/parse5-utils": "^2.1.0", "glob": "^10.0.0", "html-minifier-terser": "^7.1.0", + "lightningcss": "^1.24.0", "parse5": "^6.0.1" }, "devDependencies": { diff --git a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts index 1c84a7006..96688c91d 100644 --- a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts +++ b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts @@ -41,6 +41,8 @@ export interface RollupPluginHTMLOptions { absolutePathPrefix?: string; /** When set to true, will insert meta tags for CSP and add script-src values for inline scripts by sha256-hashing the contents */ strictCSPInlineScripts?: boolean; + /** Bundle assets reference from CSS via `url` */ + bundleAssetsFromCss?: boolean; } export interface GeneratedBundle { diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index 407a85712..6e50194b7 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -1,5 +1,7 @@ import { PluginContext } from 'rollup'; import path from 'path'; +import { transform } from 'lightningcss'; +import fs from 'fs'; import { InputAsset, InputData } from '../input/InputData'; import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPluginHTMLOptions'; @@ -9,6 +11,28 @@ export interface EmittedAssets { hashed: Map; } +const allowedFileExtensions = [ + /.*\.svg/, + /.*\.png/, + /.*\.jpg/, + /.*\.jpeg/, + /.*\.webp/, + /.*\.gif/, + /.*\.avif/, + /.*\.woff2/, + /.*\.woff/, +]; + +function shouldHandleAsset(url: string) { + return ( + allowedFileExtensions.some(f => f.test(url)) && + !url.startsWith('http') && + !url.startsWith('data') && + !url.startsWith('#') && + !url.startsWith('/') + ); +} + export async function emitAssets( this: PluginContext, inputs: InputData[], @@ -57,7 +81,55 @@ export async function emitAssets( let ref: string; let basename = path.basename(asset.filePath); + const emittedExternalAssets = new Map(); if (asset.hashed) { + if (basename.endsWith('.css') && options.bundleAssetsFromCss) { + let updatedCssSource = false; + const { code } = await transform({ + filename: basename, + code: asset.content, + minify: false, + visitor: { + Url: url => { + // Support foo.svg#bar + // https://www.w3.org/TR/html4/types.html#:~:text=ID%20and%20NAME%20tokens%20must,tokens%20defined%20by%20other%20attributes. + const [filePath, idRef] = url.url.split('#'); + + if (shouldHandleAsset(filePath)) { + // Read the font file, get the font from the source location on the FS using asset.filePath + const assetLocation = path.resolve(path.dirname(asset.filePath), filePath); + const assetContent = fs.readFileSync(assetLocation); + + // Avoid duplicates + if (!emittedExternalAssets.has(assetLocation)) { + const fontFileRef = this.emitFile({ + type: 'asset', + name: path.basename(filePath), + source: assetContent, + }); + const emittedFontFilePath = path.basename(this.getFileName(fontFileRef)); + emittedExternalAssets.set(assetLocation, emittedFontFilePath); + // Update the URL in the original CSS file to point to the emitted font file + url.url = `assets/${ + idRef ? `${emittedFontFilePath}#${idRef}` : emittedFontFilePath + }`; + } else { + const emittedFontFilePath = emittedExternalAssets.get(assetLocation); + url.url = `assets/${ + idRef ? `${emittedFontFilePath}#${idRef}` : emittedFontFilePath + }`; + } + } + updatedCssSource = true; + return url; + }, + }, + }); + if (updatedCssSource) { + source = Buffer.from(code); + } + } + ref = this.emitFile({ type: 'asset', name: basename, source }); } else { // ensure the output filename is unique diff --git a/packages/rollup-plugin-html/src/rollupPluginHTML.ts b/packages/rollup-plugin-html/src/rollupPluginHTML.ts index 388c32967..ffe1a0880 100644 --- a/packages/rollup-plugin-html/src/rollupPluginHTML.ts +++ b/packages/rollup-plugin-html/src/rollupPluginHTML.ts @@ -72,6 +72,7 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R if (pluginOptions.strictCSPInlineScripts) { strictCSPInlineScripts = pluginOptions.strictCSPInlineScripts; } + pluginOptions.bundleAssetsFromCss = !!pluginOptions.bundleAssetsFromCss; if (pluginOptions.input == null) { // we are reading rollup input, so replace whatever was there diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/fonts/font-normal.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css new file mode 100644 index 000000000..391732403 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-a.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'Font'; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css new file mode 100644 index 000000000..2c13aec86 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-duplicates/styles-b.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'Font2'; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif new file mode 100644 index 000000000..2e65efe2a --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.avif @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif new file mode 100644 index 000000000..63d8dbd40 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.gif @@ -0,0 +1 @@ +b \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg new file mode 100644 index 000000000..3410062ba --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpeg @@ -0,0 +1 @@ +c \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg new file mode 100644 index 000000000..c59d9b634 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.jpg @@ -0,0 +1 @@ +d \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png new file mode 100644 index 000000000..9cbe6ea56 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.png @@ -0,0 +1 @@ +e \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg new file mode 100644 index 000000000..4d1ae35ba --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.svg @@ -0,0 +1 @@ +f \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp new file mode 100644 index 000000000..7937c68fb --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/images/star.webp @@ -0,0 +1 @@ +g \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css new file mode 100644 index 000000000..bb0fa19d5 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-images/styles.css @@ -0,0 +1,24 @@ +#a { + background-image: url("images/star.svg"); +} +#b { + background-image: url("images/star.svg#foo"); +} +#c { + background-image: url("images/star.png"); +} +#d { + background-image: url("images/star.jpg"); +} +#e { + background-image: url("images/star.jpeg"); +} +#f { + background-image: url("images/star.webp"); +} +#g { + background-image: url("images/star.gif"); +} +#h { + background-image: url("images/star.avif"); +} \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-bold.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-bold.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/fonts/font-normal.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css new file mode 100644 index 000000000..a12383545 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles-node-modules/node_modules/foo/node_modules-styles-with-fonts.css @@ -0,0 +1,15 @@ +@font-face { + font-family: 'Font'; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Font'; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; +} diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-bold.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-bold.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-normal.woff2 b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/fonts/font-normal.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css new file mode 100644 index 000000000..a12383545 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/resolves-assets-in-styles/styles-with-fonts.css @@ -0,0 +1,15 @@ +@font-face { + font-family: 'Font'; + src: url('fonts/font-normal.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Font'; + src: url('fonts/font-bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; +} diff --git a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts index 5a2250b62..cca8ccfe9 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -1041,4 +1041,182 @@ describe('rollup-plugin-html', () => { ].join(''), ); }); + + it('handles fonts linked from css files', async () => { + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + input: { + html: ` + + + + + + + + `, + }, + rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles'), + }), + ], + }; + + const bundle = await rollup(config); + const { output } = await bundle.generate(outputConfig); + + const fontNormal = output.find(o => o.name === 'font-normal.woff2'); + const fontBold = output.find(o => o.name === 'font-normal.woff2'); + const style = output.find(o => o.name === 'styles-with-fonts.css'); + // It has emitted the font + expect(fontBold).to.exist; + expect(fontNormal).to.exist; + // e.g. "font-normal-f0mNRiTD.woff2" + const regex = /assets\/font-normal-\w+\.woff2/; + // It outputs the font to the assets folder + expect(regex.test(fontNormal!.fileName)).to.equal(true); + + // The source of the style includes the font + const source = (style as OutputAsset)?.source.toString(); + expect(source.includes(fontNormal!.fileName)); + }); + + it('handles fonts linked from css files in node_modules', async () => { + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + input: { + html: ` + + + + + + + + `, + }, + rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-node-modules'), + }), + ], + }; + + const bundle = await rollup(config); + const { output } = await bundle.generate(outputConfig); + + const font = output.find(o => o.name === 'font-normal.woff2'); + const style = output.find(o => o.name === 'node_modules-styles-with-fonts.css'); + + // It has emitted the font + expect(font).to.exist; + // e.g. "font-normal-f0mNRiTD.woff2" + const regex = /assets\/font-normal-\w+\.woff2/; + // It outputs the font to the assets folder + expect(regex.test(font!.fileName)).to.equal(true); + + // The source of the style includes the font + const source = (style as OutputAsset)?.source.toString(); + expect(source.includes(font!.fileName)); + }); + + it('handles duplicate fonts correctly', async () => { + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + input: { + html: ` + + + + + + + + + `, + }, + rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-duplicates'), + }), + ], + }; + + const bundle = await rollup(config); + const { output } = await bundle.generate(outputConfig); + + const fonts = output.filter(o => o.name === 'font-normal.woff2'); + expect(fonts.length).to.equal(1); + }); + + it('handles images referenced from css', async () => { + const config = { + plugins: [ + rollupPluginHTML({ + bundleAssetsFromCss: true, + input: { + html: ` + + + + + + + + `, + }, + rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-images'), + }), + ], + }; + + const bundle = await rollup(config); + const { output } = await bundle.generate(outputConfig); + + expect(output.find(o => o.name === 'star.avif')).to.exist; + expect(output.find(o => o.name === 'star.gif')).to.exist; + expect(output.find(o => o.name === 'star.jpeg')).to.exist; + expect(output.find(o => o.name === 'star.jpg')).to.exist; + expect(output.find(o => o.name === 'star.png')).to.exist; + expect(output.find(o => o.name === 'star.svg')).to.exist; + expect(output.find(o => o.name === 'star.webp')).to.exist; + + const rewrittenCss = (output.find(o => o.name === 'styles.css') as OutputAsset).source + .toString() + .trim(); + expect(rewrittenCss).to.equal( + `#a { + background-image: url("assets/star-mrrzn5BV.svg"); +} + +#b { + background-image: url("assets/star-mrrzn5BV.svg#foo"); +} + +#c { + background-image: url("assets/star-eErsO14u.png"); +} + +#d { + background-image: url("assets/star-yqfHyXQC.jpg"); +} + +#e { + background-image: url("assets/star-G_i5Rpoh.jpeg"); +} + +#f { + background-image: url("assets/star-l7b58t3m.webp"); +} + +#g { + background-image: url("assets/star-P4TYRBwL.gif"); +} + +#h { + background-image: url("assets/star-H06WHrYy.avif"); +}`.trim(), + ); + }); });