refactor: move fresh plugin to separate repository
This commit is contained in:
parent
b5721e3cb8
commit
c225e43ec7
|
@ -35,7 +35,7 @@
|
||||||
"$std/": "https://deno.land/std@0.208.0/",
|
"$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/mod.ts",
|
||||||
"@univoq/": "https://deno.land/x/univoq@0.1.0/",
|
"@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": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig } from '$fresh/server.ts'
|
import { defineConfig } from '$fresh/server.ts'
|
||||||
import { cssBundler } from './plugins/css_bundler/plugin.ts'
|
import { cssBundler } from 'css_bundler'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
@ -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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue