import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import { defineConfig, normalizePath, type Plugin } from 'vite'; import { mirrorTemplatesToSrc } from './scripts/shared'; const htmlCdnDir = resolve(__dirname, '../../packages/html/cdn'); const htmlCdnI18nRegistry = normalizePath(resolve(htmlCdnDir, 'i18n.dev.js')); const cdnSandboxMainSrc = resolve(__dirname, 'src/cdn/main.ts'); const cdnSandboxMainTemplate = resolve(__dirname, 'templates/cdn/main.ts'); const jsdelivrCdnI18nPattern = /https:\/\/cdn\.jsdelivr\.net\/npm\/@videojs\/html@[^"']+\/cdn\/i18n\.js/g; /** True when this import should share the single CDN i18n registry module instance. */ function resolvesToCdnI18nRegistry(source: string, importer?: string): boolean { if ( source === '@videojs/html/cdn/i18n' || source === htmlCdnI18nRegistry || source.endsWith('/packages/html/cdn/i18n.dev.js') || source.endsWith('/packages/html/src/cdn/i18n.ts') ) { return true; } const isRelativeI18nChunk = source === './i18n.dev.js' || source === '../i18n.dev.js' || source.endsWith('/i18n.dev.js'); if (isRelativeI18nChunk && importer?.includes('/packages/html/cdn/')) { return true; } if (source === '@videojs/core/i18n' && importer?.includes('/packages/html/cdn/')) { return true; } return false; } function resolveHtmlCdnDevEntry(subpath: string): string | null { const devPath = resolve(htmlCdnDir, `${subpath}.dev.js`); return existsSync(devPath) ? devPath : null; } /** Resolve CDN sandbox imports to the prebuilt development entries and registry. */ function cdnSandboxI18nPlugin(): Plugin { return { name: 'cdn-sandbox-i18n', enforce: 'pre', resolveId: { filter: { id: /^@videojs\/(?:core\/i18n|html\/cdn(?:\/.*)?)$|(?:^|\/)i18n\.dev\.js$|\/packages\/html\/src\/cdn\/i18n\.ts|\/apps\/sandbox\/src\/cdn\/main\.ts$/, }, handler(source, importer) { if (source === cdnSandboxMainSrc && existsSync(cdnSandboxMainTemplate)) { return cdnSandboxMainTemplate; } if (resolvesToCdnI18nRegistry(source, importer)) { return htmlCdnI18nRegistry; } const cdnEntryMatch = source.match(/^@videojs\/html\/cdn\/(.+)$/); if (cdnEntryMatch && cdnEntryMatch[1] !== 'i18n') { const devEntry = resolveHtmlCdnDevEntry(cdnEntryMatch[1]); if (devEntry) return devEntry; } return null; }, }, transform: { filter: { id: /\/packages\/html\/cdn\//, code: /cdn\.jsdelivr\.net\/npm\/@videojs\/html/, }, handler(code) { return { code: code.replace(jsdelivrCdnI18nPattern, htmlCdnI18nRegistry), map: null, }; }, }, }; } /** Keep gitignored `src/` aligned with `templates/` so CDN i18n markup is never stale. */ function sandboxTemplateSyncPlugin(): Plugin { return { name: 'sandbox-template-sync', async buildStart() { await mirrorTemplatesToSrc(); }, }; } /** Discover sandbox entries by finding subdirectories of src/ that contain an index.html. */ function getSandboxEntries(): Record { const srcDir = resolve(__dirname, 'src'); const entries: Record = {}; for (const entry of readdirSync(srcDir)) { const dir = resolve(srcDir, entry); const indexHtml = resolve(dir, 'index.html'); if (statSync(dir).isDirectory() && existsSync(indexHtml)) { entries[entry] = indexHtml; } } return entries; } function serveAppShell(): Plugin { const shellSrc = resolve(__dirname, 'app/index.html'); const shellEntry = normalizePath(resolve(__dirname, 'app/main.tsx')); const shellDest = resolve(__dirname, 'src/index.html'); return { name: 'serve-app-shell', buildStart() { const html = readFileSync(shellSrc, 'utf-8').replace(/(src|href)="\.\/([^"]+)"/g, '$1="../app/$2"'); writeFileSync(shellDest, html); }, closeBundle() { rmSync(shellDest, { force: true }); }, configureServer(server) { server.middlewares.use(async (req, res, next) => { const requestUrl = req.originalUrl ?? req.url ?? '/'; const { pathname } = new URL(requestUrl, 'http://localhost'); if (pathname === '/' || pathname === '/index.html') { const html = readFileSync(shellSrc, 'utf-8').replace('./main.tsx', `/@fs/${shellEntry}`); const transformed = await server.transformIndexHtml('/app/index.html', html, requestUrl); res.setHeader('Content-Type', 'text/html'); res.end(transformed); return; } next(); }); }, }; } export default defineConfig({ root: 'src', appType: 'mpa', plugins: [sandboxTemplateSyncPlugin(), cdnSandboxI18nPlugin(), tailwindcss(), react(), serveAppShell()], resolve: { alias: { '@app': resolve(__dirname, 'app'), '@videojs/html/cdn/i18n': htmlCdnI18nRegistry, ...(existsSync(cdnSandboxMainTemplate) ? { [cdnSandboxMainSrc]: cdnSandboxMainTemplate } : {}), }, conditions: ['development', 'import', 'module', 'browser', 'default'], dedupe: ['react', 'react-dom'], }, optimizeDeps: { include: ['react', 'react-dom'], exclude: [ '@videojs/core', '@videojs/html', '@videojs/icons', '@videojs/react', '@videojs/spf', '@videojs/store', '@videojs/utils', ], }, build: { outDir: resolve(__dirname, 'dist'), emptyOutDir: true, rolldownOptions: { // This resolver substitutes the prebuilt CDN graph, whose downstream // processing time is attributed to the plugin rather than its fast hooks. checks: { pluginTimings: false, }, input: { main: resolve(__dirname, 'src/index.html'), ...getSandboxEntries(), }, onwarn(warning, defaultHandler) { if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return; defaultHandler(warning); }, }, }, });