Skip to content

Instantly share code, notes, and snippets.

@obillekyle
Created June 21, 2024 09:34
Show Gist options
  • Save obillekyle/bb1ef53bc3627ddde8a9b8a13fea2822 to your computer and use it in GitHub Desktop.
Save obillekyle/bb1ef53bc3627ddde8a9b8a13fea2822 to your computer and use it in GitHub Desktop.
// for vue libs with
// - format: ['es']
// - preserveModules: true
// can't guarantee to work, tinker to your needs
// example lib: https://github.com/obillekyle/components
import path from 'node:path'
import crypto from 'node:crypto'
import fs from 'node:fs'
import { transform } from 'esbuild'
import { Plugin, ResolvedConfig, normalizePath } from 'vite'
let packSize = 0
let fileSize = 0
const css: Record<string, string> = {}
function generateHash(str: string) {
return crypto.createHash('md5').update(str).digest('hex').slice(0, 8)
}
function attachCSSFile(additionalCSS: string, prefix: string) {
return `
function injectCSS(css, hash) {
const style =
document.querySelector('style#${prefix}-' + hash)
?? document.createElement('style');
style.id = '${prefix}-' + hash;
style.textContent = css;
document.head.contains(style) || document.head.appendChild(style);
}
const helper = function() {
injectCSS(${JSON.stringify(additionalCSS)}, 'global');
return injectCSS;
}
export default helper();
`
}
async function deleteCSSFiles(dir: string, out: string, ignore: string[] = []) {
try {
const files = await fs.promises.readdir(dir)
for (const file of files) {
if (
ignore.includes(
path.relative(path.resolve(__dirname, out), path.join(dir, file))
)
)
continue
const filePath = path.join(dir, file)
const stats = await fs.promises.stat(filePath)
if (stats.isDirectory()) {
await deleteCSSFiles(filePath, out, ignore)
} else if (file.endsWith('.css')) {
fileSize += stats.size
await fs.promises.unlink(filePath)
} else {
packSize += stats.size
}
}
} catch (err) {
console.error(`Failed to delete CSS file: ${dir}`, err)
}
}
type ASOptions = {
prefix?: string
cleanIgnore?: string[]
cleanCSS?: boolean
}
function attachStyles({
prefix = 'css',
cleanIgnore = [],
cleanCSS = true
}: ASOptions = {}): Plugin {
let config: ResolvedConfig
const name: string = 'attach-styles'
async function transformCSS(code: string) {
return (
await transform(code, {
minify: config.build.minify && config.build.minify !== 'terser',
minifyWhitespace: true,
loader: 'css'
})
).code
}
return {
name,
apply: 'build',
configResolved: (_config) => {
config = _config
},
async transform(code, id) {
const isCSS = (p: string) => /\.(scss|sass|css|styl|stylus|less)$/.test(p)
if (!isCSS(id)) return
const relative: string = path.relative(path.resolve(__dirname, 'src'), id)
const entry = relative.split('?')[0]
const key = entry.endsWith('.vue')
? normalizePath(entry).replace(/\\/g, '/') + '.js'
: 'globalCss'
const cssString = await transformCSS(code)
css[key] = css[key] ? `${css[key]}\n${cssString}` : cssString
},
renderChunk(code, { name, fileName }) {
const cssObj = css[fileName]
if (cssObj) {
delete css[fileName]
const root = normalizePath(
path.relative(
path.resolve(__dirname, 'src', path.dirname(name)),
path.resolve(__dirname, 'src')
)
)
return {
code: `
import injectCSS from '${root}/attach-styles.js'
injectCSS(${JSON.stringify(cssObj)}, ${JSON.stringify(generateHash(name))});
${code}
`,
map: { mappings: '' }
}
}
if (name === 'index') {
return {
code: `
import './attach-styles.js'
${code}
`,
map: { mappings: '' }
}
}
},
async writeBundle() {
const unattachedCSS = await transformCSS(Object.values(css).join('\n'))
const script = attachCSSFile(unattachedCSS || '', prefix)
await fs.promises.writeFile(
path.resolve(__dirname, config.build.outDir, 'attach-styles.js'),
(await transform(script, { loader: 'js' })).code
)
console.log(``)
console.log(`\x1b[36m[vite:${name}]\x1b[32m Created attach-styles.js`)
console.log(`\x1b[36m[vite:${name}]\x1b[32m Injecting CSS...\n`)
Object.keys(css).forEach((key) => {
delete css[key]
})
if (cleanCSS) {
const outDir = config.build.outDir
const start = Date.now()
await deleteCSSFiles(outDir, outDir, cleanIgnore)
const end = Date.now()
const elapsed = end - start
const size = (fileSize / 1024).toFixed(2)
const pack = (packSize / 1024).toFixed(2)
console.log(`\x1b[36m[vite:${name}]\x1b[32m Clean CSS files...`)
console.log(
`\x1b[36m[vite:${name}]`,
`\x1b[32mCleaned CSS files in ${elapsed}ms.`
)
console.log(
`\x1b[36m[vite:${name}]`,
`\x1b[32mPack: \x1b[90m\x1b[1m${pack} kB\x1b[0m\x1b[90m │`,
`\x1b[32mSaved: \x1b[90m\x1b[1m${size} kB\x1b[0m\n`
)
}
}
}
}
export default attachStyles
@obillekyle
Copy link
Author

Example lib: here
Site that uses the built lib w/ plugin: okyle.xyz

Attaches styles from <style> or <style scoped> tags declared in your vue component.
Files like .css|.sass|.scss will go to attach-styles.js file created after build.
You might need to install esbuild as a devDependency to your project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment