Initial commit.
This commit is contained in:
commit
05c575bb26
19
cache.js
Normal file
19
cache.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
export const cache = {}
|
||||||
|
|
||||||
|
export function hash(key) {
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
.update(JSON.stringify(key))
|
||||||
|
.digest('hex')
|
||||||
|
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCache(hash, data) {
|
||||||
|
cache[hash] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromCache(hash) {
|
||||||
|
return cache[hash] || null
|
||||||
|
}
|
47
components/Footer.js
Normal file
47
components/Footer.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { html, css, sanitize } from '../templater.js'
|
||||||
|
import base from '../styles/base.js'
|
||||||
|
|
||||||
|
const body = async (ctx, { path, title }) => html`
|
||||||
|
<footer>
|
||||||
|
<p>Fugiat consectetur veniam sit ullamco. Aute do voluptate velit nulla nisi aliqua fugiat nulla sunt dolor nostrud. Est adipisicing sint eiusmod voluptate sunt dolor.</p>
|
||||||
|
<p>Et duis pariatur veniam aliquip. Nisi occaecat ea commodo amet. Sunt commodo aliquip ullamco ad do magna commodo ut eiusmod. Id nostrud deserunt in excepteur ipsum laborum tempor dolor. Nisi ipsum elit magna sit exercitation ut aliqua irure laboris et.</p>
|
||||||
|
<p>Eu cupidatat in ut nisi reprehenderit dolore. Consectetur reprehenderit id in ad eu duis anim. Ipsum ut velit in velit occaecat enim laboris Lorem nostrud.</p>
|
||||||
|
<dl>
|
||||||
|
<dt>Path:</dt>
|
||||||
|
<dd>${sanitize(path)}</dd>
|
||||||
|
<dt>Title:</dt>
|
||||||
|
<dd>${sanitize(title)}</dd>
|
||||||
|
</dl>
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > * {
|
||||||
|
flex: 1;
|
||||||
|
flex-basis: 30rem;
|
||||||
|
margin: 2rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer dt {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer dd {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function Footer (ctx, { path, title }) {
|
||||||
|
ctx.css.add(styles)
|
||||||
|
ctx.body = await body(ctx, { path, title })
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
24
components/Header.js
Normal file
24
components/Header.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { html, css, sanitize } from '../templater.js'
|
||||||
|
import { NavBar } from '../components/NavBar.js'
|
||||||
|
import base from '../styles/base.js'
|
||||||
|
|
||||||
|
const body = async (ctx, { path, title }) => html`
|
||||||
|
<header>
|
||||||
|
<h3 style="flex: 1;">Title: ${sanitize(title)}</h3>
|
||||||
|
${await ctx.b(NavBar, { path })}
|
||||||
|
</header>
|
||||||
|
`
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function Header (ctx, { path, title }) {
|
||||||
|
ctx.css.add(styles)
|
||||||
|
ctx.body = await body(ctx, { path, title })
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
58
components/NavBar.js
Normal file
58
components/NavBar.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { html, css } from '../templater.js'
|
||||||
|
|
||||||
|
const body = (path) => html`
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li class="${path === '/' ? 'active' : ''}"><a href="/">Home</a></li>
|
||||||
|
<li class="${path === '/about' ? 'active' : ''} "><a href="/about">About</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
`
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
color: var(--color-body);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:before {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 1rem;
|
||||||
|
transition: transform 200ms;
|
||||||
|
transform: scale(0);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover:before, nav .active a:before {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .active a {
|
||||||
|
color: var(--color-headings);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export function NavBar (ctx, { path }) {
|
||||||
|
ctx.css.add(styles)
|
||||||
|
ctx.body = body(path)
|
||||||
|
return ctx
|
||||||
|
}
|
51
index.js
Normal file
51
index.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import http from 'http'
|
||||||
|
import path from 'path'
|
||||||
|
import { page } from './pages/Page.js'
|
||||||
|
import { HomePage } from './pages/Home.js'
|
||||||
|
import { AboutPage } from './pages/About.js'
|
||||||
|
|
||||||
|
const SERVER_PORT = 8080
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url, `https://noop.local`)
|
||||||
|
const params = url.searchParams
|
||||||
|
|
||||||
|
// Ignore requests to files with extensions.
|
||||||
|
if (path.extname(url.pathname)) {
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = params.get('title') || `${url.pathname}?title=YOUR_TITLE`
|
||||||
|
|
||||||
|
|
||||||
|
const pageMap = {
|
||||||
|
'/': HomePage,
|
||||||
|
'/about': AboutPage
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPage = pageMap[url.pathname] || pageMap['/']
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = await page(selectedPage, {
|
||||||
|
path: url.pathname,
|
||||||
|
title
|
||||||
|
})
|
||||||
|
|
||||||
|
res.write(output)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
res.writeHead(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(SERVER_PORT, () => {
|
||||||
|
console.info(`Now listening for requests on ${SERVER_PORT}.`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./index.js"
|
||||||
|
}
|
||||||
|
}
|
42
pages/About.js
Normal file
42
pages/About.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { html, css, sanitize } from '../templater.js'
|
||||||
|
import { Header } from '../components/Header.js'
|
||||||
|
import { Footer } from '../components/Footer.js'
|
||||||
|
import base from '../styles/base.js'
|
||||||
|
|
||||||
|
const head = ({ title }) => html`
|
||||||
|
<title>About Page - ${sanitize(title)}</title>
|
||||||
|
`
|
||||||
|
|
||||||
|
const body = async (ctx, { path, title }) => html`
|
||||||
|
${await ctx.b(Header, { path, title })}
|
||||||
|
<main id="app">
|
||||||
|
<section>
|
||||||
|
<h1>About Page - ${sanitize(title)}</h1>
|
||||||
|
<p>
|
||||||
|
Veniam quis commodo ad Lorem do proident et elit labore amet incididunt et culpa. Non ex in deserunt veniam minim pariatur incididunt consectetur. Aliqua occaecat irure eu est.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Anim reprehenderit enim amet eu id esse duis exercitation nulla fugiat sunt esse exercitation.</li>
|
||||||
|
<li>In non ex esse et fugiat excepteur aliquip ea.</li>
|
||||||
|
<li>Reprehenderit aliqua aute cupidatat enim ipsum eu.</li>
|
||||||
|
<li>Consectetur exercitation exercitation excepteur quis in qui qui magna cillum sint.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
${await ctx.b(Footer, { path, title })}
|
||||||
|
`
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
main {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function AboutPage (ctx, { path, title }) {
|
||||||
|
ctx.css.add(base)
|
||||||
|
ctx.css.add(styles)
|
||||||
|
ctx.head.add(head({ title }))
|
||||||
|
ctx.body = await body(ctx, { path, title })
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
36
pages/Home.js
Normal file
36
pages/Home.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { html, css, sanitize } from '../templater.js'
|
||||||
|
import { Header } from '../components/Header.js'
|
||||||
|
import { Footer } from '../components/Footer.js'
|
||||||
|
import base from '../styles/base.js'
|
||||||
|
|
||||||
|
const head = ({ title }) => html`
|
||||||
|
<title>Home Page - ${sanitize(title)}</title>
|
||||||
|
`
|
||||||
|
|
||||||
|
const body = async (ctx, { path, title }) => html`
|
||||||
|
${await ctx.b(Header, { path, title })}
|
||||||
|
<main id="app">
|
||||||
|
<section>
|
||||||
|
<h1>Home Page - ${sanitize(title)}</h1>
|
||||||
|
<p>
|
||||||
|
Lorem ex excepteur occaecat et labore veniam veniam duis pariatur laboris. Aute incididunt anim laborum cupidatat mollit amet pariatur. Consectetur nostrud eu amet eu cillum duis ipsum qui ad magna nisi. Ad aliquip est fugiat dolor ad deserunt. Dolore excepteur enim ullamco duis irure elit occaecat magna mollit culpa elit amet elit minim. Fugiat nostrud sunt consequat labore dolore sunt minim nisi deserunt minim sint consequat. Cupidatat adipisicing sit ullamco proident proident id esse mollit.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
${await ctx.b(Footer, { path, title })}
|
||||||
|
`
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
main {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function HomePage (ctx, { path, title }) {
|
||||||
|
ctx.css.add(base)
|
||||||
|
ctx.css.add(styles)
|
||||||
|
ctx.head.add(head({ title }))
|
||||||
|
ctx.body = await body(ctx, { path, title })
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
29
pages/Page.js
Normal file
29
pages/Page.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { html, render } from '../templater.js'
|
||||||
|
|
||||||
|
async function Root (ctx, component, ...params) {
|
||||||
|
const result = await ctx.render(component, ...params)
|
||||||
|
const scripts = Array.from(result.js).join(';\n')
|
||||||
|
const css = Array.from(result.css).join('\n')
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
${Array.from(result.head).join('\n')}
|
||||||
|
<style>
|
||||||
|
${css}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${result.body}
|
||||||
|
${scripts ? `<script>${scripts}</script>` : ''}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function page(component, ...params) {
|
||||||
|
return render(Root, component, ...params)
|
||||||
|
}
|
58
styles/base.js
Normal file
58
styles/base.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { css } from '../templater.js'
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
:root {
|
||||||
|
--color-bg: #d3b57e;
|
||||||
|
--color-headings: #522c05;
|
||||||
|
--color-body: #664405;
|
||||||
|
--base-font-size: 2rem;
|
||||||
|
--font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--color-body);
|
||||||
|
background: var(--color-bg);
|
||||||
|
font-size: var(--base-font-size);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--base-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(var(--base-font-size) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(var(--base-font-size) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--color-headings);
|
||||||
|
}
|
||||||
|
|
||||||
|
section, header, footer {
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section ul {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
`
|
87
templater.js
Normal file
87
templater.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { hash, fromCache, toCache } from './cache.js'
|
||||||
|
|
||||||
|
// Tagged template literal helpers. Operates in the same fashion as un-tagged template literals.
|
||||||
|
function tmpl(strings, ...values) {
|
||||||
|
return strings
|
||||||
|
.map((str, index) => values[index] ? str + values[index] : str)
|
||||||
|
.join('').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for syntax highlighting in supported editors.
|
||||||
|
export const html = tmpl
|
||||||
|
export const css = tmpl
|
||||||
|
export const javascript = tmpl
|
||||||
|
|
||||||
|
// Stupid-simple "sanitization". Just enough for this demo, not for production.
|
||||||
|
export function sanitize(str) {
|
||||||
|
if (typeof str !== 'string') return str
|
||||||
|
return str.split('<').join('<')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand to render a component and retrieve the body.
|
||||||
|
async function b(component, ...params) {
|
||||||
|
return (await this.render(component, ...params)).body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand to render a component and retrieve the head.
|
||||||
|
async function h(component, ...params) {
|
||||||
|
return (await this.render(component, ...params)).head
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand to render a component and retrieve the css.
|
||||||
|
async function c(component, ...params) {
|
||||||
|
return (await this.render(component, ...params)).css
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand to render a component and retrieve the js.
|
||||||
|
async function j(component, ...params) {
|
||||||
|
return (await this.render(component, ...params)).js
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a component and return the head elements, css, js, and body contents of it and its children.
|
||||||
|
* @param {Function<context, ...any>: context} component A function
|
||||||
|
* @param {...any} params Arguments to pass to the component being rendered
|
||||||
|
* @returns {Object<head: Set, css: Set, js: Set, body: String|null, render: Function>}
|
||||||
|
* An object containing head elements, css, js, and body text returned by the rendered component.
|
||||||
|
*/
|
||||||
|
export async function render(component, ...params) {
|
||||||
|
// Create a new context to pass to a component for each rendering.
|
||||||
|
const ctx = {
|
||||||
|
head: new Set(),
|
||||||
|
css: new Set(),
|
||||||
|
js: new Set(),
|
||||||
|
body: null,
|
||||||
|
}
|
||||||
|
// Add render functions to the context object, bound to the current context.
|
||||||
|
// This ensures any calls to render child components will have the parent context bound as "this", allowing copying
|
||||||
|
// child render results into parent context without having to pass the parent context explicitly to every child component.
|
||||||
|
ctx.render = render.bind(ctx)
|
||||||
|
ctx.b = b.bind(ctx)
|
||||||
|
ctx.h = h.bind(ctx)
|
||||||
|
ctx.c = c.bind(ctx)
|
||||||
|
ctx.j = j.bind(ctx)
|
||||||
|
|
||||||
|
// Attempt to load result from cache by component name and params.
|
||||||
|
const key = hash([component.name, params])
|
||||||
|
const stored = fromCache(key)
|
||||||
|
if (stored) {
|
||||||
|
console.info(`READING COMPONENT "${component.name}" FROM CACHE`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render component or load from cache.
|
||||||
|
const result = stored || await component(ctx, ...params)
|
||||||
|
// Store result to cache if it wasn't already present.
|
||||||
|
if (!stored) toCache(key, result)
|
||||||
|
|
||||||
|
// Results with a parent context will copy child context fields into parent context.
|
||||||
|
// This enables straightforward component-level caching of body, head, css, and js at the cost of higher memory usage.
|
||||||
|
if (this) {
|
||||||
|
result.head.forEach(r => this.head.add(r))
|
||||||
|
result.css.forEach(r => this.css.add(r))
|
||||||
|
result.js.forEach(r => this.js.add(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result context object to caller.
|
||||||
|
return result
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user