import { asset } from 'fresh/runtime' import { App } from 'fresh' import { build, type Plugin } from 'esbuild' import { extname, fromFileUrl, toFileUrl } from '@std/path' import { exists } from '@std/fs/exists' import { ensureDir } from '@std/fs' import { contentType } from '@std/media-types' import { styles } from './SmartStylesheetCommon.tsx' const bundledFiles = new Set() const baseRoute = '__smart_css__' /** * Dynamic components and islands stylesheet. * * @param options - `pathname` set the pathname of the current route, `baseRoute` overrides default plugin baseRoute. * * @example * ```ts * import type { PageProps } from 'fresh' * import { SmartStylesheet } from './SmartStylesheet.tsx' * * export default function App({ Component, url }: PageProps) { * return ( * * * * * My app * * * * * * * * ) * } * ``` */ export function SmartStylesheet( options: { pathname: string; baseRoute?: string }, ) { styles.clear() return ( ) } export function StaticStylesheet( options: { href: string }, ) { return } export function smartStylesheetPlugin( app: App, options: { baseRoute?: string } = {}, ) { options.baseRoute ??= baseRoute bundleCss(['./src/stylesheets/main.css'], app, options.baseRoute) //resolve dynamic styles imports app.get(`/${options.baseRoute}/:path+`, async (ctx) => { const { path } = ctx.params if (path.startsWith('__r__')) { const css = styles.values().map(({ url, layer, scope }) => { const href = asset(`/${options.baseRoute}${url}`) const scopeParam = scope ? `&__scope=${scope}` : '' const cssImport = `@import url("${href}${scopeParam}")` const cssLayer = layer ? ` layer(${layer})` : '' return `${cssImport}${cssLayer};` }).toArray() return new Response( `/* auto generated using jsr:@jotsr/smart-stylesheet */\n${ css.join('\n') }`, { headers: { 'Content-Type': 'text/css; charset=utf-8', }, }, ) } if (path.startsWith('components') || path.startsWith('islands')) { const scope = ctx.url.searchParams.get('__scope') try { const css = await Deno.readTextFile(`./${path}`) const file = scope ? `@scope (.${scope}) {\n\n${css}\n}` : css return new Response(file, { headers: { 'Content-Type': 'text/css; charset=utf-8', }, }) } catch (error) { if (error instanceof Deno.errors.NotFound) { return new Response(null, { status: 404 }) } throw error } } try { const file = await Deno.readFile( `${app.config.build.outDir}/static/${options.baseRoute}/${path}`, ) const mime = contentType(extname(path)) ?? 'application/octet-stream' return new Response(file, { headers: { 'Content-Type': mime }, }) } catch { return new Response(null, { status: 400, statusText: 'Bad Request - Invalid url', }) } }) } async function bundleCss( files: string[], app: App, baseRoute?: string, ) { const cssPlugin: Plugin = { name: 'smart-stylesheet-bundler', setup(build) { build.onResolve({ filter: /.*/ }, async (args) => { const importer = args.importer.length ? args.importer : `${app.config.root}/` const importerUrl = importer.match(/\w+:\/\/.+/) ? new URL(importer) : toFileUrl(importer) const url = new URL(args.path, importerUrl) const external = url.protocol === 'file:' && !(await exists(url)) const local = url.protocol === 'file:' if (!external) bundledFiles.add(url.href) const watchFiles: string[] = [] if (local && !external) watchFiles.push(fromFileUrl(url.href)) return { path: external ? args.path : url.href, namespace: external ? 'external' : 'internal', external, watchFiles, } }) build.onLoad({ filter: /.*/, namespace: 'internal' }, async (args) => { const url = new URL(args.path) let isCss = url.pathname.match(/.css$/) !== null const buffer = url.protocol === 'file:' ? await Deno.readFile(url) : await fetch(url).then((r) => { if (r.headers.get('Content-type')?.includes('text/css')) { isCss = true } return r.bytes() }) if (isCss) { return { contents: new TextDecoder().decode(buffer), loader: 'css', resolveDir: url.href, } } return { contents: buffer, loader: url.protocol === 'data:' ? 'dataurl' : 'file', } }) }, } const outdir = baseRoute ? `${app.config.build.outDir}/static/${baseRoute}` : `${app.config.build.outDir}/static` await ensureDir(outdir) const result = await build({ entryPoints: files, bundle: true, minify: true, sourcemap: app.config.mode === 'development' ? 'inline' : false, plugins: [cssPlugin], outdir, write: false, }) for (const file of result.outputFiles) { const path = new URL(file.path).pathname Deno.writeFile(path, file.contents) } }