This release is 5 versions behind 0.8.4 — the latest version of @mizu/render. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
🌊 Lightweight HTML templating library for any-side rendering
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
JSR Score
100%
Published
2 months ago (0.7.6)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149// Imports import type { Arg, Arrayable, Promisable } from "jsr:@mizu/internal@^0.7.1/engine" import type { Server, ServerGenerateFileSystemOptions as FileSystemOptions, ServerGenerateOptions as GenerateOptions } from "./server.ts" import { common, dirname, globToRegExp, join, resolve } from "jsr:@std/path@1" import { readAll, readerFromStreamReader } from "jsr:@std/io@~0.225" // deno-lint-ignore no-external-import import { Buffer } from "node:buffer" /** Text encoder. */ const encoder = new TextEncoder() /** Text decoder. */ const decoder = new TextDecoder() /** Used by {@linkcode Server.generate()} to generate content. */ export async function generate(server: Server, sources: Array<StringSource | GlobSource | CallbackSource | URLSource>, { output, clean, fs } = {} as Omit<Required<GenerateOptions>, "fs"> & { fs: Required<FileSystemOptions> }): Promise<void> { // Prepare output directory output = resolve(output) if ((await Promise.resolve(fs.stat(output)).then(() => true).catch(() => false)) && clean) { await fs.rm(output, { recursive: true }) } await fs.mkdir(output, { recursive: true }) // Generate files for (const [source, destination, options = {}] of sources) { const path = join(output, destination) // Copy content from URL if (source instanceof URL) { const bytes = await fetch(source).then((response) => response.bytes()) await fs.write(path, await render(server, Buffer.from(bytes), options.render)) } // Copy content from callback else if (typeof source === "function") { let bytes = await source() if (typeof bytes === "string") { bytes = Buffer.from(encoder.encode(bytes)) } await fs.write(path, await render(server, bytes, options.render)) } // Copy content from local files else if ("directory" in options) { const root = `${options.directory}` const sources = [source].flat() for (const source of sources) { for await (const { path: from } of expandGlob(source, { fs, root })) { const path = join(output, from.replace(common([root, from]), "")) await fs.mkdir(dirname(path), { recursive: true }) await fs.write(path, await render(server, await fs.read(from), options.render)) } } } // Copy content from string else { await fs.write(path, await render(server, Buffer.from(encoder.encode(source as string)), options.render)) } } } /** Used by {@linkcode generate()} to render content. */ async function render(server: Server, content: Arg<NonNullable<FileSystemOptions["write"]>, 1>, render?: Arg<Server["render"], 1>): Promise<Arg<NonNullable<FileSystemOptions["write"]>, 1>> { if (render) { if (content instanceof ReadableStream) { content = Buffer.from(await readAll(readerFromStreamReader(content.getReader()))) } const rendered = await server.render(decoder.decode(content), render) content = Buffer.from(encoder.encode(rendered)) } return content } /** String source. */ export type StringSource = [ /** File content. */ string, /** Destination path (including filename). */ string, /** Options. */ { /** Whether to render content with {@linkcode Static.render()}. */ render?: Arg<Server["render"], 1> }?, ] /** * Glob source. * * The presence of the `directory` option is used to distinguish this type from {@linkcode StringSource}. */ export type GlobSource = [ /** A list of file globs. */ Arrayable<string>, /** Destination path (excluding filename). */ string, /** Options. */ { /** Source directory. */ directory: string /** Whether to render content with {@linkcode Static.render()}. */ render?: Arg<Server["render"], 1> }, ] /** Callback source. */ export type CallbackSource = [ /** A callback that returns file content. */ () => Promisable<Arg<NonNullable<FileSystemOptions["write"]>, 1> | string>, /** Destination path (including filename). */ string, /** Options. */ { /** Whether to render content with {@linkcode Static.render()}. */ render?: Arg<Server["render"], 1> }?, ] /** URL source. */ export type URLSource = [ /** Source URL. */ URL, /** Destination path (including filename). */ string, /** Options. */ { /** Whether to render content with {@linkcode Static.render()}. */ render?: Arg<Server["render"], 1> }?, ] /** Expand glob patterns. */ async function* expandGlob(glob: string, { fs, root, directory = root }: { fs: Pick<FileSystemOptions, "readdir" | "stat">; root: string; directory?: string }): AsyncGenerator<{ path: string }> { if (!await Promise.resolve(fs.stat(directory)).then(() => true).catch(() => false)) { return } for (const entry of await fs.readdir(directory)) { const file = typeof entry === "object" ? entry.name : entry const path = join(directory, file) const stats = await fs.stat(path) if (stats) { if (typeof stats.isDirectory === "function" ? stats.isDirectory() : stats.isDirectory) { yield* expandGlob(glob, { fs, root, directory: path }) } else { let relative = common([root, directory]).replaceAll("\\", "/") if (!relative.endsWith("/")) { relative += "/" } if (globToRegExp(glob, { extended: true, globstar: true }).test(path.replaceAll("\\", "/").replace(relative, ""))) { yield { path } } } } } }