Coding · ·

Setting up for NEA with JS or TS

Ideally your NEA won't be a single index.html file with inline <script> and <style> elements with all your JavaScript and CSS in the HTML file itself. Doing it that way will be hard to test (via unit tests) and will be hard to score highly for structure/modularity. Maybe you even want to use TypeScript instead of plain JavaScript, a good idea to easily hit some of the design section marks on the NEA.

Before you start:

Pick a 'recipe' that best suits your needs. Most should choose 'Recipe 1'. You can change to a different recipe later, but it might require you to refactor your code and file structure a bit.

Recipe 1: server-side, with bundling

This is the suggested recipe for most NEAs. It gives you:

How to run this recipe locally:

Here is the suggested folder/file structure for this recipe:

/
├─ .gitignore         # to stop eg node_modules ending up in git
├─ README.md          # what the app is and how to run it
├─ writeup            # store your writeup and images etc for it in here
│   └─ writeup.typ    # your main Typst file for your writeup
├─ src                # client-side source code
│   ├─ app.js         # main entry file to client-side JavaScript
│   ├─ [name].js      # a file with various exports, imported by app.js
│   ├─ [name].test.js # tests for the functions exported by blah.js
│   ├─ index.html     # app's HTML page
│   └─ styles.css     # CSS linked from your HTML file
└─ server.js          # a Bun server  

Suggested skeleton to start with for your server.js:

import homePage from './src/index.html'

let server = Bun.serve({
  routes: {
    '/': homePage,
    '/now': () => new Response(Date.now()), // a dynamic route
    // ... more dynamic server-processed routes here
  }
})
console.log(`Listening on ${server.url}`)

Note that this recipe doesn't include a /* wildcard route to serve all files in ./public... in fact, there is no ./public folder at all! When Bun imports an .html file it does... clever stuff... which looks for files mentioned in the HTML file (CSS, JS, images etc) and bundles them all together and serves them. You can find details here.

Want to add more HTML pages? Just import them and add routes for them.

Our shared server will see you have a server.js and will use that.

Here's a sample index.html to get started with:

<!doctype html>
<title>My NEA</title>
<div id="app">This is my NEA</div>
<script type="module" src="/app.js"></script>

...then your app.js does some import of other JavaScript files, like this:

import { some, things } from './blah.js'

Recipe 2: server-side, no bundling

Choose this recipe if you struggled getting something working with recipe 1, but it comes with these downsides:

How to run this recipe locally:

Here is the suggested folder structure for this recipe, and files within:

/
├─ .gitignore         # to stop eg node_modules ending up in git
├─ README.md          # what the app is and how to run it
├─ writeup            # store your writeup and images etc for it in here
│   └─ writeup.typ    # your main Typst file for your writeup
├─ public                # client-side source code
│   ├─ app.js         # main entry file to client-side JavaScript
│   ├─ blah.js        # a file with various exports, imported by app.js
│   ├─ blah.test.js   # tests for the functions exported by blah.js
│   ├─ index.html     # app's HTML page
│   └─ styles.css     # CSS linked from your HTML file
└─ server.js          # a Bun server  

Suggested skeleton to start with for your server.js:

let server = Bun.serve({
  routes: {

    '/now': () => new Response(Date.now()), // a dynamic route
    // ... more server-processed routes here

    // fallback for public folder inc /blah/ and /blah for /blah/index.html
    '/*': async (req) => {
      let path = new URL(req.url).pathname
      let file, folder = './public'
      if (/\/$/.test(path)) file = Bun.file(`${folder}${path}index.html`)
      else {
        file = Bun.file(`${folder}${path}`)
        if (!(await file.exists())) file = Bun.file(`${folder}${path}/index.html`)
      }
      if (await file.exists()) return new Response(file)
      else return new Response('404', { status: 404 })
    }
  },
})

console.log(`Server running at ${server.url}`)

Want more HTML pages? Just plonk them in your ./public folder - no need to change anything in your server.js.

Recipe 3: simple static website

Use this recipe if you struggled with recipe 1 and you have no need for server-side processing.

How to run this recipe locally:

How to set up:

If the shared server sees you don't have a server.js or package.json then it will just serve files from your ./public folder. There's not much more to it than that.

Optional: installing modules

If you need to bun install any dependencies/libraries (only needed for client-side dependencies, as server-side dependencies will auto-install), bun.lock and package.json files will appear. Dependencies will be installed into a folder node_modules - make sure you have a .gitignore with node_modules on a line so those files aren't added to git, as the server will run bun install if it finds a package.json.

Any guide you might find that says npm install [something] can be replaced with bun install [something] instead. You don't need to install Node.js or npm - Bun is a replacement for both.

Optional: using a build step

If you think you need a 'build step' you should add the appropriate configuration to your package.json, such as "scripts": { "build": "__your_build_command__" }. If this build script configuration is found, the shared server will run it with bun run build after bun install.

Optional: consider using TypeScript

This will work with 'Recipe 1' only.

TypeScript provides strong typing and better tooling on top of JavaScript. Many modern JavaScript projects have switched to TypeScript, and using it may make it easier for you to hit some of the marks for the 'design' section of your NEA.

JavaScript is valid TypeScript, but with TypeScript you can also optionally add types to eg variables and parameters.

