├── .gitignore
├── .tool-versions
├── README.md
├── api
├── eval.ts
├── fmt.ts
├── index.ts
└── share.ts
├── config.ts
├── deps.ts
├── lib
├── createCommandHandler.ts
└── template.ts
├── now.json
└── public
├── favicon.svg
└── font
├── iosevka-term-ss08-italic.woff
├── iosevka-term-ss08-italic.woff2
├── iosevka-term-ss08-regular.woff
├── iosevka-term-ss08-regular.woff2
├── iosevka-term-ss08-semibold.woff
├── iosevka-term-ss08-semibold.woff2
├── iosevka-term-ss08-semibolditalic.woff
└── iosevka-term-ss08-semibolditalic.woff2
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/osx,windows,linux
3 | # Edit at https://www.gitignore.io/?templates=osx,windows,linux
4 |
5 | ### Linux ###
6 | *~
7 |
8 | # temporary files which can be created if a process still has a handle open of a deleted file
9 | .fuse_hidden*
10 |
11 | # KDE directory preferences
12 | .directory
13 |
14 | # Linux trash folder which might appear on any partition or disk
15 | .Trash-*
16 |
17 | # .nfs files are created when an open file is removed but is still being accessed
18 | .nfs*
19 |
20 | ### OSX ###
21 | # General
22 | .DS_Store
23 | .AppleDouble
24 | .LSOverride
25 |
26 | # Icon must end with two \r
27 | Icon
28 |
29 | # Thumbnails
30 | ._*
31 |
32 | # Files that might appear in the root of a volume
33 | .DocumentRevisions-V100
34 | .fseventsd
35 | .Spotlight-V100
36 | .TemporaryItems
37 | .Trashes
38 | .VolumeIcon.icns
39 | .com.apple.timemachine.donotpresent
40 |
41 | # Directories potentially created on remote AFP share
42 | .AppleDB
43 | .AppleDesktop
44 | Network Trash Folder
45 | Temporary Items
46 | .apdisk
47 |
48 | ### Windows ###
49 | # Windows thumbnail cache files
50 | Thumbs.db
51 | Thumbs.db:encryptable
52 | ehthumbs.db
53 | ehthumbs_vista.db
54 |
55 | # Dump file
56 | *.stackdump
57 |
58 | # Folder config file
59 | [Dd]esktop.ini
60 |
61 | # Recycle Bin used on file shares
62 | $RECYCLE.BIN/
63 |
64 | # Windows Installer files
65 | *.cab
66 | *.msi
67 | *.msix
68 | *.msm
69 | *.msp
70 |
71 | # Windows shortcuts
72 | *.lnk
73 |
74 | # End of https://www.gitignore.io/api/osx,windows,linux
75 |
76 | .vercel
77 | .now
78 | .vscode
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 12.16.1
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Deno Playground 🦕
2 |
3 |

