lacis
Version:
Zero-dependency TypeScript web framework
23 lines (20 loc) • 6.75 kB
JavaScript
import {resolve,join,relative}from'path';import {unlink,writeFile,readdir}from'fs/promises';import {spawn}from'child_process';import {watch,existsSync,readFileSync}from'fs';async function k(e){let t=[];async function n(r,s=[]){let o;try{o=await readdir(r,{withFileTypes:!0});}catch{return}let a=o.find(i=>!i.isDirectory()&&(i.name==="index.ts"||i.name==="index.js"));if(a){let i="./"+relative(e,join(r,a.name)).replace(/\\/g,"/").replace(/\.ts$/,".js"),c="/"+s.map(d=>d.replace(/^\[(\w+)\??\]$/,":$1")).join("/");t.push({importPath:i,routePath:c==="//"?"/":c});}for(let i of o)i.isDirectory()&&!i.name.startsWith("+")&&await n(join(r,i.name),[...s,i.name]);}return await n(e),t}async function F(e){let t=[];async function n(r,s){let o;try{o=await readdir(r,{withFileTypes:!0});}catch{return}let a=o.find(c=>c.name==="+middleware.global.ts")??o.find(c=>c.name==="+middleware.global.js");a&&t.push({importPath:"./"+relative(e,join(r,a.name)).replace(/\\/g,"/").replace(/\.ts$/,".js"),routePath:s,type:"cascade"});let i=o.find(c=>c.name==="+middleware.ts")??o.find(c=>c.name==="+middleware.js");i&&t.push({importPath:"./"+relative(e,join(r,i.name)).replace(/\\/g,"/").replace(/\.ts$/,".js"),routePath:s,type:"exact"});for(let c of o)if(c.isDirectory()&&!c.name.startsWith("+")){let d=s==="/"?`/${c.name}`:`${s}/${c.name}`;await n(join(r,c.name),d);}}return await n(e,"/"),t}async function m(e){let[t,n]=await Promise.all([k(e),F(e)]),r=t.map((d,f)=>`import * as _route_${f} from '${d.importPath}'`).join(`
`),s=n.map((d,f)=>`import * as _mw_${f} from '${d.importPath}'`).join(`
`),o=[r,s].filter(Boolean).join(`
`),a=t.map((d,f)=>` { path: '${d.routePath}', handlers: _route_${f} }`).join(`,
`),i=n.map((d,f)=>` { path: '${d.routePath}', type: '${d.type}', module: _mw_${f} }`).join(`,
`),c=["// AUTO-GENERATED by lacis build \u2014 do not edit",o,"","export const routes = [",a,"]","","export const middlewares = [",i,"]",""].join(`
`);await writeFile(join(e,"_manifest.ts"),c,"utf-8"),console.log(`[lacis] Generated manifest with ${t.length} route(s) and ${n.length} middleware(s)`);}function N(e){if(existsSync(join(e,"netlify.toml")))return "netlify";if(existsSync(join(e,"vercel.json")))return "vercel";try{let t=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),n={...t.dependencies,...t.devDependencies};if(n["@netlify/functions"]||n["netlify-cli"])return "netlify";if(n.vercel||n["@vercel/node"])return "vercel"}catch{}return process.versions.bun!==void 0||existsSync(join(e,"bun.lockb"))||existsSync(join(e,"bun.lock"))?"bun":"node"}function S(e){try{let t=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),n=t.module??t.main;if(typeof n=="string"&&existsSync(join(e,n)))return n}catch{}for(let t of ["server.ts","index.ts","app.ts","main.ts"])if(existsSync(join(e,t)))return t;return null}function R(e){try{return JSON.parse(readFileSync(join(e,"tsconfig.json"),"utf-8")).compilerOptions?.outDir??null}catch{return null}}async function v(e){let t=[],n;try{n=await readdir(e,{withFileTypes:!0});}catch{return t}for(let r of n)r.isDirectory()?t.push(...await v(join(e,r.name))):r.name.endsWith(".ts")&&r.name!=="_manifest.ts"&&t.push(join(e,r.name));return t}function D(e,t,n){return new Promise((r,s)=>{let o=spawn(e,t,{stdio:"inherit",cwd:n});o.on("close",a=>{a===0?r():s(new Error(`${e} exited with code ${a}`));}),o.on("error",a=>{a.code==="ENOENT"?s(new Error(`Command not found: ${e}. Make sure it is installed.`)):s(a);});})}async function $(e,t){let n=process.cwd(),r=N(n),s=join(e,"_manifest.ts");if(await m(e),r==="vercel"||r==="netlify"){console.log(`[lacis] ${r} detected \u2014 manifest generated, platform handles compilation`);return}try{if(r==="bun"){let o=t??S(n);if(!o)throw new Error("Entry point not found. Specify one with --entry <file>.");let a=await v(e),i=[join(n,o),s,...a],c=await globalThis.Bun.build({entrypoints:i,outdir:join(n,"dist"),root:n,external:["*"],target:"bun"});if(!c.success){let d=c.logs.map(f=>f.message??String(f)).join(`
`);throw new Error(`Bun build failed:
${d}`)}}else {let o=join(n,"node_modules",".bin","tsc");if(!existsSync(o))throw new Error("TypeScript not found. Add it to your project: npm install --save-dev typescript");let a=R(n)??"dist";await D(o,["--outDir",a],n);}}finally{await unlink(s).catch(()=>{});}console.log("[lacis] Build complete \u2192 dist/");}async function p(e){await m(e);let t=null;watch(e,{recursive:true},(n,r)=>{!r||r==="_manifest.ts"||(t&&clearTimeout(t),t=setTimeout(async()=>{console.log(`[lacis] Route changed: ${r}, regenerating manifest...`);try{await m(e);}catch(s){console.error("[lacis] Failed to regenerate manifest:",s);}},100));});}function M(e){if(existsSync(join(e,"netlify.toml")))return "netlify";if(existsSync(join(e,"vercel.json")))return "vercel";try{let t=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),n={...t.dependencies,...t.devDependencies};if(n["@netlify/functions"]||n["netlify-cli"])return "netlify";if(n.vercel||n["@vercel/node"])return "vercel"}catch{}return "node"}async function P(e){let t=process.cwd();if(process.env.VERCEL==="1"){await m(e);return}if(process.env.NETLIFY==="true"){await p(e);return}let n=M(t);if(console.log(`[lacis] Detected platform: ${n}`),await p(e),n==="node"){console.log("[lacis] Node mode: watching routes for changes...");return}let[r,s]=n==="netlify"?["netlify",["dev"]]:["vercel",["dev"]],o=spawn(r,s,{stdio:"inherit",shell:true,cwd:t});o.on("error",i=>{i.code==="ENOENT"?console.error(`[lacis] ${r} CLI not found. Install it with: ${n==="netlify"?"npm i -g netlify-cli":"npm i -g vercel"}`):console.error(`[lacis] Failed to start ${r} dev:`,i.message);});let a=i=>{process.on(i,()=>o.kill(i));};a("SIGINT"),a("SIGTERM");}function B(e){let t=e.slice(2),n=t[0]??"",r=t.indexOf("--routes"),s=r!==-1?t[r+1]:void 0,o=s?resolve(process.cwd(),s):resolve(process.cwd(),"routes"),a=t.indexOf("--entry"),i=a!==-1?t[a+1]:void 0;return {command:n,routesDir:o,entry:i}}function A(){console.log(`
Usage: lacis <command> [options]
Commands:
build Compile project to dist/ (Node: tsc/tsup, Bun: bun build)
watch Watch routes and regenerate manifest on changes
dev Auto-detect platform and start dev server
To scaffold a new project: npm create lacis@latest
Options:
--routes <dir> Path to routes directory (default: ./routes)
--entry <file> Entry point for bundlers (default: auto-detected from package.json)
`);}async function J(){let{command:e,routesDir:t,entry:n}=B(process.argv);switch(e){case "build":await $(t,n);break;case "watch":await p(t);break;case "dev":await P(t);break;default:A(),e&&(console.error(`Unknown command: ${e}`),process.exit(1));}}J().catch(e=>{console.error("[lacis]",e),process.exit(1);});