Move to Deno+Bunny.net, add a way to test webring locally.

This commit is contained in:
DS 2025-02-23 23:47:07 -08:00
parent 9f66f0b37b
commit 8670aa3412
33 changed files with 599 additions and 3360 deletions

4
.gitignore vendored
View file

@ -22,7 +22,7 @@ build/Release
.env.local
# Others
.direnv/
# Template rendering artifacts
public/*.html
# Build artifacts
build/
# Nix build result
result

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"deno.enable": true
}

View file

@ -1,19 +1,5 @@
# Nixifying a new host
If you have a bunch of SSH keys in your SSH agent and get errors when trying to SSH into a fresh host, you may need to temporarily add the following config to your SSH config (obviously change the details for your case).
The key is the line "IdentitiesOnly yes" along with an identity file.
```
Host 188.245.194.78
User root
IdentityFile ~/.ssh/hetzner_personal_root.pub
IdentitiesOnly yes
```
# Adding yourself to the webring
(Please only follow this if you're an Epesooj insider. Random PRs will be closed for now.)
Add your information to `webring_data.json`.
The schema is as follows:
@ -25,6 +11,11 @@ The schema is as follows:
Create a PR with your changes.
## Testing your entry
Run `deno task dev`, which should start a local web server.
You can then navigate to it to inspect how your entry will look on the webring's main page, as well as make sure the url it points to works fine.
# Using the webring on your blog/page
Somewhere in your blog/page (footer recommended), add the following links.
@ -37,3 +28,29 @@ Whenever your PR gets approved and merged, your id will be recognised and you'll
- Previous entry: `https://akols.com/previous?id=<YOUR_ID_HERE>`
- Next entry: `https://akols.com/next?id=<YOUR_ID_HERE>`
- A link to the webring directory: `https://akols.com`
# Development setup
## Setting up VSCode (and flavours)
We're using [Deno](https://deno.com) in the webring functions, so if you want proper editor support, you'll need to install the [Deno extension](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno).
There's already a `.vscode` directory which should direct VSCode to enable the Deno extension for this project.
# Infrastructure
## Nixifying a new host
If you have a bunch of SSH keys in your SSH agent and get errors when trying to SSH into a fresh host, you may need to temporarily add the following config to your SSH config (obviously change the details for your case).
The key is the line "IdentitiesOnly yes" along with an identity file.
```
Host 188.245.194.78
User root
IdentityFile ~/.ssh/hetzner_personal_root.pub
IdentitiesOnly yes
```
## Deploying the webring
You should have a `.env` file with the id and deploy key for each script in the webring, as well as a key to deploy the index page to bunny.
When you have this, run `deno task deploy`.

16
bunny/webring.ts Normal file
View file

@ -0,0 +1,16 @@
import * as BunnySDK from '@bunny.net/edgescript-sdk';
import { handleFunctions } from '../src/main.ts';
BunnySDK.net.http
.servePullZone({ url: 'https://akols.com' })
.onOriginRequest(
(ctx: { request: Request }): Promise<Request> | Promise<Response> => {
const funcResp = handleFunctions(ctx.request);
if (funcResp !== undefined) {
return Promise.resolve(funcResp);
}
return Promise.resolve(ctx.request);
}
);

View file

@ -111,7 +111,7 @@
globalConfig = ''
# Comment this if building the prod image. The following is only useful for testing.
local_certs
# local_certs
skip_install_trust
'';

25
deno.json Normal file
View file

@ -0,0 +1,25 @@
{
"tasks": {
"dev": "deno run --watch --allow-read --allow-net src/main.ts",
"clean": "rm -rf build",
"build-index": {
"command": "mkdir -p build && deno run --allow-read --allow-write scripts/build_index.ts",
"dependencies": ["clean"]
},
"bundle": {
"command": "mkdir -p build/bunny && deno run --allow-read --allow-write --allow-env --allow-run scripts/bundle_bunny.ts",
"dependencies": ["clean"]
},
"deploy": {
"command": "deno run --env-file --allow-env --allow-net --allow-read scripts/deploy_bunny.ts",
"dependencies": ["clean", "build-index", "bundle"]
}
},
"imports": {
"@bunny.net/edgescript-sdk": "npm:@bunny.net/edgescript-sdk@^0.11.3",
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.1",
"@std/assert": "jsr:@std/assert@1",
"@std/path": "jsr:@std/path@^1.0.8",
"esbuild": "npm:esbuild@^0.25"
}
}

185
deno.lock generated Normal file
View file

@ -0,0 +1,185 @@
{
"version": "4",
"specifiers": {
"jsr:@luca/esbuild-deno-loader@~0.11.1": "0.11.1",
"jsr:@std/assert@1": "1.0.11",
"jsr:@std/bytes@^1.0.2": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.7",
"jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/path@^1.0.6": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8",
"npm:@bunny.net/edgescript-sdk@~0.11.3": "0.11.3_hono@4.7.2",
"npm:@types/node@*": "22.5.4",
"npm:esbuild@*": "0.25.0",
"npm:esbuild@0.25": "0.25.0"
},
"jsr": {
"@luca/esbuild-deno-loader@0.11.1": {
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
"dependencies": [
"jsr:@std/bytes",
"jsr:@std/encoding",
"jsr:@std/path@^1.0.6"
]
},
"@std/assert@1.0.11": {
"integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/bytes@1.0.5": {
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
},
"@std/encoding@1.0.7": {
"integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d"
},
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
}
},
"npm": {
"@bunny.net/edgescript-sdk@0.11.3_hono@4.7.2": {
"integrity": "sha512-w7x/72NMR1Ofjs5DcntR35P6d2ApeBOD7LCe8s+e49bnKQKfehIOp6O9WCD2tgX9SJg06jSHBrfCEdJFNZMQyg==",
"dependencies": [
"@hono/node-server",
"hono"
]
},
"@esbuild/aix-ppc64@0.25.0": {
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="
},
"@esbuild/android-arm64@0.25.0": {
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="
},
"@esbuild/android-arm@0.25.0": {
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="
},
"@esbuild/android-x64@0.25.0": {
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="
},
"@esbuild/darwin-arm64@0.25.0": {
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="
},
"@esbuild/darwin-x64@0.25.0": {
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="
},
"@esbuild/freebsd-arm64@0.25.0": {
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="
},
"@esbuild/freebsd-x64@0.25.0": {
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="
},
"@esbuild/linux-arm64@0.25.0": {
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="
},
"@esbuild/linux-arm@0.25.0": {
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="
},
"@esbuild/linux-ia32@0.25.0": {
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="
},
"@esbuild/linux-loong64@0.25.0": {
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="
},
"@esbuild/linux-mips64el@0.25.0": {
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="
},
"@esbuild/linux-ppc64@0.25.0": {
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="
},
"@esbuild/linux-riscv64@0.25.0": {
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="
},
"@esbuild/linux-s390x@0.25.0": {
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="
},
"@esbuild/linux-x64@0.25.0": {
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="
},
"@esbuild/netbsd-arm64@0.25.0": {
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="
},
"@esbuild/netbsd-x64@0.25.0": {
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="
},
"@esbuild/openbsd-arm64@0.25.0": {
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="
},
"@esbuild/openbsd-x64@0.25.0": {
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="
},
"@esbuild/sunos-x64@0.25.0": {
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="
},
"@esbuild/win32-arm64@0.25.0": {
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="
},
"@esbuild/win32-ia32@0.25.0": {
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="
},
"@esbuild/win32-x64@0.25.0": {
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="
},
"@hono/node-server@1.13.8_hono@4.7.2": {
"integrity": "sha512-fsn8ucecsAXUoVxrUil0m13kOEq4mkX4/4QozCqmY+HpGfKl74OYSn8JcMA8GnG0ClfdRI4/ZSeG7zhFaVg+wg==",
"dependencies": [
"hono"
]
},
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"esbuild@0.25.0": {
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
"@esbuild/android-x64",
"@esbuild/darwin-arm64",
"@esbuild/darwin-x64",
"@esbuild/freebsd-arm64",
"@esbuild/freebsd-x64",
"@esbuild/linux-arm",
"@esbuild/linux-arm64",
"@esbuild/linux-ia32",
"@esbuild/linux-loong64",
"@esbuild/linux-mips64el",
"@esbuild/linux-ppc64",
"@esbuild/linux-riscv64",
"@esbuild/linux-s390x",
"@esbuild/linux-x64",
"@esbuild/netbsd-arm64",
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
]
},
"hono@4.7.2": {
"integrity": "sha512-8V5XxoOF6SI12jkHkzX/6aLBMU5GEF5g387EjVSQipS0DlxWgWGSMeEayY3CRBjtTUQYwLHx9JYouWqKzy2Vng=="
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}
},
"workspace": {
"dependencies": [
"jsr:@luca/esbuild-deno-loader@~0.11.1",
"jsr:@std/assert@1",
"jsr:@std/path@^1.0.8",
"npm:@bunny.net/edgescript-sdk@~0.11.3",
"npm:esbuild@0.25"
]
}
}

View file

@ -61,7 +61,7 @@
default = pkgs.mkShell {
packages = with pkgs; [
just
nodePackages_latest.nodejs
deno
colmena

View file

@ -1,8 +0,0 @@
import { next } from '../src/webring';
export const onRequestGet: PagesFunction = async (context) => {
const id = new URL(context.request.url).searchParams.get('id') ?? 'main';
const item = next(id);
const url = item?.url ?? new URL(context.request.url).origin;
return Response.redirect(url, 303);
};

View file

@ -1,8 +0,0 @@
import { previous } from '../src/webring';
export const onRequestGet: PagesFunction = async (context) => {
const id = new URL(context.request.url).searchParams.get('id') ?? 'main';
const item = previous(id);
const url = item?.url ?? new URL(context.request.url).origin;
return Response.redirect(url, 303);
};

View file

@ -1,6 +0,0 @@
import { randomEntry } from '../src/webring';
export const onRequestGet: PagesFunction = async (context) => {
const url = randomEntry()?.url ?? new URL(context.request.url).origin;
return Response.redirect(url, 303);
};

3213
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +0,0 @@
{
"name": "epesooj-webring",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler pages dev",
"start": "wrangler pages dev",
"test": "vitest",
"cf-typegen": "wrangler types",
"render-templates": "tsx src/render_templates.ts"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.5.12",
"@cloudflare/workers-types": "^4.20240925.0",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"vitest": "^2.1.1",
"wrangler": "^3.78.12"
}
}

View file

@ -1,5 +0,0 @@
{
"version": 1,
"include": ["/previous", "/random", "/next"],
"exclude": ["/"]
}

10
scripts/build_index.ts Normal file
View file

@ -0,0 +1,10 @@
import { join } from 'node:path';
import { generateIndex } from '../src/render_templates.ts';
if (import.meta.main) {
const encoder = new TextEncoder();
Deno.writeFileSync(
join(import.meta.dirname ?? '', '../build/index.html'),
encoder.encode(generateIndex())
);
}

46
scripts/bundle_bunny.ts Normal file
View file

@ -0,0 +1,46 @@
import * as esbuild from 'esbuild';
import { denoPlugins } from '@luca/esbuild-deno-loader';
import { join } from '@std/path';
if (import.meta.main) {
for (const dirEntry of Deno.readDirSync(
join(import.meta.dirname ?? '', '../bunny')
)) {
if (dirEntry.isFile && dirEntry.name.endsWith('.ts')) {
const outFilename =
dirEntry.name.substring(0, dirEntry.name.length - 3) + '.js';
const res = await esbuild.build({
plugins: [
...denoPlugins({
loader: 'native',
}),
],
entryPoints: [
join(import.meta.dirname ?? '', '../bunny', dirEntry.name),
],
outfile: join(import.meta.dirname ?? '', '../build/bunny', outFilename),
bundle: true,
format: 'esm',
});
await esbuild.stop();
if (res.errors.length > 0) {
console.error(
`Got errors when trying to bundle ${dirEntry.name}: ${JSON.stringify(
res.errors
)}`
);
}
if (res.warnings.length > 0) {
console.warn(
`Got warnings when trying to bundle ${
dirEntry.name
}: ${JSON.stringify(res.warnings)}`
);
}
}
}
}

130
scripts/bunny_api/main.ts Normal file
View file

@ -0,0 +1,130 @@
export async function deployScript(scriptName: string, contents: string) {
const scriptIdEnvName = `SCRIPT_ID_${scriptName.toUpperCase()}`;
const scriptId = Deno.env.get(scriptIdEnvName);
const deployKeyEnvName = `SCRIPT_KEY_${scriptName.toUpperCase()}`;
const deployKey = Deno.env.get(deployKeyEnvName);
if (scriptId === undefined) {
throw new Error(
`Can't deploy script '${scriptName}' because we don't know its id on bunny. Please set it by setting the environment variable '${scriptIdEnvName}'.`
);
}
if (deployKey === undefined) {
throw new Error(
`Can't deploy script '${scriptName}' because we don't know its deploy key on bunny. Please set it by setting the environment variable '${scriptIdEnvName}'.`
);
}
const headers = {
accept: 'application/json',
'content-type': 'application/json',
deploymentkey: deployKey,
};
const setCodeResp = await fetch(
`https://api.bunny.net/compute/script/${scriptId}/code`,
{
method: 'POST',
headers,
body: JSON.stringify({ Code: contents }),
}
);
if (!setCodeResp.ok) {
console.error(await setCodeResp.text());
throw new Error(
`Failed to upload new script code: ${setCodeResp.statusText}`
);
}
const publishResp = await fetch(
`https://api.bunny.net/compute/script/${scriptId}/publish`,
{
method: 'POST',
headers,
}
);
if (!publishResp.ok) {
console.error(await setCodeResp.text());
throw new Error(`Failed to publish script: ${setCodeResp.statusText}`);
}
}
export async function uploadFile(filepath: string, contents: Uint8Array) {
const storageZoneNameEnvName = 'STORAGE_ZONE_NAME';
const storageZoneName = Deno.env.get(storageZoneNameEnvName);
const storageApiKeyEnvName = 'STORAGE_API_KEY';
const apiKey = Deno.env.get(storageApiKeyEnvName);
if (storageZoneName === undefined) {
throw new Error(
`Can't upload file '${filepath}' because we don't know which storage zone to upload to. Please set it by setting the environment variable '${storageZoneNameEnvName}'.`
);
}
if (apiKey === undefined) {
throw new Error(
`Can't upload file '${filepath}' because we don't have an API key. Please set it by setting the environment variable '${storageApiKeyEnvName}'.`
);
}
const headers = {
accept: 'application/json',
'content-type': 'application/octet-stream',
accesskey: apiKey,
};
const res = await fetch(
`https://storage.bunnycdn.com/${storageZoneName}/${filepath}`,
{
method: 'PUT',
headers,
body: contents,
}
);
if (!res.ok) {
console.error(await res.text());
throw new Error(`Failed to upload file: ${res.statusText}`);
}
}
export async function purgePath(filepath: string) {
const cdnBaseUrlEnvName = 'BUNNY_CDN_BASE_URL';
const cdnBaseUrl = Deno.env.get(cdnBaseUrlEnvName);
const accessKeyEnvName = 'BUNNY_ACCESS_KEY';
const accessKey = Deno.env.get(accessKeyEnvName);
if (cdnBaseUrl === undefined) {
throw new Error(
`Can't purge cache for '${filepath}' because we don't know the CDN base URL. Please set it by setting the environment variable '${cdnBaseUrlEnvName}'.`
);
}
if (accessKey === undefined) {
throw new Error(
`Can't purge cache for '${filepath}' because we don't have an API key. Please set it by setting the environment variable '${accessKeyEnvName}'.`
);
}
const headers = {
accept: 'application/json',
'content-type': 'application/json',
accesskey: accessKey,
};
const fetchUrl = new URL(`https://api.bunny.net/purge`);
fetchUrl.searchParams.append('async', 'false');
fetchUrl.searchParams.append('url', `${cdnBaseUrl}/${filepath}`);
const res = await fetch(fetchUrl.toString(), {
method: 'POST',
headers,
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Failed to purge cache: ${res.statusText}`);
}
}

32
scripts/deploy_bunny.ts Normal file
View file

@ -0,0 +1,32 @@
import { join } from '@std/path';
import { deployScript, purgePath, uploadFile } from './bunny_api/main.ts';
if (import.meta.main) {
console.log(`Attempting to upload index.html`);
const localFilepath = join(import.meta.dirname ?? '', '../build/index.html');
const indexContents = Deno.readFileSync(localFilepath);
await uploadFile('index.html', indexContents);
console.log(`Done!`);
console.log(`Attempting to purge the cache for index.html`);
await purgePath('index.html');
console.log(`Done!`);
for (const dirEntry of Deno.readDirSync(
join(import.meta.dirname ?? '', '../build/bunny')
)) {
if (dirEntry.isFile && dirEntry.name.endsWith('.js')) {
const scriptName = dirEntry.name.substring(0, dirEntry.name.length - 3);
const decoder = new TextDecoder('utf-8');
const contents = decoder.decode(
Deno.readFileSync(
join(import.meta.dirname ?? '', '../build/bunny', dirEntry.name)
)
);
console.log(`Attempting to deploy script '${scriptName}'`);
await deployScript(scriptName, contents);
console.log(`Done!`);
}
}
}

50
src/main.ts Normal file
View file

@ -0,0 +1,50 @@
import { generateIndex } from './render_templates.ts';
import { redirectNext } from './next.ts';
import { redirectPrevious } from './previous.ts';
import { redirectRandom } from './random.ts';
export function handleFunctions(req: Request) {
const url = new URL(req.url);
switch (url.pathname) {
case '/next': {
return redirectNext(req);
}
case '/previous': {
return redirectPrevious(req);
}
case '/random': {
return redirectRandom(req);
}
}
return undefined;
}
if (import.meta.main) {
const index = generateIndex();
Deno.serve({ hostname: '127.0.0.1' }, (req) => {
const funcResp = handleFunctions(req);
if (funcResp !== undefined) {
return funcResp;
}
const url = new URL(req.url);
if (url.pathname === '/') {
return new Response(index, {
status: 200,
headers: {
'content-type': 'text/html',
},
});
}
return new Response(null, {
status: 404,
});
});
}

15
src/next.ts Normal file
View file

@ -0,0 +1,15 @@
import { next } from './webring.ts';
export function redirectNext(req: Request) {
const id = new URL(req.url).searchParams.get('id') ?? 'main';
const item = next(id);
const url = item?.url ?? new URL(req.url).origin;
return new Response('', {
status: 303,
headers: {
location: url,
'cache-control': 'no-cache, no-store, no-transform',
},
});
}

15
src/previous.ts Normal file
View file

@ -0,0 +1,15 @@
import { previous } from './webring.ts';
export function redirectPrevious(req: Request) {
const id = new URL(req.url).searchParams.get('id') ?? 'main';
const item = previous(id);
const url = item?.url ?? new URL(req.url).origin;
return new Response('', {
status: 303,
headers: {
location: url,
'cache-control': 'no-cache, no-store, no-transform',
},
});
}

13
src/random.ts Normal file
View file

@ -0,0 +1,13 @@
import { randomEntry } from './webring.ts';
export function redirectRandom(req: Request) {
const url = randomEntry()?.url ?? new URL(req.url).origin;
return new Response('', {
status: 303,
headers: {
location: url,
'cache-control': 'no-cache, no-store, no-transform',
},
});
}

View file

@ -1,15 +1,19 @@
import { WebringData, WebringEntry } from './webring';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { WebringData, WebringEntry } from './webring.ts';
import { join } from '@std/path';
function generateEntryHTML(entry: WebringEntry) {
return `<div><a href="${entry.url}">${entry.title}</a><span>by ${entry.author}</span></div>`;
}
let index = readFileSync(
join(import.meta.dirname, '../templates/index.html'),
'utf8'
);
const renderedEntries = WebringData.map(generateEntryHTML).join('\n');
index = index.replace('{{WEBRING_ENTRIES}}', renderedEntries);
writeFileSync(join(import.meta.dirname, '../public/index.html'), index);
export function generateIndex() {
const decoder = new TextDecoder('utf-8');
let index = decoder.decode(
Deno.readFileSync(
join(import.meta.dirname ?? '', '../templates/index.html')
)
);
const renderedEntries = WebringData.map(generateEntryHTML).join('\n');
index = index.replace('{{WEBRING_ENTRIES}}', renderedEntries);
return index;
}

View file

@ -1,6 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["node"]
}
}

View file

@ -1,4 +1,4 @@
import WebringData from '../webring_data.json';
import WebringData from '../webring_data.json' with { type: 'json' };
export { WebringData };
export type WebringEntry = (typeof WebringData)[number];

View file

@ -54,13 +54,13 @@
<a href="/next?id=main">Next site</a>
</nav>
<h2>Epesooj webring</h2>
<h1>Epesooj webring</h1>
<h2 id="webring-directory">Directory</h2>
<nav class="directory" aria-labelledby="webring-directory">
{{WEBRING_ENTRIES}}
</nav>
<span class="source-link">The source for this webring can be viewed <a href="https://github.com/DanielSidhion/epesooj-webring">here</a>.</span>
<span class="source-link">The source for this webring can be viewed <a href="https://code.akols.com/epesooj/webring">here</a>.</span>
</body>
</html>

View file

@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('Hello World worker', () => {
it('succeeds simple test', async () => {
expect(1 + 1).toBe(2);
});
});

View file

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts", "../functions/env.d.ts"],
"exclude": []
}

7
test/webring_test.ts Normal file
View file

@ -0,0 +1,7 @@
import { assertEquals } from '@std/assert';
import { next } from '../src/webring.ts';
Deno.test(function nextTest() {
const nextEntry = next('main');
assertEquals(nextEntry.id, 'dslog');
});

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["esnext"],
"module": "esnext",
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"types": ["@cloudflare/workers-types/2023-07-01"],
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts", "functions/**/*.ts"]
}

View file

@ -1,11 +0,0 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.toml' },
},
},
},
});

View file

@ -1,5 +0,0 @@
// Generated by Wrangler by running `wrangler types`
interface Env {
NODE_VERSION: "22.8.0";
}

View file

@ -1,8 +0,0 @@
#:schema node_modules/wrangler/config-schema.json
name = "epesooj-webring"
pages_build_output_dir = "./public"
compatibility_date = "2024-09-27"
compatibility_flags = ["nodejs_compat"]
[vars]
NODE_VERSION = "22.8.0"