diff --git a/plugins/demo.tsx b/plugins/demo.tsx new file mode 100644 index 0000000..0927a1c --- /dev/null +++ b/plugins/demo.tsx @@ -0,0 +1,19 @@ +import { Plugin } from "../src/lib/types.ts"; + +const plugin: Plugin = () => { + return { + header: { + left: Demo, + right: Home, + }, + routes: ["/demo.png"], + handle: async () => { + const req = await fetch( + "https://github.com/lino-levan/pyro/raw/main/www/static/icon.png", + ); + return req; + }, + }; +}; + +export default plugin; diff --git a/src/build.ts b/src/build.ts index d3753f1..d1171c9 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,11 +1,12 @@ import { walkSync } from "std/fs/walk.ts"; -import { join, posix, resolve, win32 } from "std/path/mod.ts"; +import { posix, resolve, win32 } from "std/path/mod.ts"; import { parse } from "std/encoding/yaml.ts"; import { render } from "./lib/render.ts"; import { copySync } from "std/fs/copy.ts"; import { getMagic } from "./lib/magic.ts"; import { CSS } from "./lib/css.ts"; import { Config } from "./lib/types.ts"; +import { loadPlugins } from "./utils.ts"; export async function build() { try { @@ -23,6 +24,27 @@ export async function build() { const config = parse(Deno.readTextFileSync("pyro.yml")) as Config; const magic = getMagic(); + const plugins = config.plugins ? await loadPlugins(config.plugins) : []; + + for (const plugin of plugins) { + if (!plugin.routes || !plugin.handle) continue; + + for (const route of plugin.routes) { + const response = new Uint8Array( + await (await plugin.handle( + new Request("http://localhost:8000" + route), + )).arrayBuffer(), + ); + const path = resolve("build", route.slice(1)); + + if (route.includes(".")) { + await Deno.writeFile(path, response); + } else { + await Deno.mkdir(path, { recursive: true }); + await Deno.writeFile(resolve(path, "index.html"), response); + } + } + } for ( const entry of walkSync("./pages", { includeDirs: false, skip: [/\/_/] }) @@ -34,10 +56,10 @@ export async function build() { )!; const folder = extracted[1].slice(1).replace("index", ""); - Deno.mkdirSync(join("build", folder), { recursive: true }); + Deno.mkdirSync(resolve("build", folder), { recursive: true }); Deno.writeTextFileSync( resolve("build", folder, "index.html"), - await render(config, magic, folder), + await render(config, magic, folder, plugins), ); } } diff --git a/src/dev.ts b/src/dev.ts index f14004f..80f53e7 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -6,6 +6,7 @@ import { getMagic } from "./lib/magic.ts"; import { CSS } from "./lib/css.ts"; import { parse } from "std/encoding/yaml.ts"; import { Config } from "./lib/types.ts"; +import { loadPlugins } from "./utils.ts"; export async function dev(hostname = "0.0.0.0", port = 8000) { const config = parse(Deno.readTextFileSync("pyro.yml")) as Config; @@ -13,10 +14,20 @@ export async function dev(hostname = "0.0.0.0", port = 8000) { serve(async (req) => { const url = new URL(req.url); - const pathname = url.pathname.slice(1); + const pathname = url.pathname; + + const plugins = config.plugins ? await loadPlugins(config.plugins) : []; + + for (const plugin of plugins) { + if (!plugin.routes || !plugin.handle) continue; + + if (plugin.routes.includes(pathname)) { + return plugin.handle(req); + } + } // Handle the bundled css - if (pathname === "_pyro/bundle.css") { + if (pathname === "/_pyro/bundle.css") { return new Response(CSS, { headers: { "Content-Type": "text/css", @@ -25,7 +36,7 @@ export async function dev(hostname = "0.0.0.0", port = 8000) { } // Handle the reload js - if (pathname === "_pyro/reload.js") { + if (pathname === "/_pyro/reload.js") { return new Response( `new EventSource("/_pyro/reload").addEventListener("message", function listener(e) { if (e.data !== "${BUILD_ID}") { this.removeEventListener('message', listener); location.reload(); } });`, { @@ -36,7 +47,7 @@ export async function dev(hostname = "0.0.0.0", port = 8000) { ); } - if (pathname === "_pyro/reload") { + if (pathname === "/_pyro/reload") { let timerId: number | undefined = undefined; const body = new ReadableStream({ start(controller) { @@ -59,7 +70,7 @@ export async function dev(hostname = "0.0.0.0", port = 8000) { } // We're supposed to ignore hidden paths - if (pathname.includes("/_") || pathname.startsWith("_")) { + if (pathname.includes("/_")) { return new Response("404 File Not Found", { status: 404, }); @@ -81,7 +92,13 @@ export async function dev(hostname = "0.0.0.0", port = 8000) { } return new Response( - await render(config, getMagic(), resolve("pages", pathname), true), + await render( + config, + getMagic(), + resolve("pages", pathname.slice(1)), + plugins, + true, + ), { headers: { "Content-Type": "text/html; charset=utf-8", diff --git a/src/lib/css.ts b/src/lib/css.ts index 7c07f50..279fbd6 100644 --- a/src/lib/css.ts +++ b/src/lib/css.ts @@ -52,48 +52,7 @@ details > summary::-webkit-details-marker { --color-danger-fg:#cf222e } -/* Firefox */ -* { - scrollbar-width: auto; - scrollbar-color: #aeabd8 #dcdada; -} - -/* Chrome, Edge, and Safari */ -*::-webkit-scrollbar { - width: 14px; -} - -*::-webkit-scrollbar-track { - background: #dcdada; -} - -*::-webkit-scrollbar-thumb { - background-color: #aeabd8; - border-radius: 10px; - border: 0px none #050505; -} - @media (prefers-color-scheme:dark){ - /* Firefox */ - * { - scrollbar-width: auto; - scrollbar-color: #aeabd8 #000000; - } - - /* Chrome, Edge, and Safari */ - *::-webkit-scrollbar { - width: 14px; - } - - *::-webkit-scrollbar-track { - background: #000000; - } - - *::-webkit-scrollbar-thumb { - background-color: #aeabd8; - border-radius: 10px; - border: 0px none #050505; - } .markdown-body { --color-canvas-default-transparent:rgba(13,17,23,0); --color-prettylights-syntax-comment:#8b949e; diff --git a/src/lib/page.tsx b/src/lib/page.tsx index 0152876..033283a 100644 --- a/src/lib/page.tsx +++ b/src/lib/page.tsx @@ -1,4 +1,4 @@ -import type { Config, FileTypes, Magic, RouteMap } from "./types.ts"; +import type { Config, FileTypes, JSX, Magic, RouteMap } from "./types.ts"; import Github from "icons/brand-github.tsx"; import ExternalLink from "icons/external-link.tsx"; import { renderMD, renderMDX } from "../utils.ts"; @@ -17,6 +17,10 @@ export async function page(props: { magic: Magic; file_type: FileTypes; dev: boolean; + header: { + left: JSX.Element[]; + right: JSX.Element[]; + }; }; }) { return ( @@ -32,12 +36,14 @@ export async function page(props: { class="flex flex-col min-h-screen dark:text-gray-200" style={{ backgroundColor: props.options.magic.background }} > -
-

+
+

{props.options.config.title}

-
+ {props.options.header.left} +
+ {props.options.header.right} {props.options.config.github && ( diff --git a/src/lib/render.ts b/src/lib/render.ts index 8bf6aff..472b1b7 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -3,7 +3,7 @@ import { extract } from "std/encoding/front_matter/any.ts"; import { renderToString } from "preact-render-to-string"; import { page } from "./page.tsx"; import { get_route_map, resolve_file } from "./route_map.ts"; -import type { Config, Magic } from "./types.ts"; +import type { Config, Magic, PluginResult } from "./types.ts"; import { consume, @@ -13,6 +13,7 @@ import { tw, } from "https://esm.sh/@twind/core@1.1.3"; import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4"; +import { getHeaderElements } from "../utils.ts"; install(defineConfig({ presets: [presetTailwind()], @@ -22,6 +23,7 @@ export async function render( config: Config, magic: Magic, path: string, + plugins: PluginResult[], dev = false, ) { const [file_type, markdown] = resolve_file(resolve("pages", path)); @@ -43,6 +45,7 @@ export async function render( magic, file_type, dev, + header: getHeaderElements(plugins), }, }), ); diff --git a/src/lib/types.ts b/src/lib/types.ts index 1d1184b..8ffc5b3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,10 +1,37 @@ +import type { JSX } from "preact"; +export type { JSX } from "preact"; + +type MaybePromise = T | Promise; + export interface Config { title: string; github?: string; copyright?: string; footer?: Record; + plugins?: string[]; } +export type PluginResult = { + /** + * Header bar elements + */ + header?: { + left?: JSX.Element; + right?: JSX.Element; + }; + /** + * A method that returns a list of routes to handle. + * This has to be a finite list for static site building. + */ + routes?: string[]; + /** + * The method for actually handling whatever route + */ + handle?: (req: Request) => MaybePromise; +}; + +export type Plugin = () => MaybePromise; + export interface Magic { background: string; } diff --git a/src/utils.ts b/src/utils.ts index c524ca6..1bdddf3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import remarkGfm from "https://esm.sh/remark-gfm@3.0.1"; import rehypeHighlight from "https://esm.sh/@mapbox/rehype-prism@0.8.0"; import { compile } from "https://esm.sh/@mdx-js/mdx@2.2.1"; import { render } from "gfm"; -import type { FileTypes } from "./lib/types.ts"; +import type { FileTypes, JSX, Plugin, PluginResult } from "./lib/types.ts"; import "https://esm.sh/prismjs@1.29.0/components/prism-json?no-check"; import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check"; @@ -49,3 +49,28 @@ export async function renderMD(data: string) { disableHtmlSanitization: true, }); } + +export function loadPlugins(plugins: string[]) { + return Promise.all( + plugins.map(async (plugin) => ((await import(plugin)).default as Plugin)()), + ); +} + +export function getHeaderElements(plugins: PluginResult[]) { + const header = { + left: [] as JSX.Element[], + right: [] as JSX.Element[], + }; + + for (const plugin of plugins) { + if (!plugin.header) continue; + if (plugin.header.left) { + header.left = [...header.left, plugin.header.left]; + } + if (plugin.header.right) { + header.right = [...header.right, plugin.header.right]; + } + } + + return header; +} diff --git a/www/pages/core-concepts/plugins.md b/www/pages/core-concepts/plugins.md new file mode 100644 index 0000000..bce19d7 --- /dev/null +++ b/www/pages/core-concepts/plugins.md @@ -0,0 +1,36 @@ +--- +title: Plugins +description: Pyro was designed from the ground up to be no-config and incredibly fast. +index: 3 +--- + +While Pyro is designed to have all of the basic features you will need built-in, +there are some cases where one would want to extend the feature set. This can be +achieved with plugins. + +A simple plugin will look like so: + +```tsx +import { Plugin } from "https://deno.land/x/pyro/src/lib/types.ts"; + +const plugin: Plugin = () => { + return { + header: { + left: Demo, + right: Home, + }, + routes: ["/demo.png"], + handle: async () => { + const req = await fetch( + "https://github.com/lino-levan/pyro/raw/main/www/static/icon.png", + ); + return req; + }, + }; +}; + +export default plugin; +``` + +More examples can be found in +[the official plugins](https://github.com/lino-levan/pyro/tree/main/plugins). diff --git a/www/pages/getting-started/configuration.md b/www/pages/getting-started/configuration.md index 0cceb6d..267d933 100644 --- a/www/pages/getting-started/configuration.md +++ b/www/pages/getting-started/configuration.md @@ -33,6 +33,10 @@ footer: Community: - Discord https://discord.gg/XJMMSSC4Fj - Support https://github.com/lino-levan/pyro/issues/new + +# Any plugins you want to be used (optional) +plugin: + - https://deno.land/x/pyro/plugins/demo.tsx ``` ## How do I configure individual pages?