Typescript Executor

A couple of months ago, I gave a presentation on TypeScript decorators. I called it "Demystifying Decorators", and it focused on creating a dependency injection library in TypeScript using decorators.

I wanted to present code in an intuitive way, especially since the audience was primarily developers. I found a really awesome framework for creating presentations from HTML called Reveal.

Reveal came with a great toolkit that helped me display code and smoothly transition between slides.

Simple code like this made for a really nice slide:

html
1<section> 2 <h2>Demystifying Decorators</h2> 3 <p>How do <code>@Decorators()</code> really work?</p> 4 <p>And what can they be used for? 🤷</p> 5</section>

Decorators

Even though Reveal has great support for displaying code, it doesn't support executing code live during presentations. This was something I really wanted, as running code live can significantly enhance understanding—especially in a very techy presentation like mine.

Reveal has a solid plugin system, so I started looking for a plugin that could execute code. I found this repo, which is a plugin that exposes the terminal to run arbitrary console commands such as node or python, allowing you to execute the code displayed in your slides.

However, it had some limitations. You have to run a command in the presentation that points to a file containing the same code shown on the slide. Also, if I wanted to run code that depended on external packages, I'd have to install those packages on my machine ahead of time.

What I really wanted was the ability to press a button, run any code shown on the screen, and display the standard output right in the presentation—regardless of dependencies or whether I was using TypeScript or JavaScript.

So, I concluded that I needed two things:

A server that could:

  • Accept code,
  • Transpile it if it's TypeScript,
  • Fetch and install any external dependencies from npm,
  • Execute the code, and
  • Return whatever logs were printed to standard output.

A Reveal plugin that could:

  • Send the content of code blocks to that server, and
  • Display the returned output in the presentation.

Making the executor

To make an API which could execute typescript code and return the results we can use any http framework. I opted to use fastify since it's supposed to be blazingly fast 🔥.

index.ts
1import cors from "@fastify/cors"; 2import init from "fastify"; 3const fastify = init({ logger: true, requestTimeout: 60_000 }); 4 5fastify.post("/", async (req, _) => { 6 const tsCode = String(req.body); 7}); 8 9const start = async () => { 10 try { 11 await fastify.register(cors); 12 await fastify.listen({ port: 3000, host: "0.0.0.0" }); 13 } catch (err) { 14 fastify.log.error(err); 15 process.exit(1); 16 } 17}; 18start();

Since we want to be able to send Typescript code we have to transpile the code, this can be done with the typescript package:

index.ts
1import { transpile, ModuleKind, ScriptTarget } from "typescript"; 2 3const js = transpile(tsCode, { 4 experimentalDecorators: true, 5 emitDecoratorMetadata: true, 6 module: ModuleKind.ES2015, 7 target: ScriptTarget.ES2015, 8});

The executor should also be able to download arbitrary dependencies declared in the code. There is a great library which is made by Google called zx which can help us with that:

index.ts
1import { exec } from "child_process"; 2import { randomUUID } from "crypto"; 3import { rm, writeFile } from "fs/promises"; 4 5function cmd(command: string) { 6 let p = exec(command); 7 return new Promise<string>((res) => { 8 let stdout = ""; 9 const append = (data: any) => { 10 data = data.toString() as string; 11 if (data.includes(" npm i ")) return; 12 stdout += data; 13 }; 14 p.stdout?.on("data", append); 15 p.stderr?.on("data", append); 16 p.on("exit", () => { 17 res(stdout.trim()); 18 }); 19 }); 20} 21 22const filename = randomUUID(); 23await writeFile(filename, js); 24try { 25 const output = await cmd(`zx --install --quiet ./${filename}`); 26 console.log(output); 27 return output; 28} finally { 29 await rm(filename); 30}

zx is intended to be used for scripting generally, but it really fit this use case well since it can install dependencies with the --install flag. By writing the javascript to a temporary file and pointing zx to that file we are avoiding having to interpolate Javascript code into the command line.

Great! Now we can package this app up with Docker and run it in a container for easy management.

Dockerfile
1FROM node as builder 2 3WORKDIR /app 4 5COPY package*.json . 6 7RUN npm install 8 9COPY . . 10 11RUN npm run build 12 13FROM node as runner 14 15WORKDIR /app 16 17RUN npm install -g zx 18 19COPY --from=builder /app/node_modules node_modules 20 21COPY --from=builder /app/index.js index.js 22 23CMD [ "node", "index.js" ]

Making the plugin

The plugin needs to attach to all of the code elements in reveal.js and inject some JavaScript which will send a http request with the code content to the executor and then display the response.

reveal-code-exec.js
1window.RevealCodeExec = ({ execUrl }) => ({ 2 id: "code-exec", 3 init: () => { 4 const codeblocks = document.querySelectorAll("pre.code-wrapper[runnable]"); 5 for (const code of codeblocks) { 6 const runTheCode = "Run the code!"; 7 const div = document.createElement("div"); 8 code.contentEditable = "true"; 9 code.style.position = "relative"; 10 div.style.cursor = "pointer"; 11 div.style.width = "100%"; 12 div.style.zIndex = "10"; 13 div.style.top = "100%"; 14 div.style.bottom = "0"; 15 div.style.background = "white"; 16 div.style.position = "absolute"; 17 div.innerText = runTheCode; 18 div.style.height = "fit-content"; 19 div.onclick = async () => { 20 if (div.innerText !== runTheCode) { 21 return (div.innerText = runTheCode); 22 } 23 div.innerText = "Loading..."; 24 const body = ( 25 code.querySelector("code.visible.current-fragment") || 26 code.querySelector("code") 27 ).textContent; 28 const res = await fetch(execUrl, { 29 method: "POST", 30 body, 31 }); 32 if (!res.ok) { 33 div.innerText = runTheCode; 34 return; 35 } 36 const text = await res.text(); 37 div.innerText = text || runTheCode; 38 }; 39 code.appendChild(div); 40 } 41 }, 42});

We attach a function to the global window object and register it as a plugin in index.html

html
1<script src="plugin/code-exec/plugin.js"></script> 2<script> 3 Reveal.initialize({ 4 hash: true, 5 plugins: [ 6 RevealMarkdown, 7 RevealHighlight, 8 RevealNotes, 9 RevealCodeExec({ execUrl: "http://localhost:8080" }), 10 ], 11 }); 12</script>

Now we should be able to run the code in the presentation by pressing the Run the code! button

console-log.png

Pressing the button yields this result:

console-log-result.png

Conclusion

If you made it this far, thanks for sticking along and hope your learned something new.

Another cool thing about using reveal is that you can embed the presentation in an iframe. Below is an example of that 👇

Happy coding! 🌈