66 lines
3.4 KiB
JavaScript
66 lines
3.4 KiB
JavaScript
import http from 'node:http';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { watch } from 'node:fs';
|
|
import { loadEnvFile } from 'node:process';
|
|
import { build, root, readArticles } from './site.mjs';
|
|
import { PushService, createPushHandler } from './push.mjs';
|
|
try {loadEnvFile(path.join(root,'.env'));} catch(error) {if(error.code!=='ENOENT')throw error;}
|
|
const port=Number(process.env.PORT || 3000);
|
|
const siteUrl=new URL(process.env.SITE_URL || `http://localhost:${port}`).origin;
|
|
if(!/^https?:/.test(siteUrl))throw new Error('SITE_URL must be an HTTP(S) URL.');
|
|
let articles=await build();
|
|
let push;
|
|
if(process.env.PUSH_ENABLED!=='false') {
|
|
const subject=process.env.VAPID_SUBJECT || (siteUrl.startsWith('https:')?siteUrl:'https://localhost');
|
|
push=await new PushService({directory:path.resolve(root,process.env.PUSH_DATA_DIR || '.data/push'),subject}).init(articles);
|
|
}
|
|
const handlePush=createPushHandler(push,{siteUrl});
|
|
let rebuilding=Promise.resolve();
|
|
let timer;
|
|
let snapshot=JSON.stringify(articles);
|
|
function refresh() {
|
|
rebuilding=rebuilding.then(async()=>{
|
|
articles=await build();snapshot=JSON.stringify(articles);
|
|
if(push)await push.sync(articles);
|
|
}).catch(error=>console.error('Rebuild failed:',error.message));
|
|
return rebuilding;
|
|
}
|
|
for(const folder of ['pages','public'])watch(path.join(root,folder),{recursive:true},()=>{clearTimeout(timer);timer=setTimeout(refresh,200);});
|
|
// Polling also detects changes if filesystem events are missed. Delivery happens
|
|
// only after a successful build, so a notification never precedes its page.
|
|
let ticking=false;
|
|
setInterval(async()=>{
|
|
if(ticking)return;ticking=true;
|
|
try {
|
|
await rebuilding;
|
|
if(JSON.stringify(await readArticles())!==snapshot)await refresh();
|
|
if(push)await push.flush();
|
|
}catch(error){console.error('Notification worker:',error.message);}
|
|
finally{ticking=false;}
|
|
},5000).unref();
|
|
const types={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.jpg':'image/jpeg','.png':'image/png','.webp':'image/webp','.webmanifest':'application/manifest+json'};
|
|
const server=http.createServer(async(req,res)=>{
|
|
try {
|
|
const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);
|
|
if(await handlePush(req,res,pathname))return;
|
|
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405);res.end();return;}
|
|
await rebuilding;
|
|
const base=path.join(root,'dist');
|
|
let target=path.resolve(base,'.'+pathname);
|
|
if(!target.startsWith(base+path.sep)&&target!==base){res.writeHead(403);res.end();return;}
|
|
if((await fs.stat(target)).isDirectory()){
|
|
if(!pathname.endsWith('/')){res.writeHead(301,{Location:pathname+'/'});res.end();return;}
|
|
target=path.join(target,'index.html');
|
|
}
|
|
const content=await fs.readFile(target);
|
|
const headers={'Content-Type':types[path.extname(target)] || 'application/octet-stream'};
|
|
if(pathname==='/sw.js')headers['Cache-Control']='no-cache';
|
|
res.writeHead(200,headers);res.end(req.method==='HEAD'?undefined:content);
|
|
}catch{
|
|
res.writeHead(404,{'Content-Type':'text/html; charset=utf-8'});
|
|
res.end(req.method==='HEAD'?undefined:await fs.readFile(path.join(root,'dist/404.html')).catch(()=>'Page not found'));
|
|
}
|
|
});
|
|
server.listen(port,'0.0.0.0',()=>console.log(`TrueNews → ${siteUrl}${push?' (push notifications enabled)':''}`));
|