mirror of
https://github.com/gosticks/fresh.git
synced 2025-10-16 11:55:35 +00:00
init
This commit is contained in:
parent
f05c2e3a68
commit
6ecdf2ed73
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": false,
|
||||
"deno.config": "./tsconfig.json"
|
||||
}
|
||||
27
cli.ts
Normal file
27
cli.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { join, toFileUrl } from "https://deno.land/std@0.95.0/path/mod.ts";
|
||||
import { walk } from "https://deno.land/std@0.95.0/fs/walk.ts";
|
||||
|
||||
const files = [];
|
||||
const pagesDir = join(Deno.cwd(), "./pages");
|
||||
const pagesUrl = new URL(pagesDir, "file:///");
|
||||
for await (
|
||||
const entry of walk(pagesDir, {
|
||||
includeDirs: false,
|
||||
includeFiles: true,
|
||||
exts: ["tsx", "jsx"],
|
||||
})
|
||||
) {
|
||||
if (entry.isFile) {
|
||||
const file = toFileUrl(entry.path).href.substring(pagesUrl.href.length);
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
const output = `import { setup } from "../server.ts";
|
||||
|
||||
${files.map((file, i) => `import * as $${i} from "./pages${file}";`).join("\n")}
|
||||
|
||||
setup([${files.map((_, i) => `$${i}`).join(", ")}], import.meta.url);
|
||||
`;
|
||||
|
||||
Deno.writeTextFile("./server.ts", output);
|
||||
1
example/deps.ts
Normal file
1
example/deps.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "../runtime.ts";
|
||||
16
example/pages/[name].tsx
Normal file
16
example/pages/[name].tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { h, PageProps, useState } from "../deps.ts";
|
||||
|
||||
export default function Home(props: PageProps) {
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
Hello {props.params.name}! {counter}{" "}
|
||||
<button onClick={() => setCounter(counter + 1)}>+1</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function self() {
|
||||
return import.meta.url;
|
||||
}
|
||||
9
example/pages/index.tsx
Normal file
9
example/pages/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { h } from "../deps.ts";
|
||||
|
||||
export default function Home(props: {}) {
|
||||
return <div>Hello World!</div>;
|
||||
}
|
||||
|
||||
export function self() {
|
||||
return import.meta.url;
|
||||
}
|
||||
6
example/server.ts
Normal file
6
example/server.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setup } from "../server.ts";
|
||||
|
||||
import * as $0 from "./pages/[name].tsx";
|
||||
import * as $1 from "./pages/index.tsx";
|
||||
|
||||
setup([$0, $1], import.meta.url);
|
||||
2
runtime.ts
Normal file
2
runtime.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./src/runtime/deps.ts";
|
||||
export * from "./src/runtime/types.ts";
|
||||
23
src/runtime/deps.ts
Normal file
23
src/runtime/deps.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export * from "https://x.lcas.dev/preact@10.5.12/mod.js";
|
||||
export type {
|
||||
CreateHandle,
|
||||
EffectCallback,
|
||||
Inputs,
|
||||
PropRef,
|
||||
Reducer,
|
||||
Ref,
|
||||
StateUpdater,
|
||||
} from "https://x.lcas.dev/preact@10.5.12/hooks.js";
|
||||
export {
|
||||
useCallback,
|
||||
useContext,
|
||||
useDebugValue,
|
||||
useEffect,
|
||||
useErrorBoundary,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from "https://x.lcas.dev/preact@10.5.12/hooks.js";
|
||||
3
src/runtime/types.ts
Normal file
3
src/runtime/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface PageProps {
|
||||
params: Record<string, string | string[]>;
|
||||
}
|
||||
45
src/server/bundle.ts
Normal file
45
src/server/bundle.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { denoPlugin, esbuild } from "./deps.ts";
|
||||
import { Page } from "./routes.ts";
|
||||
|
||||
let esbuildInitalized: boolean | Promise<void> = false;
|
||||
async function ensureEsbuildInialized() {
|
||||
if (esbuildInitalized === false) {
|
||||
esbuildInitalized = await esbuild.initialize({
|
||||
wasmURL: "https://unpkg.com/esbuild-wasm@0.11.19/esbuild.wasm",
|
||||
worker: false,
|
||||
});
|
||||
await esbuildInitalized;
|
||||
esbuildInitalized = true;
|
||||
} else if (esbuildInitalized instanceof Promise) {
|
||||
await esbuildInitalized;
|
||||
}
|
||||
}
|
||||
|
||||
export async function bundle(page: Page): Promise<string> {
|
||||
const runtime = new URL("../../runtime.ts", import.meta.url);
|
||||
const contents = `
|
||||
import Page from "${page.url}";
|
||||
import { h, render } from "${runtime.href}";
|
||||
|
||||
addEventListener("DOMContentLoaded", () => {
|
||||
const props = JSON.parse(document.getElementById("__FRSH_PROPS").textContent);
|
||||
render(h(Page, props), document.body);
|
||||
});
|
||||
`;
|
||||
await ensureEsbuildInialized();
|
||||
const bundle = await esbuild.build({
|
||||
plugins: [denoPlugin({ loader: "portable" })],
|
||||
write: false,
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: "neutral",
|
||||
outfile: "",
|
||||
jsxFactory: "h",
|
||||
jsxFragment: "Fragment",
|
||||
stdin: {
|
||||
contents,
|
||||
sourcefile: `fresh://entrypoint/${page.name}`,
|
||||
},
|
||||
});
|
||||
return bundle.outputFiles[0].text;
|
||||
}
|
||||
13
src/server/deps.ts
Normal file
13
src/server/deps.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// -- preact --
|
||||
export { renderToString } from "https://x.lcas.dev/preact@10.5.12/ssr.js";
|
||||
|
||||
// -- std --
|
||||
export { extname } from "https://deno.land/std@0.95.0/path/mod.ts";
|
||||
|
||||
// -- oak --
|
||||
export * as oak from "https://deno.land/x/oak@v7.4.0/mod.ts";
|
||||
|
||||
// -- esbuild --
|
||||
import * as esbuild from "https://cdn.skypack.dev/esbuild-wasm?dts";
|
||||
export { esbuild };
|
||||
export { denoPlugin } from "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/c5ee642f4552078324badbf1b541c7222c07d5ff/mod.ts";
|
||||
13
src/server/mod.ts
Normal file
13
src/server/mod.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createPages, PageModules } from "./routes.ts";
|
||||
import { createServer } from "./server.ts";
|
||||
|
||||
export function setup(pageModules: PageModules[], selfUrl: string) {
|
||||
const baseUrl = new URL("./", selfUrl).href;
|
||||
const pages = createPages(pageModules, baseUrl);
|
||||
const app = createServer(pages);
|
||||
app.addEventListener("error", (err) => {
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
addEventListener("fetch", app.fetchEventHandler());
|
||||
}
|
||||
11
src/server/render.ts
Normal file
11
src/server/render.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { renderToString } from "./deps.ts";
|
||||
import * as rt from "../runtime/deps.ts";
|
||||
import { Page } from "./routes.ts";
|
||||
|
||||
export function render(page: Page, params: Record<string, string>): string {
|
||||
const props = { params };
|
||||
const body = renderToString(rt.h(page.component, props));
|
||||
return `<!DOCTYPE html><html><head><script src="/_frsh/s/p/${page.name}.module.js" type="module"></script></head><body>${body}<script id="__FRSH_PROPS" type="application/json">${
|
||||
JSON.stringify(props)
|
||||
}</script></body></html>`;
|
||||
}
|
||||
72
src/server/routes.ts
Normal file
72
src/server/routes.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { extname } from "./deps.ts";
|
||||
import * as rt from "../runtime/deps.ts";
|
||||
import { PageProps } from "../runtime/types.ts";
|
||||
|
||||
export interface PageModules {
|
||||
default: rt.ComponentType<PageProps>;
|
||||
self: () => string;
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
route: string;
|
||||
url: string;
|
||||
name: string;
|
||||
component: rt.ComponentType<PageProps>;
|
||||
}
|
||||
|
||||
export function createPages(pageModules: PageModules[], baseUrl: string): Page[] {
|
||||
const pages: Page[] = [];
|
||||
for (const pageModule of pageModules) {
|
||||
const url = pageModule.self();
|
||||
if (!url.startsWith(baseUrl)) {
|
||||
throw new TypeError("Page is not a child of the basepath.");
|
||||
}
|
||||
const path = url.substring(baseUrl.length).substring("pages".length);
|
||||
const name = path.substring(1, path.length - extname(path).length);
|
||||
const route = pathToRoute(name);
|
||||
const page: Page = {
|
||||
route,
|
||||
url,
|
||||
name: name.replace("/", "-"),
|
||||
component: pageModule.default,
|
||||
};
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
pages.sort((a, b) => {
|
||||
const partsA = a.route.split("/");
|
||||
const partsB = b.route.split("/");
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||
const partA = partsA[i];
|
||||
const partB = partsB[i];
|
||||
if (partA === undefined) return -1;
|
||||
if (partB === undefined) return 1;
|
||||
if (partA === partB) continue;
|
||||
const priorityA = partA.startsWith(":") ? partA.endsWith("*") ? 0 : 1 : 2;
|
||||
const priorityB = partB.startsWith(":") ? partB.endsWith("*") ? 0 : 1 : 2;
|
||||
return Math.max(Math.min(priorityB - priorityA, 1), -1);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
function pathToRoute(path: string): string {
|
||||
const parts = path.split("/");
|
||||
if (parts[parts.length - 1] === "index") {
|
||||
parts.pop();
|
||||
}
|
||||
const route = "/" + parts
|
||||
.map((part) => {
|
||||
if (part.startsWith("[...") && part.endsWith("]")) {
|
||||
return `:${part.slice(4, part.length - 1)}*`;
|
||||
}
|
||||
if (part.startsWith("[") && part.endsWith("]")) {
|
||||
return `:${part.slice(1, part.length - 1)}`;
|
||||
}
|
||||
return part;
|
||||
})
|
||||
.join("/");
|
||||
return route;
|
||||
}
|
||||
30
src/server/server.ts
Normal file
30
src/server/server.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { oak } from "./deps.ts";
|
||||
import { bundle } from "./bundle.ts";
|
||||
import { render } from "./render.ts";
|
||||
import { Page } from "./routes.ts";
|
||||
|
||||
export function createServer(pages: Page[]): oak.Application {
|
||||
const router = new oak.Router();
|
||||
|
||||
for (const page of pages) {
|
||||
router.get<Record<string, string>>(page.route, (ctx) => {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.type = "html";
|
||||
ctx.response.body = render(page, ctx.params);
|
||||
});
|
||||
|
||||
router.get(`/_frsh/s/p/${page.name}.module.js`, async (ctx) => {
|
||||
const js = await bundle(page);
|
||||
ctx.response.status = 200;
|
||||
ctx.response.type = "js";
|
||||
ctx.response.body = js;
|
||||
});
|
||||
}
|
||||
|
||||
const app = new oak.Application();
|
||||
|
||||
app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
|
||||
return app;
|
||||
}
|
||||
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user