diff --git a/build.ts b/build.ts deleted file mode 100644 index a918d13..0000000 --- a/build.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { bundleAsync } from 'lightningcss' -import { ensureDir } from '$std/fs/mod.ts' -import { parse, resolve } from '$std/path/mod.ts' - -console.log('building styles starts') -await ensureDir('_fresh') - -try { - //prevent Deno from exiting before bundle - setTimeout(() => {}, 2_000) - - const cssImports = new Map() - - const { code, map } = await bundleAsync({ - filename: './src/stylesheets/main.css', - minify: true, - sourceMap: true, - resolver: { - read(path) { - return Deno.readTextFile(path) - }, - async resolve(specifier, from) { - //resolve local files normally - if (!specifier.startsWith('https://')) { - return resolve(parse(from).dir, specifier) - } - //use cache for remote - if (cssImports.has(specifier)) return cssImports.get(specifier) - - //update cache for new remote - const response = await fetch(specifier) - const file = await response.arrayBuffer() - const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', file)) - const filename = [...hash].map(value => value.toString(16).padStart(2, '0')).join('') - - const filepath = `_fresh/${filename}` - await Deno.writeFile(filepath, new Uint8Array(file)) - cssImports.set(specifier, filepath) - return filepath - }, - }, - }) - - await Deno.writeTextFile( - './static/dev/styles.css', - new TextDecoder().decode(code), - ) - await Deno.writeTextFile( - './static/dev/styles.map.css', - new TextDecoder().decode(map ?? new Uint8Array()), - ) -} catch (error) { - console.error(error) -} - -console.log('building styles finish') diff --git a/deno.json b/deno.json index 6924172..b7e437d 100644 --- a/deno.json +++ b/deno.json @@ -8,8 +8,7 @@ "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update .", - "assets:watch": "deno run -A --watch=src/ build.ts", - "assets": "deno run -A build.ts" + "prod": "deno task build && export PORT=80 && deno task preview" }, "fmt": { "singleQuote": true, diff --git a/fresh.config.ts b/fresh.config.ts index eb793c5..30940fe 100644 --- a/fresh.config.ts +++ b/fresh.config.ts @@ -1,3 +1,8 @@ import { defineConfig } from '$fresh/server.ts' +import { cssBundler } from './plugins/css_bundler/plugin.ts' -export default defineConfig({}) +export default defineConfig({ + plugins: [ + cssBundler(import.meta.resolve('./src/stylesheets')), + ], +}) diff --git a/plugins/css_bundler/plugin.ts b/plugins/css_bundler/plugin.ts new file mode 100644 index 0000000..0fba306 --- /dev/null +++ b/plugins/css_bundler/plugin.ts @@ -0,0 +1,29 @@ +import { Plugin } from '$fresh/server.ts' +import { fromFileUrl } from '$std/path/mod.ts' +import { bundleCss } from './src/bundler.ts' +import { cssHandler } from './src/middleware.ts' + +export function cssBundler(sourceDir: string, pattern = /main.css/): Plugin { + return { + name: 'css_bundler', + middlewares: [{ + middleware: { handler: cssHandler(sourceDir) }, + 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)) + } + } + + //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 new file mode 100644 index 0000000..077ab95 --- /dev/null +++ b/plugins/css_bundler/src/builder.ts @@ -0,0 +1,59 @@ +import { join, parse, resolve, toFileUrl } from '$std/path/mod.ts' +import { bundleAsync } from 'lightningcss' +import { cssImports, hashFile, Logger } from './helpers.ts' + +export async function builder( + { filename, dev, assetDir }: { + filename: string + dev: boolean + assetDir: string + }, +) { + const { code, map } = await bundleAsync({ + filename, + minify: true, + sourceMap: dev, + resolver: { + read(path) { + return Deno.readTextFile(path) + }, + async resolve(specifier, from) { + //resolve local files normally + if (!specifier.startsWith('https://') && !from.startsWith('https://')) { + Logger.info('resolve local file', specifier) + return resolve(parse(from).dir, specifier) + } + //use cache for remote + if (cssImports.has(`${from}/${specifier}`)) { + Logger.info('using cache for', `${from}/${specifier}`) + return cssImports.get(`${from}/${specifier}`) + } + + //update cache for new remote + Logger.info('fetching and caching', `${from}/${specifier}`) + + //construct asset url + const baseUrl = from.startsWith('https://') ? from : toFileUrl(from) + const url = new URL(specifier, baseUrl) + + //fetch asset + const response = await fetch(url) + const file = await response.arrayBuffer() + const filename = await hashFile(file) + + //!TODO recursive bundle to cache all remote imports + // bundleAsync({ minify: true, sourceMap: true, '' }) + // const { code, map } = transform({ minify: true, sourceMap: dev, code: new Uint8Array(file), filename: url.toString() }) + + // const { code, map } = await builder({ filename: url.toString(), assetDir, dev }) + + const filepath = join(assetDir, filename) + await Deno.writeFile(filepath, new Uint8Array(file)) + cssImports.set(`${from}/${specifier}`, filepath) + return filepath + }, + }, + }) + + return { code, map } +} diff --git a/plugins/css_bundler/src/bundler.ts b/plugins/css_bundler/src/bundler.ts new file mode 100644 index 0000000..c16a81d --- /dev/null +++ b/plugins/css_bundler/src/bundler.ts @@ -0,0 +1,31 @@ +import { fromFileUrl, join } from '$std/path/mod.ts' +import { builder } from './builder.ts' +import { cssImports, Logger } from './helpers.ts' + +export async function bundleCss( + sourceDir: string, + assetDir: string, + pathname: string, + dev: boolean, +) { + 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 }) + + await Deno.writeFile(join(assetDir, pathname), code) + if (map) { + await Deno.writeFile( + join(assetDir, pathname.replace('.css', '.map.css')), + map, + ) + } + } 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 new file mode 100644 index 0000000..4f7f64c --- /dev/null +++ b/plugins/css_bundler/src/helpers.ts @@ -0,0 +1,28 @@ +export class Logger { + static #name = 'bundle_css' + + static info(message: string, path?: string) { + console.log( + `%c[${this.#name}]%c ${message} %c${path ?? ''}`, + 'color: blue; font-weight: bold', + '', + 'color: green', + ) + } + + static error(message: string, error?: Error) { + 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 = new Map() diff --git a/plugins/css_bundler/src/middleware.ts b/plugins/css_bundler/src/middleware.ts new file mode 100644 index 0000000..8065dc3 --- /dev/null +++ b/plugins/css_bundler/src/middleware.ts @@ -0,0 +1,27 @@ +import { MiddlewareHandler } from '$fresh/server.ts' +import { ensureDir } from '$std/fs/ensure_dir.ts' +import { join } from '$std/path/mod.ts' +import { bundleCss } from './bundler.ts' + +export function cssHandler(sourceDir: string): 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') + ) { + bundleCss(sourceDir, assetDir, ctx.url.pathname, ctx.config.dev) + const file = await Deno.readFile(join(assetDir, ctx.url.pathname)) + return new Response(file, { + headers: { + 'Content-Type': 'text/css; charset=utf-8', + }, + }) + } + } + const resp = await ctx.next() + return resp + } +}