UNPKG

@web/rollup-plugin-html

Version:
155 lines (141 loc) 4.03 kB
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'; const hashedLinkRels = ['stylesheet']; const linkRels = [...hashedLinkRels, 'icon', 'manifest', 'apple-touch-icon', 'mask-icon']; function getSrcSetUrls(srcset: string) { if (!srcset) { return []; } const srcsetParts = srcset.includes(',') ? srcset.split(',') : [srcset]; const urls = srcsetParts .map(url => url.trim()) .map(url => (url.includes(' ') ? url.split(' ')[0] : url)); return urls; } function extractFirstUrlOfSrcSet(node: Element) { const srcset = getAttribute(node, 'srcset'); if (!srcset) { return ''; } const urls = getSrcSetUrls(srcset); return urls[0]; } function isAsset(node: Element) { let path = ''; switch (getTagName(node)) { case 'img': path = getAttribute(node, 'src') ?? ''; break; case 'source': if (getAttribute(node, 'src')) { path = getAttribute(node, 'src') ?? ''; } else { path = extractFirstUrlOfSrcSet(node) ?? ''; } break; case 'link': if (linkRels.includes(getAttribute(node, 'rel') ?? '')) { path = getAttribute(node, 'href') ?? ''; } break; case 'meta': if (getAttribute(node, 'property') === 'og:image' && getAttribute(node, 'content')) { path = getAttribute(node, 'content') ?? ''; } break; case 'script': if (getAttribute(node, 'type') !== 'module' && getAttribute(node, 'src')) { path = getAttribute(node, 'src') ?? ''; } break; default: return false; } if (!path) { return false; } try { new URL(path); return false; } catch (e) { return true; } } export function isHashedAsset(node: Element) { switch (getTagName(node)) { case 'img': return true; case 'source': return true; case 'script': return true; case 'link': return hashedLinkRels.includes(getAttribute(node, 'rel')!); case 'meta': return true; default: return false; } } export function resolveAssetFilePath( browserPath: string, htmlDir: string, projectRootDir: string, absolutePathPrefix?: string, ) { const _browserPath = absolutePathPrefix && browserPath[0] === '/' ? '/' + path.posix.relative(absolutePathPrefix, browserPath) : browserPath; return path.join( _browserPath.startsWith('/') ? projectRootDir : htmlDir, _browserPath.split('/').join(path.sep), ); } export function getSourceAttribute(node: Element) { switch (getTagName(node)) { case 'img': { return 'src'; } case 'source': { return getAttribute(node, 'src') ? 'src' : 'srcset'; } case 'link': { return 'href'; } case 'script': { return 'src'; } case 'meta': { return 'content'; } default: throw new Error(`Unknown node with tagname ${getTagName(node)}`); } } export function getSourcePaths(node: Element) { const key = getSourceAttribute(node); const src = getAttribute(node, key); if (typeof key !== 'string' || src === '') { throw createError(`Missing attribute ${key} in element ${serialize(node)}`); } let paths: string[] = []; if (src) { paths = key !== 'srcset' ? [src] : getSrcSetUrls(src); } return paths; } 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 }); }