Avatar

@nbbaier

26 likes122 public vals
Joined January 12, 2023
Valin' up a storm

Empty Val Utils

Handy utility functions to see if you have vals with no code lying around your account and to delete them (is you want to).

Usage

listEmptyVals

Create valimport {listEmptyVals } from "https://esm.town/v/nbbaier/deleteEmptyVals"; console.log(await listEmptyVals(<user_id>))

deleteEmptyVals

Create valimport {listEmptyVals } from "https://esm.town/v/nbbaier/deleteEmptyVals"; await deleteEmptyVals(<user_id>)
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
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
import { deleteVal } from "https://esm.town/v/neverstew/deleteVal";
export async function listEmptyVals(id: string) {
const token = Deno.env.get("valtown");
const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.filter((v) => (v.code.length === 0)).map(v => v.name);
}
export async function deleteEmptyVals(id: string) {
const token = Deno.env.get("valtown");
const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
headers: { Authorization: `Bearer ${token}` },
});
const vals = res.filter((v) => (v.code.length === 0)).forEach(
async v => {
try {
const result = await deleteVal({
token,
id: v.id,
});
if (result.status === 204) {
console.log(`Successfully deleted val: ${v.name}`);
}
} catch (error) {
console.log(error);
}
},
);
}
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { resetStyle } from "https://esm.town/v/nbbaier/resetStyle";
import { fetchText } from "https://esm.town/v/stevekrouse/fetchText?v=6";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
import { Readability } from "npm:@mozilla/readability";
// @ts-expect-error
import jsdom from "npm:jsdom";
type Article = {
title: string;
content: string;
textContent: string;
length: number;
excerpt: string;
byline: string;
dir: string;
siteName: string;
lang: string;
publishedTime: string;
};
const pageShell = (title: string, pageContent: string) => {
return (`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<title>${title}</title>
<style>
${resetStyle}
html {
font-family: "Source Sans 3", system-ui, sans-serif;
padding: 0.75rem;
margin: auto;
max-width: 90ch;
}
.title-container {
display: flex;
flex-direction: column;
margin-block-end: 0.25rem
}
.subhead {
margin-block-start: 0.5rem;
color: gray;
}
</style>
</head>
<body>
${pageContent}
</body>
</html>
`);
};
export default async function(req: Request): Promise<Response> {
const JSDOM = jsdom.JSDOM;
const url = new URL(req.url);
const articleUrl = url.pathname.substring(1);
if (articleUrl === "") {
return html(pageShell(
"VT Reader",
`
<h1>Val Town Reader</h1>
<p>Enter a url below to get a reader view using <a href="https://github.com/mozilla/readability">Readability.js</a></p>
<form style="display: flex; gap: .25rem; margin-block-start:1rem;" id="nameForm">
<input type="text" id="name" name="name" placeholder="Enter a url" required>
<br>
<input type="submit" value="Submit">
</form>
<script>
document.getElementById('nameForm').onsubmit = function(event) {
event.preventDefault();
const targetURL = document.getElementById("name").value
const newURL = "${url.origin}" + "/" + targetURL
window.location = newURL
};
</script>
</body> `,
));
}
let body = await fetchText(articleUrl);
let doc = new JSDOM(body);
let reader = new Readability(doc.window.document);
let article = reader.parse();
let title = article.title;
return html(pageShell(
title,

Fetch Paginated Data

This val exports a function that loops through paginated API responses and returns the combined data as an array. It expects pagination with next and there to be a data property in the API response. This conforms to the Val Town API, so this function is useful when fetching paginated Val Town API responses for creating custom folders in pomdtr's vscode extension.

Usage:

Create valconst id = <vt user id> await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, { headers: { Authorization: `Bearer ${Deno.env.get("valtown")}` }, });

For demo usage in the context of the vscode extension see this val.

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
import { fetch } from "https://esm.town/v/std/fetch?v=4";
export async function fetchPaginatedData(url: string, init?: RequestInit, limit: number = 100) {
let u = new URL(url);
u.searchParams.set("limit", String(limit));
url = u.toString();
const data = [];
while (true) {
const resp = await fetch(url, init);
if (!resp.ok) {
throw new Error(`Fetch failed with status ${resp.status}`);
}
const body = await resp.json();
data.push(...body.data);
if (!body.links?.next) {
break;
}
url = body.links?.next;
}
return data;
}

Return a paginated response

A helper function to take an array and return a paginated response. This is useful when defining one's own folders for pomdtr's vscode extension.

Usage:

Create valconst data = [...] export default async function(req: Request): Promise<Response> { return paginatedResponse(req, data); }

For demo usage in the context of the vscode extension see this val.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export async function paginatedResponse(req: Request, data: any[], defaultLimit: number = 20): Promise<Response> {
const url = new URL(req.url);
const searchParams = Object.fromEntries(url.searchParams.entries());
const limit = parseInt(searchParams.limit) || defaultLimit;
const offset = parseInt(searchParams.offset) || 0;
const dataSubset = data.slice(offset, offset + limit);
const nextPageOffset = offset + limit;
const nextPageUrl = new URL(url.toString());
nextPageUrl.searchParams.set("offset", nextPageOffset.toString());
const res = {
data: dataSubset,
links: {
self: url,
next: nextPageOffset < data.length ? nextPageUrl.toString() : null,
},
};
return Response.json(res);
}
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
import { Hono } from "npm:hono";
import { PDFDocument, rgb, StandardFonts } from "npm:pdf-lib";
const pdfDoc = await PDFDocument.create();
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);
const fontSize = 30;
const page = pdfDoc.addPage();
const { width, height } = page.getSize();
page.drawText("Creating PDFs in JavaScript is awesome!", {
x: 50,
y: height - 4 * fontSize,
size: fontSize,
font: timesRomanFont,
color: rgb(0, 0.53, 0.71),
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
export function servePDF(req: Request) {
const app = new Hono();
app.get("/", (c) => {
return new Response(blob);
});
app.get("/download", (c) => {
c.header("Content-Type", "application/pdf");
c.header("Content-Disposition", "attachment; filename=cool.pdf");
return c.stream(async (stream) => {
await stream.write(pdfBytes);
});
});
return app.fetch(req);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { email } from "https://esm.town/v/std/email?v=9";
import { PDFDocument } from "npm:pdf-lib";
// PDF Creation
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
page.drawText("You can create PDFs!");
const pdf = await pdfDoc.saveAsBase64();
export const sendPDF2 = (async () => {
return await email({
text: "hello from sendPDF2",
attachments: [{ content: btoa(pdf), filename: "file.pdf", type: "application/pdf" }],
});
})();

valToGH

A utility function for programmatically updating a GitHub repository with code retrieved from a Val.

NOTE: This function currently does not change the contents of a file if it is already present. I will however be adding that functionality.

Usage

Create valimport { valToGH } from 'https://esm.town/v/nbbaier/valToGH'; const repo = "yourGitHubUser/yourRepo"; const val = "valUser/valName"; // or vals = ["valUser/valName1","valUser/valName2", ...] const ghToken = Deno.env.get("yourGitHubToken"); const result = await valToGH(repo, val, ghToken); console.log(result);

Parameters

  • repo: The GitHub repository in the format {user}/{repo}.
  • val: A single repository in the format {user}/{val}.
  • ghToken: Your GitHub token for authentication (must have write permission to the target repo)

Options

  • branch: Optional target branch. Default is main.
  • prefix: Optional directory path prefix for each file.
  • message: Optional commit message. Default is the current date and time in the format yyyy-MM-dd T HH:mm:ss UTC.
  • ts: Optional flag to use a .ts extension for the committed file instead of the default .tsx.
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { API_URL } from "https://esm.town/v/std/API_URL?v=5";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=42";
import { Octokit } from "npm:@octokit/rest";
import { DateTime } from "npm:luxon";
async function getLatestCommit(ghUser: string, ghRepo: string, branch: string, client: Octokit)
{
const { data: refData } = await client.rest.git.getRef({
owner: ghUser,
repo: ghRepo,
ref: `heads/${branch}`,
});
return refData.object.sha;
}
async function createNewTree(
ghUser: string,
ghRepo: string,
tree: {
path?: string;
mode?: "100644" | "100755" | "040000" | "160000" | "120000";
type?: "tree" | "commit" | "blob";
sha?: string;
content?: string;
}[],
commitSHA: string,
client: Octokit,
) {
const {
data: { sha: currentTreeSHA },
} = await client.rest.git.createTree({
owner: ghUser,
repo: ghRepo,
tree,
base_tree: commitSHA,
parents: [commitSHA],
});
return currentTreeSHA;
}
async function createNewCommit(
ghUser: string,
ghRepo: string,
commitSHA: string,
currentTreeSHA: string,
message: string,
client: Octokit,
) {
const {
data: { sha: newCommitSHA },
} = await client.rest.git.createCommit({
owner: ghUser,
repo: ghRepo,
tree: currentTreeSHA,
message: message,
parents: [commitSHA],
});
return newCommitSHA;
}
async function updateBranchRef(
owner: string,
repo: string,
newCommitSHA: string,
branch: string = "main",
client: Octokit,
) {
const result = await client.rest.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommitSHA,
});
return result;
}
type Options = { branch?: string; prefix?: string; message?: string; ts?: boolean };
export async function valToGH(
repo: string, // owner/repo
val: string | string[], // user/val
ghToken: string,
options?: Options,
) {
const client = new Octokit({ auth: ghToken });
// validate the repo name
const [ghUser, ghRepo] = repo.split("/");
if (repo.split("/").length !== 2) {
throw new Error("Repo name must be of the form {user}/{repo}");
}
// validate the val names
let valsToCommit = typeof val === "string" ? [val] : val;
if (valsToCommit.every(val => val.split("/").length !== 2)) {
throw new Error("All val names must be of the form {user}/{val}");
}
// Assigning reasonable defaults if options are not provided
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
import { paginatedResponse } from "https://esm.town/v/nbbaier/paginatedResponse";
import { API_URL } from "https://esm.town/v/std/API_URL?v=5";
const id = Deno.env.get("USER_ID");
const vals = await fetchPaginatedData(
`${API_URL}/v1/users/${id}/vals`,
{
headers: { Authorization: `Bearer ${Deno.env.get("valtown")}` },
},
);
export default async function(req: Request): Promise<Response> {
return Response.json({ url: `${API_URL}/v1/users/${id}/vals` }); // paginatedResponse(req, vals, 100);
}
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
51
52
53
54
55
56
57
/** @jsxImportSource https://esm.sh/react **/
import { Slot } from "https://esm.sh/@radix-ui/react-slot";
import { cva, type VariantProps } from "https://esm.sh/class-variance-authority";
import * as React from "https://esm.sh/react";
import { cn } from "https://esm.town/v/nbbaier/shadcnUtils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events
{
variants: {
variant: {
default:
"bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
destructive:
"bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90",
outline:
"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
secondary:
"bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
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
51
52
53
54
55
56
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Button } from "https://esm.town/v/nbbaier/shadcnButton";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "https://esm.town/v/nbbaier/shadcnTable";
const data = [["INV001", "Paid", "Credit Card", "$250.00"], ["INV001", "Paid", "Credit Card", "$250.00"], [
"INV001",
"Paid",
"Credit Card",
"$250.00",
]];
export function App() {
return (
<>
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map(payment => {
return (
<TableRow>
<TableCell className="font-medium">{payment[0]}</TableCell>
<TableCell>{payment[1]}</TableCell>
<TableCell>{payment[2]}</TableCell>
<TableCell className="text-right">{payment[3]}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);