From 5c5db3ff02b827acf176b6ca5ba1caf8e296ad4e Mon Sep 17 00:00:00 2001 From: Rahim Alwer Date: Wed, 3 Apr 2024 16:09:12 +1100 Subject: [PATCH] fix(player): bundler plugins should support multiple files --- packages/vidstack/package.json | 2 +- packages/vidstack/src/plugins.ts | 352 +++++++++++++++++++------------ pnpm-lock.yaml | 2 +- 3 files changed, 218 insertions(+), 138 deletions(-) diff --git a/packages/vidstack/package.json b/packages/vidstack/package.json index aed8ed82c..af65ba75b 100644 --- a/packages/vidstack/package.json +++ b/packages/vidstack/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "media-captions": "^1.0.1", - "unplugin": "^1.6.0" + "unplugin": "^1.10.1" }, "devDependencies": { "@floating-ui/dom": "^1.4.4", diff --git a/packages/vidstack/src/plugins.ts b/packages/vidstack/src/plugins.ts index a4c955fce..1017c3abf 100644 --- a/packages/vidstack/src/plugins.ts +++ b/packages/vidstack/src/plugins.ts @@ -16,34 +16,6 @@ const defaultIncludePattern = /\.(jsx|tsx|html|vue|svelte)/, elementsJSON = fs.readFileSync(elementsFilePath, 'utf8'), elementsManifest = JSON.parse(elementsJSON) as Record; -// These elements need to be registered before their children to prevent async setup flows. -const priorityImports = [ - // Core - 'media-player', - 'media-provider', - // Layout - 'media-audio-layout', - 'media-video-layout', - 'media-plyr-layout', - 'media-layout', - 'media-controls', - 'media-controls-group', - 'media-poster', - // Tooltips - 'media-tooltip', - 'media-tooltip-trigger', - 'media-tooltip-content', - // Sliders - 'media-slider', - 'media-volume-slider', - 'media-time-slider', - // Menus - 'media-menu', - 'media-menu-button', - 'media-menu-portal', - 'media-menu-items', -]; - const defaultStyles = { 'vds-buffering-indicator': 'buffering.css', 'vds-button': 'buttons.css', @@ -67,6 +39,12 @@ for (const name of Object.keys(defaultStyles)) { defaultStyles[name] = `vidstack/player/styles/default/${defaultStyles[name]}`; } +interface GraphData { + // Module-specific elements and styles (tracked for HMR). + elementsGraph: ElementsGraph; + stylesGraph: StylesGraph; +} + type ElementsGraph = Map>; type StylesGraph = Map>; @@ -80,151 +58,176 @@ export interface UserOptions { export const unplugin = createUnplugin((options = {}) => { let include = options.include ?? defaultIncludePattern, filter = createFilter(include, options.exclude), - // Elements and styles across all modules. - elements = new Set(), - styles = new Set(), - // Module-specific elements and styles (tracked for HMR). - elementsGraph: ElementsGraph = new Map(), - stylesGraph: StylesGraph = new Map(), + data: GraphData = { + elementsGraph: new Map(), + stylesGraph: new Map(), + }, + defaultAudioLayout = false, + defaultVideoLayout = false, + plyrLayout = false, + chunkToFile = new Map(), + moduleToChunk = new Map(), + invalidated = new Set(), viteServer: ViteDevServer | null = null; - function invalidateBundleModule() { - elements.clear(); - for (const tagNames of elementsGraph.values()) { - for (const tagName of tagNames) elements.add(tagName); + function hasDefaultLayout() { + return defaultAudioLayout || defaultVideoLayout; + } + + function processModule(id: string, code: string) { + const { elementsGraph, stylesGraph } = data, + { elements, styles } = parse(code); + + if (elements.size) { + if (elements.has('media-audio-layout')) defaultAudioLayout = true; + if (elements.has('media-video-layout')) defaultVideoLayout = true; + if (elements.has('media-plyr-layout')) plyrLayout = true; + elementsGraph.set(id, elements); } - styles.clear(); - for (const files of stylesGraph.values()) { - for (const file of files) styles.add(file); + if (styles.size) { + stylesGraph.set(id, styles); } - const mod = viteServer?.moduleGraph.getModuleById(bundleModuleId); - mod && viteServer?.moduleGraph.invalidateModule(mod); - viteServer?.ws.send({ type: 'full-reload' }); + return { elements, styles }; } - function parse(id: string, code: string) { - if (viteServer) { - const oldElements = elementsGraph.get(id), - oldStyles = stylesGraph.get(id), - newElements = new Set(), - newStyles = new Set(); + function invalidateAll() { + if (!viteServer) return; - parseCode(code, newElements, newStyles); + invalidateModule(bundleModuleId); - const isEqual = isSetsEqual(oldElements, newElements) && isSetsEqual(oldStyles, newStyles); + for (const id of chunkToFile.keys()) invalidateModule(id); + } - if (!isEqual) { - elementsGraph.set(id, newElements); - stylesGraph.set(id, newStyles); - invalidateBundleModule(); - } - } else { - parseCode(code, elements, styles); - } + function invalidateModule(id: string) { + const mod = viteServer?.moduleGraph.getModuleById(id); + if (mod) viteServer?.moduleGraph.invalidateModule(mod); } - return { - name: 'vidstack', - enforce: 'pre', - resolveId(id) { - if (id === bundleModuleId) return id; - }, - load(id) { - if (id === bundleModuleId) { - const imports: string[] = [], - hasDefaultLayout = - elements.has('media-audio-layout') || elements.has('media-video-layout'), - hasPlyrLayout = elements.has('media-plyr-layout'); - - if (!hasDefaultLayout) { - imports.push('import "vidstack/player/styles/base.css";'); - - for (const style of styles) { - imports.push(`import "${style}";`); - } + function reload() { + viteServer?.hot.send({ type: 'full-reload' }); + } - const elementClasses = new Set(); + return [ + { + name: 'vidstack-pre', + enforce: 'pre', + resolveId(id) { + if (id === bundleModuleId) return id; + else if (chunkToFile.has(id)) return id; + }, + load(id) { + if (id === bundleModuleId) { + return generateBundleImports({ + defaultAudioLayout, + defaultVideoLayout, + plyrLayout, + }); + } - for (const tagName of [...priorityImports, ...elements]) { - if (!elements.has(tagName)) continue; + if (chunkToFile.has(id)) { + if (hasDefaultLayout()) return ''; - if (tagName === 'media-plyr-layout') { - imports.push('vidstack/player/styles/plyr/theme.css'); - imports.push('vidstack/player/layouts/plyr'); - continue; - } + const moduleId = chunkToFile.get(id), + elements = data.elementsGraph.get(moduleId!), + styles = data.stylesGraph.get(moduleId!); - const className = elementsManifest[tagName]; + return elements || styles ? generateElementImports(elements, styles) : ''; + } - if (className) { - elementClasses.add(className); - } else { - console.warn(`[vidstack]: unknown media element was found \`${tagName}\``); - } - } + return null; + }, + transformInclude(id) { + return filter(id); + }, + transform(code, id) { + processModule(id, code); + return null; + }, + vite: { + configureServer(server) { + viteServer = server; + }, + async handleHotUpdate({ file, read }) { + if (!filter(file)) return; - const elementImports = ['defineCustomElement', ...elementClasses]; - imports.push(`import {\n${elementImports.join(',\n')}\n} from "vidstack/elements";`); + const oldElements = data.elementsGraph.get(file), + oldStyles = data.stylesGraph.get(file); - return ( - imports.join('\n') + - '\n\n' + - [...elementClasses].map((name) => `defineCustomElement(${name});`).join('\n') - ); - } else { - imports.push(`import "vidstack/player/styles/default/theme.css";`); + const { elements: newElements, styles: newStyles } = processModule(file, await read()); - if (hasPlyrLayout) { - imports.push(`vidstack/player/styles/plyr/theme.css`); - } + const hasChangedLayouts = + diff(oldElements, newElements, 'media-audio-layout') || + diff(oldElements, newElements, 'media-video-layout') || + diff(oldElements, newElements, 'media-plyr-layout'); - if (elements.has('media-audio-layout')) { - imports.push(`import "vidstack/player/styles/default/layouts/audio.css";`); - } + data.elementsGraph.set(file, newElements); + data.stylesGraph.set(file, newStyles); - if (elements.has('media-video-layout')) { - imports.push(`import "vidstack/player/styles/default/layouts/video.css";`); - } + if (hasChangedLayouts) { + defaultAudioLayout = false; + defaultVideoLayout = false; + plyrLayout = false; - imports.push('import "vidstack/player";'); - imports.push(`import "vidstack/player/layouts${!hasPlyrLayout ? '/default' : ''}";`); - imports.push('import "vidstack/player/ui";'); + invalidateAll(); + reload(); - return imports.join('\n'); - } - } + return; + } - return null; - }, - transformInclude(id) { - return filter(id); - }, - transform(code, id) { - parse(id, code); - return null; + const isEqual = + isSetsEqual(oldElements, newElements) && isSetsEqual(oldStyles, newStyles); + + if (!isEqual) { + const chunkId = moduleToChunk.get(file); + if (chunkId) { + invalidateModule(chunkId); + reload(); + } + } + }, + }, }, - vite: { - configureServer(server) { - viteServer = server; + { + name: 'vidstack-post', + enforce: 'post', + transformInclude(id) { + return filter(id); }, - async handleHotUpdate({ file, read }) { - if (!filter(file)) return; - parse(file, await read()); + transform(code, id) { + const { elementsGraph, stylesGraph } = data, + elements = elementsGraph.get(id), + styles = stylesGraph.get(id); + + if (elements?.size || styles?.size) { + let chunkId = moduleToChunk.get(id)!; + + if (!chunkId) { + chunkId = `:vidstack/chunk-${chunkToFile.size}`; + chunkToFile.set(chunkId, id); + moduleToChunk.set(id, chunkId); + } + + return code.startsWith(`import "${chunkId}";`) ? code : `import "${chunkId}";\n` + code; + } + + return null; }, }, - }; + ]; }); const PARSE_MODE_NONE = 0, PARSE_MODE_ELEMENT = 1, PARSE_MODE_STYLE = 2; -function parseCode(code: string, elements: Set, styles: Set) { +function parse(code: string) { let mode = PARSE_MODE_NONE, char = '', - buffer = ''; + buffer = '', + elements = new Set(), + styles = new Set(); for (let i = 0; i < code.length; i++) { char = code[i]; @@ -273,9 +276,86 @@ function parseCode(code: string, elements: Set, styles: Set) { break; } } + + elements.delete('media-player'); + elements.delete('media-provider'); + + return { elements, styles }; +} + +function generateBundleImports({ + defaultAudioLayout = false, + defaultVideoLayout = false, + plyrLayout = false, +}) { + const styles: string[] = [], + imports: string[] = [], + hasDefaultLayout = defaultAudioLayout || defaultVideoLayout; + + if (!hasDefaultLayout) { + styles.push('import "vidstack/player/styles/base.css";'); + if (plyrLayout) { + styles.push('vidstack/player/styles/plyr/theme.css'); + imports.push('vidstack/player/layouts/plyr'); + } + } else { + styles.push(`import "vidstack/player/styles/default/theme.css";`); + + if (plyrLayout) { + styles.push(`vidstack/player/styles/plyr/theme.css`); + } + + if (defaultAudioLayout) { + styles.push(`import "vidstack/player/styles/default/layouts/audio.css";`); + } + + if (defaultVideoLayout) { + styles.push(`import "vidstack/player/styles/default/layouts/video.css";`); + } + + imports.push(`import "vidstack/player/layouts${!plyrLayout ? '/default' : ''}";`); + imports.push('import "vidstack/player/ui";'); + } + + return [...styles, 'import "vidstack/player";', ...imports].join('\n'); +} + +function generateElementImports( + elements: Set = new Set(), + styles: Set = new Set(), +) { + const imports: string[] = [], + elementClasses = new Set(); + + for (const style of styles) imports.push(`import "${style}";`); + + for (const tagName of elements) { + if (!elements.has(tagName)) continue; + + const className = elementsManifest[tagName]; + + if (className) { + elementClasses.add(className); + } else { + console.warn(`[vidstack]: unknown media element was found \`${tagName}\``); + } + } + + if (elementClasses.size === 0) return imports.join('\n'); + + const elementImports = ['defineCustomElement', ...elementClasses]; + imports.push(`import {\n${elementImports.join(',\n')}\n} from "vidstack/elements";`); + + const definitions = [...elementClasses].map((name) => `defineCustomElement(${name});`); + + return imports.join('\n') + '\n\n' + definitions.join('\n'); +} + +function diff(a: Set | undefined, b: Set | undefined, key: string) { + return a?.has(key) !== b?.has(key); } -function isSetsEqual(a?: Set, b?: Set): boolean { +function isSetsEqual(a: Set | undefined, b: Set | undefined): boolean { return ( (!a?.size && !b?.size) || (!!a && !!b && a.size === b.size && [...a].every((value) => b.has(value))) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a97c0e4ad..6ca857e9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,7 +138,7 @@ importers: specifier: ^1.0.1 version: 1.0.3 unplugin: - specifier: ^1.6.0 + specifier: ^1.10.1 version: 1.10.1 devDependencies: '@floating-ui/dom':