UNPKG

vite-plugin-vue-hoist-ce-styles

Version:
160 lines (144 loc) 5.49 kB
import type { ViteDevServer, Plugin, ResolvedConfig } from 'vite'; import type { OutputChunk } from 'rollup'; const directRe = /\&direct/; const styleRe = /(\.css|\&type\=style)/; const defaultIndexRe = /index.*.js/; const PLACEHOLDER_BEGIN = 'ANCHOR:BEGIN'; const PLACEHOLDER_END = 'ANCHOR:END'; const placeHolderRe = /\"ANCHOR\:BEGIN(.*?)ANCHOR\:END\"/g; const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g; const assetRetrievalRE = /const __([a-z\d]{8})__ \= (.*?);/g; const virtualId = 'virtual:hoist-ce-styles/css-helper'; enum ExportDeclaration { DEFAULT = 'ExportDefaultDeclaration', } type StyleCache = ModuleCode[]; type ModuleCode = { origin: string; code: string; }; function invalidateStyles(server: ViteDevServer, id: string): void { const { moduleGraph, ws } = server; const module = moduleGraph.getModuleById(id); if (module) { moduleGraph.invalidateModule(module); if (ws) { ws.send({ type: 'full-reload', path: '*', }); } } } function toStyleCode(styleCache: StyleCache) { return styleCache.map((c) => c.code).join(''); } function toRefCode(referenceMap: Map<string, string>) { return [...referenceMap].reduce((current, [hash, linkedRef]) => `${current}\nconst __${hash}__ = ${linkedRef}` , ''); } export function hoistCeStyles({ hostComponent, indexRe = defaultIndexRe }: { hostComponent: string; indexRe: RegExp }): Plugin { const styleCache: StyleCache = []; const hostComponentRe = new RegExp(hostComponent); let refMap = new Map<string, string>(); let server: ViteDevServer; let config: ResolvedConfig; return { name: 'vite-plugin-vue-hoist-ce-styles', enforce: 'post', configureServer(_server: ViteDevServer) { server = _server; }, configResolved(resolvedConfig) { config = resolvedConfig; }, resolveId(id) { if (id === virtualId) { return virtualId; } }, load(id) { if (id === virtualId) { if (config.command === 'serve') { return `export const styles = ${JSON.stringify(toStyleCode(styleCache))}`; } } }, async transform(code, id) { if (directRe.test(id)) return null; if (!styleRe.test(id)) return null; const ast: any = this.parse(code); // default export of SFC in customElement mode will always be in the form of // "export default " ... styles " <-- this is what we care about. // we overwrite the styles with the export of our virtual module if (ast.body[0]?.type === ExportDeclaration.DEFAULT) { const styleCode = ast.body[0].declaration.value; if (styleCode) { const cachePos = styleCache.findIndex((c) => c.origin === id); const cacheObj = { origin: id, code: styleCode }; if (cachePos >= 0) { styleCache[cachePos] = cacheObj; } else { styleCache.push(cacheObj); } let code; if (config.command === 'serve') { invalidateStyles(server, virtualId); if (hostComponentRe.test(id)) { code = `import { styles } from "${virtualId}"\nexport default styles`; } else { code = `export default ''`; } } else { // if asset urls are used in css vite replaces them with __VITE_ASSET__hash__ // we need to hold a reference to these assets and leave them in the code so // they are parsed by the assetPlugin of vite during renderChunk // we retrieve these references in the generateBundle hook and replace // our unparsed assets const localAssetRefs: Map<string, string> = new Map(); let assetRefs: RegExpExecArray | null = styleCode.matchAll(assetUrlRE); if (assetRefs != null) { for (const [asset, hash] of assetRefs) { localAssetRefs.set(hash, asset); } } refMap = new Map([...refMap, ...localAssetRefs]); code = `const styles = "${PLACEHOLDER_BEGIN}${id}${PLACEHOLDER_END}";\nexport default styles;${toRefCode(localAssetRefs)};`; } return { code, moduleSideEffects: 'no-treeshake', map: null, }; } } }, generateBundle(_, bundle) { // for the bundle step we replace the styles of hostComponent with all found styles if it's the entrypoint // else we remove the styles for performance for (const [b, c] of Object.entries(bundle)) { const jsOutFile = indexRe.test(b); if (jsOutFile) { let styleCode = JSON.stringify(toStyleCode(styleCache)); let chunk = c as OutputChunk; // re-add asset urls and clean up afterwards const assets = chunk.code.matchAll(assetRetrievalRE); for (const [_, hash, url] of assets) { styleCode = styleCode.replace(refMap.get(hash)!, url); } chunk.code = chunk.code.replace(assetRetrievalRE, ''); const componentStyles = chunk.code.matchAll(placeHolderRe); for (const [anchor, id] of componentStyles) { if (hostComponentRe.test(id)) { chunk.code = chunk.code.replace(anchor, styleCode); } else { chunk.code = chunk.code.replace(anchor, "''"); } bundle[b] = chunk; } } } }, }; }