import * as tailwindcss from 'tailwindcss'
import * as assets from './assets'
import { Instrumentation } from './instrumentation'
const STYLE_TYPE = 'text/tailwindcss'
let compiler: Awaited<ReturnType<typeof tailwindcss.compile>>
let classes = new Set<string>()
let lastCss = ''
let sheet = document.createElement('style')
let buildQueue = Promise.resolve()
let nextBuildId = 1
let I = new Instrumentation()
async function createCompiler() {
I.start(`Create compiler`)
I.start('Reading Stylesheets')
let stylesheets: Iterable<HTMLStyleElement> = document.querySelectorAll(
`style[type="${STYLE_TYPE}"]`,
)
let css = ''
for (let sheet of stylesheets) {
observeSheet(sheet)
css += sheet.textContent + '\n'
}
if (!css.includes('@import')) {
css = `@import "tailwindcss";${css}`
}
I.end('Reading Stylesheets', {
size: css.length,
changed: lastCss !== css,
})
if (lastCss === css) return
lastCss = css
I.start('Compile CSS')
try {
compiler = await tailwindcss.compile(css, {
base: '/',
loadStylesheet,
loadModule,
})
} finally {
I.end('Compile CSS')
I.end(`Create compiler`)
}
classes.clear()
}
async function loadStylesheet(id: string, base: string) {
function load() {
if (id === 'tailwindcss') {
return {
path: 'virtual:tailwindcss/index.css',
base,
content: assets.css.index,
}
} else if (
id === 'tailwindcss/preflight' ||
id === 'tailwindcss/preflight.css' ||
id === './preflight.css'
) {
return {
path: 'virtual:tailwindcss/preflight.css',
base,
content: assets.css.preflight,
}
} else if (
id === 'tailwindcss/theme' ||
id === 'tailwindcss/theme.css' ||
id === './theme.css'
) {
return {
path: 'virtual:tailwindcss/theme.css',
base,
content: assets.css.theme,
}
} else if (
id === 'tailwindcss/utilities' ||
id === 'tailwindcss/utilities.css' ||
id === './utilities.css'
) {
return {
path: 'virtual:tailwindcss/utilities.css',
base,
content: assets.css.utilities,
}
}
throw new Error(`The browser build does not support @import for "${id}"`)
}
try {
let sheet = load()
I.hit(`Loaded stylesheet`, {
id,
base,
size: sheet.content.length,
})
return sheet
} catch (err) {
I.hit(`Failed to load stylesheet`, {
id,
base,
error: (err as Error).message ?? err,
})
throw err
}
}
async function loadModule(): Promise<never> {
throw new Error(`The browser build does not support plugins or config files.`)
}
async function build(kind: 'full' | 'incremental') {
if (!compiler) return
let newClasses = new Set<string>()
I.start(`Collect classes`)
for (let element of document.querySelectorAll('[class]')) {
for (let c of element.classList) {
if (classes.has(c)) continue
classes.add(c)
newClasses.add(c)
}
}
I.end(`Collect classes`, {
count: newClasses.size,
})
if (newClasses.size === 0 && kind === 'incremental') return
I.start(`Build utilities`)
sheet.textContent = compiler.build(Array.from(newClasses))
I.end(`Build utilities`)
}
function rebuild(kind: 'full' | 'incremental') {
async function run() {
if (!compiler && kind !== 'full') {
return
}
let buildId = nextBuildId++
I.start(`Build #${buildId} (${kind})`)
if (kind === 'full') {
await createCompiler()
}
I.start(`Build`)
await build(kind)
I.end(`Build`)
I.end(`Build #${buildId} (${kind})`)
}
buildQueue = buildQueue.then(run).catch((err) => I.error(err))
}
let styleObserver = new MutationObserver(() => rebuild('full'))
function observeSheet(sheet: HTMLStyleElement) {
styleObserver.observe(sheet, {
attributes: true,
attributeFilter: ['type'],
characterData: true,
subtree: true,
childList: true,
})
}
new MutationObserver((records) => {
let full = 0
let incremental = 0
for (let record of records) {
for (let node of record.addedNodes as Iterable<HTMLElement>) {
if (node.nodeType !== Node.ELEMENT_NODE) continue
if (node.tagName !== 'STYLE') continue
if (node.getAttribute('type') !== STYLE_TYPE) continue
observeSheet(node as HTMLStyleElement)
full++
}
for (let node of record.addedNodes) {
if (node.nodeType !== 1) continue
if (node === sheet) continue
incremental++
}
if (record.type === 'attributes') {
incremental++
}
}
if (full > 0) {
return rebuild('full')
} else if (incremental > 0) {
return rebuild('incremental')
}
}).observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
childList: true,
subtree: true,
})
rebuild('full')
document.head.append(sheet)