diff --git a/.github/workflows/check.tsx b/.github/workflows/check.tsx index 7646b56..b443178 100644 --- a/.github/workflows/check.tsx +++ b/.github/workflows/check.tsx @@ -1,3 +1,5 @@ +/** @jsxImportSource @fartlabs/jsonx */ + import { stringify } from "@std/yaml"; import { CheckoutStep, SetupDenoStep } from "./shared.tsx"; diff --git a/.github/workflows/shared.tsx b/.github/workflows/shared.tsx index f238fda..28dd5bd 100644 --- a/.github/workflows/shared.tsx +++ b/.github/workflows/shared.tsx @@ -1,3 +1,5 @@ +/** @jsxImportSource @fartlabs/jsonx */ + /** * CheckoutStep is a step that checks out the repository. */ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b4bdef --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# Fresh build directory +_fresh/ +# npm dependencies +node_modules/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..09cf720 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "denoland.vscode-deno" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d99cbf..3a4d063 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,19 +1,22 @@ { "deno.enable": true, + "deno.lint": true, "deno.unstable": true, "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[markdown]": { + "editor.defaultFormatter": "denoland.vscode-deno", + "[typescriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno" }, - "[jsonc]": { + "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" }, - "[typescript]": { + "[javascriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno" }, - "[typescriptreact]": { + "[javascript]": { "editor.defaultFormatter": "denoland.vscode-deno" }, - "files.eol": "\n" + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/README.md b/README.md index 294edc0..1f66a39 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,6 @@ Run `deno task generate` to generate code. Run `deno task start` to locally serve the jsonx documentation site. -### Testing - -Run `deno task test` to run the unit tests. - --- Developed with ❤️ [**@FartLabs**](https://github.com/FartLabs) diff --git a/client/api.ts b/client/api.ts new file mode 100644 index 0000000..807af01 --- /dev/null +++ b/client/api.ts @@ -0,0 +1,44 @@ +/** + * Playground is a jsonx playground. + */ +export interface Playground { + id: string; + version: string; + code: string; +} + +/** + * AddPlaygroundRequest is the request to add a playground. + */ +export type AddPlaygroundRequest = Omit; + +/** + * getPLaygroundByID gets a playground by playground ID. + */ +export function getPlaygroundByID(id: string): Promise { + return fetch( + `/playgrounds/${id}`, + { + headers: { accept: "application/json" }, + }, + ).then((response) => response.json()); +} + +/** + * addPlayground adds a new playground. + */ +export function addPlayground( + request: AddPlaygroundRequest, +): Promise { + return fetch( + `/playgrounds`, + { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body: JSON.stringify(request), + }, + ).then((response) => response.json()); +} diff --git a/client/meta.ts b/client/meta.ts new file mode 100644 index 0000000..216f79e --- /dev/null +++ b/client/meta.ts @@ -0,0 +1,32 @@ +import { compare, greaterThan, parse } from "@std/semver"; + +/** + * Meta is the module metadata. + */ +export interface Meta { + latest: string; + versions: string[]; +} + +/** + * getMeta gets the latest module metadata. + */ +export function getMeta(): Promise { + return fetch("https://jsr.io/@fartlabs/jsonx/meta.json") + .then((response) => response.json()) + .then((meta) => playgroundMeta(meta)); +} + +function playgroundMeta({ latest, versions }: { + latest: string; + versions: Record; +}): Meta { + // https://github.com/FartLabs/jsonx/issues/13 + const minCompatible = parse("0.0.8"); + return { + latest: latest, + versions: Object.keys(versions) + .filter((versionTag) => greaterThan(parse(versionTag), minCompatible)) + .sort((a, b) => compare(parse(b), parse(a))), + }; +} diff --git a/client/nav.tsx b/client/nav.tsx new file mode 100644 index 0000000..7afc393 --- /dev/null +++ b/client/nav.tsx @@ -0,0 +1,28 @@ +export default function Nav() { + return ( + + ); +} diff --git a/static/index.html b/client/playground/playground.tsx similarity index 53% rename from static/index.html rename to client/playground/playground.tsx index d12cc9f..4f595a3 100644 --- a/static/index.html +++ b/client/playground/playground.tsx @@ -1,38 +1,30 @@ - - - - - - jsonx - - - - +export interface PlaygroundProps { + code: string; + meta: Meta; + version?: string; + autoplay?: boolean; +} - -
+export default function Playground(props: PlaygroundProps) { + return ( + <> +
TSX - + @@ -49,7 +41,7 @@
    - + {/* */}
    Build logs @@ -58,7 +50,7 @@
      - + {/* */}
      Result @@ -73,16 +65,13 @@ sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation allow-downloads allow-presentation" allow="accelerometer *; camera *; encrypted-media *; display-capture *; geolocation *; gyroscope *; microphone *; midi *; clipboard-read *; clipboard-write *; web-share *; serial *; xr-spatial-tracking *" scrolling="auto" - allowtransparency="true" - allowpaymentrequest="true" - allowfullscreen="true" + allowTransparency={true} + allowFullScreen={true} loading="lazy" - spellcheck="false" + spellCheck={false} >
      -
      - - - - + + ); +} diff --git a/client/playground/scripts.tsx b/client/playground/scripts.tsx new file mode 100644 index 0000000..d07b1ac --- /dev/null +++ b/client/playground/scripts.tsx @@ -0,0 +1,23 @@ +export interface PlaygroundScriptsProps { + code: string; + autoplay?: boolean; +} + +export default function PlaygroundScripts(props: PlaygroundScriptsProps) { + return ( +
      `, + }} + > +
      + ); +} + +function initializeData(props: PlaygroundScriptsProps) { + return ``; +} diff --git a/client/playgrounds.ts b/client/playgrounds.ts new file mode 100644 index 0000000..e69de29 diff --git a/deno.jsonc b/deno.jsonc index eb7ebd0..0513f83 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,18 +1,43 @@ { - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "@fartlabs/jsonx" + "lock": false, + "tasks": { + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", + "manifest": "deno task cli manifest $(pwd)", + "start": "deno run -A --watch=static/,routes/ dev.ts", + "build": "deno run -A dev.ts build", + "preview": "deno run -A main.ts", + "update": "deno run -Ar https://fresh.deno.dev/update .", + "generate": "deno run -Ar https://deno.land/x/generate/cli/main.ts gen.ts" + }, + "unstable": ["kv"], + "lint": { + "rules": { + "tags": [ + "fresh", + "recommended" + ] + } }, + "exclude": [ + "**/_fresh/*", + "./static" + ], "imports": { - "@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.9", - "@std/cli": "jsr:@std/cli@^0.220.1", + "#/": "./", + "$fresh/": "https://deno.land/x/fresh@1.6.5/", + "$std/": "https://deno.land/std@0.211.0/", + "@fartlabs/jsonx": "jsr:@fartlabs/jsonx@^0.0.10", + "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", "@std/http": "jsr:@std/http@^0.220.1", "@std/semver": "jsr:@std/semver@^0.220.1", - "@std/yaml": "jsr:@std/yaml@^0.220.1" + "@std/yaml": "jsr:@std/yaml@^0.220.1", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/" }, - "tasks": { - "start": "deno run -A --unstable-kv main.ts", - "generate": "deno run -Ar https://deno.land/x/generate/cli/main.ts gen.ts" - }, - "exclude": ["./static"] + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } } diff --git a/deno.lock b/deno.lock deleted file mode 100644 index ff7d5ad..0000000 --- a/deno.lock +++ /dev/null @@ -1,87 +0,0 @@ -{ - "version": "3", - "packages": { - "specifiers": { - "jsr:@fartlabs/jsonx@^0.0.9": "jsr:@fartlabs/jsonx@0.0.9", - "jsr:@std/assert@^0.220.1": "jsr:@std/assert@0.220.1", - "jsr:@std/async@^0.220.1": "jsr:@std/async@0.220.1", - "jsr:@std/cli@^0.220.1": "jsr:@std/cli@0.220.1", - "jsr:@std/collections@^0.219.1": "jsr:@std/collections@0.219.1", - "jsr:@std/encoding@^0.220.1": "jsr:@std/encoding@0.220.1", - "jsr:@std/fmt@^0.220.1": "jsr:@std/fmt@0.220.1", - "jsr:@std/http@^0.220.1": "jsr:@std/http@0.220.1", - "jsr:@std/media-types@^0.220.1": "jsr:@std/media-types@0.220.1", - "jsr:@std/path@^0.220.1": "jsr:@std/path@0.220.1", - "jsr:@std/semver@^0.220.1": "jsr:@std/semver@0.220.1", - "jsr:@std/streams@^0.220.1": "jsr:@std/streams@0.220.1", - "jsr:@std/yaml@^0.220.1": "jsr:@std/yaml@0.220.1" - }, - "jsr": { - "@fartlabs/jsonx@0.0.9": { - "integrity": "ec269a8e436e3052c5ea7a60b3ac525639a1c39b847e3fe14b9396070384a72f", - "dependencies": [ - "jsr:@std/collections@^0.219.1" - ] - }, - "@std/assert@0.220.1": { - "integrity": "88710d54f3afdd7a5761e7805abba1f56cd14e4b212feffeb3e73a9f77482425" - }, - "@std/async@0.220.1": { - "integrity": "61f0e9c53bf81f516aa7f54843f307eb5f05c5cf9902575a636ead162b5dfafe" - }, - "@std/cli@0.220.1": { - "integrity": "6c38388efc899c9d98811d38a46effe01e0d91138068483f1e6ae834caad5e82", - "dependencies": [ - "jsr:@std/assert@^0.220.1" - ] - }, - "@std/collections@0.219.1": { - "integrity": "ed0adf354efaf90069a1e9af263b9941f6a370d16e8a98f2fc67d12ed75d825a" - }, - "@std/encoding@0.220.1": { - "integrity": "8dc38dd72e36cd68857a5837e24eb09a64bb296b96c295239c75eec17d45d23f" - }, - "@std/fmt@0.220.1": { - "integrity": "3b1a698477a26b1dacbbee26db1a65030a005c6d71db3b3a0e59f8a638f04d7a" - }, - "@std/http@0.220.1": { - "integrity": "1672a8d31f3e0ccf1c55221907daba4a22f3760cf2d079afb26def2dcfd0513b", - "dependencies": [ - "jsr:@std/assert@^0.220.1", - "jsr:@std/async@^0.220.1", - "jsr:@std/cli@^0.220.1", - "jsr:@std/encoding@^0.220.1", - "jsr:@std/fmt@^0.220.1", - "jsr:@std/media-types@^0.220.1", - "jsr:@std/path@^0.220.1", - "jsr:@std/streams@^0.220.1" - ] - }, - "@std/media-types@0.220.1": { - "integrity": "9dea30f7e0c87426ad8736678137ad277f1450f858efa1ef2c3aad6d9029b682" - }, - "@std/path@0.220.1": { - "integrity": "cc63c1b5e5192e2f718dc2f365a3514fffb26cc0380959bab0f8fb5988a52f0c" - }, - "@std/semver@0.220.1": { - "integrity": "f3df4abc715ad723680011f19d2934cfd436b4c41c317e82bfc14ac586b3b724" - }, - "@std/streams@0.220.1": { - "integrity": "f26c7cb98c471af89de8309a0246cccb4c9bb6f3c639bd84aee5488127dc8545" - }, - "@std/yaml@0.220.1": { - "integrity": "55406daf8582067f9989e31d9fa13cf0c8dd3aa85d322055967ff60250b3d033" - } - } - }, - "remote": {}, - "workspace": { - "dependencies": [ - "jsr:@fartlabs/jsonx@^0.0.9", - "jsr:@std/cli@^0.220.1", - "jsr:@std/http@^0.220.1", - "jsr:@std/semver@^0.220.1", - "jsr:@std/yaml@^0.220.1" - ] - } -} diff --git a/dev.ts b/dev.ts new file mode 100644 index 0000000..ae73946 --- /dev/null +++ b/dev.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; +import config from "./fresh.config.ts"; + +import "$std/dotenv/load.ts"; + +await dev(import.meta.url, "./main.ts", config); diff --git a/docs.ts b/docs.ts deleted file mode 100644 index 03d3a9e..0000000 --- a/docs.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { compare, greaterThan, parse } from "@std/semver"; -import { serveDir } from "@std/http"; - -/** - * Playground is a jsonx playground. - */ -export interface Playground { - id: string; - version: string; - code: string; -} - -/** - * Meta is the module metadata. - */ -export interface Meta { - latest: string; - versions: string[]; -} - -/** - * Docs is a class for serving the documentation site including - * jsonx playgrounds. - */ -export class Docs { - public constructor(private fsRoot: string, private kv: Deno.Kv) {} - - /** - * fetch handles incoming requests. - */ - public async fetch(request: Request): Promise { - const url = new URL(request.url); - if (request.method === "GET" && url.pathname === "/meta") { - return Response.json(await this.meta); - } - - if (request.method === "GET" && url.pathname.startsWith("/playgrounds/")) { - const id = url.pathname.split("/")[2]; - const playground = await this.getPlayground(id); - if (playground === null) { - return new Response(null, { status: 404 }); - } - - return Response.json(playground); - } - - if (request.method === "POST" && url.pathname === "/playgrounds") { - const playground = await request.json(); - await this.setPlayground(playground); - return Response.json(playground); - } - - return await serveDir(request, { fsRoot: this.fsRoot }); - } - - /** - * getPlayground gets a playground by ID. - */ - private async getPlayground(id: string): Promise { - const result = await this.kv.get(playgroundKey(id)); - return result.value; - } - - /** - * setPlayground sets a playground by ID. - */ - private async setPlayground(playground: Playground): Promise { - const result = await this.kv.set(playgroundKey(playground.id), playground); - if (!result) { - throw new Error("Failed to set playground!"); - } - } - - /** - * meta gets the latest module metadata. - */ - private get meta(): Promise { - return fetch("https://jsr.io/@fartlabs/jsonx/meta.json") - .then((response) => response.json()) - .then((meta) => playgroundMeta(meta)); - } -} - -/** - * createDocs creates a new Docs instance. - */ -export function createDocs({ fsRoot, kv }: { - fsRoot: string; - kv: Deno.Kv; -}): Docs { - return new Docs(fsRoot, kv); -} - -function playgroundMeta({ latest, versions }: { - latest: string; - versions: Record; -}): Meta { - // https://github.com/FartLabs/jsonx/issues/13 - const minCompatible = parse("0.0.8"); - return { - latest: latest, - versions: Object.keys(versions) - .filter((versionTag) => greaterThan(parse(versionTag), minCompatible)) - .sort((a, b) => compare(parse(b), parse(a))), - }; -} - -function playgroundKey(id: string): Deno.KvKey { - return ["playgrounds", id]; -} diff --git a/fresh.config.ts b/fresh.config.ts new file mode 100644 index 0000000..e7b63d9 --- /dev/null +++ b/fresh.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "$fresh/server.ts"; + +export default defineConfig({}); diff --git a/fresh.gen.ts b/fresh.gen.ts new file mode 100644 index 0000000..d9c0d9b --- /dev/null +++ b/fresh.gen.ts @@ -0,0 +1,29 @@ +// DO NOT EDIT. This file is generated by Fresh. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $_404 from "./routes/_404.tsx"; +import * as $_app from "./routes/_app.tsx"; +import * as $examples_id_ from "./routes/examples/[id].ts"; +import * as $index from "./routes/index.tsx"; +import * as $meta from "./routes/meta.ts"; +import * as $playgrounds_id_ from "./routes/playgrounds/[id].tsx"; +import * as $playgrounds_index from "./routes/playgrounds/index.ts"; + +import { type Manifest } from "$fresh/server.ts"; + +const manifest = { + routes: { + "./routes/_404.tsx": $_404, + "./routes/_app.tsx": $_app, + "./routes/examples/[id].ts": $examples_id_, + "./routes/index.tsx": $index, + "./routes/meta.ts": $meta, + "./routes/playgrounds/[id].tsx": $playgrounds_id_, + "./routes/playgrounds/index.ts": $playgrounds_index, + }, + islands: {}, + baseUrl: import.meta.url, +} satisfies Manifest; + +export default manifest; diff --git a/main.ts b/main.ts index 557bcd2..675f529 100644 --- a/main.ts +++ b/main.ts @@ -1,20 +1,13 @@ -import { parseArgs } from "@std/cli"; -import { createDocs } from "./docs.ts"; +/// +/// +/// +/// +/// -if (import.meta.main) { - const flags = parseArgs(Deno.args, { - string: ["port", "root"], - default: { - port: "8000", - root: "static", - }, - }); - const docs = createDocs({ - fsRoot: flags.root, - kv: await Deno.openKv(), - }); - Deno.serve( - { port: parseInt(flags.port) }, - docs.fetch.bind(docs), - ); -} +import "$std/dotenv/load.ts"; + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; +import config from "./fresh.config.ts"; + +await start(manifest, config); diff --git a/routes/_404.tsx b/routes/_404.tsx new file mode 100644 index 0000000..c63ae2e --- /dev/null +++ b/routes/_404.tsx @@ -0,0 +1,27 @@ +import { Head } from "$fresh/runtime.ts"; + +export default function Error404() { + return ( + <> + + 404 - Page not found + +
      +
      + the Fresh logo: a sliced lemon dripping with juice +

      404 - Page not found

      +

      + The page you were looking for doesn't exist. +

      + Go back home +
      +
      + + ); +} diff --git a/routes/_app.tsx b/routes/_app.tsx new file mode 100644 index 0000000..2748bee --- /dev/null +++ b/routes/_app.tsx @@ -0,0 +1,16 @@ +import { type PageProps } from "$fresh/server.ts"; + +export default function App({ Component }: PageProps) { + return ( + + + + + jsonx | Documentation + + + + + + ); +} diff --git a/routes/examples/[id].ts b/routes/examples/[id].ts new file mode 100644 index 0000000..19dfb2c --- /dev/null +++ b/routes/examples/[id].ts @@ -0,0 +1,9 @@ +import type { Handlers } from "$fresh/server.ts"; +import { getExampleByName } from "#/server/examples/mod.ts"; + +export const handler: Handlers = { + async GET(_request, ctx) { + const example = await getExampleByName(ctx.params.id); + return new Response(example); + }, +}; diff --git a/routes/index.tsx b/routes/index.tsx new file mode 100644 index 0000000..c43ad75 --- /dev/null +++ b/routes/index.tsx @@ -0,0 +1,42 @@ +import { Head } from "$fresh/runtime.ts"; +import Playground from "#/client/playground/playground.tsx"; +import Nav from "#/client/nav.tsx"; +import { getMeta } from "#/client/meta.ts"; +import { kv } from "#/server/kv.ts"; +import { getExampleByName } from "#/server/examples/mod.ts"; +import { getPlayground } from "#/server/playgrounds.ts"; + +export default async function Home(request: Request) { + const url = new URL(request.url); + const id = url.searchParams.get("id"); + let code = ""; + let version: string | undefined = undefined; + if (id) { + const playground = await getPlayground(kv, id); + if (!playground) { + throw new Error("Playground not found!"); + } + + code = playground.code; + version = playground.version; + } else { + code = await getExampleByName("01_animals.tsx"); + } + + const meta = await getMeta(); + + return ( + <> + + jsonx | Documentation + + + +