But browsers don't understand TypeScript, so to use it you need a process which transpiles/converts TypeScript to JavaScript. Fortunately, you've already set up our project with such a process thanks to Bun (assuming you're using 'Recipe 1'), so switching is super easy.

Rename your app.js to app.ts and change the <script> tag in index.html to reflect the file's new name. Your server should still work as normal with bun server.js.

If you want to use TypeScript on the server too, rename server.js to server.ts and be sure you run bun --hot server.ts in the future. In addition, you'll need to tell our shared server to use a different 'start script' by creating or updating a package.json file in the root of your repo:

{
  "scripts": {
    "start": "bun server.ts"
  }
}

Now that you're using TypeScript, when you declare variables and parameters you can optionally now include their type. For example, instead of:

let x = 'yes'
blah(x)

function blah (a) {
  console.log(a)
}

You can have:

let x: String = 'yes'
blah(x)

function blah (a: String) {
  console.log(a)
}

... I suggest you try a guide if you want to learn more about TypeScript.

TypeScript may complain about some of Bun's types. To fix this, run:

bun add -d @types/bun

... and make sure you're .gitignore'ing the node_modules folder that the command will install dependencies into. You may also want to look at Bun's page on setting up for TypeScript.

Only do this if you really want to, probably because you've heard of React and JSX before and know you want to use them. Generally, I recommend making your project something which mostly uses the <canvas> and doesn't need to do much DOM manipulation at all.

First, install the required dependencies for React with:

bun install react react-dom

This will create a node_modules folder with lots of files in it. It will also create a bun.lock and a package.json if you didn't already have one. You should exclude the contents of node_modules from being added to git by creating a .gitignore file with node_modules on a line within in it - make sure you do this before you do a git commit.

Change your app.js to:

import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './components.jsx'

import.meta.hot.accept()

createRoot(document.querySelector('#app')).render(<App x="someone" />)

And create a components.jsx with:

export function App ({ x }) {
  return (
    <h1>Hello {x} things and stuff!</h1>
  )
}

Then check if it works by running bun --hot server.js. As you change eg the components.jsx file, you should see HMR reloading the contents of the page.

You may want to consider using TypeScript within your JSX file, in which case give it the extension .tsx instead of .ts.

You'll want to learn React and JSX to make good use of it.

You may have used petite-vue in the past (I also have two guides), but I don't recommend using it for your project. If you're doing lots of DOM manipulation, you might consider using it. But most projects will mostly be using the <canvas> element and don't need a DOM library like this. If you still want to use it, speak to me first.

First, install the required dependencies for petite-vue with:

bun install petite-vue

[see notes in the React section above re node_modules and a .gitignore etc]

Add this above the script element in your index.html:

<div v-scope>
  {{ state.x }}
</div>

Change your app.js to:

import { createApp, reactive } from 'petite-vue'

import.meta.hot.accept()

window.state = reactive({
  x: 'Hello world'
})

createApp().mount()

Run bun server.js and you should see 'Hello world'. Note that HMR probably won't work.

If you really want to go down the petite-vue route, you're probably better off using the full-fat Vue instead.

bun install vue

Then add this above your script element in your index.html:

<div id="app">
  <button @click="count++">
    Count is: {{ count }}
  </button>
</div>

Change your app.js to:

import { createApp } from 'vue/dist/vue.esm-bundler.js'

createApp({
  data() {
    return {
      count: 10
    }
  }
}).mount('#app')

Run bun server.js and it should work.

An alternative is to follow Vue's quick start guide.

Getting set up for unit testing

It will be much easier for you to hit many of the marks for testing by starting off with unit tests from the beginning.

You should consider your app.js to be a short entry point into your client-side JavaScript, while most of your actual code is in other JavaScript files which are import'd from there (as well as by test files and a CLI version of your program). If you end up with lots of server-side code, then consider server.js to be a short entry point which itself imports other files which have tests as well.

As per the noughts and crosses guide and code try to create pure functions where possible, avoiding the use of global variables, the mutation of parameters and side-effects. This will make them much easier to unit test, and will help you hit many of the 'design' section marks too.

Remember to export functions/variables/classes which can then be import'd. If you must have a global variable accessible across different files, use the identifier globalThis.something (or window.something) instead of something when referring to it. But better is usually to pass it around as a parameter to functions.

It's a good idea to split your code across several files, and each of those files should have a matching tests file, eg:

├── blah.js
├── blah.test.js
├── something.js
└── something.test.js

Test files should import the necessary test functions from Bun as well as the file being tested, eg:

import { test, expect } from 'bun:test'
import { someFunc, somethingElse } from './blah.js'

Tests will look something like this:

test('someFunc()', () => {
  expect(someFunc(3)).toBe(5)
  expect(someFunc(5)).toEqual([5, 6, 7]) // deep equality
})

Methods include toBe(), toEqual() and several others.

You can run all tests in your project with bun test, as it searches for all files that end with .test.js. Better is to constantly test with bun test --watch.

Find out more about testing in Bun's test guide and see also the advanced testing guide.

Creating a CLI version

It is highly recommended that you create a CLI version for the core functionality of your project first, partly to help you decouple the core logic from inputs/outputs in the user interface. A recommended name is cli.js, which can be run with bun cli.js.