diff --git a/examples/1_html/build.ts b/examples/1_html/build.ts index 91965e1..0608b29 100644 --- a/examples/1_html/build.ts +++ b/examples/1_html/build.ts @@ -2,6 +2,7 @@ import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.0" import { fromFileUrl } from "jsr:@std/path" import esbuild from "npm:esbuild" import { resolveAsUrl } from "../../src/deps.ts" +import { writeOutputFiles } from "../../src/fs.ts" import { HtmlLoader } from "./loader.ts" @@ -11,7 +12,7 @@ const html_file_content = await (await fetch(html_file_path)).text() const html_file_loader = new HtmlLoader({ path: fromFileUrl(html_file_path) }) -const js_txt = await html_file_loader.parseToJs(html_file_content) +const html_in_js = await html_file_loader.parseToJs(html_file_content) const results = await esbuild.build({ absWorkingDir: fromFileUrl(this_dir_path), @@ -19,7 +20,7 @@ const results = await esbuild.build({ target: "esnext", platform: "browser", stdin: { - contents: js_txt, + contents: html_in_js, loader: "ts", resolveDir: fromFileUrl(resolveAsUrl("./", html_file_path)), sourcefile: fromFileUrl(html_file_path), @@ -34,11 +35,24 @@ const results = await esbuild.build({ plugins: [...denoPlugins()], }) -const js_compiled_text = results.outputFiles[0].text -const html_compiled_text = await html_file_loader.unparseFromJs(js_compiled_text) -console.log("%c" + "bundled html file output:", "color: green; font-weight: bold;") +const + [html_in_js_compiled, ...other_output_files] = results.outputFiles, + html_compiled_text = await html_file_loader.unparseFromJs(html_in_js_compiled.text) + +console.log("%c" + `bundled html file: "0", path: "${html_in_js_compiled.path}"`, "color: green; font-weight: bold;") console.log(html_compiled_text) -results.outputFiles.slice(1).forEach((js_file, index) => { - console.log("%c" + `bundled js file: ${index}`, "color: green; font-weight: bold;") +other_output_files.forEach((js_file, index) => { + console.log("%c" + `bundled js file: "${index + 1}", path: "${js_file.path}"`, "color: green; font-weight: bold;") console.log(js_file.text) }) + +await writeOutputFiles([ + { + path: html_in_js_compiled.path.replace(/\.js$/, ".html"), + text: html_compiled_text + }, ...other_output_files +], { + dir: "./output/", + log: "verbose", + dryrun: false, +}) diff --git a/src/deps.ts b/src/deps.ts index 7b8aacc..0f9b474 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,5 +1,5 @@ -export { json_parse, json_stringify, math_min, object_entries, object_fromEntries, object_keys, object_values, promise_all } from "jsr:@oazmi/kitchensink@0.8.5/alias" -export { getUriScheme, joinPaths, relativePath, resolveAsUrl } from "jsr:@oazmi/kitchensink@0.8.5/pathman" +export { console_log, json_parse, json_stringify, math_min, object_entries, object_fromEntries, object_keys, object_values, promise_all } from "jsr:@oazmi/kitchensink@0.8.5/alias" +export { ensureEndSlash, getUriScheme, joinPaths, pathToPosixPath, relativePath, resolveAsUrl, resolvePathFactory } from "jsr:@oazmi/kitchensink@0.8.5/pathman" export { isArray, isString } from "jsr:@oazmi/kitchensink@0.8.5/struct" /** flags used for minifying (or eliminating) debugging logs and asserts, when an intelligent bundler, such as `esbuild`, is used. */ diff --git a/src/fs.ts b/src/fs.ts new file mode 100644 index 0000000..5536a37 --- /dev/null +++ b/src/fs.ts @@ -0,0 +1,108 @@ +/** this submodule contains utility functions for writing esbuild's virtual output to the filesystem. + * + * this submodule is separated from the rest since it performs filesystem operations, which is runtime-dependant, + * and we don't want to impact the portability of the main module. + * + * @module +*/ + +import { ensureFile } from "jsr:@std/fs@1.0.6" +import { console_log, ensureEndSlash, pathToPosixPath, promise_all, resolvePathFactory } from "./deps.ts" + + +/** get the current working directory (`Deno.cwd`) in posix path format. */ +export const getCwdPath = () => { return ensureEndSlash(pathToPosixPath(Deno.cwd())) } + +/** resolve a file path so that it becomes absolute, with unix directory separator ("/"). + * TODO: refactor the name `pathResolve` to `resolvePath` +*/ +export const pathResolve = resolvePathFactory(getCwdPath) + +/** the tuple description of a writable (or appendable) file. + * - the first entry of the array must describe the destination path of the file, + * relative to the directory defined in {@link CreateFilesConfig.dir}). + * - the second entry should be the file's contents, which can either be a `string` text, a `ReadableStream`, or a `Uint8Array` binary. + * - the third and optional entry lets you specify additional {@link Deno.WriteFileOptions | deno specific file writing options}, + * such as `"append"` the new text, or permit the creation (`"create"`) of new file if it doesn't exist, etc... +*/ +export type WritableFileConfig = [ + destination: string, + content: string | Uint8Array, + options?: Deno.WriteFileOptions, +] + +/** configuration options for {@link createFiles}. */ +export interface CreateFilesConfig { + /** the desired output directory. + * if a relative path is provided, then it will be resolved as a path relative to Deno's current working directory. (which is generally where `deno.json` resides.) + */ + dir?: string + + /** select logging level: + * - `false` or `"none"`: skip logging. + * - `true` or `"basic"`: log what is being carried out at the top level. + * - `"verbose"`: in addition to basic logging, it also logs which files/folders are being copied or generated. + * + * @defaultValue `"basic"` + */ + log?: boolean | "none" | "basic" | "verbose" + + /** enable `dryrun` if you wish for nothing to be written onto the the filesystem. + * + * @defaultValue `false` + */ + dryrun?: boolean +} + +/** the in-memory output file description generated by `esbuild`. */ +export interface EsbuildOutputFile { + path: string + text?: string + contents?: Uint8Array + hash?: string +} + +/** print some basic useful information on the console. + * the print will only appear if the logging-level is either set to `"basic"` or `"verbose"` via {@link setLog} +*/ +const logBasic = (log_level: NonNullable, ...data: any[]): void => { + if (log_level === "basic" || log_level === "verbose") { console_log(...data) } +} + +/** print verbose details on the console. + * the print will only appear if the logging-level is either set to `"verbose"` via {@link setLog} +*/ +const logVerbose = (log_level: NonNullable, ...data: any[]): void => { + if (log_level === "verbose") { console_log(...data) } +} + +/** write a collection of virtual files to your filesystem. + * this function accepts virtual files that are either in text (`string`), binary (`Uint8Array`), or streamable text/binary (`ReadableStream`) formats. + * it is important that you provide the configuration parameter's {@link config["dir"] | `dir`} field, so that relative paths can be resolved according to the provided directory. +*/ +export const createFiles = async (virtual_files: Array, config: CreateFilesConfig = {}): Promise => { + const { dir = "./", log = "basic", dryrun = false } = config + // writing text or binary files + logBasic(log, "[in-fs] writing additional text/binary files to your build directory") + await promise_all(virtual_files.map(async ([dst_path, content, options]) => { + const abs_dst = pathResolve(dir, dst_path) + logVerbose(log, `[in-fs] writing file to: "${abs_dst}"`, "with the configuration:", options) + if (!dryrun) { + await ensureFile(abs_dst) + if (typeof content === "string") { + await Deno.writeTextFile(abs_dst, content, options) + } else { + await Deno.writeFile(abs_dst, content, options) + } + } + })) +} + +export const writeOutputFiles = async (virtual_files: Array, config: CreateFilesConfig = {}): Promise => { + return createFiles(virtual_files.map((virtual_file): WritableFileConfig => { + const + { path, contents, text } = virtual_file, + content = contents ?? text ?? "" + return [path, content] + }), config) +}