Newest

Code on Val Town

306790319-d5b13560-7b3a-458a-98c9-1696ef40d972.png

Tell your website visitors about your val! This val is a fetch handler middleware. It inserts a "Code on Val Town" ribbon that links to the val itself. Your website visitors will be able to view your code, fork the val, and suggest changes with pull requests!

Usage

There are 5 different ways to add the "Code on Val Town" ribbon!

1. Use the element string

import { ribbonElement } from "https://esm.town/v/andreterron/codeOnValTown";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default async (req: Request): Promise<Response> => {
  return html(`
    <h2>Hello world!</h2>
    <style>* { font-family: sans-serif }</style>
    ${ribbonElement()}
  `);
};

2. Wrap your HTML string

import { htmlString } from "https://esm.town/v/andreterron/codeOnValTown";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default async (req: Request): Promise<Response> => {
  return html(htmlString(`
    <h2>Hello world!</h2>
    <style>* { font-family: sans-serif }</style>
  `));
};

3. Pipe your HTML stream through our injector

// TODO

4. Wrap your response

import { modifyResponse } from "https://esm.town/v/andreterron/codeOnValTown";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default async (req: Request): Promise<Response> => {
  return modifyResponse(html(`
    <h2>Hello world!</h2>
    <style>* { font-family: sans-serif }</style>
  `));
};

5. Wrap your fetch handler

Check out @andreterron/openable for an example!

import { modifyFetchHandler } from "https://esm.town/v/andreterron/codeOnValTown";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";

