diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..99f2846 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": false +} diff --git a/README.md b/README.md index 14533b9..e66d1fb 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ -Docuraptor is an offline alternative to the [doc.deno.land](https://doc.deno.land) service. +Docuraptor is an offline alternative to the +[doc.deno.land](https://doc.deno.land) service. -It generates and serves HTML documentation for JS/TS modules with the help of [Deno's](https://deno.land) documentation parser. +It generates and serves HTML documentation for JS/TS modules with the help of +[Deno's](https://deno.land) documentation parser. ## Features @@ -16,8 +18,8 @@ It generates and serves HTML documentation for JS/TS modules with the help of [D `$ deno install -A https://deno.land/x/docuraptor@20200930.0/docuraptor.ts` -The permissions can be restricted. -Read the `--help` documentation for more details. +The permissions can be restricted. Read the `--help` documentation for more +details. ## Usage @@ -25,7 +27,7 @@ Read the `--help` documentation for more details. ## Examples - -_Docuraptor with `BROWSER=w3m`_ + _Docuraptor with +`BROWSER=w3m`_  diff --git a/deno_api.ts b/deno_api.ts index b2a2470..8542f28 100644 --- a/deno_api.ts +++ b/deno_api.ts @@ -1,14 +1,14 @@ +// deno-lint-ignore-file camelcase import type * as ddoc from "./deno_doc_json.ts"; -import type * as info from "./deno_info_json.ts"; +import { createGraph, ModuleGraph } from "./deps.ts"; const decoder = new TextDecoder(); export async function getDenoData( specifier?: string, { private: priv }: { private?: boolean } = {}, -): Promise<{ doc: ddoc.DocNode[]; info: info.FileInfo | null }> { +): Promise<{ doc: ddoc.DocNode[]; info: ModuleGraph | null }> { let proc_d; - let proc_i; try { proc_d = Deno.run({ cmd: [ @@ -32,36 +32,14 @@ export async function getDenoData( } const doc_j: ddoc.DocNode[] = JSON.parse(stdout); - let info_j: info.FileInfo | null = null; + let info_j: ModuleGraph | null = null; if (specifier !== undefined) { - proc_i = Deno.run({ - cmd: [ - "deno", - "info", - "--json", - "--unstable", - specifier, - ], - stdin: "null", - stdout: "piped", - stderr: "piped", - }); - - const stdout = decoder.decode(await proc_i.output()); - const stderr = decoder.decode(await proc_i.stderrOutput()); - const { success } = await proc_i.status(); - - if (!success) { - throw { stderr }; - } - - info_j = JSON.parse(stdout); + info_j = await createGraph(specifier); } return { doc: doc_j, info: info_j }; } finally { proc_d?.close(); - proc_i?.close(); } } diff --git a/deno_info_json.ts b/deno_info_json.ts deleted file mode 100644 index c1a3b14..0000000 --- a/deno_info_json.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type Info = DenoInfo | FileInfo; - -export interface DenoInfo { - denoDir: string; - modulesCache: string; - typescriptCache: string; -} - -export interface FileDependency { - size: number; - deps: string[]; -} - -export interface FileInfo { - local: string; - fileType: "TypeScript" | "JavaScript"; - compiled: string | null; - map: string | null; - depCount: number; - totalSize: number; - files: Record; -} diff --git a/deps.ts b/deps.ts index 747cf65..35dcc83 100644 --- a/deps.ts +++ b/deps.ts @@ -1,11 +1,17 @@ export { assert, unreachable, -} from "https://deno.land/std@0.69.0/testing/asserts.ts"; -export { parse as argsParse } from "https://deno.land/std@0.69.0/flags/mod.ts"; +} from "https://deno.land/std@0.106.0/testing/asserts.ts"; +export { parse as argsParse } from "https://deno.land/std@0.106.0/flags/mod.ts"; +export { join as pathJoin } from "https://deno.land/std@0.106.0/path/mod.ts"; +export { pooledMap } from "https://deno.land/std@0.106.0/async/pool.ts"; export { - serve, - ServerRequest, -} from "https://deno.land/std@0.69.0/http/server.ts"; -export { join as pathJoin } from "https://deno.land/std@0.69.0/path/mod.ts"; -export { pooledMap } from "https://deno.land/std@0.69.0/async/pool.ts"; + bold, + italic, + underline, +} from "https://deno.land/std@0.106.0/fmt/colors.ts"; +export { createGraph } from "https://deno.land/x/deno_graph@0.2.1/mod.ts"; +export type { + Module, + ModuleGraph, +} from "https://deno.land/x/deno_graph@0.2.1/mod.ts"; diff --git a/docuraptor.ts b/docuraptor.ts index 32b7f72..59d7a9f 100644 --- a/docuraptor.ts +++ b/docuraptor.ts @@ -1,10 +1,11 @@ +// deno-lint-ignore-file camelcase import assets from "./assets.ts"; import { - assert, argsParse, - pathJoin, - serve, - ServerRequest, + assert, + bold, + italic, + underline, unreachable, } from "./deps.ts"; import { generateStatic } from "./generator.ts"; @@ -18,10 +19,11 @@ const decoder = new TextDecoder(); */ const doc_prefix = "/doc/"; -async function handleDoc(req: ServerRequest): Promise { - assert(req.url.startsWith(doc_prefix)); +async function handleDoc(req: Deno.RequestEvent): Promise { + const path = new URL(req.request.url).pathname; + assert(path.startsWith(doc_prefix)); - const args = req.url.substr(doc_prefix.length); + const args = path.substr(doc_prefix.length); const search_index = args.indexOf("?"); const doc_url = decodeURIComponent( @@ -40,6 +42,7 @@ async function handleDoc(req: ServerRequest): Promise { doc_url.length > 0 ? doc_url : undefined, ); } catch (err) { + console.log(err); if (err.stderr !== undefined) { handleFail(req, 500, htmlEscape(err.stderr)); } else { @@ -48,46 +51,51 @@ async function handleDoc(req: ServerRequest): Promise { return; } - await req.respond({ - status: 200, - headers: new Headers({ - "Content-Type": "text/html", + await req.respondWith( + new Response(doc, { + status: 200, + headers: { + "Content-Type": "text/html", + }, }), - body: doc, - }); + ); } async function handleFail( - req: ServerRequest, + req: Deno.RequestEvent, status: number, message: string, ): Promise { const rend = new DocRenderer(); - await req.respond({ - status, - headers: new Headers({ - "Content-Type": "text/html", - }), - body: ` - - - ${rend.renderHead("Docuraptor Error")} - - - ${rend.renderHeader("An error occured")} - - - ${htmlEscape(message)} - - - - `, - }); + await req.respondWith( + new Response( + ` + + + ${rend.renderHead("Docuraptor Error")} + + + ${rend.renderHeader("An error occured")} + + + ${htmlEscape(message)} + + + + `, + { + status, + headers: { + "Content-Type": "text/html", + }, + }, + ), + ); } const file_url = new URL("file:/"); let deps_url: URL | undefined = undefined; -async function handleIndex(req: ServerRequest): Promise { +async function handleIndex(req: Deno.RequestEvent): Promise { const known_documentation = []; if (deps_url !== undefined) { @@ -122,109 +130,112 @@ async function handleIndex(req: ServerRequest): Promise { } const rend = new DocRenderer(); - await req.respond({ - status: 200, - headers: new Headers({ - "Content-Type": "text/html", - }), - body: ` - - ${rend.renderHead("Docuraptor Index")} - - - ${rend.renderHeader("Docuraptor Index – Locally available modules")} - - - Deno Builtin - ${ - known_documentation.sort().map( - (url) => - `${ - htmlEscape(url) - }`, - ).join("") - } - - - - `, - }); + await req.respondWith( + new Response( + ` + + ${rend.renderHead("Docuraptor Index")} + + + ${rend.renderHeader("Docuraptor Index – Locally available modules")} + + + Deno Builtin + ${ + known_documentation.sort().map( + (url) => + `${ + htmlEscape(url) + }`, + ).join("") + } + + + +`, + { + status: 200, + headers: { + "Content-Type": "text/html", + }, + }, + ), + ); } const form_prefix = "/form/"; -async function handleForm(req: ServerRequest): Promise { - assert(req.url.startsWith(form_prefix)); +async function handleForm(req: Deno.RequestEvent): Promise { + const url = new URL(req.request.url); + assert(url.pathname.startsWith(form_prefix)); - const args = req.url.substr(form_prefix.length); - const search_index = args.indexOf("?"); - const form_action = args.slice(0, search_index); - const search = new URLSearchParams( - search_index === -1 ? "" : args.slice(search_index), - ); + const args = url.pathname.substr(form_prefix.length); - switch (form_action) { + switch (args) { case "open": { - if (!search.has("url")) { + if (!url.searchParams.has("url")) { await handleFail(req, 400, "Received invalid request"); return; } - await req.respond({ - status: 301, - headers: new Headers({ - "Location": `/doc/${search.get("url")!}`, + await req.respondWith( + new Response(null, { + status: 302, + headers: { + Location: `/doc/${url.searchParams.get("url")!}`, + }, }), - }); + ); break; } default: await handleFail( req, 400, - `Invalid form action ${htmlEscape(form_action)}`, + `Invalid form action ${htmlEscape(args)}`, ); } } const static_prefix = "/static/"; -async function handleStatic(req: ServerRequest): Promise { - assert(req.url.startsWith(static_prefix)); - const resource = req.url.substr(static_prefix.length); +async function handleStatic(req: Deno.RequestEvent): Promise { + const path = new URL(req.request.url).pathname; + assert(path.startsWith(static_prefix)); + const resource = path.substr(static_prefix.length); const asset = assets[resource]; if (asset === undefined) { handleFail(req, 404, "Resource not found"); } else { - await req.respond({ - status: 200, - headers: new Headers({ - "Content-Type": asset.mimetype ?? "application/octet-stream", + await req.respondWith( + new Response(asset.content, { + status: 200, + headers: { + "Content-Type": asset.mimetype ?? "application/octet-stream", + }, }), - body: asset.content, - }); + ); } } -async function handler(req: ServerRequest): Promise { +async function handler(req: Deno.RequestEvent): Promise { try { - if (!["HEAD", "GET"].includes(req.method)) { + const path = new URL(req.request.url).pathname; + if (!["HEAD", "GET"].includes(req.request.method)) { handleFail(req, 404, "Invalid method"); } - - if (req.url.startsWith(static_prefix)) { + if (path.startsWith(static_prefix)) { await handleStatic(req); - } else if (req.url.startsWith(doc_prefix)) { + } else if (path.startsWith(doc_prefix)) { await handleDoc(req); - } else if (req.url.startsWith(form_prefix)) { + } else if (path.startsWith(form_prefix)) { await handleForm(req); - } else if (req.url === "/") { + } else if (path === "/") { await handleIndex(req); } else { await handleFail(req, 404, "Malformed path"); } } finally { - req.finalize(); - req.conn.close(); + // pass } } @@ -245,7 +256,7 @@ function argCheck( } function open(s: string): void { - let run = Deno.run({ + const run = Deno.run({ cmd: Deno.build.os === "windows" ? ["start", "", s] : Deno.build.os === "darwin" @@ -287,7 +298,6 @@ async function mainGenerate() { const { builtin, dependencies, - generate, index, out, private: priv, @@ -365,7 +375,7 @@ async function mainServer() { throw null; } - let run = Deno.run({ + const run = Deno.run({ cmd: [browser, url], }); @@ -374,16 +384,21 @@ async function mainServer() { open(url); } } - - for await (const req of serve({ hostname, port })) { - await handler(req); + const listener = Deno.listen({ port, hostname }); + for await (const conn of listener) { + (async () => { + const httpConn = Deno.serveHttp(conn); + for await (const req of httpConn) { + handler(req); + } + })(); } } if (import.meta.main) { - const usage_string = `%cDocuraptor%c (${import.meta.url}) + const usage_string = `${bold("Docuraptor")} (${import.meta.url}) -%cStart documentation server:%c +${underline("Start documentation server:")} $ docuraptor [--port=] [--hostname=] [--skip-browser] [--private] [--builtin | ] @@ -392,10 +407,10 @@ if the module specifier is omitted, the documentation index, in the system browser. Listens on 127.0.0.1:8709 by default. -%cAdditionally requires network access for hostname:port.%c +${italic("Additionally requires network access for hostname:port.")} -%cGenerate HTML documentation:%c +${underline("Generate HTML documentation:")} $ docuraptor --generate [--out=] [--index=] [--dependencies] [--private] ... @@ -405,42 +420,23 @@ current working directory. With the dependencies flag set documentation is also generated for all modules dependet upon. Writes an index of all generated documentation -to the index file, defaulting to %cindex.html%c. +to the index file, defaulting to ${italic("index.html")}. -%cAdditionally requires write access to the output directory.%c +${italic("Additionally requires write access to the output directory.")} -%cAll functions require allow-run and read access to the Deno cache.%c +${italic("All functions require allow-run and read access to the Deno cache.")} The system browser can be overwritten with the DOCURAPTOR_BROWSER and BROWSER environment variables. -%cRequires allow-env.%c`; - - const usage_css = [ - "font-weight: bold", - "", - "text-decoration: underline;", - "", - "font-style: italic;", - "", - "text-decoration: underline;", - "", - "font-style: italic;", - "", - "font-style: italic;", - "", - "font-style: italic;", - "", - "font-style: italic;", - "", - ]; +${italic("Requires allow-env.")}`; const { help, generate } = argsParse(Deno.args, { boolean: ["help", "generate"], }); if (help) { - console.log(usage_string, ...usage_css); + console.log(usage_string); Deno.exit(0); } diff --git a/generator.ts b/generator.ts index e56e939..66d1400 100644 --- a/generator.ts +++ b/generator.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file camelcase import { getDenoData } from "./deno_api.ts"; import { assert, pathJoin, pooledMap } from "./deps.ts"; import { DocRenderer } from "./renderer.ts"; @@ -32,14 +33,14 @@ export async function generateStatic( assert(info, `Deno failed to generate metadata for module ${mod}`); if (options?.recursive) { - for (const mod of Object.keys(info.files)) { + for (const mod of info.modules.map((mod) => mod.specifier)) { full_modules.add(mod); } } else { try { new URL(mod); } catch { - mod = new URL(info.local, "file:///").toString(); + mod = new URL(info.root, "file:///").toString(); } full_modules.add(mod); } diff --git a/renderer.ts b/renderer.ts index 4771cfb..bf32e23 100644 --- a/renderer.ts +++ b/renderer.ts @@ -1,12 +1,9 @@ +// deno-lint-ignore-file camelcase import assets from "./assets.ts"; import { getDenoData } from "./deno_api.ts"; import type * as ddoc from "./deno_doc_json.ts"; -import type * as info from "./deno_info_json.ts"; -import { - assert, - unreachable, -} from "./deps.ts"; -import { htmlEscape, identifierId, humanSize } from "./utility.ts"; +import { assert, Module, ModuleGraph, unreachable } from "./deps.ts"; +import { htmlEscape, humanSize, identifierId } from "./utility.ts"; const sort_order: ddoc.DocNode["kind"][] = [ "import", @@ -63,7 +60,7 @@ export class DocRenderer { try { new URL(specifier!); } catch { - specifier = new URL(info_j.local, "file:///").toString(); + specifier = new URL(info_j.root, "file:///").toString(); } } @@ -407,10 +404,10 @@ export class DocRenderer { return res; } - renderInfo(specifier: string, info: info.FileInfo): string { + renderInfo(specifier: string, info: ModuleGraph): string { const link = ( spec: string, - dep: info.FileDependency, + dep: Module, icon: string, icon_type: string, ) => { @@ -424,19 +421,22 @@ export class DocRenderer { }`; }; - const unique_deps = Object.entries(info.files); - const direct_deps = new Set(info.files[specifier].deps); + const unique_deps = info.modules; + const direct_deps = new Set( + info.modules, + ); function compare_deps( - a_name: string, - b_name: string, + a_name: Module, + b_name: Module, ): number { - let a_dir = direct_deps.has(a_name); - let b_dir = direct_deps.has(b_name); + const a_dir = direct_deps.has(a_name); + const b_dir = direct_deps.has(b_name); - return -(a_name === specifier) || +(b_name === specifier) || + return -(a_name.specifier === specifier) || + +(b_name.specifier === specifier) || (a_dir === b_dir - ? a_name.localeCompare(b_name) + ? a_name.specifier.localeCompare(b_name.specifier) : Number(b_dir) - Number(a_dir)); } @@ -447,18 +447,21 @@ export class DocRenderer { Unique dependencies: ${direct_deps.size} direct; ${transitive} transitive. ${ - humanSize(info.totalSize ?? 0) + humanSize( + info.modules.find((module) => module.specifier === info.root)?.size ?? + 0, + ) } ${ - unique_deps.sort(([sp_a], [sp_b]) => compare_deps(sp_a, sp_b)).map(( - [sp, dep], + unique_deps.sort((sp_a, sp_b) => compare_deps(sp_a, sp_b)).map(( + sp, ) => link( + sp.specifier, sp, - dep, - ...<[string, string]> (specifier === sp + ...<[string, string]> (specifier === sp.specifier ? ["S", "white"] : direct_deps.has(sp) ? ["D", "green"] diff --git a/utility.ts b/utility.ts index 0d764f0..023b781 100644 --- a/utility.ts +++ b/utility.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file camelcase export function htmlEscape(s: string): string { return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll( ">", @@ -13,7 +14,7 @@ export function humanSize(bytes: number): string { unit++; } - let visual = Math.round(bytes * 100) / 100; + const visual = Math.round(bytes * 100) / 100; return `${visual !== bytes ? "~" : ""}${visual}${size_units[unit]}`; }
- ${htmlEscape(message)} -
+ ${htmlEscape(message)} +