UNPKG

@oazmi/build-tools

Version:

general deno build tool scripts which I practically use in all of my typescript repos

208 lines (207 loc) 12.5 kB
/** this module contains common utility functions used by this package. * * @module */ import * as dntShim from "./_dnt.shims.js"; import { copyDir, detectReadableStreamType, ensureDir, ensureFile, expandGlob, isArray, isObject, memorize, object_assign, object_keys, pathIsGlobPattern, pathResolve, resolveAsUrl, trimSlashes } from "./deps.js"; import { logBasic, logVerbose, setLog } from "./logger.js"; const default_deno_json_path = "./deno.json"; const getDenoJson_FromUri = async (deno_json_path_uri) => { // unfortunately, dynamic imports can only be published in jsr if we enable the `--allow-dirty` flag, // which would then plumet this package's "no-slow-types" score to `0/5`, which I cannot accept. // return (await import(deno_json_path_uri, { with: { type: "json" } })).default return await (await fetch(deno_json_path_uri)).json(); }; const getDenoJson_FromUri_Memorized = memorize(getDenoJson_FromUri); /** this function loads your "deno.json" file at the provided path, as a fully statically typed javascript object. <br> * to get the typings of your actual "deno.json" file, you will need to do some gymnastics, like in the example at the bottom. * * @param deno_json_path the path to your "deno.json" file. it could be either an absolute path, or a path relative to your current working directory (`Deno.cwd()`). * @returns a fully typed "deno.json" like javascript object. * * @example * ```ts * import { getDenoJson } from "jsr:@oazmi/build-tools/funcdefs" * // Deno LSP annotates json imports done via dynamic static-path imports: `import("./path/to/your/deno.json", {with: "json"})` * const get_my_deno_json = async () => { * return (await import("./deno.json", { with: { type: "json" } })).default * } * // `get_my_deno_json` is never actually called. it is only used for annotation of the returned `my_deno_json` javascript object, rather than it having a generalized `DenoJson` type. * const my_deno_json = await getDenoJson<ReturnType<typeof get_my_deno_json>>("./cwd_path/to/deno.json") * ``` */ export const getDenoJson = async (deno_json_path = default_deno_json_path) => { return await getDenoJson_FromUri_Memorized(resolveAsUrl(deno_json_path).href); }; /** create a "package.json" nodejs-project file, based on your "deno.json" configuration file. <br> * the following required fields inside of your "deno.json" will get merged into the output: * - `name`, `version` * * the following fields inside of your "deno.json" will get merged into the output: * - `description`, `author`, `license`, `repository`, `bugs`, and `exports` * * the following field inside of your "deno.json" will get spread onto your output: * - `packageJson` * * note that if you use [dnt (deno-to-node)](https://jsr.io/@deno/dnt), then you will have to delete the `exports` property from the output, otherwise it will ruin/overwrite `dnt`'s output. * * @param deno_json_path the path to your "deno.json" file. it could be either an absolute path, or a path relative to your current working directory (`Deno.cwd()`). * @param merge_defaults provide default "package.json" fields to merge with the acquired `packageJson` object from "deno.json", at a depth of `1` for records. * @returns a "package.json" like javascript object. */ export const createPackageJson = async (deno_json_path = default_deno_json_path, merge_defaults = {}) => { const { name = "", version = "0.0.0", type: moduleType = "module", description, author, license, repository, bugs, exports, packageJson = {} } = await getDenoJson(deno_json_path), merged_package_json = {}; for (const key of new Set([...object_keys(merge_defaults), ...object_keys(packageJson)])) { // merging all record objects, at a depth of 1 const default_value = merge_defaults[key], current_value = packageJson[key], default_is_dict = isObject(default_value) && !isArray(default_value), current_is_dict = isObject(current_value) && !isArray(current_value), current_is_undefined = current_value === undefined; merged_package_json[key] = current_is_undefined ? default_value : (current_is_dict && default_is_dict) ? { ...default_value, ...current_value } : current_value; } return { name, version, type: moduleType, description, author, license, repository, bugs, exports, ...merged_package_json, }; }; /** create a "tsconfig.json" file, based on your "deno.json" configuration file. * * @param deno_json_path the path to your "deno.json" file. it could be either an absolute path, or a path relative to your current working directory (`Deno.cwd()`). * @param overrides provide additional overrides to apply to your output "tsconfig.json" like object. * @returns a "tsconfig.json" like javascript object. */ export const createTsConfigJson = async (deno_json_path = default_deno_json_path, overrides = {}) => { const { compilerOptions = {} } = await getDenoJson(deno_json_path), { compilerOptions: overridden_compilerOptions, ...rest_overrides } = overrides; // remove all "deno" namespace libraries (such as "deno.ns", "deno.unstable", etc...).ns" from compiler options, as they break `dnt`. compilerOptions.lib = (compilerOptions.lib ?? []).filter((v) => !v.toLowerCase().startsWith("deno.")); object_assign(compilerOptions, { target: "ESNext", forceConsistentCasingInFileNames: true, skipLibCheck: true, moduleResolution: "nodenext", }, overridden_compilerOptions); return { "$schema": "https://json.schemastore.org/tsconfig", ...rest_overrides, compilerOptions, }; }; /** convert potential git-repository url to a proper repository url. * * example: * | input | output (URL.href) | * |-------------------------------------------------------|-----------------------------------------------| * | `git+https://github.com/omar-azmi/build_tools_ts.git` | `https://github.com/omar-azmi/build_tools_ts` | * | `https://github.com/omar-azmi/build_tools_ts.git` | `https://github.com/omar-azmi/build_tools_ts` | * | `git+https://github.com/omar-azmi/build_tools_ts` | `https://github.com/omar-azmi/build_tools_ts` | * | `https://github.com/omar-azmi/build_tools_ts` | `https://github.com/omar-azmi/build_tools_ts` | */ export const gitRepositoryToUrl = (repo_git_url) => { return new URL(repo_git_url .replace(/^git\+/, "") .replace(/.git$/, "")); }; /** convert potential git-repository url to a proper github pages url. * * example: * | input | output (URL.href) | * |-------------------------------------------------------|----------------------------------------------| * | `git+https://github.com/omar-azmi/build_tools_ts.git` | `https://oamr-azmi.github.io/build_tools_ts` | * | `https://github.com/omar-azmi/build_tools_ts.git` | `https://oamr-azmi.github.io/build_tools_ts` | * | `git+https://github.com/omar-azmi/build_tools_ts` | `https://oamr-azmi.github.io/build_tools_ts` | * | `https://github.com/omar-azmi/build_tools_ts` | `https://oamr-azmi.github.io/build_tools_ts` | */ export const gitRepositoryToPagesUrl = (repo_git_url) => { const repo_url = gitRepositoryToUrl(repo_git_url), [user_name, repo_name] = trimSlashes(repo_url.pathname).split("/"); repo_url.hostname = `${user_name}.github.io`; repo_url.pathname = repo_name; return repo_url; }; // TODO: PURGE your `globToRegex` function, or consider implementing a more full featured one in `kitchensink`. // for now, I will be using `globToRegExp` from `"jsr:@std/path"` instead. // TODO: implement a more featureful glob pattern implementation that uses tokens and has feature switches (like turning ranges on or off, etc...). then move it to `kitchensink` // DONE: guess what? don't reinvent the wheel. a good implementation already exists in `import { globToRegExp } from "jsr:@std/path"` // TODO: `copyAndCreateFiles` and `createFiles` should return an artifacts info (such as `TemporaryFiles`). /** this function takes in your {@link BaseBuildConfig | generic config} object, * and figures out the files that need to be copied (specified in the {@link BaseBuildConfig.copy} field), * and the new text/binary files that need to be written (specified in the {@link BaseBuildConfig.text} field). */ export const copyAndCreateFiles = async (config) => { setLog(config); const { dir, deno, copy = [], text = [], log, dryrun = false } = config, abs_deno_dir = pathResolve(deno, "./"); // copying other files logBasic("[in-fs] copying additional files from you deno directory over to the build directory"); await Promise.all(copy.map(async ([src, dst]) => { const is_single_file = (src.endsWith("/") || dst.endsWith("/") || pathIsGlobPattern(src)) ? false : true, abs_src = pathResolve(abs_deno_dir, src), abs_dst = pathResolve(dir, dst); if (is_single_file) { logVerbose("[in-fs] copying a file", `from: "${abs_src}"`, `to: "${abs_dst}"`); if (!dryrun) { await dntShim.Deno.copyFile(abs_src, abs_dst); } } else { console.assert(abs_dst.endsWith("/"), ` provided destination folder ("${abs_dst}") path does not end with a trailing slash ("/"). folder paths MUST end with a slash, and folders and glob-patterns can only be copied over to another folder. `); if (!dryrun) { await ensureDir(abs_dst); } for await (const src_dir_entry of expandGlob(src, { root: abs_deno_dir })) { const abs_src = src_dir_entry.path, is_file = src_dir_entry.isFile, is_folder = src_dir_entry.isDirectory, basename = src_dir_entry.name, abs_dst = is_folder ? pathResolve(dir, dst) : pathResolve(dir, dst, basename); if (is_file && !dryrun) { await ensureFile(abs_dst); } if (is_folder && !dryrun) { await ensureDir(abs_dst); } // TODO: how should I handle system links? (i.e. if `isSymlink` was true) logVerbose("[in-fs] copying", `from: "${abs_src}"`, `to: "${abs_dst}"`); if (!dryrun) { await copyDir(abs_src, abs_dst, { overwrite: true }); } } } })); // writing text or binary files await createFiles(text, { dir, log, dryrun }); }; /** 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<string | Uint8Array>`) 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 (input_files, config) => { setLog(config); const { dir = "./", dryrun = false } = config; // writing text or binary files logBasic("[in-fs] writing additional text/binary files to your build directory"); for (let [dst_path, content, options] of input_files) { const abs_dst = pathResolve(dir, dst_path), data_is_stream = content.getReader ? true : false; let is_text = typeof content === "string" ? true : false; if (data_is_stream) { const { kind, stream } = (await detectReadableStreamType(content)); // the `detectReadableStreamType` function consumes the original stream, and so we must replace our stream with a returned untouched cloned-stream content = stream; is_text = kind === "string"; } if (is_text) { logVerbose("[in-fs] writing text", `to: "${abs_dst}"`, "with the configuration:", options); if (!dryrun) { await ensureFile(abs_dst); await dntShim.Deno.writeTextFile(abs_dst, content, options); } } else { logVerbose("[in-fs] writing binary", `to: "${abs_dst}"`, "with the configuration:", options); if (!dryrun) { await ensureFile(abs_dst); await dntShim.Deno.writeFile(abs_dst, content, options); } } } };