export default modifyFetchHandler(async (req: Request): Promise<Response> => {
  return html(`
    <h2>Hello world!</h2>
    <style>* { font-family: sans-serif }</style>
  `);
});
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { inferRequestVal } from "https://esm.town/v/andreterron/inferRequestVal";
import { InjectHTMLElementStream } from "https://esm.town/v/andreterron/InjectHTMLElementStream";
import { rootRef } from "https://esm.town/v/andreterron/rootRef";
export function ribbonElement(val?: ValRef) {
const valRef = val?.handle && val?.name ? val : rootValRef();
if (!valRef) {
console.error("Failed to infer val. Please set the val parameter to the desired `{ handle, name }`");
return "";
}
const valSlug = `${valRef.handle}/${valRef.name}`;
return `
<div id="code-on-vt-host">
<template shadowrootmode="open">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
<a
href="https://www.val.town/v/${valSlug}"
target="_blank"
class="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</template>
</div>`;
}
/**
* @param bodyText HTML string that will be used to inject the element.
* @param options.val Define which val should open. Defaults to the root reference.
*/
export function htmlString(
bodyText: string,
options: {
val?: {
handle: string;
name: string;
};
} = {},
) {
const val = options.val?.handle && options.val?.name ? options.val : rootValRef();
if (!val) {
console.error("Failed to infer val. Please set the options.val parameter to the desired `{ handle, name }`");
1
2
3
4
5
6
7
8
9
10
11
12
import { hydrateRoot } from "https://esm.sh/react-dom@18.2.0/client";
import React from "https://esm.sh/react@18.2.0";
import { jsx as _jsx } from "https://esm.sh/react@18.2.0/jsx-runtime";
const islands = Array.from(document.querySelectorAll("[data-hydration-val]"));
for (const island of islands) {
const slug = island.getAttribute("data-hydration-val");
console.log(`hydrating ${slug}`);
const { default: Component } = await import(`https://esm.town/v/${slug}`);
const props = JSON.parse(island.getAttribute("data-hydration-props"));
hydrateRoot(island, _jsx(Component, props));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** @jsxImportSource https://esm.sh/react */
export function Island({
children,
val,
}) {
const hydrationProps = JSON.stringify(children.props);
return (
<div
data-hydration-val={val}
data-hydration-props={hydrationProps}
>
{children}
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import * as jose from "https://deno.land/x/jose@v5.2.2/index.ts";
import type { MaybePromise } from "https://esm.town/v/postpostscript/typeUtils"
export type JWK = jose.JWK;
export function createVerifyMethod(keys: () => MaybePromise<JWK[]>) {
return async function verify(token: string, options: jose.JWTVerifyOptions) {
const publicKey = await jose.importJWK((await keys)[0]);
const { payload } = await jose.jwtVerify(token, publicKey, options);
return payload;
};
}
export function createGenerateMethod(keys: () => MaybePromise<JWK[]>) {
return async function generate(payload: any, exp?: string | number | Date) {
const _keys = await keys();
const privateKey = await jose.importJWK(_keys[0]);
let jwt = new jose.SignJWT(payload)
.setProtectedHeader({ alg: _keys[0].alg })
.setIssuedAt();
if (exp) {
jwt = jwt.setExpirationTime(exp);
}
return jwt.sign(privateKey);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export type ShaAlgorithm = "SHA-256" | "SHA-512";
export async function sha(
input: string | BufferSource | AsyncIterable<BufferSource> | Iterable<BufferSource>,
algo: ShaAlgorithm = "SHA-256",
) {
const [{ crypto }, { encodeHex }] = await Promise.all([
import("https://deno.land/std@0.207.0/crypto/mod.ts"),
import("https://deno.land/std@0.207.0/encoding/hex.ts"),
]);
const messageBuffer = typeof input === "string"
? new TextEncoder().encode(input)
: input;
const hashBuffer = await crypto.subtle.digest(algo, messageBuffer);
return encodeHex(hashBuffer);
}
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";
import { Feed } from "npm:feed";
import ky from "npm:ky";
const BASE_URL = "https://midnight.pub";
// @see: https://git.sr.ht/~m15o/midnight-pub/tree/master/item/model/user.go#L28
const reUserFeed = /^\/(?<username>~[a-z0-9-_]+)\.(?<format>atom|rss)$/;
export default async function(req: Request): Promise<Response> {
const { pathname } = new URL(req.url);
const match = pathname.match(reUserFeed);
if (!match) {
return new Response(null, { status: 400 });
}
const { format, username } = match.groups;
const profileURL = new URL(`/${username}`, BASE_URL);
const posts = await grabPosts(profileURL);
const feed = new Feed({
id: profileURL.href,
link: profileURL.href,
title: username,
description: `${username}'s posts`,
author: {
name: username,
link: profileURL.href,
},
feedLinks: {
atom: req.url,
rss: req.url,
},
copyright: undefined, // I have no idea what to put there ¯\_(ツ)_/¯
});
posts.forEach(post =>
feed.addItem({
id: post.href,
link: post.href,
date: post.createdAt,
title: post.title,
description: post.title,
})
);
if (format === "rss") {
const headers = { "content-type": "application/xml" };
return new Response(feed.rss2(), { headers });
}
const headers = { "content-type": "application/atom+xml" };
return new Response(feed.atom1(), { headers });
}

test: simple testing framework

See @postpostscript/testTest for examples

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export * from "https://deno.land/std@0.217.0/assert/mod.ts";
type Test<TOptions> = {
name: string;
method: (
options?: TOptions,
console?: {
log?: (...args: any[]) => void;
error?: (...args: any[]) => void;
},
) => any;
};
type TestContextOptions = {
name: string;
verbose?: boolean;
concurrent?: boolean;
[key: string]: any;
};
class SubtestFailed extends Error {
}
export function testContext<TOptions extends TestContextOptions>(options: TOptions) {
const tests: Test<TOptions>[] = [];
return {
tests,
options,
it(name: string, method: Test<TOptions>["method"]) {
tests.push({ name: `it ${name}`, method });
},
test(name: string, method: Test<TOptions>["method"]) {
tests.push({ name: `test ${name}`, method });
},
describe(name: string, method: Test<TOptions>["method"]) {
tests.push({ name, method });
},
sub<TSubOptions extends TestContextOptions>(
_options: TSubOptions,
method: (
context: ReturnType<typeof testContext<TOptions & TSubOptions>>,
options?: TOptions,
console?: {
log?: (...args: any[]) => void;
error?: (...args: any[]) => void;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export function hybridTaggedTemplateMethod<R = string>({
transformString = (str: string) => str,
transformReplacement = (replacement: unknown) => replacement,
transformResult = (result: string) => result as R,
}: {
transformString?: (str: string) => unknown;
transformReplacement?: (replacement: unknown) => unknown;
transformResult?: (result: string) => R;
}) {
const hybridTaggedTemplate: {
(strings: TemplateStringsArray, ...replacements: unknown[]): R;
(string: unknown): R;
} = (strings, ...replacements) => {
if (!(strings instanceof Array)) {
return hybridTaggedTemplate([strings]);
}
const result = strings.slice(1).reduce((result, string, i) => {
const _string = transformString(string);
const replacement = transformReplacement(replacements[i]);
return `${result}${replacement}${_string}`;
}, transformString(strings[0]));
return transformResult(result);
};
return hybridTaggedTemplate;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { blob } from "https://esm.town/v/std/blob?v=11";
import { email } from "https://esm.town/v/std/email?v=11";
import { fetchText } from "https://esm.town/v/stevekrouse/fetchText?v=6";
import { JSDOM } from "npm:jsdom";
const username = "ccorcos";
export default async function(interval: Interval) {
const url = "https://news.ycombinator.com/threads?id=" + username;
const html = await fetchText(url);
console.log("html", html.slice(0, 100));
const { document } = (new JSDOM(html)).window;
const comments = Array.from(document.querySelectorAll(".athing.comtr")) as any[];
const ids = new Set(comments.map(x => x.id));
const key = "hn2:" + username;
const lastIds = (await blob.getJSON(key)) || [] as string[];
for (const id of lastIds) ids.delete(id);
const newIds = Array.from(ids);
if (newIds.length === 0) {
console.log("No new comments.");
return;
}
console.log(newIds.length + " new comments.");
await blob.setJSON(key, Array.from(ids));
const newComments = comments.filter(comment => ids.has(comment.id))
.map(comment => comment.textContent);
await email({
subject: "New HN comments",
text: url + "\n\n" + newComments.join("\n---\n"),
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
await fetchJSON("https://api.val.town/v1/vals", {
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
},
method: "PUT",
body: JSON.stringify({
name: "valCreatedUsingPut",
code: "const myName = 'valCreatedUsingPut'",
privacy: "public",
}),
});