4 |
5 | Deno playground scratchpad, inspired by golang's play.golang.org
6 |
7 | Be aware that this will run unprevilleged code on your servers. For safety
8 | reasons, I'm adding a time-based execution limit (default is 3s, but can be
9 | overridden by setting `SCRIPT_EXECUTION_TIMEOUT` envvars).
10 |
11 | ## Available API routes
12 |
13 | All results are in text format. HTTP response status indicates whether the
14 | request is completed successfully or not.
15 |
16 | as always, 200 means OK - 500 means there's error somewhere in your code.
17 |
18 | ### POST /api/eval
19 |
20 | Interpret deno source code, and get result back. To use unstable features, pass
21 | `unstable=1` queryparams to the URL. To interpret as typescript, pass
22 | `typescript=1` queryparams to the URL.
23 |
24 | ```
25 | curl -X POST \
26 | 'http://localhost:3000/api/eval' \
27 | -H 'Content-Type: application/javascript' \
28 | --data-raw 'console.log(Deno)'
29 | ```
30 |
31 | ### POST /api/fmt
32 |
33 | Format deno source code, and get formatted result back.
34 |
35 | ```
36 | curl -X POST \
37 | 'http://localhost:3000/api/fmt' \
38 | -H 'Content-Type: application/javascript' \
39 | --data-raw 'console.log(Deno)'
40 | ```
41 |
42 | ## Run in development mode
43 |
44 | ```bash
45 | $ npx vercel dev
46 | ```
47 |
48 | ## Deploy to vercel
49 |
50 | ```
51 | $ npx vercel
52 | ```
53 |
--------------------------------------------------------------------------------
/api/eval.ts:
--------------------------------------------------------------------------------
1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "../deps.ts";
2 | import createCommandHandler from "../lib/createCommandHandler.ts";
3 |
4 | export async function handler(
5 | evt: APIGatewayProxyEvent,
6 | ): Promise {
7 | return createCommandHandler("run")(evt);
8 | }
9 |
--------------------------------------------------------------------------------
/api/fmt.ts:
--------------------------------------------------------------------------------
1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "../deps.ts";
2 | import createCommandHandler from "../lib/createCommandHandler.ts";
3 |
4 | export async function handler(
5 | evt: APIGatewayProxyEvent,
6 | ): Promise {
7 | return createCommandHandler("fmt")(evt);
8 | }
9 |
--------------------------------------------------------------------------------
/api/index.ts:
--------------------------------------------------------------------------------
1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from "../deps.ts";
2 | import template from "../lib/template.ts";
3 | import { checkUid } from "./share.ts";
4 |
5 | export async function handler(
6 | { body: evtBody }: APIGatewayProxyEvent,
7 | ): Promise {
8 | const { method, path } = JSON.parse(evtBody || "{}");
9 | if (method === "GET") {
10 | let templateText;
11 | let loadedText =
12 | `console.log(\`Hello from Deno:\${Deno.version.deno} 🦕\`);`;
13 | const [_, queryString] = path.split("?");
14 | const qs = new URLSearchParams(queryString || "");
15 | const isUnstable = qs.get("unstable") === "1";
16 | const isNotTypescript = qs.get("ts") === "0";
17 | const idToLoadFrom = qs.get("id");
18 | templateText = `${template}`;
19 | templateText = (isUnstable)
20 | ? templateText.replace("{{isUnstableTemplateMark}}", "checked")
21 | : templateText.replace("{{isUnstableTemplateMark}}", "");
22 | templateText = (isNotTypescript)
23 | ? templateText.replace("{{isTypescriptTemplateMark}}", "")
24 | : templateText.replace("{{isTypescriptTemplateMark}}", "checked");
25 | if (idToLoadFrom) {
26 | loadedText = await checkUid(idToLoadFrom) || "";
27 | }
28 | templateText = templateText.replace("{{source}}", loadedText);
29 | return {
30 | headers: {
31 | "Content-Type": "text/html",
32 | "X-Frame-Options": "DENY",
33 | "X-Download-Options": "noopen",
34 | "X-Content-Type-Options": "nosniff",
35 | "X-XSS-Protection": "1; mode=block",
36 | "Strict-Transport-Security": "max-age=5184000",
37 | },
38 | statusCode: 200,
39 | body: templateText,
40 | };
41 | } else {
42 | return {
43 | statusCode: 404,
44 | body: "Route not defined",
45 | };
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/api/share.ts:
--------------------------------------------------------------------------------
1 | import {
2 | APIGatewayProxyEvent,
3 | APIGatewayProxyResult,
4 | Base64,
5 | HmacSha256,
6 | } from "../deps.ts";
7 |
8 | import { JSONBIN_TOKEN, JSONBIN_URL, SHARE_SALT } from "../config.ts";
9 |
10 | function generateHash(body: string) {
11 | const h = new HmacSha256(SHARE_SALT);
12 | h.update(body);
13 | const tempHash = Base64.fromString(h.hex()).toString();
14 | let i = 11;
15 |
16 | while (tempHash.slice(0, i).endsWith("_") && i < tempHash.length) {
17 | i++;
18 | }
19 | return tempHash.slice(0, i);
20 | }
21 |
22 | export async function checkUid(hash: string): Promise {
23 | return fetch(`${JSONBIN_URL}/${hash}`, {
24 | method: "GET",
25 | headers: {
26 | "Authorization": `token ${JSONBIN_TOKEN}`,
27 | },
28 | }).then((result) => result.ok ? result.text() : null);
29 | }
30 |
31 | export async function store(body: string): Promise {
32 | const hash = generateHash(body.trim());
33 | const uid = await checkUid(hash);
34 | if (!uid) {
35 | return fetch(`${JSONBIN_URL}/${hash}`, {
36 | method: "POST",
37 | headers: {
38 | "Authorization": `token ${JSONBIN_TOKEN}`,
39 | },
40 | body,
41 | }).then((result) => {
42 | if (!result.ok) throw new Error(`${result.status}: ${result.statusText}`);
43 | return hash;
44 | });
45 | }
46 | return Promise.resolve(hash);
47 | }
48 |
49 | export async function handler(
50 | { body: evtBody }: APIGatewayProxyEvent,
51 | ): Promise {
52 | const { method, body, path, headers } = JSON.parse(evtBody || "{}");
53 | if (headers["user-agent"]?.includes("curl")) {
54 | return {
55 | statusCode: 500,
56 | body: "Cannot share text",
57 | };
58 | }
59 | if (method === "POST") {
60 | const source = Base64.fromBase64String(body).toString();
61 | return store(source)
62 | .then((uid) => ({
63 | statusCode: 200,
64 | body: uid,
65 | }))
66 | .catch((err) => {
67 | console.error(err);
68 | return {
69 | statusCode: 500,
70 | body: err.message,
71 | };
72 | });
73 | } else {
74 | return {
75 | statusCode: 500,
76 | body: "Not supported",
77 | };
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/config.ts:
--------------------------------------------------------------------------------
1 | export const SHARE_SALT = Deno.env.get("SHARE_SALT") || "CAFEBABE";
2 | export const JSONBIN_USER = Deno.env.get("JSONBIN_USER") || "";
3 | export const JSONBIN_TOKEN = Deno.env.get("JSONBIN_TOKEN") || "";
4 | export const JSONBIN_URL = `https://jsonbin.org/${JSONBIN_USER}`;
5 |
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export { HmacSha256 } from "https://deno.land/std@0.51.0/hash/sha256.ts";
2 | export { Base64 } from "https://deno.land/x/bb64/mod.ts";
3 | export type {
4 | APIGatewayProxyEvent,
5 | APIGatewayProxyResult,
6 | Context,
7 | } from "https://deno.land/x/lambda/mod.ts";
8 |
--------------------------------------------------------------------------------
/lib/createCommandHandler.ts:
--------------------------------------------------------------------------------
1 | import {
2 | APIGatewayProxyEvent,
3 | APIGatewayProxyResult,
4 | Base64,
5 | } from "../deps.ts";
6 |
7 | type SupportedDenoSubCommand = "run" | "fmt";
8 |
9 | export default function createCommandHandler(
10 | commandType: SupportedDenoSubCommand,
11 | ) {
12 | return async function handler({
13 | body: evtBody,
14 | }: APIGatewayProxyEvent): Promise {
15 | const { method, body, path } = JSON.parse(evtBody || "{}");
16 | if (method !== "POST") return { statusCode: 500, body: "Not supported" };
17 | const [_, queryString] = path.split("?");
18 | const qs = new URLSearchParams(queryString || "");
19 | const source = Base64.fromBase64String(body).toString();
20 | const encoder = new TextEncoder();
21 | const decoder = new TextDecoder();
22 | const cmd = ["deno", commandType];
23 | if (qs.has("unstable")) {
24 | cmd.push("--unstable");
25 | }
26 | cmd.push("-");
27 |
28 | const executor = Deno.run({
29 | cmd,
30 | stdin: "piped",
31 | stdout: "piped",
32 | stderr: "piped",
33 | });
34 |
35 | await executor.stdin?.write(encoder.encode(source));
36 | executor.stdin?.close();
37 | let killed = false;
38 | const timer = setTimeout(() => {
39 | killed = true;
40 | executor.kill(Deno.Signal.SIGKILL);
41 | }, parseInt(Deno.env.get("SCRIPT_EXECUTION_TIMEOUT") || "3000", 10));
42 |
43 | const [status, stdout, stderr] = await Promise.all([
44 | executor.status(),
45 | executor.output(),
46 | executor.stderrOutput(),
47 | ]);
48 |
49 | clearTimeout(timer);
50 |
51 | executor.close();
52 |
53 | if (!status.success) {
54 | if (killed) {
55 | return formatResponse("Exceeding execution time limit", 500);
56 | }
57 | return formatResponse(decoder.decode(stderr), 500);
58 | }
59 | return formatResponse(decoder.decode(stdout), 200);
60 | };
61 | }
62 |
63 | function formatResponse(body: string, statusCode: number) {
64 | return {
65 | statusCode,
66 | body,
67 | headers: {
68 | "Content-Type": "text/plain; charset=UTF-8",
69 | },
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/lib/template.ts:
--------------------------------------------------------------------------------
1 | const template = `
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Deno Playground
10 |
11 |
12 |
13 |
364 |
365 |
371 |
372 |
373 |
374 |
375 |
376 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
541 |
542 |
543 | `;
544 |
545 | export default template;
546 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "api/**/*.ts": {
4 | "runtime": "now-deno@0.5.0",
5 | "memory": 1024
6 | }
7 | },
8 | "headers": [
9 | {
10 | "source": "^/favicon.svg",
11 | "headers": [
12 | {
13 | "key": "Cache-Control",
14 | "value": "public, max-age=31536000, immutable"
15 | }
16 | ]
17 | },
18 | {
19 | "source": "^/font/(.*)",
20 | "headers": [
21 | {
22 | "key": "Cache-Control",
23 | "value": "public, max-age=31536000, immutable"
24 | }
25 | ]
26 | },
27 | {
28 | "source": "/(.*)",
29 | "headers": [
30 | {
31 | "key": "X-Frame-Options",
32 | "value": "DENY"
33 | },
34 | {
35 | "key": "X-XSS-Protection",
36 | "value": "1; mode=block"
37 | },
38 | {
39 | "key": "X-Content-Type-Options",
40 | "value": "nosniff"
41 | }
42 | ]
43 | }
44 | ],
45 | "rewrites": [
46 | {
47 | "source": "/",
48 | "destination": "/api"
49 | }
50 | ],
51 | "env": {
52 | "DENO_VERSION": "latest"
53 | },
54 | "build": {
55 | "env": {
56 | "DENO_UNSTABLE": "true"
57 | }
58 | },
59 | "regions": ["sin1"]
60 | }
61 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-italic.woff
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-italic.woff2
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-regular.woff
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-regular.woff2
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-semibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-semibold.woff
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-semibold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-semibold.woff2
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-semibolditalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-semibolditalic.woff
--------------------------------------------------------------------------------
/public/font/iosevka-term-ss08-semibolditalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maman/deno-playground/5fdd744f0f0bc74b3ff00f991a08317763d98548/public/font/iosevka-term-ss08-semibolditalic.woff2
--------------------------------------------------------------------------------