UNPKG

@oazmi/build-tools

Version:

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

229 lines (228 loc) 12.9 kB
/** create typedoc based documentation. * read the documentation of the {@link buildDocs} function, and its configuration interface {@link BuildDocsConfig}. * * @module */ import * as dntShim from "./_dnt.shims.js"; import { Application as typedocApp } from "typedoc"; // TODO: import { bundle, transform } from "./dist.ts" and then create statically hosted distribution version of the library being documented // TODO: allow for user-customization of `entryPoints`, using an approach similar to `/src/dist.ts`. import { emptyDir, ensureEndSlash, ensureFile, ensureStartDotSlash, ensureStartSlash, joinPaths, object_values, parseFilepathInfo, pathResolve, trimSlashes } from "./deps.js"; import { denoPlugins, esBuild, esStop } from "./dist.js"; import { copyAndCreateFiles, createPackageJson, createTsConfigJson, getDenoJson, gitRepositoryToPagesUrl, gitRepositoryToUrl } from "./funcdefs.js"; import { console_warn, logBasic, logVerbose, setLog } from "./logger.js"; /** the default configuration used by the {@link buildDocs} function, for missing/unprovided configuration fields. */ export const defaultBuildDocsConfig = { dir: "./docs/", deno: "./deno.json", copy: [ ["./readme.md", "./readme.md"], ["./license.md", "./license.md"], // copy the source code to the docs' "src" subdirectory, so that it can be hosted on github pages similar to a cdn. // assuming `site_root` is the root url of the hosted site, `${site_root}/src/*.ts` will contain all of the source typescript files. ["./src/", "./src/"], ], css: ` table { border-collapse: collapse; } th { background-color: rgba(128, 128, 128, 0.50); } th, td { border: 0.1em solid rgba(0, 0, 0, 0.75); padding: 0.1em; } pre code { tab-size: 4; } `, }; /** this function generates code documentation of your deno-project, using [`typedoc`](https://github.com/TypeStrong/typedoc). * this function first reads your "deno.json" file to generate an equivalent "package.json" and "tsconfig.json" files, and then it runs `typedoc` to generate the documentation html site. <br> * take a look at {@link BuildDocsConfig} to see what configuration options are available. <br> * moreover, to use this transformer via cli, use the [`./cli/docs.ts`](./cli/docs.ts) script file (or [`jsr:@oazmi/build-tools/cli/docs`](https://jsr.io/@oazmi/build-tools) if using jsr), and take a look at its {@link CliArgs} for list of supported cli args. */ export const buildDocs = async (build_config = {}) => { setLog(build_config); const { dir, deno, copy = [], text = [], site, css, typedoc = {}, preserveTemporary = false, dryrun = false } = { ...defaultBuildDocsConfig, ...build_config }, abs_dir = pathResolve(dir), abs_deno_dir = pathResolve(deno, "./"); logVerbose("current docs-build configuration is:", { dir, deno, site, preserveTemporary, copy, text, typedoc, css, dryrun }); // first we generate the "package.json" and "tsconfig.json" files (in the directory of "deno.json"), which are required by "npm:typedoc" to function in deno environment. logVerbose("[in-memory] creating a \"package.json\" file from your \"deno.json\" file"); const package_json = await createPackageJson(deno), package_json_abs_path = pathResolve(abs_deno_dir, "package.json"); logVerbose("[in-memory] creating a \"tsconfig.json\" file from your \"deno.json\" file"); const tsconfig_json = await createTsConfigJson(deno), tsconfig_json_asb_path = pathResolve(abs_deno_dir, "tsconfig.json"); logBasic("[in-memory] generated \"package.json\" and \"tsconfig.json\" files."); if (!dryrun) { await ensureFile(package_json_abs_path); await ensureFile(tsconfig_json_asb_path); await dntShim.Deno.writeTextFile(package_json_abs_path, JSON.stringify(package_json)); await dntShim.Deno.writeTextFile(tsconfig_json_asb_path, JSON.stringify(tsconfig_json)); } logVerbose("[in-fs] wrote \"package.json\" to:", package_json_abs_path); logVerbose("[in-fs] wrote \"tsconfig.json\" to:", tsconfig_json_asb_path); const node_project_temp_files = { dir: pathResolve(abs_deno_dir), files: [package_json_abs_path, tsconfig_json_asb_path], cleanup: async () => { if (!dryrun) { await dntShim.Deno.remove(package_json_abs_path); await dntShim.Deno.remove(tsconfig_json_asb_path); } logVerbose("[in-fs] deleted \"package.json\" at:", package_json_abs_path); logVerbose("[in-fs] deleted \"tsconfig.json\" at:", tsconfig_json_asb_path); }, }; // generating a custom css file is requested let custom_css_temp_files; if (css) { const temp_dir = pathResolve(abs_deno_dir, "temp"), file_path = pathResolve(temp_dir, "custom.css"); logVerbose("[in-fs] generating a \"custom.css\" file for rendered html at:", file_path); typedoc.customCss = file_path; if (!dryrun) { await ensureFile(file_path); await dntShim.Deno.writeTextFile(file_path, css); } custom_css_temp_files = { dir: temp_dir, files: ["custom.css"], cleanup: async () => { if (!dryrun) { await dntShim.Deno.remove(file_path); // check if the "./temp/" folder is now empty, and delete it if it is empty. let temp_dir_is_empty = true; for await (const entry of dntShim.Deno.readDir(temp_dir)) { temp_dir_is_empty = false; break; } if (temp_dir_is_empty) { await dntShim.Deno.remove(temp_dir); } } logVerbose("[in-fs] deleted \"custom.css\" at:", file_path); } }; } // time to run the `typedoc` documentation generator logVerbose("[in-fs] emptying your docs-build directory:", abs_dir); if (!dryrun) { await emptyDir(dir); } const { exports, repository } = await getDenoJson(deno), repo_url = gitRepositoryToUrl(repository?.url ?? "git+https://github.com/404/404.git"), site_root_path = ensureStartSlash(ensureEndSlash(trimSlashes(site ?? (repository?.url ? gitRepositoryToPagesUrl(repository.url).pathname : "")))), { ".": mainEntrypoint = undefined, ...subEntrypoints } = typeof exports === "string" ? { ".": exports } : exports, entryPoints = [...(mainEntrypoint ? [mainEntrypoint] : []), ...object_values(subEntrypoints)], distribution_file_info = parseFilepathInfo(mainEntrypoint ?? "./src/mod.ts"), distribution_file_name = ensureStartDotSlash(distribution_file_info.basename + ".js"); logVerbose("[in-memory] bundling the mermaid graphs plugin into a data-uri"); const mermaid_plugin_data_uri = await typedocPluginToDataUriScript("./extra/docs/mermaid_plugin.ts", { relativeTo: "build-tools" }); logVerbose("[in-memory] bootstrapping TypeDoc"); const typedoc_app = await typedocApp.bootstrapWithPlugins({ // even though the intermediate "package.json" that we created contains the `exports` field, `typedoc` can't figure out the entrypoints on its own. entryPoints, out: dir, readme: pathResolve(abs_deno_dir, "./readme.md"), hostedBaseUrl: repository ? gitRepositoryToPagesUrl(repository.url).href : undefined, // TODO: navigation links should be customizable, but shouldn't overwrite the github repo link navigationLinks: { "github": repo_url.href, "readme": joinPaths(site_root_path), "src": joinPaths(site_root_path, mainEntrypoint ?? "./src/mod.ts"), "dist": joinPaths(site_root_path, "./dist/", distribution_file_name), "examples": joinPaths(site_root_path, "./examples/", "./index.html"), }, skipErrorChecking: true, githubPages: true, includeVersion: true, logLevel: "Warn", sort: ["source-order", "required-first", "kind"], visibilityFilters: { "protected": true, "private": true, "inherited": true, "external": true, }, plugin: [mermaid_plugin_data_uri], ...typedoc, }); logBasic("[in-memory] generating TypeDoc documents in-memory."); const typedoc_project = await typedoc_app.convert(); logBasic("[in-fs] rendering TypeDoc documents to html and outputting to:", abs_dir); if (typedoc_project === undefined) { console_warn("TypeDoc document parsing failed"); } if (!dryrun && typedoc_project) { await typedoc_app.generateDocs(typedoc_project, dir); } // creating new text files and copying over files to the destination `dir` folder. await copyAndCreateFiles({ dir, deno, copy, text, dryrun }); // clean up temporary files if `preserveTemporary` is false if (!preserveTemporary) { await node_project_temp_files.cleanup(); await custom_css_temp_files?.cleanup(); } // stop esbuild if we dynamically bundled the mermaid plugin into a data-uri await esStop(); return { dir: abs_dir, files: [], cleanup: async () => { logBasic("[in-fs] deleting your docs-build directory:", abs_dir); if (!dryrun) { await emptyDir(abs_dir); await dntShim.Deno.remove(abs_dir); } // if the temporary files were originally presereved/kept, then we should clean them up now, since their cleaning got skipped earlier. if (preserveTemporary) { await node_project_temp_files.cleanup(); await custom_css_temp_files?.cleanup(); } } }; }; const defaultTypedocPluginToDataUriScriptConfig = { relativeTo: "cwd" }; const typedocPluginToDataUriScript = async (plugin_script_path, config) => { const { relativeTo } = { ...defaultTypedocPluginToDataUriScriptConfig, ...config }, plugin_script_path_fileinfo = parseFilepathInfo(plugin_script_path), plugin_script_path_without_ext = joinPaths(plugin_script_path_fileinfo.dirpath, plugin_script_path_fileinfo.basename), plugin_script_paths_to_try = [ plugin_script_path, // try using the original path plugin_script_path_without_ext + ".ts", // try using typescirpt extension (works for deno and node with typsecript support) plugin_script_path_without_ext + ".js", // try using javascript extension (works for transpiled and distributed version of this library) plugin_script_path_without_ext + ".mts", plugin_script_path_without_ext + ".mjs", plugin_script_path_without_ext + ".cts", plugin_script_path_without_ext + ".cjs", plugin_script_path_without_ext + ".tsx", plugin_script_path_without_ext + ".jsx", plugin_script_path_without_ext + "index.js", plugin_script_path_without_ext + "index.ts", ].map((path) => { return relativeTo === "build-tools" ? globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).resolve(path) : path; }); let working_script_path; for await (const script_path of plugin_script_paths_to_try) { // why am I not using `method: "HEAD"`? that's because "file://" urls do not support the head method unfortunately (at least not on windows). if ((await fetch(script_path, { method: "GET" })).ok) { working_script_path = script_path; break; } } if (!working_script_path) { logBasic(`[error] failed to find the typedoc plugin with the path: "${plugin_script_path}"\n\tnow using a dummy plugin instead.\n\ttried fetching the following paths:`, plugin_script_paths_to_try); return "data:application/javascript;utf8,export const load = (app) => { }"; } logBasic("[in-memory] resolved mermaid plugin script path to:", working_script_path); const script_contents = await (await fetch(working_script_path)).text(); const bundle_result = (await esBuild({ stdin: { contents: script_contents, sourcefile: working_script_path, loader: "ts", }, // entryPoints: [working_script_path], plugins: [...denoPlugins({ initialPluginData: { runtimePackage: "./" } })], outdir: "./virtual-dist/", format: "esm", platform: "node", minify: true, bundle: true, write: false, })); const bundled_code = bundle_result.outputFiles.find((outfile) => outfile.path.endsWith(".js")); logVerbose("[in-memory] successfully transpiled the typedoc plugin:", working_script_path); return "data:application/javascript;base64," + btoa(bundled_code.text); };