Skip to content

Commit

Permalink
feat: plugin system (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
lino-levan authored Mar 27, 2023
1 parent 4464c6a commit 5b63610
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 56 deletions.
19 changes: 19 additions & 0 deletions plugins/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Plugin } from "../src/lib/types.ts";

const plugin: Plugin = () => {
return {
header: {
left: <a href="/demo.png">Demo</a>,
right: <a href="/" class="hover:bg-red-500">Home</a>,
},
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;
28 changes: 25 additions & 3 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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: [/\/_/] })
Expand All @@ -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),
);
}
}
29 changes: 23 additions & 6 deletions src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,28 @@ 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;
let BUILD_ID = Math.random().toString();

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",
Expand All @@ -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(); } });`,
{
Expand All @@ -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) {
Expand All @@ -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,
});
Expand All @@ -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",
Expand Down
41 changes: 0 additions & 41 deletions src/lib/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions src/lib/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -17,6 +17,10 @@ export async function page(props: {
magic: Magic;
file_type: FileTypes;
dev: boolean;
header: {
left: JSX.Element[];
right: JSX.Element[];
};
};
}) {
return (
Expand All @@ -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 }}
>
<header class="w-full h-16 shadow-sm flex items-center px-4 justify-between bg-white dark:bg-black z-10">
<h1 class="font-semibold text-lg text-gray-800 flex items-center gap-2 dark:text-gray-200">
<header class="w-full h-16 shadow-sm flex gap-4 items-center px-4 bg-white dark:bg-black z-10">
<h1 class="font-semibold text-lg text-gray-800 flex items-center gap-2 dark:text-gray-200 mr-4">
<image src="/icon.png" class="w-8 h-8" />
{props.options.config.title}
</h1>
<div class="flex items-center">
{props.options.header.left}
<div class="flex gap-4 items-center ml-auto">
{props.options.header.right}
{props.options.config.github && (
<a target="_blank" href={props.options.config.github}>
<Github />
Expand Down
5 changes: 4 additions & 1 deletion src/lib/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +13,7 @@ import {
tw,
} from "https://esm.sh/@twind/[email protected]";
import presetTailwind from "https://esm.sh/@twind/[email protected]";
import { getHeaderElements } from "../utils.ts";

install(defineConfig({
presets: [presetTailwind()],
Expand All @@ -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));
Expand All @@ -43,6 +45,7 @@ export async function render(
magic,
file_type,
dev,
header: getHeaderElements(plugins),
},
}),
);
Expand Down
27 changes: 27 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import type { JSX } from "preact";
export type { JSX } from "preact";

type MaybePromise<T> = T | Promise<T>;

export interface Config {
title: string;
github?: string;
copyright?: string;
footer?: Record<string, string[]>;
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<Response>;
};

export type Plugin = () => MaybePromise<PluginResult>;

export interface Magic {
background: string;
}
Expand Down
27 changes: 26 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import remarkGfm from "https://esm.sh/[email protected]";
import rehypeHighlight from "https://esm.sh/@mapbox/[email protected]";
import { compile } from "https://esm.sh/@mdx-js/[email protected]";
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/[email protected]/components/prism-json?no-check";
import "https://esm.sh/[email protected]/components/prism-bash?no-check";
Expand Down Expand Up @@ -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;
}
36 changes: 36 additions & 0 deletions www/pages/core-concepts/plugins.md
Original file line number Diff line number Diff line change
@@ -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: <a href="/demo.png">Demo</a>,
right: <a href="/" class="hover:bg-red-500">Home</a>,
},
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).
4 changes: 4 additions & 0 deletions www/pages/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down

0 comments on commit 5b63610

Please sign in to comment.