Skip to content

Commit 6edd26c

Browse files
committed
chore: move routing logic and share code loading to the backend
1 parent 94361e0 commit 6edd26c

File tree

8 files changed

+94
-110
lines changed

8 files changed

+94
-110
lines changed

src/client/App.tsx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,40 +36,17 @@ import {
3636
import isEqual from "lodash/isEqual";
3737
import { MoonIcon, ShareIcon, SunIcon, SunMoonIcon } from "lucide-react";
3838
import { type FC, useEffect, useMemo, useRef, useState } from "react";
39-
import { type LoaderFunctionArgs, useLoaderData } from "react-router";
4039
import { useDebouncedValue } from "./hooks/debounce";
4140

42-
/**
43-
* Load the shared code if present.
44-
*/
45-
export const loader = async ({ params }: LoaderFunctionArgs) => {
46-
const { id } = params;
47-
if (!id) {
48-
return;
49-
}
50-
51-
try {
52-
const res = await rpc.parameters[":id"].$get({ param: { id } });
53-
if (res.ok) {
54-
const { code } = await res.json();
55-
return code;
56-
}
57-
} catch (e) {
58-
console.error(`Error loading playground: ${e}`);
59-
return;
60-
}
61-
};
62-
6341
export const App = () => {
6442
const [wasmLoadState, setWasmLoadingState] = useState<WasmLoadState>(() => {
6543
if (window.go_preview) {
6644
return "loaded";
6745
}
6846
return "loading";
6947
});
70-
const loadedCode = useLoaderData<typeof loader>();
7148
const [code, setCode] = useState(
72-
loadedCode ?? window.EXAMPLE_CODE ?? defaultCode,
49+
window.CODE ?? defaultCode,
7350
);
7451
const [debouncedCode, isDebouncing] = useDebouncedValue(code, 1000);
7552
const [parameterValues, setParameterValues] = useState<

src/client/index.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@ import { TooltipProvider } from "@/client/components/Tooltip";
22
import { ThemeProvider } from "@/client/contexts/theme.tsx";
33
import { StrictMode } from "react";
44
import { createRoot } from "react-dom/client";
5-
import { RouterProvider, createBrowserRouter, redirect } from "react-router";
6-
import { App, loader as appLoader } from "./App.tsx";
5+
import { RouterProvider, createBrowserRouter } from "react-router";
6+
import { App } from "./App.tsx";
77
import "@/client/index.css";
88
import { EditorProvider } from "./contexts/editor.tsx";
99

1010
const router = createBrowserRouter([
11-
{
12-
loader: appLoader,
13-
path: "/parameters/:id?",
14-
Component: App,
15-
},
1611
{
1712
path: "*",
18-
loader: () => {
19-
return redirect("/parameters");
20-
},
13+
Component: App,
2114
},
2215
]);
2316

src/server/api.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

src/server/blob.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { head, put } from "@vercel/blob";
2+
import { nanoid } from "nanoid";
3+
import * as v from "valibot";
4+
5+
export const BLOG_PATH = "parameters/share";
6+
7+
export const ShareDataSchema = v.object({ code: v.string() });
8+
type ShareData = v.InferInput<typeof ShareDataSchema>;
9+
10+
export const putShareData = async (data: ShareData): Promise<string> => {
11+
const id = nanoid(10);
12+
await put(`${BLOG_PATH}/${id}.json`, JSON.stringify(data), {
13+
addRandomSuffix: false,
14+
access: "public",
15+
});
16+
17+
return id;
18+
};
19+
20+
export const getShareData = async (id: string): Promise<{ code: string; } | null> => {
21+
try {
22+
const { url } = await head(`${BLOG_PATH}/${id}.json`);
23+
const res = await fetch(url);
24+
const data = JSON.parse(new TextDecoder().decode(await res.arrayBuffer()));
25+
26+
const parsedData = v.safeParse(ShareDataSchema, data);
27+
if (!parsedData.success) {
28+
console.error("Unable to parse share data", parsedData.issues);
29+
return null;
30+
}
31+
32+
return parsedData.output;
33+
} catch (e) {
34+
console.error(`Failed to load playground with id ${id}: ${e}`);
35+
return null;
36+
}
37+
};

src/server/index.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { examples } from "@/examples/code";
2-
import { api } from "@/server/api";
2+
import { api } from "@/server/routes/api";
33
import { Hono } from "hono";
44
import { renderToString } from "react-dom/server";
5+
import { getShareData } from "./blob";
6+
import { trimTrailingSlash } from "hono/trailing-slash";
57

68
// This must be exported for the dev server to work
79
export const app = new Hono();
@@ -16,12 +18,21 @@ app.use("*", async (ctx, next) => {
1618
});
1719
app.route("/api", api);
1820

21+
app.use(trimTrailingSlash());
22+
1923
// Serves the main web application. This must come after the API route.
20-
app.get("*", (c) => {
21-
const getExampleCode = () => {
24+
app.get("/parameters/:shareId?", async (c) => {
25+
const getExampleCode = async (): Promise<string | null> => {
26+
const { shareId } = c.req.param();
2227
const { example } = c.req.query();
28+
29+
if (shareId) {
30+
const shareData = await getShareData(shareId);
31+
return shareData?.code ?? null;
32+
}
33+
2334
if (!example) {
24-
return;
35+
return null;
2536
}
2637

2738
return examples[example];
@@ -54,7 +65,7 @@ app.get("*", (c) => {
5465
? "/assets/wasm_exec.js"
5566
: "/wasm_exec.js";
5667
const iconPath = import.meta.env.PROD ? "/assets/logo.svg" : "/logo.svg";
57-
const exampleCode = getExampleCode();
68+
const exampleCode = await getExampleCode();
5869

5970
return c.html(
6071
[
@@ -76,7 +87,7 @@ app.get("*", (c) => {
7687
<body>
7788
<div id="root"></div>
7889
{exampleCode ? (
79-
<script type="module">{`window.EXAMPLE_CODE = ${JSON.stringify(exampleCode)}`}</script>
90+
<script type="module">{`window.CODE = ${JSON.stringify(exampleCode)}`}</script>
8091
) : null}
8192
<script type="module" src={clientScriptPath}></script>
8293
</body>
@@ -85,3 +96,5 @@ app.get("*", (c) => {
8596
].join("\n"),
8697
);
8798
});
99+
100+
app.get("*", (c) => c.redirect("/parameters"));

src/server/routes/api.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { vValidator } from "@hono/valibot-validator";
2+
import { Hono } from "hono";
3+
import * as v from "valibot";
4+
import { putShareData } from "@/server/blob";
5+
6+
7+
const parameters = new Hono().post(
8+
"/",
9+
vValidator(
10+
"json",
11+
v.object({
12+
code: v.string(),
13+
}),
14+
),
15+
async (c) => {
16+
const data = c.req.valid("json");
17+
const bytes = new TextEncoder().encode(JSON.stringify(data));
18+
19+
// Check if the data is larger than 1mb
20+
if (bytes.length > 1024 * 1000) {
21+
console.error("Data larger than 10kb");
22+
return c.json({ id: "" }, 500);
23+
}
24+
25+
const id = await putShareData(data);
26+
return c.json({ id });
27+
},
28+
);
29+
30+
export const api = new Hono().route("/parameters", parameters);
31+
32+
export type ApiType = typeof api;

src/utils/rpc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ApiType } from "@/server/api";
1+
import type { ApiType } from "@/server/routes/api";
22
import { hc } from "hono/client";
33

44
export const rpc = hc<ApiType>("/api");

src/utils/wasm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ declare global {
2020
// Loaded from wasm
2121
go_preview?: GoPreviewDef;
2222
Go: { new (): Go };
23-
EXAMPLE_CODE?: string;
23+
CODE?: string;
2424
}
2525
}
2626

0 commit comments

Comments
 (0)