├── .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 |

image

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 |
377 |

Deno Playground

378 | 407 |
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 --------------------------------------------------------------------------------