ShadeJS 🌴 Part 2 - The Magic HTTP Server 💫

f1lt3r

F1LT3R

Posted on December 20, 2023

ShadeJS 🌴 Part 2 - The Magic HTTP Server 💫

This article is Part 2 of the series: Build Your Own SPA Framework with Modular JavaScript, NodeJS and Closed-Shadow Web Components.
Part 1 | Part 2


The Journey Continues

So I got a logo 🌴

And I grabbed some namespaces:

It's time to start working on the HTTP server!

Server Design

I'll be honest. I hate using bundlers and transpilers. But one of the things I love about them, is the way that imports don't require a file extension.

For example:

import foo from './foo' // imports ./foo.mjs
Enter fullscreen mode Exit fullscreen mode

Also the ability to get index.mjs from it's component directory.

import foo from './my-comp' // imports ./my-comp/index.mjs
Enter fullscreen mode Exit fullscreen mode

Rewrites

When you setup a web server such as Apache, or Nginx, the server is already using these kind of rules. They are called "rewrites". That's how the server knows to fetch index.html when being asked for the root /.

So I will need a stack of rewrite rules to check through.

Like this:

const rewritePaths = (pathname) => [
    `${pathname}/index.html`,
    `${pathname}.html`,
    `${pathname}.mjs`,
    `${pathname}/index.mjs`,
    `${pathname}.css`,
    `${pathname}/index.css`,
]
Enter fullscreen mode Exit fullscreen mode

Now we need to add some logic to:

  1. Check for the file if a file extension was found in the request.
  2. Check the rewrite paths if there was no extension.
  3. Return the OS file location, content-type, etc.
const rewrite = (pathname, extension) => {
    const rewrites = extension ? removeDoubleSlashes([pathname]) : rewritePaths(pathname)

    for (const rewrite of rewrites) {
        const rewriteTarget  = stripStartSlash(rewrite)
        const location = path.resolve('./web', rewriteTarget)

        const stat = getStat(location)
        const {ext} = path.parse(rewriteTarget, true)
        const contentType = headers[ext]

        if (stat) {
            return {location, stat, contentType}
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

But UT-OH.

I've broken something!

Relative imports no longer work.

Code 404 in Browsers Developer Tools

301 Moved Permanently

When I'm serving a response without the file extension, and that response uses a file import, the browser does not know what that new import is relative to. This breaks my relative imports, throwing a 404 response code.

To handle this I will use the 301 Moved Permanently response code.

const hasNoEndSlash = (url) => url.slice(-1) !== '/'

const wasRewritten = (url, location) => url.slice(1) !== path.relative(WEB_DIR, location)

const MovedPermanently301 = (res, location) => {
    res.writeHead(301, {
        Location: `http://localhost:${PORT}/${location}`
    })
    res.write('301 Moved Permanently')
    res.end()
}

const requestHandler = (req, res) => {

    ...

    if (hasNoEndSlash(pathname) && wasRewritten(pathname, file.location)) {
        const relativeLocation =  path.relative(WEB_DIR, file.location)
        return MovedPermanently301(res, relativeLocation)
    }

    ...

}
Enter fullscreen mode Exit fullscreen mode

Fantastic!

Everything loads as expected.

Expected loading of resources without file extensions

Project Structure

So what do these imports look like, and what is the project structure?

Project structure in VS Code

In the ./web/index.html file I am loading my counter component without the file extension.

<!DOCTYPE html>
<html>
<body>
    <h1>Index</h1>
    <my-counter></my-counter>
    <script src="/components/counter" type="module"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

And my counter component imports index.mjs from ./web/vendor/Shade/index.mjs. I'm using a ../ to test that relative imports are really working.

import Shade, {css, html} from '../vendor/Shade'

class MyCounter extends HTMLElement {
    title = 'My Awesome Counter'
    count = 0

    style = ({count}) => css`
        h1 {
            color: ${count >= 8 ? 'red' : 'green'};
        }
        ...
    `

    template = ({title, count}) => html`
        <div>
            <h1>${title}</h1>
            ...
        </div>
    `

    constructor() {
        super()
        Shade(this)
    }
    ...
}

window.customElements.define('my-counter', MyCounter)
Enter fullscreen mode Exit fullscreen mode

And css.mjs and html.mjs are exported from ./web/vendor/Shade/index.mjs via the lib directory. Eg: ./web/vendor/Shade/lib/[html,css].mjs

import html from './lib/html' 
import css from './lib/css' 
import shade from './lib/shade' 

export {html, css}

export default shade
Enter fullscreen mode Exit fullscreen mode

Try It!

Check out Part 2 of the code for yourself. Download it from GitHub and play around with the imports.

https://github.com/Shade-JS/ShadeJS/tree/part-2

git clone https://github.com/Shade-JS/ShadeJS.git
cd ShadeJS
git checkout part-2
node run server/server.mjs
Enter fullscreen mode Exit fullscreen mode

What do you think?

ShadeJS GitHub Page

💖 💪 🙅 🚩
f1lt3r
F1LT3R

Posted on December 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related