Skip to content

Commit 3dd7d5c

Browse files
authored
Merge pull request #2671 from JulianCataldo/feat/support-external-assets-glob-patterns
feat(rollup-plugin-html): glob patterns exclusion for external assets
2 parents 2f04ee7 + 6207e41 commit 3dd7d5c

16 files changed

+164
-6
lines changed

.changeset/nervous-bugs-rhyme.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web/rollup-plugin-html': minor
3+
---
4+
5+
glob patterns exclusion for external assets

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

+2
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ export interface RollupPluginHTMLOptions {
362362
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
363363
/** Whether to extract and bundle assets referenced in HTML. Defaults to true. */
364364
extractAssets?: boolean;
365+
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
366+
externalAssets?: string | string[];
365367
/** Define a full absolute url to your site (e.g. https://domain.com) */
366368
absoluteBaseUrl?: string;
367369
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Default to true. */

package-lock.json

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/rollup-plugin-html/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@
4848
"glob": "^10.0.0",
4949
"html-minifier-terser": "^7.1.0",
5050
"lightningcss": "^1.24.0",
51-
"parse5": "^6.0.1"
51+
"parse5": "^6.0.1",
52+
"picomatch": "^2.2.2"
5253
},
5354
"devDependencies": {
5455
"@types/html-minifier-terser": "^7.0.0",
56+
"@types/picomatch": "^2.2.1",
5557
"rollup": "^4.4.0"
5658
}
5759
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface RollupPluginHTMLOptions {
2929
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
3030
/** Whether to extract and bundle assets referenced in HTML. Defaults to true. */
3131
extractAssets?: boolean;
32+
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
33+
externalAssets?: string | string[];
3234
/** Define a full absolute url to your site (e.g. https://domain.com) */
3335
absoluteBaseUrl?: string;
3436
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Default to true. */

packages/rollup-plugin-html/src/assets/utils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Document, Element } from 'parse5';
22
import path from 'path';
3+
import picomatch from 'picomatch';
34
import { findElements, getTagName, getAttribute } from '@web/parse5-utils';
45
import { createError } from '../utils.js';
56
import { serialize } from 'v8';
@@ -143,3 +144,11 @@ export function getSourcePaths(node: Element) {
143144
export function findAssets(document: Document) {
144145
return findElements(document, isAsset);
145146
}
147+
148+
// picomatch follows glob spec and requires "./" to be removed for the matcher to work
149+
// it is safe, because with or without it resolves to the same file
150+
// read more: https://github.com/micromatch/picomatch/issues/77
151+
const removeLeadingSlash = (str: string) => (str.startsWith('./') ? str.slice(2) : str);
152+
export function createAssetPicomatchMatcher(glob?: string | string[]) {
153+
return picomatch(glob || [], { format: removeLeadingSlash });
154+
}

packages/rollup-plugin-html/src/input/extract/extractAssets.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,28 @@ import {
77
getSourcePaths,
88
isHashedAsset,
99
resolveAssetFilePath,
10+
createAssetPicomatchMatcher,
1011
} from '../../assets/utils.js';
1112

1213
export interface ExtractAssetsParams {
1314
document: Document;
1415
htmlFilePath: string;
1516
htmlDir: string;
1617
rootDir: string;
18+
externalAssets?: string | string[];
1719
absolutePathPrefix?: string;
1820
}
1921

2022
export function extractAssets(params: ExtractAssetsParams): InputAsset[] {
2123
const assetNodes = findAssets(params.document);
2224
const allAssets: InputAsset[] = [];
25+
const isExternal = createAssetPicomatchMatcher(params.externalAssets);
2326

2427
for (const node of assetNodes) {
2528
const sourcePaths = getSourcePaths(node);
2629
for (const sourcePath of sourcePaths) {
30+
if (isExternal(sourcePath)) continue;
31+
2732
const filePath = resolveAssetFilePath(
2833
sourcePath,
2934
params.htmlDir,

packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ export interface ExtractParams {
88
htmlFilePath: string;
99
rootDir: string;
1010
extractAssets: boolean;
11+
externalAssets?: string | string[];
1112
absolutePathPrefix?: string;
1213
}
1314

1415
export function extractModulesAndAssets(params: ExtractParams) {
15-
const { html, htmlFilePath, rootDir, absolutePathPrefix } = params;
16+
const { html, htmlFilePath, rootDir, externalAssets, absolutePathPrefix } = params;
1617
const htmlDir = path.dirname(htmlFilePath);
1718
const document = parse(html);
1819

@@ -24,7 +25,14 @@ export function extractModulesAndAssets(params: ExtractParams) {
2425
absolutePathPrefix,
2526
});
2627
const assets = params.extractAssets
27-
? extractAssets({ document, htmlDir, htmlFilePath, rootDir, absolutePathPrefix })
28+
? extractAssets({
29+
document,
30+
htmlDir,
31+
htmlFilePath,
32+
rootDir,
33+
externalAssets,
34+
absolutePathPrefix,
35+
})
2836
: [];
2937

3038
// turn mutated AST back to a string

packages/rollup-plugin-html/src/input/getInputData.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,20 @@ export interface CreateInputDataParams {
3131
rootDir: string;
3232
filePath?: string;
3333
extractAssets: boolean;
34+
externalAssets?: string | string[];
3435
absolutePathPrefix?: string;
3536
}
3637

3738
function createInputData(params: CreateInputDataParams): InputData {
38-
const { name, html, rootDir, filePath, extractAssets, absolutePathPrefix } = params;
39+
const { name, html, rootDir, filePath, extractAssets, externalAssets, absolutePathPrefix } =
40+
params;
3941
const htmlFilePath = filePath ? filePath : path.resolve(rootDir, name);
4042
const result = extractModulesAndAssets({
4143
html,
4244
htmlFilePath,
4345
rootDir,
4446
extractAssets,
47+
externalAssets,
4548
absolutePathPrefix,
4649
});
4750

@@ -63,6 +66,7 @@ export function getInputData(
6366
rootDir = process.cwd(),
6467
flattenOutput,
6568
extractAssets = true,
69+
externalAssets,
6670
absolutePathPrefix,
6771
exclude: ignore,
6872
} = pluginOptions;
@@ -77,6 +81,7 @@ export function getInputData(
7781
html: input.html,
7882
rootDir,
7983
extractAssets,
84+
externalAssets,
8085
absolutePathPrefix,
8186
});
8287
result.push(data);
@@ -97,6 +102,7 @@ export function getInputData(
97102
rootDir,
98103
filePath,
99104
extractAssets,
105+
externalAssets,
100106
absolutePathPrefix,
101107
});
102108
result.push(data);

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { transform } from 'lightningcss';
44
import fs from 'fs';
55

66
import { InputAsset, InputData } from '../input/InputData';
7+
import { createAssetPicomatchMatcher } from '../assets/utils.js';
78
import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPluginHTMLOptions';
89

910
export interface EmittedAssets {
@@ -81,6 +82,7 @@ export async function emitAssets(
8182

8283
let ref: string;
8384
let basename = path.basename(asset.filePath);
85+
const isExternal = createAssetPicomatchMatcher(options.externalAssets);
8486
const emittedExternalAssets = new Map();
8587
if (asset.hashed) {
8688
if (basename.endsWith('.css') && options.bundleAssetsFromCss) {
@@ -95,7 +97,7 @@ export async function emitAssets(
9597
// https://www.w3.org/TR/html4/types.html#:~:text=ID%20and%20NAME%20tokens%20must,tokens%20defined%20by%20other%20attributes.
9698
const [filePath, idRef] = url.url.split('#');
9799

98-
if (shouldHandleAsset(filePath)) {
100+
if (shouldHandleAsset(filePath) && !isExternal(filePath)) {
99101
// Read the asset file, get the asset from the source location on the FS using asset.filePath
100102
const assetLocation = path.resolve(path.dirname(asset.filePath), filePath);
101103
const assetContent = fs.readFileSync(assetLocation);

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

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export async function getOutputHTML(params: GetOutputHTMLParams) {
5353
outputDir,
5454
rootDir,
5555
emittedAssets,
56+
externalAssets: pluginOptions.externalAssets,
5657
absolutePathPrefix,
5758
publicPath: pluginOptions.publicPath,
5859
});

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

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getSourcePaths,
99
isHashedAsset,
1010
resolveAssetFilePath,
11+
createAssetPicomatchMatcher,
1112
} from '../assets/utils.js';
1213
import { InputData } from '../input/InputData.js';
1314
import { createError } from '../utils.js';
@@ -20,6 +21,7 @@ export interface InjectUpdatedAssetPathsArgs {
2021
outputDir: string;
2122
rootDir: string;
2223
emittedAssets: EmittedAssets;
24+
externalAssets?: string | string[];
2325
publicPath?: string;
2426
absolutePathPrefix?: string;
2527
}
@@ -42,14 +44,18 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) {
4244
outputDir,
4345
rootDir,
4446
emittedAssets,
47+
externalAssets,
4548
publicPath = './',
4649
absolutePathPrefix,
4750
} = args;
4851
const assetNodes = findAssets(document);
52+
const isExternal = createAssetPicomatchMatcher(externalAssets);
4953

5054
for (const node of assetNodes) {
5155
const sourcePaths = getSourcePaths(node);
5256
for (const sourcePath of sourcePaths) {
57+
if (isExternal(sourcePath)) continue;
58+
5359
const htmlFilePath = input.filePath ? input.filePath : path.join(rootDir, input.name);
5460
const htmlDir = path.dirname(htmlFilePath);
5561
const filePath = resolveAssetFilePath(sourcePath, htmlDir, rootDir, absolutePathPrefix);
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#a1 {
2+
background-image: url('image-a.png');
3+
}
4+
5+
#a2 {
6+
background-image: url('image-a.svg');
7+
}
8+
9+
#d1 {
10+
background-image: url('./image-d.png');
11+
}
12+
13+
#d2 {
14+
background-image: url('./image-d.svg');
15+
}

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

+91
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,97 @@ describe('rollup-plugin-html', () => {
12161216
12171217
#h {
12181218
background-image: url("assets/star-H06WHrYy.avif");
1219+
}`.trim(),
1220+
);
1221+
});
1222+
1223+
it('allows to exclude external assets usign a glob pattern', async () => {
1224+
const config = {
1225+
plugins: [
1226+
rollupPluginHTML({
1227+
input: {
1228+
html: `<html>
1229+
<head>
1230+
<link rel="apple-touch-icon" sizes="180x180" href="./image-a.png" />
1231+
<link rel="icon" type="image/png" sizes="32x32" href="image-d.png" />
1232+
<link rel="manifest" href="./webmanifest.json" />
1233+
<link rel="mask-icon" href="./image-a.svg" color="#3f93ce" />
1234+
<link rel="mask-icon" href="image-d.svg" color="#3f93ce" />
1235+
<link rel="stylesheet" href="./styles-with-referenced-assets.css" />
1236+
<link rel="stylesheet" href="./foo/x.css" />
1237+
<link rel="stylesheet" href="foo/bar/y.css" />
1238+
</head>
1239+
<body>
1240+
<img src="./image-d.png" />
1241+
<div>
1242+
<img src="./image-d.svg" />
1243+
</div>
1244+
</body>
1245+
</html>`,
1246+
},
1247+
bundleAssetsFromCss: true,
1248+
externalAssets: ['**/foo/**/*', '*.svg'],
1249+
rootDir: path.join(__dirname, 'fixtures', 'assets'),
1250+
}),
1251+
],
1252+
};
1253+
1254+
const bundle = await rollup(config);
1255+
const { output } = await bundle.generate(outputConfig);
1256+
1257+
expect(output.length).to.equal(8);
1258+
1259+
const expectedAssets = [
1260+
'assets/image-a.png',
1261+
'assets/image-d.png',
1262+
'styles-with-referenced-assets.css',
1263+
'image-a.png',
1264+
'image-d.png',
1265+
'webmanifest.json',
1266+
];
1267+
1268+
for (const name of expectedAssets) {
1269+
const asset = getAsset(output, name);
1270+
expect(asset).to.exist;
1271+
expect(asset.source).to.exist;
1272+
}
1273+
1274+
const outputHtml = getAsset(output, 'index.html').source;
1275+
expect(outputHtml).to.include(
1276+
'<link rel="apple-touch-icon" sizes="180x180" href="assets/image-a.png">',
1277+
);
1278+
expect(outputHtml).to.include(
1279+
'<link rel="icon" type="image/png" sizes="32x32" href="assets/image-d.png">',
1280+
);
1281+
expect(outputHtml).to.include('<link rel="manifest" href="assets/webmanifest.json">');
1282+
expect(outputHtml).to.include('<link rel="mask-icon" href="./image-a.svg" color="#3f93ce">');
1283+
expect(outputHtml).to.include('<link rel="mask-icon" href="image-d.svg" color="#3f93ce">');
1284+
expect(outputHtml).to.include(
1285+
'<link rel="stylesheet" href="assets/styles-with-referenced-assets-NuwIw8gN.css">',
1286+
);
1287+
expect(outputHtml).to.include('<link rel="stylesheet" href="./foo/x.css">');
1288+
expect(outputHtml).to.include('<link rel="stylesheet" href="foo/bar/y.css">');
1289+
expect(outputHtml).to.include('<img src="assets/assets/image-d-y8_AQMDl.png">');
1290+
expect(outputHtml).to.include('<img src="./image-d.svg">');
1291+
1292+
const rewrittenCss = getAsset(output, 'styles-with-referenced-assets.css')
1293+
.source.toString()
1294+
.trim();
1295+
expect(rewrittenCss).to.equal(
1296+
`#a1 {
1297+
background-image: url("assets/image-a-Mr5Lb2jQ.png");
1298+
}
1299+
1300+
#a2 {
1301+
background-image: url("image-a.svg");
1302+
}
1303+
1304+
#d1 {
1305+
background-image: url("assets/image-d-y8_AQMDl.png");
1306+
}
1307+
1308+
#d2 {
1309+
background-image: url("./image-d.svg");
12191310
}`.trim(),
12201311
);
12211312
});

0 commit comments

Comments
 (0)