import { readFile } from 'fs/promises'; import { basename, dirname } from 'path'; import { LibraryFormats, Plugin, ResolvedConfig } from 'vite'; export interface UserScriptOptions { entry: string; format: LibraryFormats; name?: string; port?: number; cdn?: string; } export function _userscript(options: UserScriptOptions): Plugin { let config: ResolvedConfig; return { name: 'userscript', config() { const name = options.name ?? basename(dirname(options.entry)); return { build: { lib: { name: name, formats: [ options.format ], entry: { [name]: options.entry } }, rollupOptions: { output: { entryFileNames: '[name].user.js' } } } }; }, configResolved(resolvedConfig) { config = resolvedConfig; }, async generateBundle(outputOptions, bundle) { for (const chunk of Object.values(bundle)) { if (chunk.type !== 'chunk') continue; if (!chunk.isEntry) continue; if (!chunk.facadeModuleId) continue; const code = await readFile(chunk.facadeModuleId, { encoding: 'utf8' }); let header = ''; for (const line of code.split('\n')) { if (!line.startsWith('//')) break; header += line + '\n'; } let url = ''; if (config.mode === 'production') { if (options.cdn) url = `${options.cdn}${chunk.name}.user.js`; } else { if (options.port) url = `http://localhost:${options.port}/${chunk.name}.user.js`; } if (url) { header = header.replaceAll( '// ==/UserScript==', `// @downloadURL ${url}\n` + `// @updateURL ${url}\n` + '// ==/UserScript==' ); } chunk.code = header + '\n' + chunk.code; } } }; }