import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs' import { basename, dirname, join, resolve } from 'pathe' import type { Plugin } from 'vite' // @ts-expect-error Invalid types in beastcss import _BeastCSS from 'beastcss' import { template } from 'lodash-es' import { genObjectFromRawEntries } from 'knitwork' import htmlMinifier from 'html-minifier' import { globby } from 'globby' import { camelCase } from 'scule' import genericMessages from '../templates/messages.json' const BeastCSS = (_BeastCSS.default || _BeastCSS) as typeof import('beastcss') const r = (...path: string[]) => resolve(join(__dirname, '..', ...path)) const replaceAll = (input: string, search: string | RegExp, replace: string) => input.split(search).join(replace) export const RenderPlugin = () => { return { name: 'render', enforce: 'post', async writeBundle () { const distDir = r('dist') const beast = new BeastCSS({ path: distDir }) const htmlFiles = await globby(r('dist/templates/**/*.html')) const templateExports = [] for (const fileName of htmlFiles) { // Infer template name const templateName = basename(dirname(fileName)) // eslint-disable-next-line no-console console.log('Processing', templateName) // Read source template let html = readFileSync(fileName, 'utf-8') const isCompleteHTML = html.includes('') if (html.includes(']*>/g, '') // Inline SVGs const svgSources = Array.from(html.matchAll(/src="([^"]+)"|url([^)]+)/g)) .map(m => m[1]) .filter(src => src?.match(/\.svg$/)) for (const src of svgSources) { const svg = readFileSync(r('dist', src), 'utf-8') const base64Source = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` html = replaceAll(html, src, base64Source) } // Inline our scripts const scriptSources = Array.from(html.matchAll(/]*src="(.*)"[^>]*>[\s\S]*?<\/script>/g)) .filter(([_block, src]) => src?.match(/^\/.*\.js$/)) for (const [scriptBlock, src] of scriptSources) { let contents = readFileSync(r('dist', src), 'utf-8') contents = replaceAll(contents, '/* empty css */', '').trim() html = html.replace(scriptBlock, contents.length ? `` : '') } // Minify HTML html = htmlMinifier.minify(html, { collapseWhitespace: true }) if (!isCompleteHTML) { html = html.replace('', '') html = html.replace('', '') } // Load messages const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8')) // Serialize into a js function const jsCode = [ `const _messages = ${JSON.stringify({ ...genericMessages, ...messages })}`, `const _render = ${template(html, { variable: '__var__', interpolate: /{{{?([\s\S]+?)}?}}/g }).toString().replace('__var__', '{ messages }')}`, 'const _template = (messages) => _render({ messages: { ..._messages, ...messages } })', ].join('\n').trim() const templateContent = html .match(/([\s\S]*)<\/body>/)?.[0] .replace(/(?<=<|<\/)body/g, 'div') .replace(/messages\./g, '') .replace(/]*>([\s\S]*?)<\/script>/g, '') .replace(/]*)>([\s\S]*)<\/a>/g, '\n$3\n') .replace(/<([^>]+) ([a-z]+)="([^"]*)({{\s*(\w+?)\s*}})([^"]*)"([^>]*)>/g, '<$1 :$2="`$3${$5}$6`"$7>') .replace(/>{{\s*(\w+?)\s*}}<\/[\w-]*>/g, ' v-text="$1" />') .replace(/>{{{\s*(\w+?)\s*}}}<\/[\w-]*>/g, ' v-html="$1" />') // We are not matching ', '', '', ].filter(Boolean).join('\n').trim() // Generate types const types = [ `export type DefaultMessages = Record<${Object.keys(messages).map(a => `"${a}"`).join(' | ') || 'string'}, string | boolean | number >`, 'declare const template: (data: Partial) => string', 'export { template }', ].join('\n') // Register exports templateExports.push({ exportName: camelCase(templateName), templateName, types, }) // Write new template writeFileSync(fileName.replace('/index.html', '.js'), `${jsCode}\nexport const template = _template`) writeFileSync(fileName.replace('/index.html', '.vue'), vueCode) writeFileSync(fileName.replace('/index.html', '.d.ts'), `${types}`) // Remove original html file unlinkSync(fileName) rmdirSync(dirname(fileName)) } // Write an index file with named exports for each template const contents = templateExports.map(exp => `export { template as ${exp.exportName} } from './templates/${exp.templateName}.js'`).join('\n') writeFileSync(r('dist/index.js'), contents, 'utf8') writeFileSync(r('dist/index.d.ts'), replaceAll(contents, /\.js/g, ''), 'utf8') }, } }