refactor: move fresh plugin to separate repository

This commit is contained in:
Julien Oculi 2024-02-08 11:03:34 +01:00
parent b5721e3cb8
commit c225e43ec7
7 changed files with 2 additions and 313 deletions

View file

@ -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",

View file

@ -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: [

View file

@ -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<void>[] = []
//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)
},
}
}

View file

@ -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))
}

View file

@ -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()
}
}

View file

@ -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<string> {
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<string, string> = new Map()
export const cssUrls: Map<string, string> = 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
}

View file

@ -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
}
}