Skip to content

Commit 549eaf7

Browse files
committed
feat: support additional assets in css
1 parent 47728ce commit 549eaf7

File tree

13 files changed

+183
-16
lines changed

13 files changed

+183
-16
lines changed

docs/docs/building/rollup-plugin-html.md

+39
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,45 @@ export default {
125125
};
126126
```
127127

128+
#### Including assets referenced from css
129+
130+
If your css files reference other assets via `url`, like for example:
131+
132+
```css
133+
body {
134+
background-image: url('images/star.gif');
135+
}
136+
137+
/* or */
138+
@font-face {
139+
src: url('fonts/font-bold.woff2') format('woff2');
140+
/* ...etc */
141+
}
142+
```
143+
144+
You can enable the `bundleAssetsFromCss` option:
145+
146+
```js
147+
rollupPluginHTML({
148+
bundleAssetsFromCss: true,
149+
// ...etc
150+
});
151+
```
152+
153+
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.:
154+
155+
```css
156+
body {
157+
background-image: url('assets/star-P4TYRBwL.gif');
158+
}
159+
160+
/* or */
161+
@font-face {
162+
src: url('assets/font-bold-f0mNRiTD.woff2') format('woff2');
163+
/* ...etc */
164+
}
165+
```
166+
128167
### Handling absolute paths
129168

130169
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.

packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface RollupPluginHTMLOptions {
4141
absolutePathPrefix?: string;
4242
/** When set to true, will insert meta tags for CSP and add script-src values for inline scripts by sha256-hashing the contents */
4343
strictCSPInlineScripts?: boolean;
44+
/** Bundle assets reference from CSS via `url` */
45+
bundleAssetsFromCss?: boolean;
4446
}
4547

4648
export interface GeneratedBundle {

packages/rollup-plugin-html/src/output/emitAssets.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,22 @@ export interface EmittedAssets {
1111
hashed: Map<string, string>;
1212
}
1313

14-
function shouldHandleFontFile(url: string) {
14+
const allowedFileExtensions = [
15+
// https://www.w3.org/TR/html4/types.html#:~:text=ID%20and%20NAME%20tokens%20must,tokens%20defined%20by%20other%20attributes.
16+
/.*\.svg(#[A-Za-z][A-Za-z0-9\-_:.]*)?/,
17+
/.*\.png/,
18+
/.*\.jpg/,
19+
/.*\.jpeg/,
20+
/.*\.webp/,
21+
/.*\.gif/,
22+
/.*\.avif/,
23+
/.*\.woff2/,
24+
/.*\.woff/,
25+
];
26+
27+
function shouldHandleAsset(url: string) {
1528
return (
16-
(url.endsWith('.woff2') || url.endsWith('.woff')) &&
29+
allowedFileExtensions.some(f => f.test(url)) &&
1730
!url.startsWith('http') &&
1831
!url.startsWith('data') &&
1932
!url.startsWith('#') &&
@@ -69,34 +82,42 @@ export async function emitAssets(
6982

7083
let ref: string;
7184
let basename = path.basename(asset.filePath);
72-
const emittedFonts = new Map();
85+
const emittedExternalAssets = new Map();
7386
if (asset.hashed) {
74-
if (basename.endsWith('.css')) {
87+
if (basename.endsWith('.css') && options.bundleAssetsFromCss) {
7588
let updatedCssSource = false;
7689
const { code } = await transform({
7790
filename: basename,
7891
code: asset.content,
7992
minify: false,
8093
visitor: {
8194
Url: url => {
82-
if (shouldHandleFontFile(url.url)) {
95+
// Support foo.svg#bar
96+
const [filePath, idRef] = url.url.split('#');
97+
98+
if (shouldHandleAsset(url.url)) {
8399
// Read the font file, get the font from the source location on the FS using asset.filePath
84-
const fontLocation = path.resolve(path.dirname(asset.filePath), url.url);
85-
const fontContent = fs.readFileSync(fontLocation);
100+
const assetLocation = path.resolve(path.dirname(asset.filePath), filePath);
101+
const assetContent = fs.readFileSync(assetLocation);
86102

87103
// Avoid duplicates
88-
if (!emittedFonts.has(fontLocation)) {
104+
if (!emittedExternalAssets.has(assetLocation)) {
89105
const fontFileRef = this.emitFile({
90106
type: 'asset',
91-
name: path.basename(url.url),
92-
source: fontContent,
107+
name: path.basename(filePath),
108+
source: assetContent,
93109
});
94110
const emittedFontFilePath = path.basename(this.getFileName(fontFileRef));
95-
emittedFonts.set(fontLocation, emittedFontFilePath);
111+
emittedExternalAssets.set(assetLocation, emittedFontFilePath);
96112
// Update the URL in the original CSS file to point to the emitted font file
97-
url.url = emittedFontFilePath;
113+
url.url = `assets/${
114+
idRef ? `${emittedFontFilePath}#${idRef}` : emittedFontFilePath
115+
}`;
98116
} else {
99-
url.url = emittedFonts.get(fontLocation);
117+
const emittedFontFilePath = emittedExternalAssets.get(assetLocation);
118+
url.url = `assets/${
119+
idRef ? `${emittedFontFilePath}#${idRef}` : emittedFontFilePath
120+
}`;
100121
}
101122
}
102123
updatedCssSource = true;

packages/rollup-plugin-html/src/rollupPluginHTML.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): R
7272
if (pluginOptions.strictCSPInlineScripts) {
7373
strictCSPInlineScripts = pluginOptions.strictCSPInlineScripts;
7474
}
75+
pluginOptions.bundleAssetsFromCss = !!pluginOptions.bundleAssetsFromCss;
7576

7677
if (pluginOptions.input == null) {
7778
// we are reading rollup input, so replace whatever was there
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
g
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#a {
2+
background-image: url("images/star.svg");
3+
}
4+
#b {
5+
background-image: url("images/star.svg#foo");
6+
}
7+
#c {
8+
background-image: url("images/star.png");
9+
}
10+
#d {
11+
background-image: url("images/star.jpg");
12+
}
13+
#e {
14+
background-image: url("images/star.jpeg");
15+
}
16+
#f {
17+
background-image: url("images/star.webp");
18+
}
19+
#g {
20+
background-image: url("images/star.gif");
21+
}
22+
#h {
23+
background-image: url("images/star.avif");
24+
}

packages/rollup-plugin-html/test/rollup-plugin-html.test.ts

+76-3
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ describe('rollup-plugin-html', () => {
10461046
const config = {
10471047
plugins: [
10481048
rollupPluginHTML({
1049+
bundleAssetsFromCss: true,
10491050
input: {
10501051
html: `
10511052
<html>
@@ -1069,8 +1070,8 @@ describe('rollup-plugin-html', () => {
10691070
const fontBold = output.find(o => o.name === 'font-normal.woff2');
10701071
const style = output.find(o => o.name === 'styles-with-fonts.css');
10711072
// It has emitted the font
1072-
expect(!!fontBold).to.equal(true);
1073-
expect(!!fontNormal).to.equal(true);
1073+
expect(fontBold).to.exist;
1074+
expect(fontNormal).to.exist;
10741075
// e.g. "font-normal-f0mNRiTD.woff2"
10751076
const regex = /assets\/font-normal-\w+\.woff2/;
10761077
// It outputs the font to the assets folder
@@ -1085,6 +1086,7 @@ describe('rollup-plugin-html', () => {
10851086
const config = {
10861087
plugins: [
10871088
rollupPluginHTML({
1089+
bundleAssetsFromCss: true,
10881090
input: {
10891091
html: `
10901092
<html>
@@ -1108,7 +1110,7 @@ describe('rollup-plugin-html', () => {
11081110
const style = output.find(o => o.name === 'node_modules-styles-with-fonts.css');
11091111

11101112
// It has emitted the font
1111-
expect(!!font).to.equal(true);
1113+
expect(font).to.exist;
11121114
// e.g. "font-normal-f0mNRiTD.woff2"
11131115
const regex = /assets\/font-normal-\w+\.woff2/;
11141116
// It outputs the font to the assets folder
@@ -1123,6 +1125,7 @@ describe('rollup-plugin-html', () => {
11231125
const config = {
11241126
plugins: [
11251127
rollupPluginHTML({
1128+
bundleAssetsFromCss: true,
11261129
input: {
11271130
html: `
11281131
<html>
@@ -1146,4 +1149,74 @@ describe('rollup-plugin-html', () => {
11461149
const fonts = output.filter(o => o.name === 'font-normal.woff2');
11471150
expect(fonts.length).to.equal(1);
11481151
});
1152+
1153+
it('handles images referenced from css', async () => {
1154+
const config = {
1155+
plugins: [
1156+
rollupPluginHTML({
1157+
bundleAssetsFromCss: true,
1158+
input: {
1159+
html: `
1160+
<html>
1161+
<head>
1162+
<link rel="stylesheet" href="./styles.css" />
1163+
</head>
1164+
<body>
1165+
</body>
1166+
</html>
1167+
`,
1168+
},
1169+
rootDir: path.join(__dirname, 'fixtures', 'resolves-assets-in-styles-images'),
1170+
}),
1171+
],
1172+
};
1173+
1174+
const bundle = await rollup(config);
1175+
const { output } = await bundle.generate(outputConfig);
1176+
1177+
expect(output.find(o => o.name === 'star.avif')).to.exist;
1178+
expect(output.find(o => o.name === 'star.gif')).to.exist;
1179+
expect(output.find(o => o.name === 'star.jpeg')).to.exist;
1180+
expect(output.find(o => o.name === 'star.jpg')).to.exist;
1181+
expect(output.find(o => o.name === 'star.png')).to.exist;
1182+
expect(output.find(o => o.name === 'star.svg')).to.exist;
1183+
expect(output.find(o => o.name === 'star.webp')).to.exist;
1184+
1185+
const rewrittenCss = (output.find(o => o.name === 'styles.css') as OutputAsset).source
1186+
.toString()
1187+
.trim();
1188+
expect(rewrittenCss).to.equal(
1189+
`#a {
1190+
background-image: url("assets/star-mrrzn5BV.svg");
1191+
}
1192+
1193+
#b {
1194+
background-image: url("assets/star-mrrzn5BV.svg#foo");
1195+
}
1196+
1197+
#c {
1198+
background-image: url("assets/star-eErsO14u.png");
1199+
}
1200+
1201+
#d {
1202+
background-image: url("assets/star-yqfHyXQC.jpg");
1203+
}
1204+
1205+
#e {
1206+
background-image: url("assets/star-G_i5Rpoh.jpeg");
1207+
}
1208+
1209+
#f {
1210+
background-image: url("assets/star-l7b58t3m.webp");
1211+
}
1212+
1213+
#g {
1214+
background-image: url("assets/star-P4TYRBwL.gif");
1215+
}
1216+
1217+
#h {
1218+
background-image: url("assets/star-H06WHrYy.avif");
1219+
}`.trim(),
1220+
);
1221+
});
11491222
});

0 commit comments

Comments
 (0)