mapspinner 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/workflows/fps-perf.js +147 -0
- package/.claude/workflows/optimize-dna.js +201 -0
- package/.claude/workflows/shader-bottleneck-dna.js +168 -0
- package/.claude/workflows/speed-dna.js +209 -0
- package/.claude/workflows/startup-perf.js +117 -0
- package/.github/workflows/publish.yml +43 -0
- package/AGENTS.md +265 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +1 -0
- package/README.md +82 -0
- package/examples/basic-sdk-usage.html +114 -0
- package/package.json +28 -0
- package/planet.html +2181 -0
- package/planet.zip +0 -0
- package/scripts/backend-ab.mjs +88 -0
- package/scripts/dev-chrome.cmd +22 -0
- package/scripts/verify.mjs +69 -0
- package/server.js +127 -0
- package/src/anchor-field.js +559 -0
- package/src/gl-render.js +944 -0
- package/src/index.js +41 -0
- package/src/planet-orchestrator.js +790 -0
- package/src/quadtree.js +160 -0
- package/src/shaders/atmosphere.glsl +215 -0
- package/src/shaders/terrain.glsl +2109 -0
- package/src/terrain-gen-controls.js +122 -0
- package/tests/run.js +58 -0
- package/textures/grass-color.jpg +0 -0
- package/textures/grass-displacement.jpg +0 -0
- package/textures/rock-color.jpg +0 -0
- package/textures/rock-displacement.jpg +0 -0
- package/textures/sand-color.jpg +0 -0
- package/textures/sand-displacement.jpg +0 -0
- package/textures/snow-color.jpg +0 -0
- package/textures/snow-displacement.jpg +0 -0
package/planet.zip
ADDED
|
Binary file
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// backend-ab.mjs -- the "is this GPU/backend-keyed?" one-command answer (2026-06-12 tooling; the
|
|
2
|
+
// question that cost a full day on the FXC per-callsite hunt). Launches TWO Chromes against the
|
|
3
|
+
// live server -- default ANGLE (d3d11 on Windows) and vulkan -- parks BOTH at the same pose, and
|
|
4
|
+
// prints renderer string + luminance stats + a verdict, saving side-by-side screenshots to .gm/.
|
|
5
|
+
//
|
|
6
|
+
// node scripts/backend-ab.mjs # deterministic lowland pose
|
|
7
|
+
// node scripts/backend-ab.mjs 0.21 0.43 0.87 5 # camDir x y z + altKm
|
|
8
|
+
//
|
|
9
|
+
// Needs: server.js on :8080, Chrome installed. Cold d3d11 compile is ONCE per profile (cached after).
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
|
|
13
|
+
const CHROME = ['C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
14
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'].find(p => fs.existsSync(p));
|
|
15
|
+
const [dx, dy, dz, altKm] = process.argv.slice(2).map(Number);
|
|
16
|
+
const havePose = [dx, dy, dz].every(Number.isFinite);
|
|
17
|
+
|
|
18
|
+
const CFG = [
|
|
19
|
+
{ name: 'd3d11', port: 9231, args: [] },
|
|
20
|
+
{ name: 'vulkan', port: 9232, args: ['--use-angle=vulkan'] },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function cdp(port) {
|
|
24
|
+
const ver = await (await fetch(`http://localhost:${port}/json/version`)).json();
|
|
25
|
+
const list = await (await fetch(`http://localhost:${port}/json`)).json();
|
|
26
|
+
const pg = list.find(t => t.type === 'page' && t.url.includes('localhost:8080'));
|
|
27
|
+
const ws = new WebSocket(ver.webSocketDebuggerUrl);
|
|
28
|
+
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
|
29
|
+
let seq = 0; const pending = new Map();
|
|
30
|
+
ws.onmessage = (ev) => { const m = JSON.parse(ev.data);
|
|
31
|
+
if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id);
|
|
32
|
+
m.error ? rej(new Error(m.error.message)) : res(m.result); } };
|
|
33
|
+
const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
|
|
34
|
+
const id = ++seq; pending.set(id, { res, rej });
|
|
35
|
+
ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params })); });
|
|
36
|
+
const { sessionId } = await send('Target.attachToTarget', { targetId: pg.id, flatten: true });
|
|
37
|
+
await send('Runtime.enable', {}, sessionId);
|
|
38
|
+
// foreground so the compile poll runs at full rate (background rAF throttling)
|
|
39
|
+
await fetch(`http://localhost:${port}/json/activate/${pg.id}`).catch(() => {});
|
|
40
|
+
const evalIn = async (expression) => {
|
|
41
|
+
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, sessionId);
|
|
42
|
+
if (r.exceptionDetails) throw new Error((r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300));
|
|
43
|
+
return r.result.value; };
|
|
44
|
+
return { evalIn, send, sessionId, ws };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const POSE = havePose
|
|
48
|
+
? `const u0=[${dx},${dy},${dz}];const l0=Math.hypot(...u0);const land={u:[u0[0]/l0,u0[1]/l0,u0[2]/l0]};`
|
|
49
|
+
: `let land=null;for(let i=0;i<3000&&!land;i++){const y=1-2*(i+0.5)/3000,rr=Math.sqrt(Math.max(0,1-y*y)),t=i*2.399963229;
|
|
50
|
+
const u=[Math.cos(t)*rr,y,Math.sin(t)*rr];const h=sg(u);if(h>200&&h<900&&Math.abs(y)<0.5)land={u,h};}`;
|
|
51
|
+
|
|
52
|
+
async function measure(name, port) {
|
|
53
|
+
const c = await cdp(port);
|
|
54
|
+
for (;;) { const st = await c.evalIn(`window.__planetOrchStatus||'init'`).catch(() => 'nav');
|
|
55
|
+
if (st === 'ready') break; await new Promise(r => setTimeout(r, 5000)); }
|
|
56
|
+
const out = await c.evalIn(`(async()=>{
|
|
57
|
+
const d=window.__diag; await d.probeWarm();
|
|
58
|
+
const sg=window.__planetOrch.render.sampleGroundM;
|
|
59
|
+
${POSE}
|
|
60
|
+
const camC=window.__dbg.cam,latC=Math.asin(land.u[1]),lonC=Math.atan2(land.u[0],land.u[2]);
|
|
61
|
+
camC.sunLatBase=Math.max(-1.4,latC-0.5);camC.sunLonBase=lonC;camC.sunLonAccum=0;camC.timeScale=0;
|
|
62
|
+
await d.aimDir(land.u, ${Number.isFinite(altKm) ? altKm : 2}, 50);
|
|
63
|
+
const A=d._read(); let n=0,s=0,s2=0,grey=0;
|
|
64
|
+
for(let i=0;i<A.px.length;i+=28){const r0=A.px[i],g0=A.px[i+1],b0=A.px[i+2];
|
|
65
|
+
const lum=0.2126*r0+0.7152*g0+0.0722*b0;n++;s+=lum;s2+=lum*lum;
|
|
66
|
+
const mx=Math.max(r0,g0,b0),mn=Math.min(r0,g0,b0);if(mx>30&&(mx-mn)<18)grey++;}
|
|
67
|
+
const mean=s/n,sd=Math.sqrt(Math.max(0,s2/n-mean*mean));
|
|
68
|
+
return {gpu:(window.__gpuRenderer||'').slice(0,70),lumMean:+mean.toFixed(1),lumSD:+sd.toFixed(2),greyFrac:+(grey/n).toFixed(3)};
|
|
69
|
+
})()`);
|
|
70
|
+
const shot = await c.send('Page.captureScreenshot', { format: 'png' }, c.sessionId);
|
|
71
|
+
fs.writeFileSync(`.gm/ab-${name}.png`, Buffer.from(shot.data, 'base64'));
|
|
72
|
+
c.ws.close();
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const cfg of CFG) {
|
|
77
|
+
spawn(CHROME, [`--user-data-dir=C:/dev/tv8/.gm/tmp/ab-${cfg.name}`, `--remote-debugging-port=${cfg.port}`,
|
|
78
|
+
'--no-first-run', '--no-default-browser-check', ...cfg.args, 'http://localhost:8080/planet.html'],
|
|
79
|
+
{ detached: true, stdio: 'ignore' }).unref();
|
|
80
|
+
}
|
|
81
|
+
await new Promise(r => setTimeout(r, 8000));
|
|
82
|
+
const results = {};
|
|
83
|
+
for (const cfg of CFG) results[cfg.name] = await measure(cfg.name, cfg.port);
|
|
84
|
+
const dSD = Math.abs(results.d3d11.lumSD - results.vulkan.lumSD);
|
|
85
|
+
const dGrey = Math.abs(results.d3d11.greyFrac - results.vulkan.greyFrac);
|
|
86
|
+
console.log(JSON.stringify({ ...results,
|
|
87
|
+
verdict: (dSD > 5 || dGrey > 0.15) ? 'BACKEND-DIVERGENT (suspect FXC translation -- see AGENTS.md FXC section)' : 'backends agree',
|
|
88
|
+
screenshots: ['.gm/ab-d3d11.png', '.gm/ab-vulkan.png'] }, null, 1));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem TV8 dev Chrome launcher -- kills the 150s cold shader compile (measured 2026-06-10).
|
|
3
|
+
rem
|
|
4
|
+
rem The ~150s cold compile of terrain.glsl is the ANGLE D3D11 backend's FXC
|
|
5
|
+
rem HLSL-optimize pass (default Chrome on Windows). Switching the ANGLE backend
|
|
6
|
+
rem removes FXC entirely; same shader source, measured cold shaderCompileMs:
|
|
7
|
+
rem d3d11 (default) : 152379 ms
|
|
8
|
+
rem opengl : 4653 ms cold / 96 ms warm (NVIDIA GL driver cache)
|
|
9
|
+
rem vulkan : 140 ms
|
|
10
|
+
rem Every shader EDIT recompiles cold, so on vulkan the edit loop is ~0.1s not 150s.
|
|
11
|
+
rem
|
|
12
|
+
rem Usage: scripts\dev-chrome.cmd [gl|d3d11] (default: vulkan)
|
|
13
|
+
rem A persistent profile keeps driver/browser caches warm across runs.
|
|
14
|
+
|
|
15
|
+
set BACKEND=vulkan
|
|
16
|
+
if /i "%1"=="gl" set BACKEND=gl
|
|
17
|
+
if /i "%1"=="d3d11" set BACKEND=d3d11
|
|
18
|
+
|
|
19
|
+
set CHROME="C:\Program Files\Google\Chrome\Application\chrome.exe"
|
|
20
|
+
if not exist %CHROME% set CHROME="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
|
|
21
|
+
|
|
22
|
+
%CHROME% --user-data-dir="%~dp0..\.gm\tmp\chrome-dev-%BACKEND%" --use-angle=%BACKEND% --remote-debugging-port=9222 --no-first-run --no-default-browser-check http://localhost:8080/
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// verify.mjs -- TV8 permanent make-sure-it-works runner (2026-06-11 policy: every render/shader
|
|
2
|
+
// fix is verified against the LIVE page before commit; compile-clean alone never ships).
|
|
3
|
+
//
|
|
4
|
+
// Drives the in-page witness suite (window.__diag.verifyAll / coastWitness / materialWitness /
|
|
5
|
+
// shadeKeyWitness / limbScan / hazeProbe ... in planet.html) over RAW CDP using Node's built-in
|
|
6
|
+
// WebSocket -- no relay, no per-call execution cap, no session recycling, zero dependencies.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node scripts/verify.mjs # full suite (__diag.verifyAll)
|
|
10
|
+
// node scripts/verify.mjs materialWitness # one probe
|
|
11
|
+
// node scripts/verify.mjs "expr" # any expression on the planet page (await'ed)
|
|
12
|
+
// Needs: dev server on :8080 and a chrome with --remote-debugging-port=9222 (headless ok):
|
|
13
|
+
// chrome --headless=new --remote-debugging-port=9222 --user-data-dir=.gm/.cdp-profile about:blank
|
|
14
|
+
// Exit code 0 = pass, 1 = fail/error. Prints the JSON verdict.
|
|
15
|
+
|
|
16
|
+
const CDP_HTTP = process.env.CDP_URL || 'http://localhost:9222';
|
|
17
|
+
const PAGE_URL = process.env.PAGE_URL || 'http://localhost:8080/planet.html';
|
|
18
|
+
const probe = process.argv[2] || 'verifyAll';
|
|
19
|
+
|
|
20
|
+
const ver = await (await fetch(CDP_HTTP + '/json/version')).json();
|
|
21
|
+
const ws = new WebSocket(ver.webSocketDebuggerUrl);
|
|
22
|
+
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
|
23
|
+
|
|
24
|
+
let seq = 0; const pending = new Map();
|
|
25
|
+
ws.onmessage = (ev) => {
|
|
26
|
+
const m = JSON.parse(ev.data);
|
|
27
|
+
if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id);
|
|
28
|
+
m.error ? rej(new Error(m.error.message)) : res(m.result); }
|
|
29
|
+
};
|
|
30
|
+
const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
|
|
31
|
+
const id = ++seq; pending.set(id, { res, rej });
|
|
32
|
+
ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params }));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { targetId } = await send('Target.createTarget', { url: PAGE_URL });
|
|
36
|
+
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
|
|
37
|
+
await send('Runtime.enable', {}, sessionId);
|
|
38
|
+
|
|
39
|
+
const evalIn = async (expression, awaitPromise = true) => {
|
|
40
|
+
const r = await send('Runtime.evaluate', { expression, awaitPromise, returnByValue: true }, sessionId);
|
|
41
|
+
if (r.exceptionDetails) throw new Error(r.exceptionDetails.text + ' ' + (r.exceptionDetails.exception?.description || '').slice(0, 300));
|
|
42
|
+
return r.result.value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// wait for the orchestrator (cold shader compile can take minutes on SwiftShader)
|
|
46
|
+
const deadline = Date.now() + 8 * 60 * 1000;
|
|
47
|
+
// Close the created page on EVERY exit path (perf sweep follow-up 2026-06-11: early-exit/killed runs
|
|
48
|
+
// leaked one headless planet.html per run; leaked pages poll /cmd and steal live-tab diagnostics).
|
|
49
|
+
const closeTarget = () => send('Target.closeTarget', { targetId }).catch(() => {});
|
|
50
|
+
process.on('SIGINT', async () => { await closeTarget(); process.exit(130); });
|
|
51
|
+
process.on('SIGTERM', async () => { await closeTarget(); process.exit(143); });
|
|
52
|
+
for (;;) {
|
|
53
|
+
const st = await evalIn('window.__planetOrchStatus || "init"', false).catch(() => 'navigating');
|
|
54
|
+
if (st === 'ready') break;
|
|
55
|
+
if (st === 'error') { console.log(JSON.stringify({ pass: false, err: 'orch-error' })); await closeTarget(); process.exit(1); }
|
|
56
|
+
if (Date.now() > deadline) { console.log(JSON.stringify({ pass: false, err: 'ready-timeout' })); await closeTarget(); process.exit(1); }
|
|
57
|
+
await new Promise(r => setTimeout(r, 4000));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const expr = /^[A-Za-z]\w*$/.test(probe)
|
|
61
|
+
? `window.__diag.${probe}()`
|
|
62
|
+
: probe;
|
|
63
|
+
let verdict;
|
|
64
|
+
try { verdict = await evalIn(`(async()=>{ const r = await (${expr}); return r; })()`); }
|
|
65
|
+
catch (e) { verdict = { pass: false, err: String(e.message || e).slice(0, 500) }; }
|
|
66
|
+
|
|
67
|
+
await closeTarget();
|
|
68
|
+
console.log(JSON.stringify(verdict, null, 1));
|
|
69
|
+
process.exit(verdict && (verdict.pass || verdict.ok) ? 0 : 1);
|
package/server.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 8080;
|
|
6
|
+
// Static file server for the GPU one-fractal planet (planet.html + src/*.js + shaders/*.glsl).
|
|
7
|
+
// No C++/wasm binary, no capture/diag sinks (the old GPU-capture pipeline is deleted).
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
'.html': 'text/html; charset=utf-8',
|
|
10
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
11
|
+
'.mjs': 'text/javascript; charset=utf-8', // ES module imports require a JS MIME (browser rejects octet-stream)
|
|
12
|
+
'.json': 'application/json',
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.css': 'text/css',
|
|
15
|
+
'.glsl': 'text/plain; charset=utf-8',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const server = http.createServer((req, res) => {
|
|
19
|
+
// Set COOP/COEP headers for SharedArrayBuffer support
|
|
20
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
21
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
22
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
23
|
+
// Disable caching so iterative dev edits show up on reload
|
|
24
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
25
|
+
res.setHeader('Pragma', 'no-cache');
|
|
26
|
+
res.setHeader('Expires', '0');
|
|
27
|
+
|
|
28
|
+
const urlPath = req.url.split('?')[0];
|
|
29
|
+
|
|
30
|
+
// DIAG SINK (re-hosted 2026-06-07, user: 'it wants to contact /diag, we're missing diagnostic info by
|
|
31
|
+
// not hosting that'). planet.html postDiag() POSTs a per-frame JSON line; without a handler it 404s and
|
|
32
|
+
// the live diagnostic stream is lost. The last 200 lines are kept in an in-memory ring so a headless
|
|
33
|
+
// agent can GET /diag/tail and read the live render state (face/alt/quads/glError/height) WITHOUT a
|
|
34
|
+
// browser tool. (The old capture/diag.ndjson file append was a dead ENOENT no-op after capture/ was
|
|
35
|
+
// deleted in c131284 -- removed 2026-06-11; the ring is the only consumer anyone reads.)
|
|
36
|
+
if (urlPath === '/diag' && req.method === 'POST') {
|
|
37
|
+
let body = '';
|
|
38
|
+
req.on('data', c => { body += c; if (body.length > 1e6) req.destroy(); });
|
|
39
|
+
req.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
const line = body.trim();
|
|
42
|
+
if (line) {
|
|
43
|
+
server._diagRing = server._diagRing || [];
|
|
44
|
+
server._diagRing.push(line);
|
|
45
|
+
if (server._diagRing.length > 200) server._diagRing.shift();
|
|
46
|
+
}
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
res.writeHead(204); res.end();
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (urlPath === '/diag/tail') {
|
|
53
|
+
const ring = server._diagRing || [];
|
|
54
|
+
const n = Math.min(ring.length, parseInt((req.url.split('?')[1] || '').replace(/^n=/, '')) || 30);
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'application/x-ndjson; charset=utf-8' });
|
|
56
|
+
res.end(ring.slice(-n).join('\n') + '\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (urlPath === '/diag/clear') { server._diagRing = []; res.writeHead(204); res.end(); return; }
|
|
60
|
+
|
|
61
|
+
// AGENT COMMAND CHANNEL (2026-06-07, user: 'maximize live code execution + hot reloading'). Lets a
|
|
62
|
+
// headless agent drive the warm tab WITHOUT a reload or browser tool: POST /cmd {js} enqueues a JS
|
|
63
|
+
// snippet; the page polls GET /cmd/next, runs it, and POSTs the result to /diag (kind:'cmd-result').
|
|
64
|
+
// So toggling the atlas, hot-reloading the shader (window.__diag.recompile), and probing live state
|
|
65
|
+
// are all one curl away -- the simplest possible iterate loop given the cold compile blocks the tool.
|
|
66
|
+
if (urlPath === '/cmd' && req.method === 'POST') {
|
|
67
|
+
let body = '';
|
|
68
|
+
req.on('data', c => { body += c; if (body.length > 2e6) req.destroy(); });
|
|
69
|
+
req.on('end', () => {
|
|
70
|
+
try { const o = JSON.parse(body); server._cmdQ = server._cmdQ || []; server._cmdQ.push({ id: (server._cmdId = (server._cmdId || 0) + 1), js: o.js }); res.writeHead(200, {'Content-Type':'application/json'}); res.end(JSON.stringify({ queued: server._cmdId })); }
|
|
71
|
+
catch (e) { res.writeHead(400); res.end(String(e)); }
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (urlPath === '/cmd/next') {
|
|
76
|
+
const q = server._cmdQ || [];
|
|
77
|
+
const cmd = q.shift() || null;
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
79
|
+
res.end(JSON.stringify(cmd));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Root IS the GPU planet (planet.html): one-fractal terrain on WebGL, no wasm. /rewrite.html
|
|
83
|
+
// kept as an alias. Everything else falls through to a static file (src/, shaders, etc.).
|
|
84
|
+
let filepath;
|
|
85
|
+
if (urlPath === '/' || urlPath === '/index.html' || urlPath === '/rewrite.html' || urlPath === '/rewrite') {
|
|
86
|
+
filepath = path.join(__dirname, 'planet.html');
|
|
87
|
+
} else {
|
|
88
|
+
filepath = path.join(__dirname, urlPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prevent directory traversal
|
|
92
|
+
if (!filepath.startsWith(__dirname)) {
|
|
93
|
+
res.writeHead(403);
|
|
94
|
+
res.end('Forbidden');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.stat(filepath, (err, stats) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
res.writeHead(404);
|
|
101
|
+
res.end('Not Found');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (stats.isDirectory()) {
|
|
106
|
+
filepath = path.join(filepath, 'index.html');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ext = path.extname(filepath);
|
|
110
|
+
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
111
|
+
|
|
112
|
+
fs.readFile(filepath, (err, content) => {
|
|
113
|
+
if (err) {
|
|
114
|
+
res.writeHead(500);
|
|
115
|
+
res.end('Server Error');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
120
|
+
res.end(content);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.listen(PORT, () => {
|
|
126
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
127
|
+
});
|