From 93641751a937f1be6d7561d407f0fe650bcb2f73 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 12 Jan 2026 15:50:09 +0200 Subject: [PATCH 1/5] added create from template --- src/cli/commands/project/create.ts | 95 ++++++++++++ src/cli/commands/project/init.ts | 75 ---------- src/cli/index.ts | 4 +- src/cli/utils/prompts.ts | 26 +--- src/core/config.ts | 11 +- src/core/project/create.ts | 48 +++++++ src/core/project/index.ts | 3 +- src/core/project/init.ts | 63 -------- src/core/project/schema.ts | 16 +++ src/core/project/template.ts | 56 ++++++++ src/core/project/templates/index.ts | 30 ---- src/core/utils/fs.ts | 9 ++ templates/backend-and-client/.gitignore | 16 +++ templates/backend-and-client/README.md | 41 ++++++ .../base44/config.jsonc.ejs | 17 +++ .../base44/entities/task.jsonc | 16 +++ templates/backend-and-client/components.json | 16 +++ templates/backend-and-client/index.html | 13 ++ templates/backend-and-client/jsconfig.json | 13 ++ templates/backend-and-client/package.json | 24 ++++ .../backend-and-client/postcss.config.js | 6 + templates/backend-and-client/src/App.jsx | 135 ++++++++++++++++++ .../src/api/base44Client.js.ejs | 5 + .../src/components/ui/button.jsx | 23 +++ .../src/components/ui/checkbox.jsx | 20 +++ .../src/components/ui/input.jsx | 13 ++ templates/backend-and-client/src/index.css | 37 +++++ templates/backend-and-client/src/main.jsx | 6 + .../backend-and-client/tailwind.config.js | 41 ++++++ templates/backend-and-client/vite.config.js | 12 ++ .../backend-only/base44/.env.local.ejs | 0 .../backend-only/base44}/config.jsonc.ejs | 0 templates/templates.json | 16 +++ tsdown.config.mjs | 2 +- 34 files changed, 716 insertions(+), 192 deletions(-) create mode 100644 src/cli/commands/project/create.ts delete mode 100644 src/cli/commands/project/init.ts create mode 100644 src/core/project/create.ts delete mode 100644 src/core/project/init.ts create mode 100644 src/core/project/template.ts delete mode 100644 src/core/project/templates/index.ts create mode 100644 templates/backend-and-client/.gitignore create mode 100644 templates/backend-and-client/README.md create mode 100644 templates/backend-and-client/base44/config.jsonc.ejs create mode 100644 templates/backend-and-client/base44/entities/task.jsonc create mode 100644 templates/backend-and-client/components.json create mode 100644 templates/backend-and-client/index.html create mode 100644 templates/backend-and-client/jsconfig.json create mode 100644 templates/backend-and-client/package.json create mode 100644 templates/backend-and-client/postcss.config.js create mode 100644 templates/backend-and-client/src/App.jsx create mode 100644 templates/backend-and-client/src/api/base44Client.js.ejs create mode 100644 templates/backend-and-client/src/components/ui/button.jsx create mode 100644 templates/backend-and-client/src/components/ui/checkbox.jsx create mode 100644 templates/backend-and-client/src/components/ui/input.jsx create mode 100644 templates/backend-and-client/src/index.css create mode 100644 templates/backend-and-client/src/main.jsx create mode 100644 templates/backend-and-client/tailwind.config.js create mode 100644 templates/backend-and-client/vite.config.js rename src/core/project/templates/env.local.ejs => templates/backend-only/base44/.env.local.ejs (100%) rename {src/core/project/templates => templates/backend-only/base44}/config.jsonc.ejs (100%) create mode 100644 templates/templates.json diff --git a/src/cli/commands/project/create.ts b/src/cli/commands/project/create.ts new file mode 100644 index 000000000..74bec6074 --- /dev/null +++ b/src/cli/commands/project/create.ts @@ -0,0 +1,95 @@ +import { resolve } from "node:path"; +import { Command } from "commander"; +import { log, group, text, select } from "@clack/prompts"; +import chalk from "chalk"; +import { loadProjectEnv } from "@core/config.js"; +import { createProjectFiles, listTemplates } from "@core/project/index.js"; +import type { Template } from "@core/project/index.js"; +import { runTask, printBanner, onPromptCancel } from "../../utils/index.js"; + +async function create(): Promise { + printBanner(); + + // Load .env.local from project root (if in a project) + await loadProjectEnv(); + + // Load available templates for the select options + const templates = await listTemplates(); + const templateOptions = templates.map((t: Template) => ({ + value: t, + label: t.name, + hint: t.description, + })); + + // Gather all project details in a single group + const { template, name, description, projectPath } = await group( + { + template: () => + select({ + message: "Select a project template", + options: templateOptions, + }), + name: () => + text({ + message: "What is the name of your project?", + placeholder: "my-app-backend", + validate: (value) => { + if (!value || value.trim().length === 0) { + return "Project name is required"; + } + }, + }), + description: () => + text({ + message: "Project description (optional)", + placeholder: "A brief description of your project", + }), + projectPath: () => + text({ + message: "Where should we create the base44 folder?", + placeholder: "./", + initialValue: "./", + }), + }, + { + onCancel: onPromptCancel, + } + ); + + const resolvedPath = resolve(projectPath || "./"); + + // Create the project + await runTask( + "Creating project...", + async () => { + return await createProjectFiles({ + name: name.trim(), + description: description ? description.trim() : undefined, + path: resolvedPath, + template, + }); + }, + { + successMessage: "Project created successfully", + errorMessage: "Failed to create project", + } + ); + + // Display success message with details + log.success(`Project ${chalk.bold(name)} has been initialized!`); +} + +export const createCommand = new Command("create") + .description("Create a new Base44 project") + .action(async () => { + try { + await create(); + } catch (e) { + if (e instanceof Error) { + log.error(e.stack ?? e.message); + } else { + log.error(String(e)); + } + process.exit(1); + } + }); diff --git a/src/cli/commands/project/init.ts b/src/cli/commands/project/init.ts deleted file mode 100644 index 9e63b357f..000000000 --- a/src/cli/commands/project/init.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { resolve } from "node:path"; -import { Command } from "commander"; -import { log } from "@clack/prompts"; -import chalk from "chalk"; -import { loadProjectEnv } from "@core/config.js"; -import { initProject } from "@core/project/index.js"; -import { runTask, textPrompt, printBanner } from "../../utils/index.js"; - -async function init(): Promise { - printBanner(); - - // Load .env.local from project root (if in a project) - await loadProjectEnv(); - - const name = await textPrompt({ - message: "What is the name of your project?", - placeholder: "my-app-backend", - validate: (value) => { - if (!value || value.trim().length === 0) { - return "Project name is required"; - } - }, - }); - - // Ask for description (optional) - const description: string | undefined = await textPrompt({ - message: "Project description (optional)", - placeholder: "A brief description of your project", - }); - - // Ask for project path with default - const defaultPath = "./"; - const projectPath = await textPrompt({ - message: "Where should we create the base44 folder?", - placeholder: defaultPath, - initialValue: defaultPath, - }); - - const resolvedPath = resolve(projectPath || defaultPath); - - // Create the project - await runTask( - "Creating project...", - async () => { - return await initProject({ - name: name.trim(), - description: description ? description.trim() : undefined, - path: resolvedPath, - }); - }, - { - successMessage: "Project created successfully", - errorMessage: "Failed to create project", - } - ); - - // Display success message with details - log.success(`Project ${chalk.bold(name)} has been initialized!`); -} - -export const initCommand = new Command("init") - .alias("create") - .description("Initialize a new Base44 project") - .action(async () => { - try { - await init(); - } catch (e) { - if (e instanceof Error) { - log.error(e.stack ?? e.message); - } else { - log.error(String(e)); - } - process.exit(1); - } - }); diff --git a/src/cli/index.ts b/src/cli/index.ts index 1c8e985d5..20d618ad7 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,7 +6,7 @@ import { whoamiCommand } from "./commands/auth/whoami.js"; import { logoutCommand } from "./commands/auth/logout.js"; import { showProjectCommand } from "./commands/project/show-project.js"; import { entitiesPushCommand } from "./commands/entities/push.js"; -import { initCommand } from "./commands/project/init.js"; +import { createCommand } from "./commands/project/create.js"; import packageJson from "../../package.json"; const program = new Command(); @@ -24,7 +24,7 @@ program.addCommand(whoamiCommand); program.addCommand(logoutCommand); // Register project commands -program.addCommand(initCommand); +program.addCommand(createCommand); program.addCommand(showProjectCommand); // Register entities commands diff --git a/src/cli/utils/prompts.ts b/src/cli/utils/prompts.ts index f21ec8151..5179fa1a7 100644 --- a/src/cli/utils/prompts.ts +++ b/src/cli/utils/prompts.ts @@ -1,22 +1,10 @@ -import { text, isCancel, cancel } from "@clack/prompts"; -import type { TextOptions } from "@clack/prompts"; +import { cancel } from "@clack/prompts"; /** - * Handles prompt cancellation by exiting gracefully. + * Standard onCancel handler for prompt groups. + * Exits the process gracefully when the user cancels. */ -function handleCancel(value: T | symbol): asserts value is T { - if (isCancel(value)) { - cancel("Operation cancelled."); - process.exit(0); - } -} - -/** - * Wrapper around @clack/prompts text() that handles cancellation automatically. - * Returns the string value directly, exits process if cancelled. - */ -export async function textPrompt(options: TextOptions): Promise { - const value = await text(options); - handleCancel(value); - return value; -} +export const onPromptCancel = () => { + cancel("Operation cancelled."); + process.exit(0); +}; diff --git a/src/core/config.ts b/src/core/config.ts index c384b9043..f14dfc0a5 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,8 +1,13 @@ -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { homedir } from "node:os"; +import { fileURLToPath } from "node:url"; import { config } from "dotenv"; import { findProjectRoot } from "./project/index.js"; +// After bundling, import.meta.url points to dist/cli/index.js +// Templates are copied to dist/cli/templates/ +const __dirname = dirname(fileURLToPath(import.meta.url)); + // Static constants export const PROJECT_SUBDIR = "base44"; export const FUNCTION_CONFIG_FILE = "function.jsonc"; @@ -17,6 +22,10 @@ export function getAuthFilePath() { return join(getBase44Dir(), "auth", "auth.json"); } +export function getTemplatesDir() { + return join(__dirname, "templates"); +} + export function getProjectConfigPatterns() { return [ `${PROJECT_SUBDIR}/config.jsonc`, diff --git a/src/core/project/create.ts b/src/core/project/create.ts new file mode 100644 index 000000000..2d5a1341e --- /dev/null +++ b/src/core/project/create.ts @@ -0,0 +1,48 @@ +import { globby } from "globby"; +import { getProjectConfigPatterns } from "../config.js"; +import { createProject } from "./api.js"; +import { renderTemplate } from "./template.js"; +import type { Template } from "./schema.js"; + +export interface CreateProjectOptions { + name: string; + description?: string; + path: string; + template: Template; +} + +export interface CreateProjectResult { + projectDir: string; +} + +export async function createProjectFiles( + options: CreateProjectOptions +): Promise { + const { name, description, path: basePath, template } = options; + + // Check if project already exists + const existingConfigs = await globby(getProjectConfigPatterns(), { + cwd: basePath, + absolute: true, + }); + + if (existingConfigs.length > 0) { + throw new Error( + `A Base44 project already exists at ${existingConfigs[0]}. Please choose a different location.` + ); + } + + // Create the project via API to get the app ID + const { projectId } = await createProject(name, description); + + // Render the template to the destination path + await renderTemplate(template, basePath, { + name, + description, + projectId, + }); + + return { + projectDir: basePath, + }; +} diff --git a/src/core/project/index.ts b/src/core/project/index.ts index 92d3bcf45..32fc54046 100644 --- a/src/core/project/index.ts +++ b/src/core/project/index.ts @@ -2,4 +2,5 @@ export type * from "./baseResource.js"; export * from "./config.js"; export * from "./schema.js"; export * from "./api.js"; -export * from "./init.js"; +export * from "./create.js"; +export * from "./template.js"; diff --git a/src/core/project/init.ts b/src/core/project/init.ts deleted file mode 100644 index 0a8873546..000000000 --- a/src/core/project/init.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { join } from "node:path"; -import { globby } from "globby"; -import { getProjectConfigPatterns, PROJECT_SUBDIR } from "../config.js"; -import { writeFile } from "../utils/fs.js"; -import { createProject } from "./api.js"; -import { renderConfigTemplate, renderEnvTemplate } from "./templates/index.js"; - -export interface InitProjectOptions { - name: string; - description?: string; - path: string; -} - -export interface InitProjectResult { - projectId: string; - projectDir: string; - configPath: string; - envPath: string; -} - -/** - * Initialize a new Base44 project. - * Creates the base44 directory, config.jsonc, and .env.local files. - */ -export async function initProject( - options: InitProjectOptions -): Promise { - const { name, description, path: basePath } = options; - - const projectDir = join(basePath, PROJECT_SUBDIR); - const configPath = join(projectDir, "config.jsonc"); - const envPath = join(projectDir, ".env.local"); - - // Check if project already exists - const existingConfigs = await globby(getProjectConfigPatterns(), { - cwd: basePath, - absolute: true, - }); - - if (existingConfigs.length > 0) { - throw new Error( - `A Base44 project already exists at ${existingConfigs[0]}. Please choose a different location.` - ); - } - - // Create the project via API to get the app ID - const { projectId } = await createProject(name, description); - - // Create config.jsonc from template - const configContent = await renderConfigTemplate({ name, description }); - await writeFile(configPath, configContent); - - // Create .env.local from template - const envContent = await renderEnvTemplate({ projectId }); - await writeFile(envPath, envContent); - - return { - projectId, - projectDir, - configPath, - envPath, - }; -} diff --git a/src/core/project/schema.ts b/src/core/project/schema.ts index 02d6bac35..c146b97cf 100644 --- a/src/core/project/schema.ts +++ b/src/core/project/schema.ts @@ -1,5 +1,21 @@ import { z } from "zod"; +// Template schemas +export const TemplateSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + path: z.string(), +}); + +export const TemplatesConfigSchema = z.object({ + templates: z.array(TemplateSchema), +}); + +export type Template = z.infer; +export type TemplatesConfig = z.infer; + +// App config schemas const SiteConfigSchema = z.object({ buildCommand: z.string().optional(), serveCommand: z.string().optional(), diff --git a/src/core/project/template.ts b/src/core/project/template.ts new file mode 100644 index 000000000..970e70bbb --- /dev/null +++ b/src/core/project/template.ts @@ -0,0 +1,56 @@ +import { join } from "node:path"; +import { globby } from "globby"; +import ejs from "ejs"; +import { getTemplatesDir } from "../config.js"; +import { readJsonFile, writeFile, copyFile } from "../utils/fs.js"; +import { TemplatesConfigSchema } from "./schema.js"; +import type { Template } from "./schema.js"; + +export interface TemplateData { + name: string; + description?: string; + projectId: string; +} + +export async function listTemplates(): Promise { + const configPath = join(getTemplatesDir(), "templates.json"); + const parsed = await readJsonFile(configPath); + const validated = TemplatesConfigSchema.parse(parsed); + return validated.templates; +} + +/** + * Render a template directory to a destination path. + * - Files ending in .ejs are rendered with EJS and written without the .ejs extension + * - All other files are copied directly + */ +export async function renderTemplate( + template: Template, + destPath: string, + data: TemplateData +): Promise { + const templateDir = join(getTemplatesDir(), template.path); + + // Get all files in the template directory + const files = await globby("**/*", { + cwd: templateDir, + dot: true, + onlyFiles: true, + }); + + for (const file of files) { + const srcPath = join(templateDir, file); + + if (file.endsWith(".ejs")) { + // Render EJS template and write without .ejs extension + const destFile = file.slice(0, -4); // Remove .ejs + const destFilePath = join(destPath, destFile); + const rendered = await ejs.renderFile(srcPath, data); + await writeFile(destFilePath, rendered); + } else { + // Copy file directly + const destFilePath = join(destPath, file); + await copyFile(srcPath, destFilePath); + } + } +} diff --git a/src/core/project/templates/index.ts b/src/core/project/templates/index.ts deleted file mode 100644 index 4a22e8c86..000000000 --- a/src/core/project/templates/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import ejs from "ejs"; - -// After bundling, import.meta.url points to dist/cli/index.js -// Templates are copied to dist/cli/templates/ -const __dirname = dirname(fileURLToPath(import.meta.url)); -const TEMPLATES_DIR = join(__dirname, "templates"); - -const CONFIG_TEMPLATE_PATH = join(TEMPLATES_DIR, "config.jsonc.ejs"); -const ENV_TEMPLATE_PATH = join(TEMPLATES_DIR, "env.local.ejs"); - -interface ConfigTemplateData { - name: string; - description?: string; -} - -interface EnvTemplateData { - projectId: string; -} - -export async function renderConfigTemplate( - data: ConfigTemplateData -): Promise { - return ejs.renderFile(CONFIG_TEMPLATE_PATH, data); -} - -export async function renderEnvTemplate(data: EnvTemplateData): Promise { - return ejs.renderFile(ENV_TEMPLATE_PATH, data); -} diff --git a/src/core/utils/fs.ts b/src/core/utils/fs.ts index 2a9f837ec..7d4c51b85 100644 --- a/src/core/utils/fs.ts +++ b/src/core/utils/fs.ts @@ -1,6 +1,7 @@ import { readFile as fsReadFile, writeFile as fsWriteFile, + copyFile as fsCopyFile, mkdir, unlink, access, @@ -29,6 +30,14 @@ export async function writeFile( await fsWriteFile(filePath, content, "utf-8"); } +export async function copyFile(src: string, dest: string): Promise { + const dir = dirname(dest); + if (!(await pathExists(dir))) { + await mkdir(dir, { recursive: true }); + } + await fsCopyFile(src, dest); +} + export async function readJsonFile(filePath: string): Promise { if (!(await pathExists(filePath))) { throw new Error(`File not found: ${filePath}`); diff --git a/templates/backend-and-client/.gitignore b/templates/backend-and-client/.gitignore new file mode 100644 index 000000000..93ff455d9 --- /dev/null +++ b/templates/backend-and-client/.gitignore @@ -0,0 +1,16 @@ +# Dependencies +node_modules + +# Build +dist + +# Environment +.env +.env.* +*.local + +# Editor +.vscode +.idea +.DS_Store +*.swp diff --git a/templates/backend-and-client/README.md b/templates/backend-and-client/README.md new file mode 100644 index 000000000..0fa84faa6 --- /dev/null +++ b/templates/backend-and-client/README.md @@ -0,0 +1,41 @@ +# Todo App + +A simple todo list app built with React and Base44 backend. + +## Structure + +``` +base44/ # Backend configuration +├── config.jsonc # Project settings +└── entities/ # Data schemas + └── task.jsonc # Task entity + +src/ # Frontend code +├── App.jsx # Main todo app +├── api/ # Base44 client +├── components/ui/ # UI components +└── lib/ # Utilities +``` + +## Development + +```bash +npm install +npm run dev +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `npm run dev` | Start dev server | +| `npm run build` | Build for production | +| `npm run preview` | Preview production build | + +## Base44 CLI + +```bash +base44 login # Authenticate +base44 entities push # Push entity schemas +base44 deploy # Deploy backend + hosting +``` diff --git a/templates/backend-and-client/base44/config.jsonc.ejs b/templates/backend-and-client/base44/config.jsonc.ejs new file mode 100644 index 000000000..fed116bbd --- /dev/null +++ b/templates/backend-and-client/base44/config.jsonc.ejs @@ -0,0 +1,17 @@ +// Base44 Project Configuration +// JSONC enables inline documentation and discoverability directly in config files. +// Full-stack template with backend and client configuration. + +{ + "name": "<%= name %>"<% if (description) { %>, + "description": "<%= description %>"<% } %>, + + // Site/hosting configuration for the client application + // Docs: https://docs.base44.com/configuration/hosting + "site": { + "buildCommand": "npm run build", + "serveCommand": "npm run dev", + "outputDirectory": "./dist", + "installCommand": "npm ci" + } +} diff --git a/templates/backend-and-client/base44/entities/task.jsonc b/templates/backend-and-client/base44/entities/task.jsonc new file mode 100644 index 000000000..82b825678 --- /dev/null +++ b/templates/backend-and-client/base44/entities/task.jsonc @@ -0,0 +1,16 @@ +{ + "name": "Task", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Task title" + }, + "completed": { + "type": "boolean", + "default": false, + "description": "Whether the task is completed" + } + }, + "required": ["title"] +} diff --git a/templates/backend-and-client/components.json b/templates/backend-and-client/components.json new file mode 100644 index 000000000..068bddc0b --- /dev/null +++ b/templates/backend-and-client/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "ui": "@/components/ui" + } +} diff --git a/templates/backend-and-client/index.html b/templates/backend-and-client/index.html new file mode 100644 index 000000000..01d835319 --- /dev/null +++ b/templates/backend-and-client/index.html @@ -0,0 +1,13 @@ + + + + + + + Todo App + + +
+ + + diff --git a/templates/backend-and-client/jsconfig.json b/templates/backend-and-client/jsconfig.json new file mode 100644 index 000000000..39ebe9130 --- /dev/null +++ b/templates/backend-and-client/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "bundler", + "target": "esnext" + }, + "include": ["src"] +} diff --git a/templates/backend-and-client/package.json b/templates/backend-and-client/package.json new file mode 100644 index 000000000..91b53dfb8 --- /dev/null +++ b/templates/backend-and-client/package.json @@ -0,0 +1,24 @@ +{ + "name": "base44-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@base44/sdk": "^0.8.3", + "lucide-react": "^0.475.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "vite": "^6.1.0" + } +} diff --git a/templates/backend-and-client/postcss.config.js b/templates/backend-and-client/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/templates/backend-and-client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/templates/backend-and-client/src/App.jsx b/templates/backend-and-client/src/App.jsx new file mode 100644 index 000000000..3098c9708 --- /dev/null +++ b/templates/backend-and-client/src/App.jsx @@ -0,0 +1,135 @@ +import { useState, useEffect } from 'react'; +import { base44 } from '@/api/base44Client'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Plus, Trash2, CheckCircle2 } from 'lucide-react'; + +const Task = base44.entities.Task; + +export default function App() { + const [tasks, setTasks] = useState([]); + const [newTaskTitle, setNewTaskTitle] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + const fetchTasks = async () => { + const data = await Task.list(); + setTasks(data); + setIsLoading(false); + }; + + useEffect(() => { + fetchTasks(); + }, []); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!newTaskTitle.trim()) return; + await Task.create({ title: newTaskTitle.trim(), completed: false }); + setNewTaskTitle(''); + fetchTasks(); + }; + + const toggleTask = async (id, completed) => { + await Task.update(id, { completed }); + fetchTasks(); + }; + + const deleteTask = async (id) => { + await Task.delete(id); + fetchTasks(); + }; + + const clearCompleted = async () => { + await Promise.all(tasks.filter((t) => t.completed).map((t) => Task.delete(t.id))); + fetchTasks(); + }; + + const completedCount = tasks.filter((t) => t.completed).length; + const totalCount = tasks.length; + + return ( +
+
+ {/* Header */} +
+
+ +
+

Tasks

+ {totalCount > 0 && ( +

+ {completedCount} of {totalCount} completed +

+ )} +
+ + {/* Add Task Form */} +
+
+ setNewTaskTitle(e.target.value)} + placeholder="What needs to be done?" + className="flex-1 h-12 bg-white border-slate-200 rounded-xl shadow-sm" + /> + +
+
+ + {/* Task List */} +
+ {isLoading ? ( +
+
+
+ ) : tasks.length === 0 ? ( +
+

No tasks yet. Add one above!

+
+ ) : ( + tasks.map((task) => ( +
+ toggleTask(task.id, checked)} + className="w-5 h-5 rounded-md border-slate-300 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" + /> + + {task.title} + + +
+ )) + )} +
+ + {/* Footer */} + {completedCount > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/templates/backend-and-client/src/api/base44Client.js.ejs b/templates/backend-and-client/src/api/base44Client.js.ejs new file mode 100644 index 000000000..c4c0d5967 --- /dev/null +++ b/templates/backend-and-client/src/api/base44Client.js.ejs @@ -0,0 +1,5 @@ +import { createClient } from '@base44/sdk'; + +export const base44 = createClient({ + appId: '<%= projectId %>', +}); diff --git a/templates/backend-and-client/src/components/ui/button.jsx b/templates/backend-and-client/src/components/ui/button.jsx new file mode 100644 index 000000000..f6ee99bc5 --- /dev/null +++ b/templates/backend-and-client/src/components/ui/button.jsx @@ -0,0 +1,23 @@ +import { forwardRef } from 'react'; + +const Button = forwardRef(({ className = '', variant, size, ...props }, ref) => { + const baseStyles = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50'; + + const variantStyles = variant === 'ghost' + ? 'hover:bg-slate-100' + : 'bg-slate-900 text-white shadow hover:bg-slate-800'; + + const sizeStyles = size === 'icon' ? 'h-9 w-9' : 'h-9 px-4 py-2'; + + return ( + +)); + +Checkbox.displayName = 'Checkbox'; + +export { Checkbox }; diff --git a/templates/backend-and-client/src/components/ui/input.jsx b/templates/backend-and-client/src/components/ui/input.jsx new file mode 100644 index 000000000..33b32d077 --- /dev/null +++ b/templates/backend-and-client/src/components/ui/input.jsx @@ -0,0 +1,13 @@ +import { forwardRef } from 'react'; + +const Input = forwardRef(({ className = '', ...props }, ref) => ( + +)); + +Input.displayName = 'Input'; + +export { Input }; diff --git a/templates/backend-and-client/src/index.css b/templates/backend-and-client/src/index.css new file mode 100644 index 000000000..863c42165 --- /dev/null +++ b/templates/backend-and-client/src/index.css @@ -0,0 +1,37 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground antialiased; + } +} diff --git a/templates/backend-and-client/src/main.jsx b/templates/backend-and-client/src/main.jsx new file mode 100644 index 000000000..f2bf72b45 --- /dev/null +++ b/templates/backend-and-client/src/main.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from '@/App.jsx'; +import '@/index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/templates/backend-and-client/tailwind.config.js b/templates/backend-and-client/tailwind.config.js new file mode 100644 index 000000000..d04fdc369 --- /dev/null +++ b/templates/backend-and-client/tailwind.config.js @@ -0,0 +1,41 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,jsx}'], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + }, + }, + }, + plugins: [], +}; diff --git a/templates/backend-and-client/vite.config.js b/templates/backend-and-client/vite.config.js new file mode 100644 index 000000000..95d423e2d --- /dev/null +++ b/templates/backend-and-client/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); diff --git a/src/core/project/templates/env.local.ejs b/templates/backend-only/base44/.env.local.ejs similarity index 100% rename from src/core/project/templates/env.local.ejs rename to templates/backend-only/base44/.env.local.ejs diff --git a/src/core/project/templates/config.jsonc.ejs b/templates/backend-only/base44/config.jsonc.ejs similarity index 100% rename from src/core/project/templates/config.jsonc.ejs rename to templates/backend-only/base44/config.jsonc.ejs diff --git a/templates/templates.json b/templates/templates.json new file mode 100644 index 000000000..015c4625a --- /dev/null +++ b/templates/templates.json @@ -0,0 +1,16 @@ +{ + "templates": [ + { + "id": "backend-only", + "name": "Backend Only", + "description": "Create a Base44 backend project with entities, functions, and APIs", + "path": "backend-only" + }, + { + "id": "backend-and-client", + "name": "Backend + Client", + "description": "Full-stack project with Base44 backend and a React client application", + "path": "backend-and-client" + } + ] +} diff --git a/tsdown.config.mjs b/tsdown.config.mjs index 85863d1ca..85e87974e 100644 --- a/tsdown.config.mjs +++ b/tsdown.config.mjs @@ -7,5 +7,5 @@ export default defineConfig({ outDir: "dist/cli", clean: true, tsconfig: "tsconfig.json", - copy: ["src/core/project/templates"], + copy: ["templates"], }); From 4d6dc301770b918d0eed963970559ae9c98601b9 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 12 Jan 2026 16:20:47 +0200 Subject: [PATCH 2/5] make the project folder be in kebab case --- package-lock.json | 25 +++++++++++++++++++++++++ package.json | 2 ++ src/cli/commands/project/create.ts | 22 ++++++++++++---------- templates/templates.json | 4 ++-- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57498fac8..8a01b96b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "globby": "^16.1.0", "jsonc-parser": "^3.3.1", "ky": "^1.14.2", + "lodash.kebabcase": "^4.1.1", "p-wait-for": "^6.0.0", "zod": "^4.3.5" }, @@ -26,6 +27,7 @@ "devDependencies": { "@stylistic/eslint-plugin": "^5.6.1", "@types/ejs": "^3.1.5", + "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.51.0", "@typescript-eslint/parser": "^8.51.0", @@ -1656,6 +1658,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.kebabcase": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.kebabcase/-/lodash.kebabcase-4.1.9.tgz", + "integrity": "sha512-kPrrmcVOhSsjAVRovN0lRfrbuidfg0wYsrQa5IYuoQO1fpHHGSme66oyiYA/5eQPVl8Z95OA3HG0+d2SvYC85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "22.19.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", @@ -4575,6 +4594,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index a57fe2ec2..9b0339b00 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,14 @@ "globby": "^16.1.0", "jsonc-parser": "^3.3.1", "ky": "^1.14.2", + "lodash.kebabcase": "^4.1.1", "p-wait-for": "^6.0.0", "zod": "^4.3.5" }, "devDependencies": { "@stylistic/eslint-plugin": "^5.6.1", "@types/ejs": "^3.1.5", + "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@typescript-eslint/eslint-plugin": "^8.51.0", "@typescript-eslint/parser": "^8.51.0", diff --git a/src/cli/commands/project/create.ts b/src/cli/commands/project/create.ts index 74bec6074..1c13a4433 100644 --- a/src/cli/commands/project/create.ts +++ b/src/cli/commands/project/create.ts @@ -1,7 +1,9 @@ import { resolve } from "node:path"; import { Command } from "commander"; import { log, group, text, select } from "@clack/prompts"; +import type { Option } from "@clack/prompts"; import chalk from "chalk"; +import kebabCase from "lodash.kebabcase"; import { loadProjectEnv } from "@core/config.js"; import { createProjectFiles, listTemplates } from "@core/project/index.js"; import type { Template } from "@core/project/index.js"; @@ -11,17 +13,16 @@ async function create(): Promise { printBanner(); // Load .env.local from project root (if in a project) + // TODO load this differenty? await loadProjectEnv(); - // Load available templates for the select options const templates = await listTemplates(); - const templateOptions = templates.map((t: Template) => ({ + const templateOptions: Array> = templates.map((t) => ({ value: t, label: t.name, hint: t.description, })); - // Gather all project details in a single group const { template, name, description, projectPath } = await group( { template: () => @@ -44,19 +45,21 @@ async function create(): Promise { message: "Project description (optional)", placeholder: "A brief description of your project", }), - projectPath: () => - text({ + projectPath: async ({ results }) => { + const suggestedPath = `./${kebabCase(results.name)}`; + return text({ message: "Where should we create the base44 folder?", - placeholder: "./", - initialValue: "./", - }), + placeholder: suggestedPath, + initialValue: suggestedPath, + }); + }, }, { onCancel: onPromptCancel, } ); - const resolvedPath = resolve(projectPath || "./"); + const resolvedPath = resolve(projectPath as string); // Create the project await runTask( @@ -75,7 +78,6 @@ async function create(): Promise { } ); - // Display success message with details log.success(`Project ${chalk.bold(name)} has been initialized!`); } diff --git a/templates/templates.json b/templates/templates.json index 015c4625a..cc652f788 100644 --- a/templates/templates.json +++ b/templates/templates.json @@ -8,8 +8,8 @@ }, { "id": "backend-and-client", - "name": "Backend + Client", - "description": "Full-stack project with Base44 backend and a React client application", + "name": "To Do App - Backend + Client", + "description": "Full-stack project with Base44 backend, Vite and a React client application", "path": "backend-and-client" } ] From b2b880fa7789c568a0db8897fa27545d1044c3b9 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 12 Jan 2026 16:29:57 +0200 Subject: [PATCH 3/5] Remove the ejs extension --- src/cli/commands/project/create.ts | 1 - src/core/project/template.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/project/create.ts b/src/cli/commands/project/create.ts index 1c13a4433..73c00f654 100644 --- a/src/cli/commands/project/create.ts +++ b/src/cli/commands/project/create.ts @@ -13,7 +13,6 @@ async function create(): Promise { printBanner(); // Load .env.local from project root (if in a project) - // TODO load this differenty? await loadProjectEnv(); const templates = await listTemplates(); diff --git a/src/core/project/template.ts b/src/core/project/template.ts index 970e70bbb..3b22ae1bd 100644 --- a/src/core/project/template.ts +++ b/src/core/project/template.ts @@ -15,8 +15,8 @@ export interface TemplateData { export async function listTemplates(): Promise { const configPath = join(getTemplatesDir(), "templates.json"); const parsed = await readJsonFile(configPath); - const validated = TemplatesConfigSchema.parse(parsed); - return validated.templates; + const result = TemplatesConfigSchema.parse(parsed); + return result.templates; } /** @@ -43,7 +43,7 @@ export async function renderTemplate( if (file.endsWith(".ejs")) { // Render EJS template and write without .ejs extension - const destFile = file.slice(0, -4); // Remove .ejs + const destFile = file.replace(".ejs", ""); const destFilePath = join(destPath, destFile); const rendered = await ejs.renderFile(srcPath, data); await writeFile(destFilePath, rendered); From 0fe53281b43868ffbae1644e8760ee3d6401181e Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 12 Jan 2026 16:48:26 +0200 Subject: [PATCH 4/5] make template a bit nicer --- templates/backend-and-client/src/App.jsx | 43 ++++++++++++------- .../src/components/Base44Logo.jsx | 15 +++++++ templates/templates.json | 2 +- 3 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 templates/backend-and-client/src/components/Base44Logo.jsx diff --git a/templates/backend-and-client/src/App.jsx b/templates/backend-and-client/src/App.jsx index 3098c9708..712964463 100644 --- a/templates/backend-and-client/src/App.jsx +++ b/templates/backend-and-client/src/App.jsx @@ -1,15 +1,16 @@ -import { useState, useEffect } from 'react'; -import { base44 } from '@/api/base44Client'; -import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Input } from '@/components/ui/input'; -import { Plus, Trash2, CheckCircle2 } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { base44 } from "@/api/base44Client"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Base44Logo } from "@/components/Base44Logo"; +import { Plus, Trash2, CheckCircle2 } from "lucide-react"; const Task = base44.entities.Task; export default function App() { const [tasks, setTasks] = useState([]); - const [newTaskTitle, setNewTaskTitle] = useState(''); + const [newTaskTitle, setNewTaskTitle] = useState(""); const [isLoading, setIsLoading] = useState(true); const fetchTasks = async () => { @@ -26,7 +27,7 @@ export default function App() { e.preventDefault(); if (!newTaskTitle.trim()) return; await Task.create({ title: newTaskTitle.trim(), completed: false }); - setNewTaskTitle(''); + setNewTaskTitle(""); fetchTasks(); }; @@ -41,7 +42,9 @@ export default function App() { }; const clearCompleted = async () => { - await Promise.all(tasks.filter((t) => t.completed).map((t) => Task.delete(t.id))); + await Promise.all( + tasks.filter((t) => t.completed).map((t) => Task.delete(t.id)) + ); fetchTasks(); }; @@ -53,10 +56,13 @@ export default function App() {
{/* Header */}
-
- -
-

Tasks

+

+ + + Base44 + Tasks + +

{totalCount > 0 && (

{completedCount} of {totalCount} completed @@ -105,7 +111,11 @@ export default function App() { onCheckedChange={(checked) => toggleTask(task.id, checked)} className="w-5 h-5 rounded-md border-slate-300 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" /> - + {task.title}

diff --git a/templates/backend-and-client/src/components/Base44Logo.jsx b/templates/backend-and-client/src/components/Base44Logo.jsx new file mode 100644 index 000000000..c80bf52e8 --- /dev/null +++ b/templates/backend-and-client/src/components/Base44Logo.jsx @@ -0,0 +1,15 @@ +export function Base44Logo({ className = "w-8 h-8" }) { + return ( + + + + ); +} diff --git a/templates/templates.json b/templates/templates.json index cc652f788..257d4ff1b 100644 --- a/templates/templates.json +++ b/templates/templates.json @@ -8,7 +8,7 @@ }, { "id": "backend-and-client", - "name": "To Do App - Backend + Client", + "name": "Backend & Client", "description": "Full-stack project with Base44 backend, Vite and a React client application", "path": "backend-and-client" } From fa6a588f3c050761da631c491806e72e289c95f6 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 12 Jan 2026 16:57:49 +0200 Subject: [PATCH 5/5] small fixes to template --- src/core/project/template.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/core/project/template.ts b/src/core/project/template.ts index 3b22ae1bd..6bc60f0fd 100644 --- a/src/core/project/template.ts +++ b/src/core/project/template.ts @@ -1,4 +1,4 @@ -import { join } from "node:path"; +import { join, isAbsolute } from "node:path"; import { globby } from "globby"; import ejs from "ejs"; import { getTemplatesDir } from "../config.js"; @@ -29,6 +29,11 @@ export async function renderTemplate( destPath: string, data: TemplateData ): Promise { + // Validate template path to prevent directory traversal + if (template.path.includes("..") || isAbsolute(template.path)) { + throw new Error(`Invalid template path: ${template.path}`); + } + const templateDir = join(getTemplatesDir(), template.path); // Get all files in the template directory @@ -41,16 +46,21 @@ export async function renderTemplate( for (const file of files) { const srcPath = join(templateDir, file); - if (file.endsWith(".ejs")) { - // Render EJS template and write without .ejs extension - const destFile = file.replace(".ejs", ""); - const destFilePath = join(destPath, destFile); - const rendered = await ejs.renderFile(srcPath, data); - await writeFile(destFilePath, rendered); - } else { - // Copy file directly - const destFilePath = join(destPath, file); - await copyFile(srcPath, destFilePath); + try { + if (file.endsWith(".ejs")) { + // Render EJS template and write without .ejs extension + const destFile = file.replace(/\.ejs$/, ""); + const destFilePath = join(destPath, destFile); + const rendered = await ejs.renderFile(srcPath, data); + await writeFile(destFilePath, rendered); + } else { + // Copy file directly + const destFilePath = join(destPath, file); + await copyFile(srcPath, destFilePath); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to process template file "${file}": ${message}`); } } }