UNPKG

lacis

Version:

Zero-dependency TypeScript web framework

24 lines (21 loc) 8.21 kB
#!/usr/bin/env node import {resolve,join,relative,dirname}from'path';import {unlink,writeFile,rm,readdir,mkdir}from'fs/promises';import {spawn}from'child_process';import {watch,existsSync,readFileSync}from'fs';async function O(e){let n=[];async function t(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"),u="/"+s.map(c=>c.replace(/^\[(\w+)\??\]$/,":$1")).join("/");n.push({importPath:i,routePath:u==="//"?"/":u});}for(let i of o)i.isDirectory()&&!i.name.startsWith("+")&&await t(join(r,i.name),[...s,i.name]);}return await t(e),n}async function _(e){let n=[];async function t(r,s){let o;try{o=await readdir(r,{withFileTypes:!0});}catch{return}let a=o.find(u=>u.name==="+middleware.global.ts")??o.find(u=>u.name==="+middleware.global.js");a&&n.push({importPath:"./"+relative(e,join(r,a.name)).replace(/\\/g,"/").replace(/\.ts$/,".js"),routePath:s,type:"cascade"});let i=o.find(u=>u.name==="+middleware.ts")??o.find(u=>u.name==="+middleware.js");i&&n.push({importPath:"./"+relative(e,join(r,i.name)).replace(/\\/g,"/").replace(/\.ts$/,".js"),routePath:s,type:"exact"});for(let u of o)if(u.isDirectory()&&!u.name.startsWith("+")){let c=s==="/"?`/${u.name}`:`${s}/${u.name}`;await t(join(r,u.name),c);}}return await t(e,"/"),n}async function m(e){let[n,t]=await Promise.all([O(e),_(e)]),r=n.map((c,d)=>`import * as _route_${d} from '${c.importPath}'`).join(` `),s=t.map((c,d)=>`import * as _mw_${d} from '${c.importPath}'`).join(` `),o=[r,s].filter(Boolean).join(` `),a=n.map((c,d)=>` { path: '${c.routePath}', handlers: _route_${d} }`).join(`, `),i=t.map((c,d)=>` { path: '${c.routePath}', type: '${c.type}', module: _mw_${d} }`).join(`, `),u=["// 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"),u,"utf-8"),console.log(`[lacis] Generated manifest with ${n.length} route(s) and ${t.length} middleware(s)`);}function D(e){let n=e.map(t=>t.match(/^\[(\w+)(\?)?\]$/)).filter(t=>t!==null).map(t=>({name:t[1],optional:t[2]==="?"}));return n.length===0?"Record<string, never>":"{ "+n.map(t=>`${t.name}${t.optional?"?":""}: string`).join("; ")+" }"}function I(e){return ["// AUTO-GENERATED by lacis \u2014 do not edit.","import type { Request, Response } from 'lacis'","",`export type RouteParams = ${e}`,"export type RouteRequest = Omit<Request, 'params'> & { params: RouteParams }","export type RouteHandler = (req: RouteRequest, res: Response) => void | Promise<void>",""].join(` `)}async function p(e){let n=process.cwd(),t=join(n,".lacis","types");await rm(t,{recursive:true,force:true});let r=0;async function s(o,a){let i;try{i=await readdir(o,{withFileTypes:!0});}catch{return}if(i.some(c=>!c.isDirectory()&&(c.name==="index.ts"||c.name==="index.js"))){let c=relative(n,resolve(o)).replace(/\\/g,"/"),d=join(t,c,"$types.d.ts");await mkdir(dirname(d),{recursive:true}),await writeFile(d,I(D(a)),"utf-8"),r++;}for(let c of i)c.isDirectory()&&!c.name.startsWith("+")&&await s(join(o,c.name),[...a,c.name]);}await s(e,[]),console.log(`[lacis] Generated $types for ${r} route(s)`);}function A(e){if(existsSync(join(e,"wrangler.toml")))return "cloudflare";if(existsSync(join(e,"netlify.toml")))return "netlify";if(existsSync(join(e,"vercel.json")))return "vercel";try{let n=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),t={...n.dependencies,...n.devDependencies};if(t.wrangler)return "cloudflare";if(t["@netlify/functions"]||t["netlify-cli"])return "netlify";if(t.vercel||t["@vercel/node"])return "vercel"}catch{}return process.versions.bun!==void 0||existsSync(join(e,"bun.lockb"))||existsSync(join(e,"bun.lock"))?"bun":"node"}function M(e){try{let n=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),t=n.module??n.main;if(typeof t=="string"&&existsSync(join(e,t)))return t}catch{}for(let n of ["server.ts","index.ts","app.ts","main.ts"])if(existsSync(join(e,n)))return n;return null}function B(e){try{return JSON.parse(readFileSync(join(e,"tsconfig.json"),"utf-8")).compilerOptions?.outDir??null}catch{return null}}async function R(e){let n=[],t;try{t=await readdir(e,{withFileTypes:!0});}catch{return n}for(let r of t)r.isDirectory()?n.push(...await R(join(e,r.name))):r.name.endsWith(".ts")&&r.name!=="_manifest.ts"&&n.push(join(e,r.name));return n}function C(e,n,t){return new Promise((r,s)=>{let o=spawn(e,n,{stdio:"inherit",cwd:t});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 P(e,n){let t=process.cwd(),r=A(t),s=join(e,"_manifest.ts");if(await m(e),await p(e),r==="vercel"||r==="netlify"||r==="cloudflare"){console.log(`[lacis] ${r} detected \u2014 manifest generated, platform handles compilation`);return}try{if(r==="bun"){let o=n??M(t);if(!o)throw new Error("Entry point not found. Specify one with --entry <file>.");let a=await R(e),i=[join(t,o),s,...a],u=await globalThis.Bun.build({entrypoints:i,outdir:join(t,"dist"),root:t,external:["*"],target:"bun"});if(!u.success){let c=u.logs.map(d=>d.message??String(d)).join(` `);throw new Error(`Bun build failed: ${c}`)}}else {let o=join(t,"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=B(t)??"dist";await C(o,["--outDir",a],t);}}finally{await unlink(s).catch(()=>{});}console.log("[lacis] Build complete \u2192 dist/");}async function g(e){await m(e),await p(e);let n=null;watch(e,{recursive:true},(t,r)=>{!r||r==="_manifest.ts"||(n&&clearTimeout(n),n=setTimeout(async()=>{console.log(`[lacis] Route changed: ${r}, regenerating...`);try{await m(e),await p(e);}catch(s){console.error("[lacis] Failed to regenerate:",s);}},100));});}function W(e){if(existsSync(join(e,"wrangler.toml")))return "cloudflare";if(existsSync(join(e,"netlify.toml")))return "netlify";if(existsSync(join(e,"vercel.json")))return "vercel";try{let n=JSON.parse(readFileSync(join(e,"package.json"),"utf-8")),t={...n.dependencies,...n.devDependencies};if(t.wrangler)return "cloudflare";if(t["@netlify/functions"]||t["netlify-cli"])return "netlify";if(t.vercel||t["@vercel/node"])return "vercel"}catch{}return "node"}async function E(e){let n=process.cwd();if(process.env.VERCEL==="1"){await m(e),await p(e);return}if(process.env.NETLIFY==="true"){await g(e);return}let t=W(n);if(console.log(`[lacis] Detected platform: ${t}`),await g(e),t==="node"){console.log("[lacis] Node mode: watching routes for changes...");return}let[r,s]=t==="netlify"?["netlify",["dev"]]:t==="cloudflare"?["wrangler",["dev"]]:["vercel",["dev"]],o=spawn(r,s,{stdio:"inherit",shell:true,cwd:n});o.on("error",i=>{i.code==="ENOENT"?console.error(`[lacis] ${r} CLI not found. Install it with: ${t==="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 L(e){let n=e.slice(2),t=n[0]??"",r=n.indexOf("--routes"),s=r!==-1?n[r+1]:void 0,o=s?resolve(process.cwd(),s):resolve(process.cwd(),"routes"),a=n.indexOf("--entry"),i=a!==-1?n[a+1]:void 0;return {command:t,routesDir:o,entry:i}}function U(){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 H(){let{command:e,routesDir:n,entry:t}=L(process.argv);switch(e){case "build":await P(n,t);break;case "watch":await g(n);break;case "dev":await E(n);break;default:U(),e&&(console.error(`Unknown command: ${e}`),process.exit(1));}}H().catch(e=>{console.error("[lacis]",e),process.exit(1);});