diff --git a/deno.json b/deno.json index b7e437d..4c76c20 100644 --- a/deno.json +++ b/deno.json @@ -35,7 +35,7 @@ "$std/": "https://deno.land/std@0.208.0/", "univoq": "https://deno.land/x/univoq@0.1.0/mod.ts", "@univoq/": "https://deno.land/x/univoq@0.1.0/", - "lightningcss": "npm:lightningcss@1.22.1" + "css_bundler": "../../../github.com/JOTSR/fresh_css_bundler/plugin.ts" }, "compilerOptions": { "jsx": "react-jsx", diff --git a/fresh.config.ts b/fresh.config.ts index 0f059ef..88cc126 100644 --- a/fresh.config.ts +++ b/fresh.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from '$fresh/server.ts' -import { cssBundler } from './plugins/css_bundler/plugin.ts' +import { cssBundler } from 'css_bundler' export default defineConfig({ plugins: [ diff --git a/plugins/css_bundler/plugin.ts b/plugins/css_bundler/plugin.ts deleted file mode 100644 index c9c095a..0000000 --- a/plugins/css_bundler/plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Plugin } from '$fresh/server.ts' -import { fromFileUrl } from '$std/path/mod.ts' -import { bundleCss } from './src/bundler.ts' -import { Logger } from './src/helpers.ts' -import { cssHandler } from './src/middleware.ts' - -export function cssBundler( - sourceDir: string, - { pattern = /main.css/, logLevel }: { - pattern?: RegExp - logLevel?: 'disabled' | 'info' | 'error' - }, -): Plugin { - const logger = new Logger({ - logLevel: logLevel === 'info' ? 2 : logLevel === 'error' ? 1 : 0, - }) - - return { - name: 'css_bundler', - middlewares: [{ - middleware: { handler: cssHandler(sourceDir, logger) }, - path: '/', - }], - async buildStart(config) { - //Get fresh build directory - const { outDir } = config.build - const tasks: Promise[] = [] - - //Get all source stylesheets - for await (const entry of Deno.readDir(fromFileUrl(sourceDir))) { - if (entry.isFile && entry.name.match(pattern)) { - tasks.push( - bundleCss(sourceDir, outDir, entry.name, config.dev, logger), - ) - } - } - - //Await for all bundle to finish - await Promise.all(tasks) - }, - } -} diff --git a/plugins/css_bundler/src/builder.ts b/plugins/css_bundler/src/builder.ts deleted file mode 100644 index 1f54d3d..0000000 --- a/plugins/css_bundler/src/builder.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { join, parse, resolve, toFileUrl } from '$std/path/mod.ts' -import { bundleAsync } from 'lightningcss' -import { cssImports, cssUrls, Logger, uInt8ArrayConcat } from './helpers.ts' - -export async function builder( - { filename, dev, assetDir, remote, logger }: { - filename: string - dev: boolean - assetDir: string - remote?: string - logger: Logger - }, -) { - const { code, map } = await bundleAsync({ - filename, - minify: true, - sourceMap: dev, - resolver: { - async read(pathOrUrl) { - if (pathOrUrl.startsWith('https%3A%2F%2F')) { - pathOrUrl = decodeURIComponent(pathOrUrl) - } - if (pathOrUrl.startsWith('https://')) { - //use cache for remote - if (cssImports.has(pathOrUrl)) { - logger.info('using cache for', `${pathOrUrl}`) - const filepath = cssImports.get(pathOrUrl)! - return Deno.readTextFile(filepath) - } - - //update cache for new remote - logger.info('fetching and caching', `${pathOrUrl}`) - const response = await fetch(pathOrUrl) - const file = await response.arrayBuffer() - const filename = encodeURIComponent(pathOrUrl.toString()) - - const filepath = join(assetDir, `${filename}.css`) - await Deno.writeFile(filepath, new Uint8Array(file)) - - setTimeout(() => {}, 3_000) - const { code, map } = await builder({ - filename: filepath, - dev, - assetDir, - remote: pathOrUrl, - logger, - }) - - if (map) { - const sourceMappingURL = new TextEncoder().encode( - `\n/*# sourceMappingURL=${filepath}.map.css */`, - ) - await Deno.writeFile( - filepath, - uInt8ArrayConcat(code, sourceMappingURL), - ) - await Deno.writeFile(`${filepath}.map.css`, map) - } else { - await Deno.writeFile(filepath, code) - } - - cssImports.set(pathOrUrl, filepath) - - return new TextDecoder().decode(file) - } - return Deno.readTextFile(pathOrUrl) - }, - resolve(specifier, from) { - if (remote) { - const url = new URL(specifier, remote) - logger.info('resolve remote imports', url.toString()) - - return url.toString() - } - - //resolve local files normally - if (!specifier.startsWith('https://') && !from.startsWith('https://')) { - logger.info('resolve local file', specifier) - return resolve(parse(from).dir, specifier) - } - - //construct asset url - const baseUrl = from.startsWith('https://') ? from : toFileUrl(from) - const url = new URL(specifier, baseUrl) - logger.info('resolve remote file', url.toString()) - - return url.toString() - }, - }, - visitor: { - Url({ loc, url }) { - if (remote && !url.startsWith('data:')) { - cache(url, remote, assetDir) - } - return { loc, url } - }, - }, - }) - - return { code, map } -} - -async function cache(url: string, base: string, assetDir: string) { - const filepath = join(assetDir, url.split('?')[0]) - const fullUrl = new URL(url, base) - - if (cssUrls.has(fullUrl.pathname)) { - return - } - - cssUrls.set(fullUrl.pathname, filepath) - const response = await fetch(fullUrl) - const file = await response.arrayBuffer() - await Deno.writeFile(filepath, new Uint8Array(file)) -} diff --git a/plugins/css_bundler/src/bundler.ts b/plugins/css_bundler/src/bundler.ts deleted file mode 100644 index 6e0378c..0000000 --- a/plugins/css_bundler/src/bundler.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { fromFileUrl, join } from '$std/path/mod.ts' -import { builder } from './builder.ts' -import { cssImports, Logger, uInt8ArrayConcat } from './helpers.ts' - -export async function bundleCss( - sourceDir: string, - assetDir: string, - pathname: string, - dev: boolean, - logger: Logger, -) { - const filename = fromFileUrl(join(sourceDir, pathname)) - logger.info('bundling', filename) - - //prevent Deno from exiting before bundle - setTimeout(() => {}, 3_000) - - try { - const { code, map } = await builder({ filename, dev, assetDir, logger }) - - if (map) { - //# sourceMappingURL=fresh_dev_client.js.map - const sourceMappingURL = new TextEncoder().encode( - `\n/*# sourceMappingURL=${pathname.replace('.css', '.map.css')} */`, - ) - await Deno.writeFile( - join(assetDir, pathname), - uInt8ArrayConcat(code, sourceMappingURL), - ) - - await Deno.writeFile( - join(assetDir, pathname.replace('.css', '.map.css')), - map, - ) - } else { - await Deno.writeFile(join(assetDir, pathname), code) - } - } catch (error) { - logger.error('error during bundle, cleaning cache', error) - cssImports.clear() - } -} diff --git a/plugins/css_bundler/src/helpers.ts b/plugins/css_bundler/src/helpers.ts deleted file mode 100644 index 6c6a765..0000000 --- a/plugins/css_bundler/src/helpers.ts +++ /dev/null @@ -1,45 +0,0 @@ -export class Logger { - readonly #name = 'bundle_css' - #logLevel: 0 | 1 | 2 - - constructor({ logLevel }: { logLevel: 0 | 1 | 2 }) { - this.#logLevel = logLevel - } - - info(message: string, path?: string) { - if (this.#logLevel < 2) return - - console.log( - `%c[${this.#name}]%c ${message} %c${path ?? ''}`, - 'color: blue; font-weight: bold', - '', - 'color: green', - ) - } - - error(message: string, error?: Error) { - if (this.#logLevel < 1) return - - console.error( - `%c[${this.#name}]%c ${message} %c${error?.toString() ?? ''}`, - 'color: red; font-weight: bold', - '', - 'color: yellow', - ) - } -} - -export async function hashFile(file: ArrayBuffer): Promise { - const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', file)) - return [...hash].map((value) => value.toString(16).padStart(2, '0')).join('') -} - -export const cssImports: Map = new Map() -export const cssUrls: Map = new Map() - -export function uInt8ArrayConcat(a: Uint8Array, b: Uint8Array) { - const dest = new Uint8Array(a.length + b.length) - dest.set(a) - dest.set(b, a.length) - return dest -} diff --git a/plugins/css_bundler/src/middleware.ts b/plugins/css_bundler/src/middleware.ts deleted file mode 100644 index d3302ec..0000000 --- a/plugins/css_bundler/src/middleware.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MiddlewareHandler } from '$fresh/server.ts' -import { contentType } from '$fresh/src/server/deps.ts' -import { ensureDir } from '$std/fs/ensure_dir.ts' -import { join, parse } from '$std/path/mod.ts' -import { bundleCss } from './bundler.ts' -import { Logger } from './helpers.ts' - -export function cssHandler( - sourceDir: string, - logger: Logger, -): MiddlewareHandler { - return async (_, ctx) => { - const assetDir = join(ctx.config.build.outDir, '/static') - await ensureDir(assetDir) - - if (ctx.config.dev) { - if ( - ctx.url.pathname.startsWith('/') && ctx.url.pathname.endsWith('.css') - ) { - const filename = join(assetDir, ctx.url.pathname) - - if (ctx.url.pathname.endsWith('.map.css')) { - const file = await Deno.readFile(filename) - return new Response(file, { - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }) - } - - await bundleCss( - sourceDir, - assetDir, - ctx.url.pathname, - ctx.config.dev, - logger, - ) - const file = await Deno.readFile(filename) - return new Response(file, { - headers: { - 'Content-Type': 'text/css; charset=utf-8', - }, - }) - } - } - - const resp = await ctx.next() - - if (resp.status === 404) { - try { - const file = await Deno.readFile(join(assetDir, ctx.url.pathname)) - const { ext } = parse(ctx.url.pathname) - - return new Response(file, { - headers: { - 'Content-Type': contentType(ext) ?? 'text/plain; charset=utf-8', - }, - }) - } catch { - return resp - } - } - - //anyway - return resp - } -}