create-caspian-app 0.1.0 ā 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/dist/index.js +1 -1
- package/dist/settings/component-map.ts +848 -214
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+8).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${n}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=n.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(n.scripts={...n.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...n.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,n.scripts=c,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),i=n&&fs.statSync(e);if(n&&i&&i.isDirectory()){const n=s.toLowerCase();if(!t.mcp&&n.includes("src\\lib\\mcp"))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\ts")||n.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\vite-plugins")||n.includes("\\vite-plugins\\")||n.includes("\\vite-plugins")))return;if(t.backendOnly&&n.includes("public\\js")||t.backendOnly&&n.includes("public\\css")||t.backendOnly&&n.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)})}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(t.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(t.backendOnly&&s.includes("layout.html"))return;if(t.tailwindcss&&s.includes("index.css"))return;if(!t.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach(({src:s,dest:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="/css/index.css" rel="stylesheet" />'),n+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${n}`:n),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return s.endsWith("/"+t)||s===t})}async function createDirectoryStructure(e,s){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"}];s.typescript&&!s.backendOnly&&n.push({src:"/ts",dest:"/ts"}),t.forEach(({src:s,dest:t})=>{const n=path.join(__dirname,s),i=path.join(e,t);if(checkExcludeFiles(i))return;const c=fs.readFileSync(n,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,n,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\nAUTH_TOKEN_VALIDITY="1h"\nAUTH_TOKEN_AUTO_REFRESH="true"\n\nAUTH_ALL_ROUTES_PRIVATE="true"\nAUTH_PUBLIC_ROUTES="/"\nAUTH_ROUTES="/signin,/signup"\nAUTH_PRIVATE_ROUTES=""\n\nAUTH_ROLE_BASED="false"\nAUTH_ROLE_IDENTIFIER="role"\n# Role-based route allowlist (JSON)\n# Format: {"roleName": ["/route1", "/route2"]}\n# Example:\n# AUTH_ROLE_BASED_ROUTES_JSON="{"admin": ["/admin", "/reports"], "user": ["/dashboard"]}"\nAUTH_ROLE_BASED_ROUTES_JSON="{}"\n\nAUTH_DEFAULT_SIGNIN_REDIRECT="/dashboard"\nAUTH_DEFAULT_SIGNOUT_REDIRECT="/signin"\nAUTH_API_PREFIX="/api/auth"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(n.backendOnly=!0),i.includes("--tailwindcss")&&(n.tailwindcss=!0),i.includes("--mcp")&&(n.mcp=!0),i.includes("--prisma")&&(n.prisma=!0),i.includes("--typescript")&&(n.typescript=!0),n}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(t.backendOnly=!0),n.includes("--tailwindcss")&&(t.tailwindcss=!0),n.includes("--mcp")&&(t.mcp=!0),n.includes("--prisma")&&(t.prisma=!0),n.includes("--typescript")&&(t.typescript=!0),t}}const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const n=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:n}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:n});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,t=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let n="";e.on("data",e=>n+=e),e.on("end",()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:(console.error(`Package ${e} is not installed`),null)}catch(e){return console.error(e instanceof Error?e.message:String(e)),null}}async function installNpmDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.1.18","@types/browser-sync":"2.29.1","@types/node":"25.0.3","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.2","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.6","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.1.18",tsx:"4.21.0",typescript:"5.9.3",vite:"7.3.0","fast-glob":"3.3.3","tree-sitter":"^0.25.0","tree-sitter-python":"^0.25.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}async function setupStarterKit(e,s){if(!s.starterKit)return;let t=null;if(STARTER_KITS[s.starterKit]?t=STARTER_KITS[s.starterKit]:s.starterKitSource&&(t={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const n=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} ${e}`:`git clone --depth 1 ${t.source.url} ${e}`;execSync(n,{stdio:"inherit"});const i=path.join(e,".git");fs.existsSync(i)&&fs.rmSync(i,{recursive:!0,force:!0}),console.log(chalk.blue("Starter kit cloned successfully!"));const c=path.join(e,"caspian.config.json");if(fs.existsSync(c))try{const t=JSON.parse(fs.readFileSync(c,"utf8")),n=e.replace(/\\/g,"\\"),i=bsConfigUrls(n);t.projectName=s.projectName,t.projectRootPath=n,t.bsTarget=i.bsTarget,t.bsPathRewrite=i.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");t.version=t.version||a,fs.writeFileSync(c,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,s),console.log(chalk.green(`ā ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nš Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(n.error)throw n.error;if(0!==n.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${n.status}`)}function tryRunCmd(e,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!n.error&&0===n.status}function getVenvPythonPath(e){const s=path.join(e,".venv","Scripts","python.exe");if(fs.existsSync(s))return s;return path.join(e,".venv","bin","python")}async function uninstallPythonDependencies(e,s){const t=getVenvPythonPath(e);fs.existsSync(t)?(console.log("Uninstalling Python dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`)),runCmd(t,["-m","pip","uninstall","-y",...s],e)):console.warn(chalk.yellow("Virtual environment not found. Skipping Python dependency uninstallation."))}function fetchPyPiPackageVersion(e){return new Promise((s,t)=>{https.get(`https://pypi.org/pypi/${e}/json`,e=>{let n="";e.on("data",e=>n+=e),e.on("end",()=>{if(200===e.statusCode)try{const e=JSON.parse(n);s(e.info.version)}catch(e){t(new Error("Failed to parse PyPI JSON response"))}else t(new Error(`Request failed with status code ${e.statusCode}`))})}).on("error",e=>t(e))})}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: creating venv + installing dependencies")),console.log(chalk.green("=========================\n"));const t=path.join(e,".venv");if(fs.existsSync(t)&&(fs.existsSync(path.join(e,".venv","Scripts","python.exe"))||fs.existsSync(path.join(e,".venv","bin","python"))))console.log(chalk.gray("Venv already exists: .venv"));else if(console.log(chalk.blue("Creating virtual environment: .venv")),tryRunCmd("py",["-m","venv",".venv"],e));else if(tryRunCmd("python",["-m","venv",".venv"],e));else if(!tryRunCmd("python3",["-m","venv",".venv"],e))throw new Error("Could not create venv. Install Python and ensure `py` or `python` is in PATH.");const n=getVenvPythonPath(e);if(!fs.existsSync(n))throw new Error(`Venv python not found at: ${n}`);console.log(chalk.blue("Upgrading pip...")),runCmd(n,["-m","pip","install","--upgrade","pip"],e);let i="caspian-utils";try{const e=await fetchPyPiPackageVersion("caspian-utils");i=`caspian-utils==${e}`,console.log(chalk.blue(`Pinned caspian-utils to version ${e}`))}catch(e){console.warn(chalk.yellow("Could not fetch latest caspian-utils version. Using unpinned dependency."))}const c=["fastapi>=0.110,<0.128","uvicorn>=0.27,<0.40","python-dotenv>=1.0,<2.0","jinja2>=3.1,<4.0","beautifulsoup4>=4.12,<5.0","slowapi>=0.1,<0.2","python-multipart>=0.0.9,<0.1","starsessions>=1.3,<2.2","httpx>=0.27,<0.29","werkzeug>=3.0,<4.0","cuid2>=2.0,<3.0","nanoid>=2.0,<3.0","python-ulid>=2.7,<3.1","cuid>=0.4.0,<0.5.0",i];s.tailwindcss&&c.push("tailwind-merge>=0.3.3,<0.4.0"),s.mcp&&c.push("fastmcp<3"),s.prisma&&(c.push("psycopg2-binary>=2.9,<3.0"),c.push("asyncpg>=0.31.0,<1.0"),c.push("aiosqlite>=0.22.1,<1.0"),c.push("aiomysql>=0.3.2,<1.0")),console.log(chalk.blue("Generating requirements.txt..."));const a=path.join(e,"requirements.txt"),r=c.join("\n")+"\n";fs.writeFileSync(a,r,"utf8"),console.log(chalk.gray(`Created requirements.txt with ${c.length} packages.`)),console.log(chalk.blue("Installing dependencies from requirements.txt...")),c.forEach(e=>console.log(`- ${chalk.gray(e)}`));runCmd(n,["-m","pip","install",...updateAnswer?.isUpdate?["--upgrade","--upgrade-strategy","only-if-needed"]:[],...updateAnswer?.isUpdate?["--no-cache-dir"]:[],"-r","requirements.txt"],e),console.log(chalk.green("\nā Python venv ready and FastAPI dependencies installed.\n"))}function setupVsCodeSettings(e){const s=path.join(e,".vscode");fs.existsSync(s)||fs.mkdirSync(s);const t={"python.defaultInterpreterPath":"win32"===process.platform?"${workspaceFolder}/.venv/Scripts/python.exe":"${workspaceFolder}/.venv/bin/python","python.terminal.activateEnvironment":!0,"python.analysis.typeCheckingMode":"basic","python.analysis.autoImportCompletions":!0};fs.writeFileSync(path.join(s,"settings.json"),JSON.stringify(t,null,2)),console.log(chalk.gray("Created .vscode/settings.json for auto-activation."))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let t=e[0];const n=e.find(e=>e.startsWith("--starter-kit=")),i=n?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(t){const n=process.cwd(),c=path.join(n,"caspian.config.json");if(i&&a){o=!0;const n={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(n,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(n,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:n};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:n})}else{const n={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(n,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageVersion("create-caspian-app");p?-1===compareVersions(p,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(t)if(o){const s=path.join(d,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const n=path.join(u,"caspian.config.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let t=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,t),n=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(n)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("tree-sitter"),npmPkg("tree-sitter-python")];if(r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),r.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{t(s)&&e.push(s)}),s.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("mcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{t(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const n=path.join(u,"settings","vite-plugins");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{t(s)&&e.push(s)})}const n=(e=>Array.from(new Set(e)))(e);n.length>0&&(console.log(`Uninstalling npm packages: ${n.join(", ")}`),await uninstallNpmDependencies(u,n,!0)),s.length>0&&await uninstallPythonDependencies(u,s)}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r),setupVsCodeSettings(u),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
|
2
|
+
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const n=e.substring(0,s+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${n}`),"").replace(/\\/g,"/");let i=`http://localhost/${t}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=t.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,s){const n=path.join(e,"package.json");if(checkExcludeFiles(n))return;const t=JSON.parse(fs.readFileSync(n,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(t.scripts={...t.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(t.scripts={...t.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(t.scripts={...t.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...t.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(n,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,n){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=s.toLowerCase();if(!n.mcp&&t.includes("src\\lib\\mcp"))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(n.backendOnly&&t.includes("public\\js")||n.backendOnly&&t.includes("public\\css")||n.backendOnly&&t.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(t=>{copyRecursiveSync(path.join(e,t),path.join(s,t),n)})}else{if(checkExcludeFiles(s))return;if(!n.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!n.mcp&&s.includes("restart-mcp.ts"))return;if(n.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(n.backendOnly&&s.includes("layout.html"))return;if(n.tailwindcss&&s.includes("index.css"))return;if(!n.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,n){s.forEach(({src:s,dest:t})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,t),n)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const n=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(n))try{let e=fs.readFileSync(n,"utf8"),t="";s.backendOnly||(s.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(n,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const n=path.join(e,".env");checkExcludeFiles(n)||fs.writeFileSync(n,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const n=e.replace(/\\/g,"/");return s.endsWith("/"+n)||s===n})}async function createDirectoryStructure(e,s){const n=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&n.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&n.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const t=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"}];s.typescript&&!s.backendOnly&&t.push({src:"/ts",dest:"/ts"}),n.forEach(({src:s,dest:n})=>{const t=path.join(__dirname,s),i=path.join(e,n);if(checkExcludeFiles(i))return;const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\nAUTH_TOKEN_VALIDITY="1h"\nAUTH_TOKEN_AUTO_REFRESH="true"\n\nAUTH_ALL_ROUTES_PRIVATE="true"\nAUTH_PUBLIC_ROUTES="/"\nAUTH_ROUTES="/signin,/signup"\nAUTH_PRIVATE_ROUTES=""\n\nAUTH_ROLE_BASED="false"\nAUTH_ROLE_IDENTIFIER="role"\n# Role-based route allowlist (JSON)\n# Format: {"roleName": ["/route1", "/route2"]}\n# Example:\n# AUTH_ROLE_BASED_ROUTES_JSON="{"admin": ["/admin", "/reports"], "user": ["/dashboard"]}"\nAUTH_ROLE_BASED_ROUTES_JSON="{}"\n\nAUTH_DEFAULT_SIGNIN_REDIRECT="/dashboard"\nAUTH_DEFAULT_SIGNOUT_REDIRECT="/signin"\nAUTH_API_PREFIX="/api/auth"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let n=null;if(STARTER_KITS[s]&&(n=STARTER_KITS[s]),n){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:n.features.backendOnly??!1,tailwindcss:n.features.tailwindcss??!1,prisma:n.features.prisma??!1,mcp:n.features.mcp??!1,typescript:n.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(t.backendOnly=!0),i.includes("--tailwindcss")&&(t.tailwindcss=!0),i.includes("--mcp")&&(t.mcp=!0),i.includes("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(n.backendOnly=!0),t.includes("--tailwindcss")&&(n.tailwindcss=!0),t.includes("--mcp")&&(n.mcp=!0),t.includes("--prisma")&&(n.prisma=!0),t.includes("--typescript")&&(n.typescript=!0),n}}const n=[];e.projectName||n.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||n.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(n,{onCancel:t}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:t});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,n=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm uninstall ${n?"--save-dev":"--save"} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,n)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);s(e["dist-tags"].latest)}catch(e){n(new Error("Failed to parse JSON response"))}})}).on("error",e=>n(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const n=e.split(".").map(Number),t=s.split(".").map(Number);for(let e=0;e<n.length;e++){if(n[e]>t[e])return 1;if(n[e]<t[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:(console.error(`Package ${e} is not installed`),null)}catch(e){return console.error(e instanceof Error?e.message:String(e)),null}}async function installNpmDependencies(e,s,n=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((n?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm install ${n?"--save-dev":""} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.1.18","@types/browser-sync":"2.29.1","@types/node":"25.0.3","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.2","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.6","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.1.18",tsx:"4.21.0",typescript:"5.9.3",vite:"7.3.0","fast-glob":"3.3.3","@lezer/common":"^1.5.1","@lezer/python":"^1.1.18"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}async function setupStarterKit(e,s){if(!s.starterKit)return;let n=null;if(STARTER_KITS[s.starterKit]?n=STARTER_KITS[s.starterKit]:s.starterKitSource&&(n={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),n){if(console.log(chalk.green(`Setting up ${n.name}...`)),n.source)try{const t=n.source.branch?`git clone -b ${n.source.branch} --depth 1 ${n.source.url} ${e}`:`git clone --depth 1 ${n.source.url} ${e}`;execSync(t,{stdio:"inherit"});const i=path.join(e,".git");fs.existsSync(i)&&fs.rmSync(i,{recursive:!0,force:!0}),console.log(chalk.blue("Starter kit cloned successfully!"));const c=path.join(e,"caspian.config.json");if(fs.existsSync(c))try{const n=JSON.parse(fs.readFileSync(c,"utf8")),t=e.replace(/\\/g,"\\"),i=bsConfigUrls(t);n.projectName=s.projectName,n.projectRootPath=t,n.bsTarget=i.bsTarget,n.bsPathRewrite=i.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");n.version=n.version||a,fs.writeFileSync(c,JSON.stringify(n,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}n.customSetup&&await n.customSetup(e,s),console.log(chalk.green(`ā ${n.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nš Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const n=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");n&&console.log(chalk.magenta(` Features: ${n}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"ignore",shell:!1,encoding:"utf8"});return!t.error&&0===t.status}function getVenvPythonPath(e){const s=path.join(e,".venv","Scripts","python.exe");if(fs.existsSync(s))return s;return path.join(e,".venv","bin","python")}async function uninstallPythonDependencies(e,s){const n=getVenvPythonPath(e);fs.existsSync(n)?(console.log("Uninstalling Python dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`)),runCmd(n,["-m","pip","uninstall","-y",...s],e)):console.warn(chalk.yellow("Virtual environment not found. Skipping Python dependency uninstallation."))}function fetchPyPiPackageVersion(e){return new Promise((s,n)=>{https.get(`https://pypi.org/pypi/${e}/json`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{if(200===e.statusCode)try{const e=JSON.parse(t);s(e.info.version)}catch(e){n(new Error("Failed to parse PyPI JSON response"))}else n(new Error(`Request failed with status code ${e.statusCode}`))})}).on("error",e=>n(e))})}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: creating venv + installing dependencies")),console.log(chalk.green("=========================\n"));const n=path.join(e,".venv");if(fs.existsSync(n)&&(fs.existsSync(path.join(e,".venv","Scripts","python.exe"))||fs.existsSync(path.join(e,".venv","bin","python"))))console.log(chalk.gray("Venv already exists: .venv"));else if(console.log(chalk.blue("Creating virtual environment: .venv")),tryRunCmd("py",["-m","venv",".venv"],e));else if(tryRunCmd("python",["-m","venv",".venv"],e));else if(!tryRunCmd("python3",["-m","venv",".venv"],e))throw new Error("Could not create venv. Install Python and ensure `py` or `python` is in PATH.");const t=getVenvPythonPath(e);if(!fs.existsSync(t))throw new Error(`Venv python not found at: ${t}`);console.log(chalk.blue("Upgrading pip...")),runCmd(t,["-m","pip","install","--upgrade","pip"],e);let i="caspian-utils";try{const e=await fetchPyPiPackageVersion("caspian-utils");i=`caspian-utils==${e}`,console.log(chalk.blue(`Pinned caspian-utils to version ${e}`))}catch(e){console.warn(chalk.yellow("Could not fetch latest caspian-utils version. Using unpinned dependency."))}const c=["fastapi>=0.110,<0.128","uvicorn>=0.27,<0.40","python-dotenv>=1.0,<2.0","jinja2>=3.1,<4.0","beautifulsoup4>=4.12,<5.0","slowapi>=0.1,<0.2","python-multipart>=0.0.9,<0.1","starsessions>=1.3,<2.2","httpx>=0.27,<0.29","werkzeug>=3.0,<4.0","cuid2>=2.0,<3.0","nanoid>=2.0,<3.0","python-ulid>=2.7,<3.1","cuid>=0.4.0,<0.5.0",i];s.tailwindcss&&c.push("tailwind-merge>=0.3.3,<0.4.0"),s.mcp&&c.push("fastmcp<3"),s.prisma&&(c.push("psycopg2-binary>=2.9,<3.0"),c.push("asyncpg>=0.31.0,<1.0"),c.push("aiosqlite>=0.22.1,<1.0"),c.push("aiomysql>=0.3.2,<1.0")),console.log(chalk.blue("Generating requirements.txt..."));const a=path.join(e,"requirements.txt"),r=c.join("\n")+"\n";fs.writeFileSync(a,r,"utf8"),console.log(chalk.gray(`Created requirements.txt with ${c.length} packages.`)),console.log(chalk.blue("Installing dependencies from requirements.txt...")),c.forEach(e=>console.log(`- ${chalk.gray(e)}`));runCmd(t,["-m","pip","install",...updateAnswer?.isUpdate?["--upgrade","--upgrade-strategy","only-if-needed"]:[],...updateAnswer?.isUpdate?["--no-cache-dir"]:[],"-r","requirements.txt"],e),console.log(chalk.green("\nā Python venv ready and FastAPI dependencies installed.\n"))}function setupVsCodeSettings(e){const s=path.join(e,".vscode");fs.existsSync(s)||fs.mkdirSync(s);const n={"python.defaultInterpreterPath":"win32"===process.platform?"${workspaceFolder}/.venv/Scripts/python.exe":"${workspaceFolder}/.venv/bin/python","python.terminal.activateEnvironment":!0,"python.analysis.typeCheckingMode":"basic","python.analysis.autoImportCompletions":!0};fs.writeFileSync(path.join(s,"settings.json"),JSON.stringify(n,null,2)),console.log(chalk.gray("Created .vscode/settings.json for auto-activation."))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let n=e[0];const t=e.find(e=>e.startsWith("--starter-kit=")),i=t?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(n){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&a){o=!0;const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(t,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:n,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t};const o={projectName:n,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:n,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t})}else{const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageVersion("create-caspian-app");p?-1===compareVersions(p,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(n)if(o){const s=path.join(d,n);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const s=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let n=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&n.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:n??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,n),t=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(t)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python")];if(r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),r.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),n||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));return!!(n.dependencies&&n.dependencies[e]||n.devDependencies&&n.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{n(s)&&e.push(s)}),s.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("mcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{n(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{n(s)&&e.push(s)})}const t=(e=>Array.from(new Set(e)))(e);t.length>0&&(console.log(`Uninstalling npm packages: ${t.join(", ")}`),await uninstallNpmDependencies(u,t,!0)),s.length>0&&await uninstallPythonDependencies(u,s)}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),n={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(n,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r),setupVsCodeSettings(u),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { parser as pythonParser } from "@lezer/python";
|
|
4
|
+
import type { Tree, TreeCursor } from "@lezer/common";
|
|
5
5
|
import { getFileMeta } from "./utils";
|
|
6
6
|
|
|
7
7
|
const { __dirname } = getFileMeta();
|
|
@@ -42,291 +42,920 @@ const CONFIG_PATH = path.join(PROJECT_ROOT, CONFIG_FILENAME);
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* ---------------------------------------------------------
|
|
45
|
-
* AST Helpers
|
|
45
|
+
* AST Helpers (Lezer)
|
|
46
46
|
* ---------------------------------------------------------
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
type NodeSpan = {
|
|
50
|
+
name: string;
|
|
51
|
+
from: number;
|
|
52
|
+
to: number;
|
|
53
|
+
children: NodeSpan[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const PY_KEYWORDS = new Set([
|
|
57
|
+
"def",
|
|
58
|
+
"async",
|
|
59
|
+
"class",
|
|
60
|
+
"return",
|
|
61
|
+
"pass",
|
|
62
|
+
"if",
|
|
63
|
+
"elif",
|
|
64
|
+
"else",
|
|
65
|
+
"for",
|
|
66
|
+
"while",
|
|
67
|
+
"try",
|
|
68
|
+
"except",
|
|
69
|
+
"finally",
|
|
70
|
+
"with",
|
|
71
|
+
"lambda",
|
|
72
|
+
"yield",
|
|
73
|
+
"from",
|
|
74
|
+
"import",
|
|
75
|
+
"as",
|
|
76
|
+
"global",
|
|
77
|
+
"nonlocal",
|
|
78
|
+
"assert",
|
|
79
|
+
"raise",
|
|
80
|
+
"del",
|
|
81
|
+
"and",
|
|
82
|
+
"or",
|
|
83
|
+
"not",
|
|
84
|
+
"in",
|
|
85
|
+
"is",
|
|
86
|
+
"True",
|
|
87
|
+
"False",
|
|
88
|
+
"None",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
function slice(source: string, node: NodeSpan): string {
|
|
92
|
+
return source.slice(node.from, node.to);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stripQuotes(value: string): string {
|
|
96
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lower(s: string): string {
|
|
100
|
+
return s.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isProbablyIdentifierText(text: string): boolean {
|
|
104
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(text) && !PY_KEYWORDS.has(text);
|
|
105
|
+
}
|
|
51
106
|
|
|
52
107
|
/**
|
|
53
|
-
*
|
|
108
|
+
* Convert Lezer cursor subtree into a plain recursive span tree.
|
|
109
|
+
* This makes traversals simpler and stable for custom heuristics.
|
|
54
110
|
*/
|
|
55
|
-
function
|
|
56
|
-
const
|
|
111
|
+
function cursorToSpanTree(cursor: TreeCursor): NodeSpan {
|
|
112
|
+
const node: NodeSpan = {
|
|
113
|
+
name: cursor.name,
|
|
114
|
+
from: cursor.from,
|
|
115
|
+
to: cursor.to,
|
|
116
|
+
children: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (cursor.firstChild()) {
|
|
120
|
+
do {
|
|
121
|
+
node.children.push(cursorToSpanTree(cursor));
|
|
122
|
+
} while (cursor.nextSibling());
|
|
123
|
+
cursor.parent();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return node;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePythonToSpanTree(source: string): { tree: Tree; root: NodeSpan } {
|
|
130
|
+
const tree = pythonParser.parse(source);
|
|
131
|
+
const cursor = tree.cursor();
|
|
132
|
+
const root = cursorToSpanTree(cursor);
|
|
133
|
+
return { tree, root };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function walk(
|
|
137
|
+
node: NodeSpan,
|
|
138
|
+
cb: (node: NodeSpan, parent: NodeSpan | null) => void,
|
|
139
|
+
parent: NodeSpan | null = null,
|
|
140
|
+
) {
|
|
141
|
+
cb(node, parent);
|
|
142
|
+
for (const child of node.children) walk(child, cb, node);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function findFirstDesc(
|
|
146
|
+
node: NodeSpan,
|
|
147
|
+
predicate: (n: NodeSpan) => boolean,
|
|
148
|
+
): NodeSpan | null {
|
|
57
149
|
for (const child of node.children) {
|
|
58
|
-
if (child
|
|
150
|
+
if (predicate(child)) return child;
|
|
151
|
+
const deep = findFirstDesc(child, predicate);
|
|
152
|
+
if (deep) return deep;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
59
156
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
157
|
+
function isFunctionLikeNode(n: NodeSpan, source: string): boolean {
|
|
158
|
+
const name = lower(n.name);
|
|
159
|
+
if (
|
|
160
|
+
!(
|
|
161
|
+
name.includes("function") ||
|
|
162
|
+
name.includes("definition") ||
|
|
163
|
+
name.includes("def")
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const text = slice(source, n).trimStart();
|
|
169
|
+
return text.startsWith("def ") || text.startsWith("async def ");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isDecoratedLikeNode(n: NodeSpan, source: string): boolean {
|
|
173
|
+
const name = lower(n.name);
|
|
174
|
+
if (name.includes("decorated")) return true;
|
|
175
|
+
|
|
176
|
+
// fallback heuristic: node text starts with @ and contains a function definition
|
|
177
|
+
const text = slice(source, n).trimStart();
|
|
178
|
+
return (
|
|
179
|
+
text.startsWith("@") &&
|
|
180
|
+
(text.includes("\ndef ") ||
|
|
181
|
+
text.includes("\nasync def ") ||
|
|
182
|
+
text.includes("def "))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function splitTopLevelComma(input: string): string[] {
|
|
187
|
+
const parts: string[] = [];
|
|
188
|
+
let current = "";
|
|
189
|
+
let depthParen = 0;
|
|
190
|
+
let depthBracket = 0;
|
|
191
|
+
let depthBrace = 0;
|
|
192
|
+
let inSingle = false;
|
|
193
|
+
let inDouble = false;
|
|
194
|
+
let escape = false;
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < input.length; i++) {
|
|
197
|
+
const ch = input[i];
|
|
198
|
+
|
|
199
|
+
if (escape) {
|
|
200
|
+
current += ch;
|
|
201
|
+
escape = false;
|
|
202
|
+
continue;
|
|
68
203
|
}
|
|
204
|
+
if (ch === "\\") {
|
|
205
|
+
current += ch;
|
|
206
|
+
escape = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!inDouble && ch === "'" && !inSingle) {
|
|
211
|
+
inSingle = true;
|
|
212
|
+
current += ch;
|
|
213
|
+
continue;
|
|
214
|
+
} else if (inSingle && ch === "'") {
|
|
215
|
+
inSingle = false;
|
|
216
|
+
current += ch;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!inSingle && ch === '"' && !inDouble) {
|
|
221
|
+
inDouble = true;
|
|
222
|
+
current += ch;
|
|
223
|
+
continue;
|
|
224
|
+
} else if (inDouble && ch === '"') {
|
|
225
|
+
inDouble = false;
|
|
226
|
+
current += ch;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (inSingle || inDouble) {
|
|
231
|
+
current += ch;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (ch === "(") depthParen++;
|
|
236
|
+
else if (ch === ")") depthParen--;
|
|
237
|
+
else if (ch === "[") depthBracket++;
|
|
238
|
+
else if (ch === "]") depthBracket--;
|
|
239
|
+
else if (ch === "{") depthBrace++;
|
|
240
|
+
else if (ch === "}") depthBrace--;
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
ch === "," &&
|
|
244
|
+
depthParen === 0 &&
|
|
245
|
+
depthBracket === 0 &&
|
|
246
|
+
depthBrace === 0
|
|
247
|
+
) {
|
|
248
|
+
if (current.trim()) parts.push(current.trim());
|
|
249
|
+
current = "";
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
current += ch;
|
|
69
254
|
}
|
|
70
|
-
|
|
255
|
+
|
|
256
|
+
if (current.trim()) parts.push(current.trim());
|
|
257
|
+
return parts;
|
|
71
258
|
}
|
|
72
259
|
|
|
73
260
|
/**
|
|
74
|
-
*
|
|
261
|
+
* Extract values from Literal[...] / Union[...] strings.
|
|
262
|
+
* (Leaf text parsing is okay here)
|
|
75
263
|
*/
|
|
76
|
-
function
|
|
264
|
+
function extractLiteralValuesFromTypeString(typeExpr: string): string[] {
|
|
265
|
+
const expr = typeExpr.trim();
|
|
77
266
|
const values: string[] = [];
|
|
78
267
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
268
|
+
// Literal[...]
|
|
269
|
+
if (expr.startsWith("Literal[") && expr.endsWith("]")) {
|
|
270
|
+
const inner = expr.slice(expr.indexOf("[") + 1, -1);
|
|
271
|
+
for (const part of splitTopLevelComma(inner)) {
|
|
272
|
+
const p = part.trim();
|
|
273
|
+
if (!p) continue;
|
|
274
|
+
if (/^['"].*['"]$/s.test(p)) values.push(stripQuotes(p));
|
|
275
|
+
else values.push(p);
|
|
276
|
+
}
|
|
277
|
+
return values;
|
|
86
278
|
}
|
|
87
279
|
|
|
88
|
-
//
|
|
89
|
-
if (
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
values.push(n.text);
|
|
280
|
+
// Union[...]
|
|
281
|
+
if (expr.startsWith("Union[") && expr.endsWith("]")) {
|
|
282
|
+
const inner = expr.slice(expr.indexOf("[") + 1, -1);
|
|
283
|
+
for (const part of splitTopLevelComma(inner)) {
|
|
284
|
+
const p = part.trim();
|
|
285
|
+
if (!p) continue;
|
|
286
|
+
if (p.startsWith("Literal[") && p.endsWith("]")) {
|
|
287
|
+
values.push(...extractLiteralValuesFromTypeString(p));
|
|
288
|
+
} else {
|
|
289
|
+
values.push(p);
|
|
99
290
|
}
|
|
100
|
-
|
|
101
|
-
|
|
291
|
+
}
|
|
292
|
+
return values;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return values;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* ---------------------------------------------------------
|
|
300
|
+
* AST-driven Python structure extraction
|
|
301
|
+
* ---------------------------------------------------------
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
type DecoratedFunctionAst = {
|
|
305
|
+
decoratedNode: NodeSpan;
|
|
306
|
+
functionNode: NodeSpan;
|
|
307
|
+
decoratorNodes: NodeSpan[];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function getTopLevelStatements(root: NodeSpan): NodeSpan[] {
|
|
311
|
+
// Lezer root usually has direct statement nodes; keep generic
|
|
312
|
+
return root.children;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Find decorated functions by AST traversal (no regex structure scan)
|
|
317
|
+
*/
|
|
318
|
+
function findDecoratedFunctionsAst(
|
|
319
|
+
root: NodeSpan,
|
|
320
|
+
source: string,
|
|
321
|
+
): DecoratedFunctionAst[] {
|
|
322
|
+
const results: DecoratedFunctionAst[] = [];
|
|
323
|
+
|
|
324
|
+
walk(root, (node) => {
|
|
325
|
+
if (!isDecoratedLikeNode(node, source)) return;
|
|
326
|
+
|
|
327
|
+
const functionNode =
|
|
328
|
+
node.children.find((c) => isFunctionLikeNode(c, source)) ||
|
|
329
|
+
findFirstDesc(node, (c) => isFunctionLikeNode(c, source));
|
|
330
|
+
|
|
331
|
+
if (!functionNode) return;
|
|
332
|
+
|
|
333
|
+
// Decorators are usually sibling children before function node
|
|
334
|
+
const decoratorNodes = node.children.filter((c) => {
|
|
335
|
+
if (c === functionNode) return false;
|
|
336
|
+
const txt = slice(source, c).trimStart();
|
|
337
|
+
return txt.startsWith("@");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
results.push({
|
|
341
|
+
decoratedNode: node,
|
|
342
|
+
functionNode,
|
|
343
|
+
decoratorNodes,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return results;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function decoratorMatchesComponent(node: NodeSpan, source: string): boolean {
|
|
351
|
+
const txt = slice(source, node).trim();
|
|
352
|
+
// Supports @component and @component(...)
|
|
353
|
+
return txt === "@component" || txt.startsWith("@component(");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function extractFunctionNameAst(
|
|
357
|
+
functionNode: NodeSpan,
|
|
358
|
+
source: string,
|
|
359
|
+
): string {
|
|
360
|
+
const txt = slice(source, functionNode);
|
|
361
|
+
|
|
362
|
+
// Header-based extraction (robust fix for "def" bug)
|
|
363
|
+
// Supports:
|
|
364
|
+
// def Profile(...)
|
|
365
|
+
// async def Profile(...)
|
|
366
|
+
const parenIndex = txt.indexOf("(");
|
|
367
|
+
if (parenIndex > 0) {
|
|
368
|
+
const header = txt.slice(0, parenIndex);
|
|
369
|
+
const defIndex = header.indexOf("def");
|
|
370
|
+
|
|
371
|
+
if (defIndex >= 0) {
|
|
372
|
+
const afterDef = header.slice(defIndex + 3).trim();
|
|
373
|
+
|
|
374
|
+
let i = 0;
|
|
375
|
+
while (i < afterDef.length && /\s/.test(afterDef[i])) i++;
|
|
376
|
+
|
|
377
|
+
let j = i;
|
|
378
|
+
while (j < afterDef.length && /[A-Za-z0-9_]/.test(afterDef[j])) j++;
|
|
379
|
+
|
|
380
|
+
const candidate = afterDef.slice(i, j).trim();
|
|
381
|
+
if (candidate && isProbablyIdentifierText(candidate)) {
|
|
382
|
+
return candidate;
|
|
102
383
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// AST fallback: direct children identifiers excluding keywords
|
|
388
|
+
for (const child of functionNode.children) {
|
|
389
|
+
const t = slice(source, child).trim();
|
|
390
|
+
if (isProbablyIdentifierText(t)) return t;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Deep fallback
|
|
394
|
+
const nameNode = findFirstDesc(functionNode, (n) => {
|
|
395
|
+
const t = slice(source, n).trim();
|
|
396
|
+
return isProbablyIdentifierText(t);
|
|
397
|
+
});
|
|
398
|
+
if (nameNode) return slice(source, nameNode).trim();
|
|
399
|
+
|
|
400
|
+
return "Unknown";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function findParamListNode(
|
|
404
|
+
functionNode: NodeSpan,
|
|
405
|
+
source: string,
|
|
406
|
+
): NodeSpan | null {
|
|
407
|
+
// Prefer child whose text starts with "(" and ends with ")"
|
|
408
|
+
for (const child of functionNode.children) {
|
|
409
|
+
const txt = slice(source, child).trim();
|
|
410
|
+
if (txt.startsWith("(") && txt.endsWith(")")) return child;
|
|
105
411
|
}
|
|
106
412
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
413
|
+
// fallback deep search
|
|
414
|
+
return findFirstDesc(functionNode, (n) => {
|
|
415
|
+
const txt = slice(source, n).trim();
|
|
416
|
+
return txt.startsWith("(") && txt.endsWith(")");
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function extractParamListTextAst(
|
|
421
|
+
functionNode: NodeSpan,
|
|
422
|
+
source: string,
|
|
423
|
+
): string {
|
|
424
|
+
const paramsNode = findParamListNode(functionNode, source);
|
|
425
|
+
if (!paramsNode) return "";
|
|
426
|
+
|
|
427
|
+
const txt = slice(source, paramsNode).trim();
|
|
428
|
+
if (txt.startsWith("(") && txt.endsWith(")")) {
|
|
429
|
+
return txt.slice(1, -1);
|
|
430
|
+
}
|
|
431
|
+
return txt;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function findFunctionBodyNode(
|
|
435
|
+
functionNode: NodeSpan,
|
|
436
|
+
source: string,
|
|
437
|
+
): NodeSpan | null {
|
|
438
|
+
// Heuristic: child block after ":" with multiline content often is the body
|
|
439
|
+
const candidates = functionNode.children.filter((c) => {
|
|
440
|
+
const txt = slice(source, c);
|
|
441
|
+
return (
|
|
442
|
+
txt.includes("\n") ||
|
|
443
|
+
txt.trimStart().startsWith("return") ||
|
|
444
|
+
txt.trimStart().startsWith("pass")
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (candidates.length > 0) {
|
|
449
|
+
// last candidate tends to be body
|
|
450
|
+
return candidates[candidates.length - 1];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// fallback deep search for a block/suite-like name
|
|
454
|
+
return findFirstDesc(functionNode, (n) => {
|
|
455
|
+
const name = lower(n.name);
|
|
456
|
+
return (
|
|
457
|
+
name.includes("body") || name.includes("suite") || name.includes("block")
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function extractFunctionBodyTextAst(
|
|
463
|
+
functionNode: NodeSpan,
|
|
464
|
+
source: string,
|
|
465
|
+
): string {
|
|
466
|
+
const bodyNode = findFunctionBodyNode(functionNode, source);
|
|
467
|
+
if (!bodyNode) {
|
|
468
|
+
// fallback: slice after first colon in function text
|
|
469
|
+
const txt = slice(source, functionNode);
|
|
470
|
+
const idx = txt.indexOf(":");
|
|
471
|
+
return idx >= 0 ? txt.slice(idx + 1) : "";
|
|
472
|
+
}
|
|
473
|
+
return slice(source, bodyNode);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse a single parameter chunk (still text-level, but only for leaf content)
|
|
478
|
+
*/
|
|
479
|
+
function parseParameterChunk(raw: string): {
|
|
480
|
+
name: string;
|
|
481
|
+
type?: string;
|
|
482
|
+
defaultValue?: string;
|
|
483
|
+
arbitraryDict?: boolean;
|
|
484
|
+
listSplat?: boolean;
|
|
485
|
+
} | null {
|
|
486
|
+
let s = raw.trim();
|
|
487
|
+
if (!s) return null;
|
|
488
|
+
|
|
489
|
+
// Positional-only / keyword-only separators
|
|
490
|
+
if (s === "/" || s === "*") return null;
|
|
491
|
+
|
|
492
|
+
// **kwargs
|
|
493
|
+
if (s.startsWith("**")) {
|
|
494
|
+
return { name: s.slice(2).trim(), arbitraryDict: true };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// *args
|
|
498
|
+
if (s.startsWith("*")) {
|
|
499
|
+
return { name: s.slice(1).trim(), listSplat: true };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Split default at top-level "="
|
|
503
|
+
let left = s;
|
|
504
|
+
let defaultValue: string | undefined;
|
|
505
|
+
|
|
506
|
+
{
|
|
507
|
+
let depthParen = 0,
|
|
508
|
+
depthBracket = 0,
|
|
509
|
+
depthBrace = 0;
|
|
510
|
+
let inSingle = false,
|
|
511
|
+
inDouble = false,
|
|
512
|
+
escape = false;
|
|
513
|
+
let eqIndex = -1;
|
|
514
|
+
|
|
515
|
+
for (let i = 0; i < s.length; i++) {
|
|
516
|
+
const ch = s[i];
|
|
517
|
+
|
|
518
|
+
if (escape) {
|
|
519
|
+
escape = false;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (ch === "\\") {
|
|
523
|
+
escape = true;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!inDouble && ch === "'" && !inSingle) {
|
|
528
|
+
inSingle = true;
|
|
529
|
+
continue;
|
|
530
|
+
} else if (inSingle && ch === "'") {
|
|
531
|
+
inSingle = false;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!inSingle && ch === '"' && !inDouble) {
|
|
536
|
+
inDouble = true;
|
|
537
|
+
continue;
|
|
538
|
+
} else if (inDouble && ch === '"') {
|
|
539
|
+
inDouble = false;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (inSingle || inDouble) continue;
|
|
544
|
+
|
|
545
|
+
if (ch === "(") depthParen++;
|
|
546
|
+
else if (ch === ")") depthParen--;
|
|
547
|
+
else if (ch === "[") depthBracket++;
|
|
548
|
+
else if (ch === "]") depthBracket--;
|
|
549
|
+
else if (ch === "{") depthBrace++;
|
|
550
|
+
else if (ch === "}") depthBrace--;
|
|
551
|
+
|
|
111
552
|
if (
|
|
112
|
-
|
|
113
|
-
|
|
553
|
+
ch === "=" &&
|
|
554
|
+
depthParen === 0 &&
|
|
555
|
+
depthBracket === 0 &&
|
|
556
|
+
depthBrace === 0
|
|
114
557
|
) {
|
|
558
|
+
eqIndex = i;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (eqIndex >= 0) {
|
|
564
|
+
left = s.slice(0, eqIndex).trim();
|
|
565
|
+
defaultValue = s.slice(eqIndex + 1).trim();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Split type annotation at top-level ":"
|
|
570
|
+
let name = left;
|
|
571
|
+
let type: string | undefined;
|
|
572
|
+
|
|
573
|
+
{
|
|
574
|
+
let depthParen = 0,
|
|
575
|
+
depthBracket = 0,
|
|
576
|
+
depthBrace = 0;
|
|
577
|
+
let inSingle = false,
|
|
578
|
+
inDouble = false,
|
|
579
|
+
escape = false;
|
|
580
|
+
let colonIndex = -1;
|
|
581
|
+
|
|
582
|
+
for (let i = 0; i < left.length; i++) {
|
|
583
|
+
const ch = left[i];
|
|
584
|
+
|
|
585
|
+
if (escape) {
|
|
586
|
+
escape = false;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (ch === "\\") {
|
|
590
|
+
escape = true;
|
|
115
591
|
continue;
|
|
116
592
|
}
|
|
117
593
|
|
|
118
|
-
|
|
594
|
+
if (!inDouble && ch === "'" && !inSingle) {
|
|
595
|
+
inSingle = true;
|
|
596
|
+
continue;
|
|
597
|
+
} else if (inSingle && ch === "'") {
|
|
598
|
+
inSingle = false;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!inSingle && ch === '"' && !inDouble) {
|
|
603
|
+
inDouble = true;
|
|
604
|
+
continue;
|
|
605
|
+
} else if (inDouble && ch === '"') {
|
|
606
|
+
inDouble = false;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (inSingle || inDouble) continue;
|
|
611
|
+
|
|
612
|
+
if (ch === "(") depthParen++;
|
|
613
|
+
else if (ch === ")") depthParen--;
|
|
614
|
+
else if (ch === "[") depthBracket++;
|
|
615
|
+
else if (ch === "]") depthBracket--;
|
|
616
|
+
else if (ch === "{") depthBrace++;
|
|
617
|
+
else if (ch === "}") depthBrace--;
|
|
618
|
+
|
|
119
619
|
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
620
|
+
ch === ":" &&
|
|
621
|
+
depthParen === 0 &&
|
|
622
|
+
depthBracket === 0 &&
|
|
623
|
+
depthBrace === 0
|
|
124
624
|
) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Recursively handle nested structures (e.g. Union[str, Literal["a"]])
|
|
128
|
-
else if (child.type === "subscript" || child.type === "generic_type") {
|
|
129
|
-
values.push(...extractLiteralValues(child));
|
|
625
|
+
colonIndex = i;
|
|
626
|
+
break;
|
|
130
627
|
}
|
|
131
628
|
}
|
|
629
|
+
|
|
630
|
+
if (colonIndex >= 0) {
|
|
631
|
+
name = left.slice(0, colonIndex).trim();
|
|
632
|
+
type = left.slice(colonIndex + 1).trim();
|
|
633
|
+
}
|
|
132
634
|
}
|
|
133
635
|
|
|
134
|
-
return
|
|
636
|
+
if (!name) return null;
|
|
637
|
+
return { name, type, defaultValue };
|
|
135
638
|
}
|
|
136
639
|
|
|
137
640
|
/**
|
|
138
|
-
*
|
|
641
|
+
* AST-based top-level alias collection:
|
|
642
|
+
* Size = Literal["sm","md"]
|
|
643
|
+
* Foo = Union[str, Literal["x"]]
|
|
139
644
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
645
|
+
function collectTypeAliasesAst(
|
|
646
|
+
root: NodeSpan,
|
|
647
|
+
source: string,
|
|
142
648
|
): Map<string, string[]> {
|
|
143
649
|
const aliases = new Map<string, string[]>();
|
|
144
650
|
|
|
145
|
-
for (const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
651
|
+
for (const stmt of getTopLevelStatements(root)) {
|
|
652
|
+
const stmtText = slice(source, stmt).trim();
|
|
653
|
+
if (!stmtText || stmtText.startsWith("@")) continue;
|
|
654
|
+
|
|
655
|
+
// structure is AST (top-level statement); leaf extraction uses text
|
|
656
|
+
const eqIndex = stmtText.indexOf("=");
|
|
657
|
+
if (eqIndex < 0) continue;
|
|
658
|
+
|
|
659
|
+
const left = stmtText.slice(0, eqIndex).trim();
|
|
660
|
+
const right = stmtText.slice(eqIndex + 1).trim();
|
|
661
|
+
|
|
662
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(left)) continue;
|
|
663
|
+
|
|
664
|
+
const values = extractLiteralValuesFromTypeString(right);
|
|
665
|
+
if (values.length > 0) {
|
|
666
|
+
aliases.set(left, values);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return aliases;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Dictionary key extraction from dict literal text (token-aware, no regex)
|
|
675
|
+
*/
|
|
676
|
+
function extractDictKeysFromText(dictText: string): string[] {
|
|
677
|
+
const keys: string[] = [];
|
|
678
|
+
|
|
679
|
+
const inner = dictText.trim();
|
|
680
|
+
if (!(inner.startsWith("{") && inner.endsWith("}"))) return keys;
|
|
681
|
+
|
|
682
|
+
const body = inner.slice(1, -1);
|
|
683
|
+
const pairs = splitTopLevelComma(body);
|
|
684
|
+
|
|
685
|
+
for (const part of pairs) {
|
|
686
|
+
const p = part.trim();
|
|
687
|
+
if (!p || p.startsWith("**")) continue;
|
|
688
|
+
|
|
689
|
+
// split top-level colon
|
|
690
|
+
let colonIndex = -1;
|
|
691
|
+
let depthParen = 0,
|
|
692
|
+
depthBracket = 0,
|
|
693
|
+
depthBrace = 0;
|
|
694
|
+
let inSingle = false,
|
|
695
|
+
inDouble = false,
|
|
696
|
+
escape = false;
|
|
697
|
+
|
|
698
|
+
for (let i = 0; i < p.length; i++) {
|
|
699
|
+
const ch = p[i];
|
|
700
|
+
if (escape) {
|
|
701
|
+
escape = false;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (ch === "\\") {
|
|
705
|
+
escape = true;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (!inDouble && ch === "'" && !inSingle) {
|
|
710
|
+
inSingle = true;
|
|
711
|
+
continue;
|
|
712
|
+
} else if (inSingle && ch === "'") {
|
|
713
|
+
inSingle = false;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!inSingle && ch === '"' && !inDouble) {
|
|
718
|
+
inDouble = true;
|
|
719
|
+
continue;
|
|
720
|
+
} else if (inDouble && ch === '"') {
|
|
721
|
+
inDouble = false;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (inSingle || inDouble) continue;
|
|
726
|
+
|
|
727
|
+
if (ch === "(") depthParen++;
|
|
728
|
+
else if (ch === ")") depthParen--;
|
|
729
|
+
else if (ch === "[") depthBracket++;
|
|
730
|
+
else if (ch === "]") depthBracket--;
|
|
731
|
+
else if (ch === "{") depthBrace++;
|
|
732
|
+
else if (ch === "}") depthBrace--;
|
|
733
|
+
|
|
734
|
+
if (
|
|
735
|
+
ch === ":" &&
|
|
736
|
+
depthParen === 0 &&
|
|
737
|
+
depthBracket === 0 &&
|
|
738
|
+
depthBrace === 0
|
|
739
|
+
) {
|
|
740
|
+
colonIndex = i;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (colonIndex < 0) continue;
|
|
746
|
+
const keyExpr = p.slice(0, colonIndex).trim();
|
|
747
|
+
|
|
748
|
+
if (/^['"].*['"]$/s.test(keyExpr)) {
|
|
749
|
+
keys.push(stripQuotes(keyExpr));
|
|
750
|
+
} else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(keyExpr)) {
|
|
751
|
+
keys.push(keyExpr);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return [...new Set(keys)];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* AST-ish body dictionary assignments:
|
|
760
|
+
* scoped body parsing (less fragile than whole-file regex)
|
|
761
|
+
*/
|
|
762
|
+
function extractBodyDictAssignmentsAst(
|
|
763
|
+
bodyText: string,
|
|
764
|
+
): Map<string, string[]> {
|
|
765
|
+
const out = new Map<string, string[]>();
|
|
766
|
+
const lines = bodyText.split(/\r?\n/);
|
|
767
|
+
|
|
768
|
+
let i = 0;
|
|
769
|
+
while (i < lines.length) {
|
|
770
|
+
const line = lines[i];
|
|
771
|
+
const trimmed = line.trim();
|
|
772
|
+
|
|
773
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
774
|
+
i++;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Look for `name = {`
|
|
779
|
+
const eq = line.indexOf("=");
|
|
780
|
+
if (eq < 0) {
|
|
781
|
+
i++;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const left = line.slice(0, eq).trim();
|
|
786
|
+
const rightStart = line.slice(eq + 1).trimStart();
|
|
787
|
+
|
|
788
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(left) || !rightStart.startsWith("{")) {
|
|
789
|
+
i++;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// collect balanced dict literal across lines
|
|
794
|
+
let dictText = line.slice(line.indexOf("{"));
|
|
795
|
+
let depth = 0;
|
|
796
|
+
let started = false;
|
|
797
|
+
let done = false;
|
|
798
|
+
|
|
799
|
+
const countBraces = (s: string) => {
|
|
800
|
+
let inSingle = false;
|
|
801
|
+
let inDouble = false;
|
|
802
|
+
let escape = false;
|
|
803
|
+
|
|
804
|
+
for (const ch of s) {
|
|
805
|
+
if (escape) {
|
|
806
|
+
escape = false;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (ch === "\\") {
|
|
810
|
+
escape = true;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (!inDouble && ch === "'" && !inSingle) {
|
|
815
|
+
inSingle = true;
|
|
816
|
+
continue;
|
|
817
|
+
} else if (inSingle && ch === "'") {
|
|
818
|
+
inSingle = false;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!inSingle && ch === '"' && !inDouble) {
|
|
823
|
+
inDouble = true;
|
|
824
|
+
continue;
|
|
825
|
+
} else if (inDouble && ch === '"') {
|
|
826
|
+
inDouble = false;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (inSingle || inDouble) continue;
|
|
831
|
+
|
|
832
|
+
if (ch === "{") {
|
|
833
|
+
depth++;
|
|
834
|
+
started = true;
|
|
835
|
+
} else if (ch === "}") {
|
|
836
|
+
depth--;
|
|
837
|
+
if (started && depth === 0) done = true;
|
|
157
838
|
}
|
|
158
839
|
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
countBraces(dictText);
|
|
843
|
+
|
|
844
|
+
let j = i + 1;
|
|
845
|
+
while (!done && j < lines.length) {
|
|
846
|
+
dictText += "\n" + lines[j];
|
|
847
|
+
countBraces(lines[j]);
|
|
848
|
+
j++;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (done) {
|
|
852
|
+
out.set(left, extractDictKeysFromText(dictText));
|
|
853
|
+
i = j;
|
|
854
|
+
continue;
|
|
159
855
|
}
|
|
856
|
+
|
|
857
|
+
i++;
|
|
160
858
|
}
|
|
161
|
-
|
|
859
|
+
|
|
860
|
+
return out;
|
|
162
861
|
}
|
|
163
862
|
|
|
164
863
|
/**
|
|
165
864
|
* ---------------------------------------------------------
|
|
166
|
-
* Core Analysis Logic (AST
|
|
865
|
+
* Core Analysis Logic (AST-first)
|
|
167
866
|
* ---------------------------------------------------------
|
|
168
867
|
*/
|
|
169
868
|
|
|
170
869
|
function analyzeFile(filePath: string, rootDir: string): ComponentMetadata[] {
|
|
171
870
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
172
|
-
const
|
|
871
|
+
const { root } = parsePythonToSpanTree(fileContent);
|
|
872
|
+
|
|
173
873
|
const components: ComponentMetadata[] = [];
|
|
174
|
-
const typeAliases =
|
|
175
|
-
|
|
176
|
-
const query = new Parser.Query(
|
|
177
|
-
Python as any,
|
|
178
|
-
`(decorated_definition
|
|
179
|
-
(decorator) @dec
|
|
180
|
-
(function_definition) @func
|
|
181
|
-
)`
|
|
182
|
-
);
|
|
874
|
+
const typeAliases = collectTypeAliasesAst(root, fileContent);
|
|
875
|
+
const decoratedFns = findDecoratedFunctionsAst(root, fileContent);
|
|
183
876
|
|
|
184
|
-
const
|
|
877
|
+
for (const item of decoratedFns) {
|
|
878
|
+
const hasComponentDecorator = item.decoratorNodes.some((d) =>
|
|
879
|
+
decoratorMatchesComponent(d, fileContent),
|
|
880
|
+
);
|
|
185
881
|
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
882
|
+
// fallback for grammars that don't expose decorator children cleanly
|
|
883
|
+
const decoratedText = slice(fileContent, item.decoratedNode);
|
|
884
|
+
const fallbackHasComponent =
|
|
885
|
+
decoratedText.trimStart().startsWith("@component") ||
|
|
886
|
+
decoratedText.includes("\n@component");
|
|
189
887
|
|
|
190
|
-
|
|
191
|
-
if (!funcNode) continue;
|
|
888
|
+
if (!hasComponentDecorator && !fallbackHasComponent) continue;
|
|
192
889
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
890
|
+
const componentName = extractFunctionNameAst(
|
|
891
|
+
item.functionNode,
|
|
892
|
+
fileContent,
|
|
893
|
+
);
|
|
894
|
+
const paramsText = extractParamListTextAst(item.functionNode, fileContent);
|
|
895
|
+
const bodyText = extractFunctionBodyTextAst(item.functionNode, fileContent);
|
|
197
896
|
|
|
198
|
-
// 1
|
|
897
|
+
// 1) Parse Props (parameters)
|
|
199
898
|
const props: ComponentProp[] = [];
|
|
200
899
|
const propMap = new Map<string, ComponentProp>();
|
|
201
900
|
let acceptsArbitraryProps = false;
|
|
202
901
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (param.type === "dictionary_splat_pattern") {
|
|
207
|
-
acceptsArbitraryProps = true;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
902
|
+
for (const rawParam of splitTopLevelComma(paramsText)) {
|
|
903
|
+
const parsed = parseParameterChunk(rawParam);
|
|
904
|
+
if (!parsed) continue;
|
|
210
905
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
906
|
+
if (parsed.arbitraryDict) {
|
|
907
|
+
acceptsArbitraryProps = true;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (parsed.listSplat) {
|
|
911
|
+
continue; // skip *args
|
|
912
|
+
}
|
|
214
913
|
|
|
215
|
-
|
|
216
|
-
let defaultValue: string | undefined = undefined;
|
|
217
|
-
let typeNode: Parser.SyntaxNode | null = null;
|
|
218
|
-
|
|
219
|
-
if (param.type === "identifier") {
|
|
220
|
-
name = param.text;
|
|
221
|
-
} else if (param.type === "default_parameter") {
|
|
222
|
-
name = param.childForFieldName("name")?.text || "";
|
|
223
|
-
defaultValue = param.childForFieldName("value")?.text;
|
|
224
|
-
} else if (param.type === "typed_parameter") {
|
|
225
|
-
const nameNode =
|
|
226
|
-
param.childForFieldName("name") ??
|
|
227
|
-
param.children.find((c) => c.type === "identifier") ??
|
|
228
|
-
null;
|
|
229
|
-
|
|
230
|
-
name = nameNode?.text || "";
|
|
231
|
-
typeNode =
|
|
232
|
-
param.childForFieldName("type") ??
|
|
233
|
-
param.children.find(
|
|
234
|
-
(c) => c.type !== "identifier" && c.type !== ":"
|
|
235
|
-
) ??
|
|
236
|
-
null;
|
|
237
|
-
} else if (param.type === "typed_default_parameter") {
|
|
238
|
-
name = param.childForFieldName("name")?.text || "";
|
|
239
|
-
defaultValue = param.childForFieldName("value")?.text;
|
|
240
|
-
typeNode = param.childForFieldName("type") ?? null;
|
|
241
|
-
}
|
|
914
|
+
let { name, type, defaultValue } = parsed;
|
|
242
915
|
|
|
243
|
-
|
|
244
|
-
if (defaultValue)
|
|
245
|
-
defaultValue = defaultValue.replace(/^['"]|['"]$/g, "");
|
|
916
|
+
if (name === "self" || name === "cls") continue;
|
|
246
917
|
|
|
247
|
-
|
|
248
|
-
|
|
918
|
+
if (defaultValue && /^['"].*['"]$/s.test(defaultValue)) {
|
|
919
|
+
defaultValue = stripQuotes(defaultValue);
|
|
920
|
+
}
|
|
249
921
|
|
|
250
|
-
|
|
251
|
-
let propType = "Any";
|
|
252
|
-
if (typeNode) {
|
|
253
|
-
propType = typeNode.text;
|
|
254
|
-
}
|
|
922
|
+
const propType = type?.trim() || "Any";
|
|
255
923
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (actualType?.type === "identifier") {
|
|
265
|
-
options = typeAliases.get(actualType.text) || [];
|
|
266
|
-
} else if (
|
|
267
|
-
actualType?.type === "subscript" ||
|
|
268
|
-
actualType?.type === "generic_type"
|
|
269
|
-
) {
|
|
270
|
-
options = extractLiteralValues(actualType);
|
|
271
|
-
} else if (actualType) {
|
|
272
|
-
const findSubscript = (
|
|
273
|
-
n: Parser.SyntaxNode
|
|
274
|
-
): Parser.SyntaxNode | null => {
|
|
275
|
-
if (n.type === "subscript") return n;
|
|
276
|
-
for (const c of n.children) {
|
|
277
|
-
const found = findSubscript(c);
|
|
278
|
-
if (found) return found;
|
|
279
|
-
}
|
|
280
|
-
return null;
|
|
281
|
-
};
|
|
282
|
-
const subscript = findSubscript(actualType);
|
|
283
|
-
if (subscript) {
|
|
284
|
-
options = extractLiteralValues(subscript);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
924
|
+
let options: string[] = [];
|
|
925
|
+
if (type) {
|
|
926
|
+
const t = type.trim();
|
|
927
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t)) {
|
|
928
|
+
options = typeAliases.get(t) || [];
|
|
929
|
+
} else {
|
|
930
|
+
options = extractLiteralValuesFromTypeString(t);
|
|
287
931
|
}
|
|
932
|
+
}
|
|
288
933
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
934
|
+
const propObj: ComponentProp = {
|
|
935
|
+
name,
|
|
936
|
+
type: propType,
|
|
937
|
+
hasDefault: defaultValue !== undefined,
|
|
938
|
+
defaultValue,
|
|
939
|
+
options: options.length > 0 ? options : undefined,
|
|
940
|
+
};
|
|
296
941
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
942
|
+
props.push(propObj);
|
|
943
|
+
propMap.set(name, propObj);
|
|
300
944
|
}
|
|
301
945
|
|
|
302
|
-
// 2
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (left?.type === "identifier" && right?.type === "dictionary") {
|
|
312
|
-
const varName = left.text;
|
|
313
|
-
if (varName.endsWith("s")) {
|
|
314
|
-
const propName = varName.slice(0, -1);
|
|
315
|
-
const relatedProp = propMap.get(propName);
|
|
316
|
-
|
|
317
|
-
if (relatedProp) {
|
|
318
|
-
if (!relatedProp.options) {
|
|
319
|
-
relatedProp.options = extractDictKeysFromNode(right);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
946
|
+
// 2) Infer options from body dict assignments
|
|
947
|
+
const dictAssignments = extractBodyDictAssignmentsAst(bodyText);
|
|
948
|
+
|
|
949
|
+
for (const [varName, keys] of dictAssignments.entries()) {
|
|
950
|
+
if (!varName.endsWith("s")) continue;
|
|
951
|
+
const propName = varName.slice(0, -1);
|
|
952
|
+
const relatedProp = propMap.get(propName);
|
|
953
|
+
if (relatedProp && !relatedProp.options && keys.length > 0) {
|
|
954
|
+
relatedProp.options = keys;
|
|
326
955
|
}
|
|
327
956
|
}
|
|
328
957
|
|
|
329
|
-
//
|
|
958
|
+
// 3) Paths
|
|
330
959
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
331
960
|
const pathNoExt = relativePath.replace(/\.py$/, "");
|
|
332
961
|
const importRoute = pathNoExt.replace(/\//g, ".");
|
|
@@ -349,6 +978,7 @@ function analyzeFile(filePath: string, rootDir: string): ComponentMetadata[] {
|
|
|
349
978
|
* File System Helpers
|
|
350
979
|
* ---------------------------------------------------------
|
|
351
980
|
*/
|
|
981
|
+
|
|
352
982
|
function loadConfig(): CaspianConfig {
|
|
353
983
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
354
984
|
console.error(`ā Configuration file not found at: ${CONFIG_PATH}`);
|
|
@@ -360,18 +990,20 @@ function loadConfig(): CaspianConfig {
|
|
|
360
990
|
function walkDirectory(
|
|
361
991
|
dir: string,
|
|
362
992
|
fileList: string[] = [],
|
|
363
|
-
excludeList: string[] = []
|
|
993
|
+
excludeList: string[] = [],
|
|
364
994
|
): string[] {
|
|
365
995
|
if (!fs.existsSync(dir)) return fileList;
|
|
366
996
|
const files = fs.readdirSync(dir);
|
|
997
|
+
|
|
367
998
|
files.forEach((file) => {
|
|
368
999
|
const fullPath = path.join(dir, file);
|
|
369
1000
|
const stat = fs.statSync(fullPath);
|
|
370
1001
|
const relativeCheck = path
|
|
371
1002
|
.relative(PROJECT_ROOT, fullPath)
|
|
372
1003
|
.replace(/\\/g, "/");
|
|
1004
|
+
|
|
373
1005
|
const isExcluded = excludeList.some((ex) =>
|
|
374
|
-
relativeCheck.includes(ex.replace(/^\.\//, ""))
|
|
1006
|
+
relativeCheck.includes(ex.replace(/^\.\//, "")),
|
|
375
1007
|
);
|
|
376
1008
|
if (isExcluded) return;
|
|
377
1009
|
|
|
@@ -381,6 +1013,7 @@ function walkDirectory(
|
|
|
381
1013
|
fileList.push(fullPath);
|
|
382
1014
|
}
|
|
383
1015
|
});
|
|
1016
|
+
|
|
384
1017
|
return fileList;
|
|
385
1018
|
}
|
|
386
1019
|
|
|
@@ -389,8 +1022,9 @@ function walkDirectory(
|
|
|
389
1022
|
* Execution Entry Point
|
|
390
1023
|
* ---------------------------------------------------------
|
|
391
1024
|
*/
|
|
1025
|
+
|
|
392
1026
|
export async function componentMap() {
|
|
393
|
-
console.log(`š Starting Component Analysis (AST
|
|
1027
|
+
console.log(`š Starting Component Analysis (Lezer AST-first)...`);
|
|
394
1028
|
const config = loadConfig();
|
|
395
1029
|
let allFiles: string[] = [];
|
|
396
1030
|
|
|
@@ -398,7 +1032,7 @@ export async function componentMap() {
|
|
|
398
1032
|
allFiles = walkDirectory(
|
|
399
1033
|
path.join(PROJECT_ROOT, scanDir),
|
|
400
1034
|
allFiles,
|
|
401
|
-
config.excludeFiles || []
|
|
1035
|
+
config.excludeFiles || [],
|
|
402
1036
|
);
|
|
403
1037
|
});
|
|
404
1038
|
|