diff --git a/.changeset/nervous-bugs-rhyme.md b/.changeset/nervous-bugs-rhyme.md new file mode 100644 index 000000000..58e00582b --- /dev/null +++ b/.changeset/nervous-bugs-rhyme.md @@ -0,0 +1,5 @@ +--- +'@web/rollup-plugin-html': minor +--- + +glob patterns exclusion for external assets diff --git a/docs/docs/building/rollup-plugin-html.md b/docs/docs/building/rollup-plugin-html.md index 0796bdc8f..bdab0cfa5 100644 --- a/docs/docs/building/rollup-plugin-html.md +++ b/docs/docs/building/rollup-plugin-html.md @@ -362,6 +362,8 @@ export interface RollupPluginHTMLOptions { transformHtml?: TransformHtmlFunction | TransformHtmlFunction[]; /** Whether to extract and bundle assets referenced in HTML. Defaults to true. */ extractAssets?: boolean; + /** Whether to ignore assets referenced in HTML and CSS with glob patterns. */ + externalAssets?: string | string[]; /** Define a full absolute url to your site (e.g. https://domain.com) */ absoluteBaseUrl?: string; /** 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. */ diff --git a/package-lock.json b/package-lock.json index 10eea6e94..441ccf080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41551,10 +41551,12 @@ "glob": "^10.0.0", "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", - "parse5": "^6.0.1" + "parse5": "^6.0.1", + "picomatch": "^2.2.2" }, "devDependencies": { "@types/html-minifier-terser": "^7.0.0", + "@types/picomatch": "^2.2.1", "rollup": "^4.4.0" }, "engines": { diff --git a/packages/rollup-plugin-html/package.json b/packages/rollup-plugin-html/package.json index 1d92b9020..8e3220c59 100644 --- a/packages/rollup-plugin-html/package.json +++ b/packages/rollup-plugin-html/package.json @@ -48,10 +48,12 @@ "glob": "^10.0.0", "html-minifier-terser": "^7.1.0", "lightningcss": "^1.24.0", - "parse5": "^6.0.1" + "parse5": "^6.0.1", + "picomatch": "^2.2.2" }, "devDependencies": { "@types/html-minifier-terser": "^7.0.0", + "@types/picomatch": "^2.2.1", "rollup": "^4.4.0" } } diff --git a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts index 96688c91d..c65198af3 100644 --- a/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts +++ b/packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts @@ -29,6 +29,8 @@ export interface RollupPluginHTMLOptions { transformHtml?: TransformHtmlFunction | TransformHtmlFunction[]; /** Whether to extract and bundle assets referenced in HTML. Defaults to true. */ extractAssets?: boolean; + /** Whether to ignore assets referenced in HTML and CSS with glob patterns. */ + externalAssets?: string | string[]; /** Define a full absolute url to your site (e.g. https://domain.com) */ absoluteBaseUrl?: string; /** 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. */ diff --git a/packages/rollup-plugin-html/src/assets/utils.ts b/packages/rollup-plugin-html/src/assets/utils.ts index 672f5ec1e..5af29a52f 100644 --- a/packages/rollup-plugin-html/src/assets/utils.ts +++ b/packages/rollup-plugin-html/src/assets/utils.ts @@ -1,5 +1,6 @@ import { Document, Element } from 'parse5'; import path from 'path'; +import picomatch from 'picomatch'; import { findElements, getTagName, getAttribute } from '@web/parse5-utils'; import { createError } from '../utils.js'; import { serialize } from 'v8'; @@ -143,3 +144,11 @@ export function getSourcePaths(node: Element) { export function findAssets(document: Document) { return findElements(document, isAsset); } + +// picomatch follows glob spec and requires "./" to be removed for the matcher to work +// it is safe, because with or without it resolves to the same file +// read more: https://github.com/micromatch/picomatch/issues/77 +const removeLeadingSlash = (str: string) => (str.startsWith('./') ? str.slice(2) : str); +export function createAssetPicomatchMatcher(glob?: string | string[]) { + return picomatch(glob || [], { format: removeLeadingSlash }); +} diff --git a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts index 7eb032b2a..b2ae8b8fc 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractAssets.ts @@ -7,6 +7,7 @@ import { getSourcePaths, isHashedAsset, resolveAssetFilePath, + createAssetPicomatchMatcher, } from '../../assets/utils.js'; export interface ExtractAssetsParams { @@ -14,16 +15,20 @@ export interface ExtractAssetsParams { htmlFilePath: string; htmlDir: string; rootDir: string; + externalAssets?: string | string[]; absolutePathPrefix?: string; } export function extractAssets(params: ExtractAssetsParams): InputAsset[] { const assetNodes = findAssets(params.document); const allAssets: InputAsset[] = []; + const isExternal = createAssetPicomatchMatcher(params.externalAssets); for (const node of assetNodes) { const sourcePaths = getSourcePaths(node); for (const sourcePath of sourcePaths) { + if (isExternal(sourcePath)) continue; + const filePath = resolveAssetFilePath( sourcePath, params.htmlDir, diff --git a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts index e955ccfed..b06d59ded 100644 --- a/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts +++ b/packages/rollup-plugin-html/src/input/extract/extractModulesAndAssets.ts @@ -8,11 +8,12 @@ export interface ExtractParams { htmlFilePath: string; rootDir: string; extractAssets: boolean; + externalAssets?: string | string[]; absolutePathPrefix?: string; } export function extractModulesAndAssets(params: ExtractParams) { - const { html, htmlFilePath, rootDir, absolutePathPrefix } = params; + const { html, htmlFilePath, rootDir, externalAssets, absolutePathPrefix } = params; const htmlDir = path.dirname(htmlFilePath); const document = parse(html); @@ -24,7 +25,14 @@ export function extractModulesAndAssets(params: ExtractParams) { absolutePathPrefix, }); const assets = params.extractAssets - ? extractAssets({ document, htmlDir, htmlFilePath, rootDir, absolutePathPrefix }) + ? extractAssets({ + document, + htmlDir, + htmlFilePath, + rootDir, + externalAssets, + absolutePathPrefix, + }) : []; // turn mutated AST back to a string diff --git a/packages/rollup-plugin-html/src/input/getInputData.ts b/packages/rollup-plugin-html/src/input/getInputData.ts index 25d3a02e6..102cb062e 100644 --- a/packages/rollup-plugin-html/src/input/getInputData.ts +++ b/packages/rollup-plugin-html/src/input/getInputData.ts @@ -31,17 +31,20 @@ export interface CreateInputDataParams { rootDir: string; filePath?: string; extractAssets: boolean; + externalAssets?: string | string[]; absolutePathPrefix?: string; } function createInputData(params: CreateInputDataParams): InputData { - const { name, html, rootDir, filePath, extractAssets, absolutePathPrefix } = params; + const { name, html, rootDir, filePath, extractAssets, externalAssets, absolutePathPrefix } = + params; const htmlFilePath = filePath ? filePath : path.resolve(rootDir, name); const result = extractModulesAndAssets({ html, htmlFilePath, rootDir, extractAssets, + externalAssets, absolutePathPrefix, }); @@ -63,6 +66,7 @@ export function getInputData( rootDir = process.cwd(), flattenOutput, extractAssets = true, + externalAssets, absolutePathPrefix, exclude: ignore, } = pluginOptions; @@ -77,6 +81,7 @@ export function getInputData( html: input.html, rootDir, extractAssets, + externalAssets, absolutePathPrefix, }); result.push(data); @@ -97,6 +102,7 @@ export function getInputData( rootDir, filePath, extractAssets, + externalAssets, absolutePathPrefix, }); result.push(data); diff --git a/packages/rollup-plugin-html/src/output/emitAssets.ts b/packages/rollup-plugin-html/src/output/emitAssets.ts index bf4f89060..19a790ce4 100644 --- a/packages/rollup-plugin-html/src/output/emitAssets.ts +++ b/packages/rollup-plugin-html/src/output/emitAssets.ts @@ -4,6 +4,7 @@ import { transform } from 'lightningcss'; import fs from 'fs'; import { InputAsset, InputData } from '../input/InputData'; +import { createAssetPicomatchMatcher } from '../assets/utils.js'; import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPluginHTMLOptions'; export interface EmittedAssets { @@ -81,6 +82,7 @@ export async function emitAssets( let ref: string; let basename = path.basename(asset.filePath); + const isExternal = createAssetPicomatchMatcher(options.externalAssets); const emittedExternalAssets = new Map(); if (asset.hashed) { if (basename.endsWith('.css') && options.bundleAssetsFromCss) { @@ -95,7 +97,7 @@ export async function emitAssets( // 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)) { + if (shouldHandleAsset(filePath) && !isExternal(filePath)) { // Read the asset file, get the asset from the source location on the FS using asset.filePath const assetLocation = path.resolve(path.dirname(asset.filePath), filePath); const assetContent = fs.readFileSync(assetLocation); diff --git a/packages/rollup-plugin-html/src/output/getOutputHTML.ts b/packages/rollup-plugin-html/src/output/getOutputHTML.ts index d3c1b8256..7f2725ae6 100644 --- a/packages/rollup-plugin-html/src/output/getOutputHTML.ts +++ b/packages/rollup-plugin-html/src/output/getOutputHTML.ts @@ -53,6 +53,7 @@ export async function getOutputHTML(params: GetOutputHTMLParams) { outputDir, rootDir, emittedAssets, + externalAssets: pluginOptions.externalAssets, absolutePathPrefix, publicPath: pluginOptions.publicPath, }); diff --git a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts index 32d4d0980..e5ba484ae 100644 --- a/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts +++ b/packages/rollup-plugin-html/src/output/injectedUpdatedAssetPaths.ts @@ -8,6 +8,7 @@ import { getSourcePaths, isHashedAsset, resolveAssetFilePath, + createAssetPicomatchMatcher, } from '../assets/utils.js'; import { InputData } from '../input/InputData.js'; import { createError } from '../utils.js'; @@ -20,6 +21,7 @@ export interface InjectUpdatedAssetPathsArgs { outputDir: string; rootDir: string; emittedAssets: EmittedAssets; + externalAssets?: string | string[]; publicPath?: string; absolutePathPrefix?: string; } @@ -42,14 +44,18 @@ export function injectedUpdatedAssetPaths(args: InjectUpdatedAssetPathsArgs) { outputDir, rootDir, emittedAssets, + externalAssets, publicPath = './', absolutePathPrefix, } = args; const assetNodes = findAssets(document); + const isExternal = createAssetPicomatchMatcher(externalAssets); for (const node of assetNodes) { const sourcePaths = getSourcePaths(node); for (const sourcePath of sourcePaths) { + if (isExternal(sourcePath)) continue; + const htmlFilePath = input.filePath ? input.filePath : path.join(rootDir, input.name); const htmlDir = path.dirname(htmlFilePath); const filePath = resolveAssetFilePath(sourcePath, htmlDir, rootDir, absolutePathPrefix); diff --git a/packages/rollup-plugin-html/test/fixtures/assets/image-d.png b/packages/rollup-plugin-html/test/fixtures/assets/image-d.png new file mode 100644 index 000000000..731a4fb7b --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/assets/image-d.png @@ -0,0 +1 @@ +image-d.png \ No newline at end of file diff --git a/packages/rollup-plugin-html/test/fixtures/assets/image-d.svg b/packages/rollup-plugin-html/test/fixtures/assets/image-d.svg new file mode 100644 index 000000000..82fe9253a --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/assets/image-d.svg @@ -0,0 +1 @@ +image-d.svg diff --git a/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css b/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css new file mode 100644 index 000000000..acb8baa56 --- /dev/null +++ b/packages/rollup-plugin-html/test/fixtures/assets/styles-with-referenced-assets.css @@ -0,0 +1,15 @@ +#a1 { + background-image: url('image-a.png'); +} + +#a2 { + background-image: url('image-a.svg'); +} + +#d1 { + background-image: url('./image-d.png'); +} + +#d2 { + background-image: url('./image-d.svg'); +} 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 395c70b8e..b1660e7e5 100644 --- a/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts +++ b/packages/rollup-plugin-html/test/rollup-plugin-html.test.ts @@ -1216,6 +1216,97 @@ describe('rollup-plugin-html', () => { #h { background-image: url("assets/star-H06WHrYy.avif"); +}`.trim(), + ); + }); + + it('allows to exclude external assets usign a glob pattern', async () => { + const config = { + plugins: [ + rollupPluginHTML({ + input: { + html: ` +
+ + + + + + + + + + +