Reworked to be in Vite
This commit is contained in:
parent
d686066339
commit
74fd3f25ee
25 changed files with 1623 additions and 336 deletions
24
block-01/week-01-the_web_under_the_hood/demo/.gitignore
vendored
Normal file
24
block-01/week-01-the_web_under_the_hood/demo/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
15
block-01/week-01-the_web_under_the_hood/demo/delay.html
Normal file
15
block-01/week-01-the_web_under_the_hood/demo/delay.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Delayed Response | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Delayed Response
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<p>This response was delayed by 5 seconds.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<h1>
|
||||
Server received your POST
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<pre><!--LOGIN_DATA--></pre>
|
||||
<p class="alert">
|
||||
⚠️ Over plain HTTP, anyone between you and the server could read this.
|
||||
</p>
|
||||
25
block-01/week-01-the_web_under_the_hood/demo/form.html
Normal file
25
block-01/week-01-the_web_under_the_hood/demo/form.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Form | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="form-data">
|
||||
<h1>
|
||||
Login Form
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<form id="login-form" class="flex flex-col gap-4 max-w-sm">
|
||||
<label>Username: <input name="username" type="text" /></label>
|
||||
<label>Password: <input name="password" type="password" /></label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p>
|
||||
Submit this and watch the terminal. Also check DevTools → Network tab.
|
||||
</p>
|
||||
<script type="module" src="/src/form.ts"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
24
block-01/week-01-the_web_under_the_hood/demo/index.html
Normal file
24
block-01/week-01-the_web_under_the_hood/demo/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
HTTP Echo Server
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<p>Check your terminal - the server printed everything it received.</p>
|
||||
<h2>Try these:</h2>
|
||||
<ul>
|
||||
<li><a href="/form">POST form demo</a></li>
|
||||
<li><a href="/set-cookie">Set a cookie</a></li>
|
||||
<li><a href="/show-cookies">Show cookies the server received</a></li>
|
||||
<li><a href="/json">JSON response</a></li>
|
||||
<li><a href="/delay">Delayed response</a></li>
|
||||
<li><a href="/status">HTTP status codes</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
{
|
||||
"name": "week-01-the_web_under_the_hood-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.ts",
|
||||
"name": "dem",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch server.ts",
|
||||
"build": "tsc server.ts --outDir dist",
|
||||
"start": "node dist/server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"http-status": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"tsx": "^4.22.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -1,76 +0,0 @@
|
|||
* {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #040b04;
|
||||
color: #7cff7c;
|
||||
text-shadow: 0 0 5px rgba(124, 255, 124, 0.35);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #a8ff9f;
|
||||
text-shadow: 0 0 8px rgba(124, 255, 124, 0.55);
|
||||
}
|
||||
|
||||
p {
|
||||
color: #7cff7c;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9fffa0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #d6ffd6;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(124, 255, 124, 0.25);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-top: 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 8px;
|
||||
border: 1px solid #7cff7c;
|
||||
background-color: transparent;
|
||||
color: #7cff7c;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
background-color: #7cff7c;
|
||||
color: #040b04;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #9fffa0;
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
import express from "express"
|
||||
import fs from "fs/promises"
|
||||
import { status } from "http-status"
|
||||
import { createServer as createViteServer } from "vite"
|
||||
import { injectIntoHtml } from "./src/utils/inject"
|
||||
import { partials } from "./src/plugins/injectPartials"
|
||||
|
||||
const app = express()
|
||||
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "mpa",
|
||||
})
|
||||
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
app.use(express.json())
|
||||
|
||||
|
|
@ -24,52 +33,22 @@ app.use((req, res, next) => {
|
|||
next()
|
||||
})
|
||||
|
||||
// Serve static files from the "public" directory (for CSS, images, etc.)
|
||||
app.use(express.static("public"))
|
||||
|
||||
// Simple GET: echoes request info back as HTML
|
||||
app.get("/", (req, res) => {
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>HTTP Echo Server</h1>
|
||||
<p>Check your terminal — the server printed everything it received.</p>
|
||||
<h2>Try these:</h2>
|
||||
<ul>
|
||||
<li><a href="/form">POST form demo</a></li>
|
||||
<li><a href="/set-cookie">Set a cookie</a></li>
|
||||
<li><a href="/show-cookies">Show cookies the server received</a></li>
|
||||
<li><a href="/json">JSON response</a></li>
|
||||
<li><a href="/delay">Delayed response</a></li>
|
||||
<li><a href="/status">HTTP status codes</a></li>
|
||||
</ul>
|
||||
`)
|
||||
})
|
||||
|
||||
// Form page: allows to submit a POST and see the body
|
||||
app.get("/form", (req, res) => {
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Login Form</h1>
|
||||
<form method="POST" action="/login">
|
||||
<label>Username: <input name="username" type="text" /></label>
|
||||
<label>Password: <input name="password" type="password" /></label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p>Submit this and watch the terminal. Also check DevTools → Network tab.</p>
|
||||
`)
|
||||
})
|
||||
|
||||
// Receives the POST: echoes the body back
|
||||
app.post("/login", (req, res) => {
|
||||
app.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Server received your POST</h1>
|
||||
<p><strong>Username:</strong> ${username}</p>
|
||||
<p><strong>Password:</strong> ${password}</p>
|
||||
<p style="color:red">⚠️ Over plain HTTP, anyone between you and the server could read this.</p>
|
||||
<a href="/form">Back</a>
|
||||
`)
|
||||
const base = await fs.readFile("src/partials/form-response.html", "utf-8")
|
||||
|
||||
let result = await injectIntoHtml(base, [
|
||||
...partials,
|
||||
{
|
||||
key: "<!--LOGIN_DATA-->",
|
||||
placement: "replace",
|
||||
type: "string",
|
||||
value: JSON.stringify({ username, password }, null, 2),
|
||||
},
|
||||
])
|
||||
|
||||
res.send(result)
|
||||
})
|
||||
|
||||
// Sets a cookie: shows Set-Cookie header in DevTools
|
||||
|
|
@ -78,23 +57,25 @@ app.get("/set-cookie", (req, res) => {
|
|||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 1000, // 1 hour
|
||||
})
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Cookie Set</h1>
|
||||
<p>The server sent a <code>Set-Cookie</code> header. Check DevTools → Network → Response Headers.</p>
|
||||
<p>Now visit <a href="/show-cookies">show-cookies</a> — the browser will automatically send it back.</p>
|
||||
`)
|
||||
req.next?.()
|
||||
})
|
||||
|
||||
// Shows what cookies the server received
|
||||
app.get("/show-cookies", (req, res) => {
|
||||
app.get("/show-cookies", async (req, res) => {
|
||||
const cookieHeader = req.headers["cookie"] || "none"
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Cookies the server received</h1>
|
||||
<p><code>Cookie: ${cookieHeader}</code></p>
|
||||
<p>The browser sent this automatically — you didn't write any JavaScript for that.</p>
|
||||
`)
|
||||
const base = await fs.readFile("show-cookie.html", "utf-8")
|
||||
|
||||
let result = await injectIntoHtml(base, [
|
||||
...partials,
|
||||
{
|
||||
key: "<!--COOKIES-->",
|
||||
placement: "replace",
|
||||
type: "string",
|
||||
value: cookieHeader,
|
||||
},
|
||||
])
|
||||
|
||||
res.send(result)
|
||||
})
|
||||
|
||||
// Returns JSON: shows Content-Type in action
|
||||
|
|
@ -110,68 +91,44 @@ app.get("/json", (req, res) => {
|
|||
// Delayed response: simulates a slow server
|
||||
app.get("/delay", (req, res) => {
|
||||
setTimeout(() => {
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Delayed Response</h1><p>This response was delayed by 5 seconds.</p>
|
||||
`)
|
||||
req.next?.()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
// Show all the different HTTP status codes
|
||||
app.get("/status", (req, res) => {
|
||||
res.send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>HTTP Status Codes</h1>
|
||||
<h2>2xx Success</h2>
|
||||
<ul>
|
||||
${[200, 201, 202, 203, 204, 205, 206, 207, 208, 226]
|
||||
.map(
|
||||
(code) =>
|
||||
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
|
||||
)
|
||||
.join("")}
|
||||
</ul>
|
||||
<h2>3xx Redirection</h2>
|
||||
<ul>
|
||||
${[300, 301, 302, 303, 304, 307, 308]
|
||||
.map(
|
||||
(code) =>
|
||||
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
|
||||
)
|
||||
.join("")}
|
||||
</ul>
|
||||
<h2>4xx Client Errors</h2>
|
||||
<ul>
|
||||
${[
|
||||
400, 401, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414,
|
||||
415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
|
||||
]
|
||||
.map(
|
||||
(code) =>
|
||||
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
|
||||
)
|
||||
.join("")}
|
||||
</ul>
|
||||
<h2>5xx Server Errors</h2>
|
||||
<ul>
|
||||
${[500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511]
|
||||
.map(
|
||||
(code) =>
|
||||
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
|
||||
)
|
||||
.join("")}
|
||||
</ul>
|
||||
`)
|
||||
// Demonstrates different status codes
|
||||
app.get("/status/:code", async (req, res) => {
|
||||
const code = parseInt(req.params.code)
|
||||
const base = await fs.readFile("status-code.html", "utf-8")
|
||||
|
||||
let result = await injectIntoHtml(base, [
|
||||
...partials,
|
||||
{
|
||||
key: "<!--HTTP_STATUS_CODE-->",
|
||||
placement: "replace",
|
||||
type: "string",
|
||||
value: code.toString(),
|
||||
occurrence: "all",
|
||||
},
|
||||
{
|
||||
key: "<!--HTTP_STATUS_NAME-->",
|
||||
placement: "replace",
|
||||
type: "string",
|
||||
value: String(status[code as keyof typeof status]),
|
||||
occurrence: "all",
|
||||
},
|
||||
{
|
||||
key: "<!--HTTP_HEADERS-->",
|
||||
placement: "replace",
|
||||
type: "string",
|
||||
value: JSON.stringify(req.headers, null, 2),
|
||||
occurrence: "all",
|
||||
},
|
||||
])
|
||||
|
||||
res.status(code).send(result)
|
||||
})
|
||||
|
||||
// Demonstrates different status codes
|
||||
app.get("/status/:code", (req, res) => {
|
||||
const code = parseInt(req.params.code)
|
||||
res.status(code).send(`
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<h1>Status ${code}</h1><p>This response was sent with HTTP status ${code} (${status[code as keyof typeof status]}).</p>
|
||||
`)
|
||||
})
|
||||
app.use(vite.middlewares)
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log("Echo server running on http://localhost:3000")
|
||||
|
|
|
|||
22
block-01/week-01-the_web_under_the_hood/demo/set-cookie.html
Normal file
22
block-01/week-01-the_web_under_the_hood/demo/set-cookie.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Set Cookie | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Cookie Set
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<p>
|
||||
The server sent a <code>Set-Cookie</code> header. Check DevTools → Network
|
||||
→ Response Headers.
|
||||
</p>
|
||||
<p>
|
||||
Now visit <a href="/show-cookies">show-cookies</a> — the browser will
|
||||
automatically send it back.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Show Cookie | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Show Cookies
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<p>The server received the following cookies from the browser:</p>
|
||||
<pre><!--COOKIES--></pre>
|
||||
</body>
|
||||
</html>
|
||||
25
block-01/week-01-the_web_under_the_hood/demo/src/form.ts
Normal file
25
block-01/week-01-the_web_under_the_hood/demo/src/form.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
document
|
||||
.querySelector("#login-form")!
|
||||
.addEventListener("submit", async (event) => {
|
||||
event.preventDefault()
|
||||
const form = event.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const data = Object.fromEntries(formData.entries())
|
||||
console.log("Submitting form with data:", data)
|
||||
|
||||
const response = await fetch("/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Login failed:", response.status, response.statusText)
|
||||
return
|
||||
}
|
||||
|
||||
const resultHtml = await response.text()
|
||||
document.querySelector("#form-data")!.innerHTML = resultHtml
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
<span class="blink">█</span>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<h1>
|
||||
Server received your POST
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<pre><!--LOGIN_DATA--></pre>
|
||||
<p class="alert">
|
||||
⚠️ Over plain HTTP, anyone between you and the server could read this.
|
||||
</p>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<nav>
|
||||
<ul class="flex gap-4">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/form">Form</a></li>
|
||||
<li><a href="/set-cookie">Set Cookie</a></li>
|
||||
<li><a href="/show-cookies">Show cookies</a></li>
|
||||
<li><a href="/json">JSON</a></li>
|
||||
<li><a href="/delay">Delayed</a></li>
|
||||
<li><a href="/status">HTTP Status codes</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import type { Plugin } from "vite"
|
||||
import { injectIntoHtml, type Injection } from "../utils/inject"
|
||||
|
||||
export function injectPartials(injections: Injection[]): Plugin {
|
||||
return {
|
||||
name: "inject-partials",
|
||||
transformIndexHtml(html) {
|
||||
return injectIntoHtml(html, injections, {
|
||||
onMissing: (key, occurrence) =>
|
||||
this.warn(
|
||||
`[inject-partials] Marker "${key}" (occurrence: ${occurrence}) not found — skipping.`,
|
||||
),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const partials: Injection[] = [
|
||||
{
|
||||
key: "<body>",
|
||||
placement: "after",
|
||||
type: "html",
|
||||
path: "src/partials/navigation.html",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
key: "</head>",
|
||||
placement: "before",
|
||||
type: "string",
|
||||
value: "<link rel='stylesheet' href='/src/style.css' />",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
key: "<!--BLINK-->",
|
||||
placement: "replace",
|
||||
type: "html",
|
||||
path: "src/partials/blink.html",
|
||||
occurrence: "all",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
key: "</head>",
|
||||
placement: "before",
|
||||
type: "string",
|
||||
value: "<link rel='icon' type='image/svg+xml' href='/favicon.svg' />",
|
||||
optional: true,
|
||||
},
|
||||
]
|
||||
54
block-01/week-01-the_web_under_the_hood/demo/src/status.ts
Normal file
54
block-01/week-01-the_web_under_the_hood/demo/src/status.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { status } from "http-status"
|
||||
|
||||
document.querySelector("#http-200")!.innerHTML = [
|
||||
200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
|
||||
]
|
||||
.map(
|
||||
(code) =>
|
||||
`<li><a href="/status/${code}">${code} ${status[code as keyof typeof status]}</a></li>`,
|
||||
)
|
||||
.join("")
|
||||
|
||||
document.querySelector("#http-300")!.innerHTML = [
|
||||
300, 301, 302, 303, 304, 307, 308,
|
||||
]
|
||||
.map(
|
||||
(code) => `
|
||||
<li>
|
||||
<a href="/status/${code}"
|
||||
>${code} ${status[code as keyof typeof status]}</a
|
||||
>
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
|
||||
document.querySelector("#http-400")!.innerHTML = [
|
||||
400, 401, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415,
|
||||
416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
|
||||
]
|
||||
.map(
|
||||
(code) => `
|
||||
<li>
|
||||
<a href="/status/${code}"
|
||||
>${code} ${status[code as keyof typeof status]}</a
|
||||
>
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
|
||||
document.querySelector("#http-500")!.innerHTML = [
|
||||
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
|
||||
]
|
||||
.map(
|
||||
(code) =>
|
||||
`
|
||||
<li>
|
||||
<a href="/status/${code}"
|
||||
>${code} ${status[code as keyof typeof status]}</a
|
||||
>
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
165
block-01/week-01-the_web_under_the_hood/demo/src/style.css
Normal file
165
block-01/week-01-the_web_under_the_hood/demo/src/style.css
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap");
|
||||
@import "tailwindcss";
|
||||
|
||||
@keyframes flicker {
|
||||
0%,
|
||||
95%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
96% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
97% {
|
||||
opacity: 1;
|
||||
}
|
||||
98% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
99% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(100vh);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
@apply box-border leading-[1.85] font-normal tracking-[0.04em];
|
||||
@apply selection:bg-lime-800/40 selection:text-lime-200;
|
||||
font-family: "Share Tech Mono", "Courier New", monospace;
|
||||
color: #5abf00;
|
||||
text-shadow: 0 0 5px rgba(90, 191, 0, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply relative p-6 bg-[#0a0a00];
|
||||
}
|
||||
|
||||
/* scanlines */
|
||||
body::before {
|
||||
@apply content-[''] fixed inset-0 pointer-events-none z-[9999];
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0px,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.18) 2px,
|
||||
rgba(0, 0, 0, 0.18) 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* vignette */
|
||||
body::after {
|
||||
@apply content-[''] fixed inset-0 pointer-events-none z-[9998];
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 55%,
|
||||
rgba(0, 0, 0, 0.65) 100%
|
||||
);
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl font-normal tracking-[0.08em];
|
||||
color: #a8ff40;
|
||||
text-shadow:
|
||||
0 0 10px rgba(140, 255, 60, 0.55),
|
||||
0 0 22px rgba(140, 255, 60, 0.2);
|
||||
animation: flicker 8s infinite;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-normal tracking-[0.06em] border-b border-[#1a2a00] pb-1.5 mt-8;
|
||||
color: #7dff40;
|
||||
text-shadow: 0 0 8px rgba(120, 255, 60, 0.45);
|
||||
}
|
||||
|
||||
a {
|
||||
@apply border-b border-[#4a7a00] no-underline transition-colors duration-100;
|
||||
color: #a8ff40;
|
||||
text-shadow: 0 0 6px rgba(140, 255, 60, 0.4);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply border-[#8abf00];
|
||||
color: #d4ff80;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-none pl-0;
|
||||
}
|
||||
|
||||
ul li::before {
|
||||
@apply content-['//\00a0'];
|
||||
color: #2a4a00;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-[#030300] border border-[#1a2a00] border-l-[3px] border-l-[#4a7a00] px-5 py-4 overflow-x-auto text-sm leading-[1.75];
|
||||
color: #5abf00;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply relative bg-transparent border border-[#4a7a00] px-5 py-2 text-xs tracking-[0.12em] uppercase cursor-pointer transition-all duration-100;
|
||||
font-family: "Share Tech Mono", monospace;
|
||||
color: #a8ff40;
|
||||
text-shadow: 0 0 6px rgba(140, 255, 60, 0.4);
|
||||
box-shadow:
|
||||
inset 0 0 8px rgba(100, 200, 0, 0.05),
|
||||
0 0 6px rgba(100, 200, 0, 0.1);
|
||||
}
|
||||
|
||||
button::before {
|
||||
@apply content-['▶\00a0'] text-[0.7rem];
|
||||
color: #4a7a00;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow:
|
||||
inset 0 0 12px rgba(100, 200, 0, 0.1),
|
||||
0 0 10px rgba(100, 200, 0, 0.2);
|
||||
background: rgba(100, 200, 0, 0.08);
|
||||
border-color: #7dff40;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-transparent border-0 border-b border-[#2a4a00] pb-1 pt-1 text-sm outline-none transition-colors duration-150;
|
||||
font-family: "Share Tech Mono", monospace;
|
||||
color: #7dff40;
|
||||
caret-color: #a8ff40;
|
||||
text-shadow: 0 0 4px rgba(120, 255, 60, 0.3);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
@apply border-[#7dff40];
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
@apply tracking-[0.1em];
|
||||
color: #1a3000;
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@apply text-sm mt-4 p-3 border border-[#7a7a00] bg-[#303000] rounded;
|
||||
color: #ff9f40;
|
||||
text-shadow: 0 0 4px rgba(255, 159, 64, 0.3);
|
||||
}
|
||||
146
block-01/week-01-the_web_under_the_hood/demo/src/utils/inject.ts
Normal file
146
block-01/week-01-the_web_under_the_hood/demo/src/utils/inject.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type Placement = "before" | "after" | "replace"
|
||||
|
||||
export type InjectionBase = {
|
||||
key: string
|
||||
placement: Placement
|
||||
occurrence?: number | "all"
|
||||
optional?: boolean
|
||||
}
|
||||
|
||||
export type InjectionString = InjectionBase & {
|
||||
type: "string"
|
||||
value: string
|
||||
}
|
||||
|
||||
export type InjectionHTML = InjectionBase & {
|
||||
type: "html"
|
||||
path: string
|
||||
}
|
||||
|
||||
export type Injection = InjectionString | InjectionHTML
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function nthIndexOf(haystack: string, needle: string, n: number): number {
|
||||
let index = -1
|
||||
let remaining = n
|
||||
let searchFrom = 0
|
||||
while (remaining > 0) {
|
||||
index = haystack.indexOf(needle, searchFrom)
|
||||
if (index === -1) return -1
|
||||
searchFrom = index + 1
|
||||
remaining--
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
function applyInjection(
|
||||
html: string,
|
||||
content: string,
|
||||
markerIndex: number,
|
||||
markerLength: number,
|
||||
placement: Placement,
|
||||
): string {
|
||||
switch (placement) {
|
||||
case "before":
|
||||
return html.slice(0, markerIndex) + content + html.slice(markerIndex)
|
||||
case "after": {
|
||||
const after = markerIndex + markerLength
|
||||
return html.slice(0, after) + content + html.slice(after)
|
||||
}
|
||||
case "replace":
|
||||
return (
|
||||
html.slice(0, markerIndex) +
|
||||
content +
|
||||
html.slice(markerIndex + markerLength)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Core ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function resolveContent(injection: Injection): Promise<string> {
|
||||
if (injection.type === "html") {
|
||||
try {
|
||||
return await fs.readFile(path.resolve(injection.path), "utf-8")
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
throw new Error(`[inject] Failed to read "${injection.path}": ${message}`)
|
||||
}
|
||||
}
|
||||
return injection.value
|
||||
}
|
||||
|
||||
export type InjectOptions = {
|
||||
/** Called when a marker is not found instead of throwing. Defaults to throwing. */
|
||||
onMissing?: (key: string, occurrence: number | "all") => void
|
||||
}
|
||||
|
||||
export async function injectIntoHtml(
|
||||
html: string,
|
||||
injections: Injection[],
|
||||
options: InjectOptions = {},
|
||||
): Promise<string> {
|
||||
const contents = await Promise.all(injections.map(resolveContent))
|
||||
|
||||
for (let i = 0; i < injections.length; i++) {
|
||||
const injection = injections[i]
|
||||
const content = contents[i]
|
||||
const occurrence = injection.occurrence ?? 1
|
||||
|
||||
const handleMissing = () => {
|
||||
if (injection.optional) return
|
||||
|
||||
if (options.onMissing) {
|
||||
options.onMissing(injection.key, occurrence)
|
||||
} else {
|
||||
throw new Error(
|
||||
`[inject] Marker "${injection.key}" (occurrence: ${occurrence}) not found.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (occurrence === "all") {
|
||||
const indices: number[] = []
|
||||
let searchFrom = 0
|
||||
let idx: number
|
||||
while ((idx = html.indexOf(injection.key, searchFrom)) !== -1) {
|
||||
indices.push(idx)
|
||||
searchFrom = idx + 1
|
||||
}
|
||||
if (indices.length === 0) {
|
||||
handleMissing()
|
||||
continue
|
||||
}
|
||||
for (let j = indices.length - 1; j >= 0; j--) {
|
||||
html = applyInjection(
|
||||
html,
|
||||
content,
|
||||
indices[j],
|
||||
injection.key.length,
|
||||
injection.placement,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const markerIndex = nthIndexOf(html, injection.key, occurrence)
|
||||
if (markerIndex === -1) {
|
||||
handleMissing()
|
||||
continue
|
||||
}
|
||||
html = applyInjection(
|
||||
html,
|
||||
content,
|
||||
markerIndex,
|
||||
injection.key.length,
|
||||
injection.placement,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><!--HTTP_STATUS_CODE--> | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Status
|
||||
<!--HTTP_STATUS_CODE-->
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<p>
|
||||
This response was sent with HTTP status
|
||||
<!--HTTP_STATUS_CODE-->
|
||||
(<!--HTTP_STATUS_NAME-->).
|
||||
</p>
|
||||
<h2>Headers</h2>
|
||||
<pre><!--HTTP_HEADERS--></pre>
|
||||
</body>
|
||||
</html>
|
||||
23
block-01/week-01-the_web_under_the_hood/demo/status.html
Normal file
23
block-01/week-01-the_web_under_the_hood/demo/status.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HTTP Status Codes | Web Under the Hood</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
HTTP Status Codes
|
||||
<!--BLINK-->
|
||||
</h1>
|
||||
<h2>2xx Success</h2>
|
||||
<ul id="http-200"></ul>
|
||||
<h2>3xx Redirection</h2>
|
||||
<ul id="http-300"></ul>
|
||||
<h2>4xx Client Errors</h2>
|
||||
<ul id="http-400"></ul>
|
||||
<h2>5xx Server Errors</h2>
|
||||
<ul id="http-500"></ul>
|
||||
<script type="module" src="/src/status.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
block-01/week-01-the_web_under_the_hood/demo/tsconfig.json
Normal file
23
block-01/week-01-the_web_under_the_hood/demo/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2023",
|
||||
"module": "esnext",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vite"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { injectPartials, partials } from "./src/plugins/injectPartials"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), injectPartials(partials)],
|
||||
})
|
||||
Loading…
Reference in a new issue