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
Also the ability to get index.mjs
from it's component directory.
import foo from './my-comp' // imports ./my-comp/index.mjs
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`,
]
Now we need to add some logic to:
- Check for the file if a file extension was found in the request.
- Check the rewrite paths if there was no extension.
- 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}
}
}
}
But UT-OH.
I've broken something!
Relative imports no longer work.
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)
}
...
}
Fantastic!
Everything loads as expected.
Project Structure
So what do these imports look like, and what is the project structure?
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>
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)
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
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
What do you think?
Top comments (0)