上一会话遗留的完整功能补提交: - 新增 4 个三端体系内置模板:React Web / React + Capacitor / React + Tauri (Antd) / React Tri-Platform(同一架构按需起步) - tech_stack 升级为分类 JSON 格式(客户端/语言/框架), 新增 techStackUtils.renderTechStack 兼容旧逗号格式, ManagePage / NewProjectModal 接线,TemplateEditor 占位符同步 Feature-Confirmed: true
2429 lines
75 KiB
Rust
2429 lines
75 KiB
Rust
use crate::db;
|
||
use rusqlite::params;
|
||
use serde::{Deserialize, Serialize};
|
||
use std::io::{BufRead, BufReader};
|
||
use std::path::Path;
|
||
use std::process::Stdio;
|
||
use uuid::Uuid;
|
||
|
||
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
||
|
||
#[derive(Serialize, Deserialize, Clone)]
|
||
pub struct TemplateFile {
|
||
pub id: String,
|
||
pub template_id: String,
|
||
pub path: String,
|
||
pub content: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Clone)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProjectTemplate {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub platform: String, // "win" | "wsl" | "both"
|
||
pub tech_stack: Option<String>,
|
||
pub init_commands: Option<String>,
|
||
pub is_builtin: bool,
|
||
pub created_at: Option<String>,
|
||
#[serde(default)]
|
||
pub files: Vec<TemplateFile>,
|
||
/// JSON 数组,格式:[{label, description, initCommands}]
|
||
/// 存在时前端显示下拉让用户选择初始化方式
|
||
pub variants: Option<String>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct TemplateFileInput {
|
||
pub path: String,
|
||
pub content: String,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SaveTemplateInput {
|
||
pub id: Option<String>,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub platform: String,
|
||
pub tech_stack: Option<String>,
|
||
pub init_commands: Option<String>,
|
||
pub files: Vec<TemplateFileInput>,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CreateProjectResult {
|
||
pub project_id: String,
|
||
pub logs: Vec<String>,
|
||
}
|
||
|
||
// ── Tauri 命令 ────────────────────────────────────────────────────────────────
|
||
|
||
#[tauri::command]
|
||
pub fn list_templates() -> Result<Vec<ProjectTemplate>, String> {
|
||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||
let mut stmt = conn
|
||
.prepare(
|
||
"SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants
|
||
FROM project_templates ORDER BY is_builtin DESC, name",
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
let templates = stmt
|
||
.query_map([], |row| {
|
||
Ok(ProjectTemplate {
|
||
id: row.get(0)?,
|
||
name: row.get(1)?,
|
||
description: row.get(2)?,
|
||
platform: row.get(3)?,
|
||
tech_stack: row.get(4)?,
|
||
init_commands: row.get(5)?,
|
||
is_builtin: row.get::<_, i64>(6)? == 1,
|
||
created_at: row.get(7)?,
|
||
files: vec![],
|
||
variants: row.get(8)?,
|
||
})
|
||
})
|
||
.map_err(|e| e.to_string())?
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
Ok(templates)
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn get_template(id: String) -> Result<ProjectTemplate, String> {
|
||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||
|
||
let mut tmpl = conn
|
||
.query_row(
|
||
"SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants
|
||
FROM project_templates WHERE id = ?1",
|
||
params![id],
|
||
|row| {
|
||
Ok(ProjectTemplate {
|
||
id: row.get(0)?,
|
||
name: row.get(1)?,
|
||
description: row.get(2)?,
|
||
platform: row.get(3)?,
|
||
tech_stack: row.get(4)?,
|
||
init_commands: row.get(5)?,
|
||
is_builtin: row.get::<_, i64>(6)? == 1,
|
||
created_at: row.get(7)?,
|
||
files: vec![],
|
||
variants: row.get(8)?,
|
||
})
|
||
},
|
||
)
|
||
.map_err(|e| format!("模板不存在: {e}"))?;
|
||
|
||
let mut file_stmt = conn
|
||
.prepare(
|
||
"SELECT id, template_id, path, content
|
||
FROM project_template_files WHERE template_id = ?1 ORDER BY path",
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
tmpl.files = file_stmt
|
||
.query_map(params![id], |row| {
|
||
Ok(TemplateFile {
|
||
id: row.get(0)?,
|
||
template_id: row.get(1)?,
|
||
path: row.get(2)?,
|
||
content: row.get(3)?,
|
||
})
|
||
})
|
||
.map_err(|e| e.to_string())?
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
Ok(tmpl)
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn save_template(input: SaveTemplateInput) -> Result<String, String> {
|
||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||
let id = input.id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||
|
||
conn.execute(
|
||
"INSERT INTO project_templates (id, name, description, platform, tech_stack, init_commands, is_builtin)
|
||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)
|
||
ON CONFLICT(id) DO UPDATE SET
|
||
name = excluded.name,
|
||
description = excluded.description,
|
||
platform = excluded.platform,
|
||
tech_stack = excluded.tech_stack,
|
||
init_commands = excluded.init_commands",
|
||
params![id, input.name, input.description, input.platform, input.tech_stack, input.init_commands],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
// 全量替换文件列表
|
||
conn.execute(
|
||
"DELETE FROM project_template_files WHERE template_id = ?1",
|
||
params![id],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
for file in &input.files {
|
||
let file_id = Uuid::new_v4().to_string();
|
||
conn.execute(
|
||
"INSERT INTO project_template_files (id, template_id, path, content) VALUES (?1, ?2, ?3, ?4)",
|
||
params![file_id, id, file.path, file.content],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
}
|
||
|
||
Ok(id)
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn delete_template(id: String) -> Result<(), String> {
|
||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||
conn.execute(
|
||
"DELETE FROM project_templates WHERE id = ?1 AND is_builtin = 0",
|
||
params![id],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
Ok(())
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn create_project_from_template(
|
||
app: tauri::AppHandle,
|
||
template_id: String,
|
||
parent_path: String,
|
||
project_name: String,
|
||
platform: String,
|
||
space_id: Option<String>,
|
||
variant_init_commands: Option<String>,
|
||
) -> Result<CreateProjectResult, String> {
|
||
use tauri::Emitter;
|
||
|
||
let mut tmpl = get_template(template_id)?;
|
||
// 如果用户选择了 variant,用其 initCommands 覆盖模板默认值
|
||
if let Some(v) = variant_init_commands {
|
||
tmpl.init_commands = Some(v);
|
||
}
|
||
let mut logs: Vec<String> = Vec::new();
|
||
|
||
// 辅助:同时 emit 事件 + 记录到 logs
|
||
macro_rules! log_push {
|
||
($msg:expr) => {{
|
||
let m: String = $msg;
|
||
let _ = app.emit("template:progress", &m);
|
||
logs.push(m);
|
||
}};
|
||
}
|
||
|
||
// ── 1. 计算项目路径 ───────────────────────────────────────────────────────
|
||
let win_parent = crate::commands::publish::wsl_to_win(&parent_path);
|
||
let project_win_path = format!(
|
||
"{}\\{}",
|
||
win_parent.trim_end_matches('\\'),
|
||
project_name
|
||
);
|
||
let project_root = Path::new(&project_win_path);
|
||
|
||
if project_root.exists() {
|
||
return Err(format!("目录已存在: {}", project_win_path));
|
||
}
|
||
|
||
// ── 2. 创建根目录 ─────────────────────────────────────────────────────────
|
||
let _ = app.emit("template:progress", "__step:1");
|
||
std::fs::create_dir_all(project_root)
|
||
.map_err(|e| format!("创建目录失败: {e}"))?;
|
||
log_push!(format!("📁 已创建目录: {}", project_win_path));
|
||
|
||
// ── 3. 写入模板文件 ───────────────────────────────────────────────────────
|
||
for file in &tmpl.files {
|
||
if file.path.trim().is_empty() {
|
||
continue;
|
||
}
|
||
let content = apply_vars(&file.content, &project_name);
|
||
let file_path = project_root.join(&file.path);
|
||
if let Some(parent) = file_path.parent() {
|
||
std::fs::create_dir_all(parent)
|
||
.map_err(|e| format!("创建子目录失败 {}: {e}", file.path))?;
|
||
}
|
||
std::fs::write(&file_path, content)
|
||
.map_err(|e| format!("写入 {} 失败: {e}", file.path))?;
|
||
log_push!(format!("📄 {}", file.path));
|
||
}
|
||
|
||
// ── 4. 执行初始化命令 ─────────────────────────────────────────────────────
|
||
let _ = app.emit("template:progress", "__step:2");
|
||
|
||
if let Some(cmds) = &tmpl.init_commands {
|
||
let linux_project_path = if platform == "wsl" {
|
||
crate::commands::publish::extract_linux_path(&parent_path, &win_parent)
|
||
.map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
for line in cmds.lines() {
|
||
let cmd = line.trim();
|
||
if cmd.is_empty() || cmd.starts_with('#') {
|
||
continue;
|
||
}
|
||
|
||
let _ = app.emit("template:progress", &format!("__cmd:{}", cmd));
|
||
log_push!(format!("⚙ 执行: {}", cmd));
|
||
|
||
// 用 spawn 流式读 stdout,实时推送每行输出
|
||
let spawn_result = if platform == "wsl" {
|
||
let lp = linux_project_path.as_deref().unwrap_or(&project_win_path);
|
||
crate::silent_cmd("wsl")
|
||
.args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)])
|
||
.stdout(Stdio::piped())
|
||
.stderr(Stdio::piped())
|
||
.spawn()
|
||
} else {
|
||
crate::silent_cmd("cmd")
|
||
.args(["/c", &format!("chcp 65001 > nul && {}", cmd)])
|
||
.current_dir(&project_win_path)
|
||
.stdout(Stdio::piped())
|
||
.stderr(Stdio::piped())
|
||
.spawn()
|
||
};
|
||
|
||
match spawn_result {
|
||
Ok(mut child) => {
|
||
// 流式读 stdout,每行立即 emit
|
||
if let Some(stdout) = child.stdout.take() {
|
||
for line in BufReader::new(stdout).lines() {
|
||
if let Ok(line) = line {
|
||
let line = line.trim().to_string();
|
||
if !line.is_empty() {
|
||
log_push!(line);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 等进程结束,收集 stderr
|
||
let output = child.wait_with_output().map_err(|e| e.to_string())?;
|
||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||
if !stderr.is_empty() {
|
||
log_push!(format!("⚠ {}", stderr));
|
||
}
|
||
if output.status.success() {
|
||
log_push!(" ✓".to_string());
|
||
} else {
|
||
log_push!(format!(" ✗ 退出码: {}", output.status));
|
||
}
|
||
}
|
||
Err(e) => {
|
||
log_push!(format!(" ✗ 启动失败: {e}"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 5. 注册到数据库 ───────────────────────────────────────────────────────
|
||
let _ = app.emit("template:progress", "__step:3");
|
||
|
||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||
let profile_id = Uuid::new_v4().to_string();
|
||
let workspace_id = Uuid::new_v4().to_string();
|
||
|
||
conn.execute(
|
||
"INSERT INTO project_profiles (id, name, description, type, status, priority, tech_stack)
|
||
VALUES (?1, ?2, ?3, 'app', 'active', 'mid', ?4)",
|
||
params![profile_id, project_name, tmpl.description, tmpl.tech_stack],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
let is_wsl = platform == "wsl";
|
||
let (wsl_path, win_path_col): (Option<&str>, Option<&str>) = if is_wsl {
|
||
(Some(&project_win_path), None)
|
||
} else {
|
||
(None, Some(&project_win_path))
|
||
};
|
||
let stored_platform = if is_wsl { "wsl" } else { "windows" };
|
||
|
||
conn.execute(
|
||
"INSERT INTO project_workspaces (id, profile_id, space_id, wsl_path, win_path, platform)
|
||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||
params![workspace_id, profile_id, space_id, wsl_path, win_path_col, stored_platform],
|
||
)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
log_push!(format!("✅ 项目已注册,ID: {}", workspace_id));
|
||
|
||
Ok(CreateProjectResult {
|
||
project_id: workspace_id,
|
||
logs,
|
||
})
|
||
}
|
||
|
||
// ── 变量替换 ──────────────────────────────────────────────────────────────────
|
||
|
||
fn apply_vars(content: &str, project_name: &str) -> String {
|
||
let kebab = project_name
|
||
.to_lowercase()
|
||
.replace(' ', "-")
|
||
.replace('_', "-");
|
||
let snake = project_name
|
||
.to_lowercase()
|
||
.replace(' ', "_")
|
||
.replace('-', "_");
|
||
content
|
||
.replace("{{project_name}}", project_name)
|
||
.replace("{{project_name_kebab}}", &kebab)
|
||
.replace("{{project_name_snake}}", &snake)
|
||
}
|
||
|
||
// ── 内置模板种子数据 ──────────────────────────────────────────────────────────
|
||
|
||
pub fn seed_builtin_templates() {
|
||
let conn = match db::pool().get() {
|
||
Ok(c) => c,
|
||
Err(_) => return,
|
||
};
|
||
|
||
// 不做全局跳过,直接用 INSERT OR IGNORE 保证幂等
|
||
// 这样新增内置模板时,已有用户也能自动获得
|
||
|
||
struct Tpl {
|
||
id: &'static str,
|
||
name: &'static str,
|
||
description: &'static str,
|
||
platform: &'static str,
|
||
tech_stack: &'static str,
|
||
init_commands: &'static str,
|
||
files: Vec<(&'static str, &'static str)>,
|
||
/// JSON 字符串,存在时前端显示下拉选择初始化方式
|
||
variants: Option<&'static str>,
|
||
}
|
||
|
||
let templates = vec![
|
||
Tpl {
|
||
id: "builtin-pnpm-monorepo",
|
||
name: "pnpm Monorepo",
|
||
description: "多包 monorepo,含 apps/ 与 packages/ 目录",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"scripts": {
|
||
"dev": "pnpm -r --filter './apps/**' dev",
|
||
"build": "pnpm -r --filter './apps/**' build",
|
||
"lint": "pnpm -r lint"
|
||
},
|
||
"engines": {
|
||
"node": ">=18",
|
||
"pnpm": ">=8"
|
||
}
|
||
}
|
||
"#),
|
||
("pnpm-workspace.yaml", r#"packages:
|
||
- 'apps/*'
|
||
- 'packages/*'
|
||
"#),
|
||
(".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),
|
||
("apps/.gitkeep", ""),
|
||
("packages/.gitkeep", ""),
|
||
(".gitignore", "node_modules/\ndist/\n.next/\n*.log\n.env\n.env.*\n!.env.example\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-pnpm-app",
|
||
name: "pnpm App",
|
||
description: "单包 Node.js 应用",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"start": "node src/index.js",
|
||
"dev": "node --watch src/index.js"
|
||
}
|
||
}
|
||
"#),
|
||
("src/index.js", "console.log('Hello from {{project_name}}!')\n"),
|
||
(".gitignore", "node_modules/\ndist/\n*.log\n.env\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-rust",
|
||
name: "Rust",
|
||
description: "Rust 二进制项目",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["CLI","Desktop"],"语言":["Rust"]}"#,
|
||
init_commands: "cargo build",
|
||
variants: None,
|
||
files: vec![
|
||
("Cargo.toml", r#"[package]
|
||
name = "{{project_name_kebab}}"
|
||
version = "0.1.0"
|
||
edition = "2021"
|
||
|
||
[dependencies]
|
||
"#),
|
||
("src/main.rs", "fn main() {\n println!(\"Hello from {{project_name}}!\");\n}\n"),
|
||
(".gitignore", "target/\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-empty",
|
||
name: "空文件夹",
|
||
description: "只创建目录,不添加任何文件",
|
||
platform: "both",
|
||
tech_stack: "",
|
||
init_commands: "",
|
||
variants: None,
|
||
files: vec![],
|
||
},
|
||
Tpl {
|
||
id: "builtin-tauri-react",
|
||
name: "Tauri + React",
|
||
description: "桌面应用,Tauri v2 + React 19 + TypeScript + Vite",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","Node","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "tauri dev",
|
||
"build": "tauri build",
|
||
"preview": "vite preview"
|
||
},
|
||
"dependencies": {
|
||
"@tauri-apps/api": "^2",
|
||
"react": "^19",
|
||
"react-dom": "^19"
|
||
},
|
||
"devDependencies": {
|
||
"@tauri-apps/cli": "^2",
|
||
"@types/react": "^19",
|
||
"@types/react-dom": "^19",
|
||
"@vitejs/plugin-react": "^4",
|
||
"typescript": "^5",
|
||
"vite": "^6"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig(async () => ({
|
||
plugins: [react()],
|
||
clearScreen: false,
|
||
server: {
|
||
port: 1420,
|
||
strictPort: true,
|
||
watch: {
|
||
ignored: ["**/src-tauri/**"],
|
||
},
|
||
},
|
||
}));
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { useState } from "react";
|
||
|
||
function App() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
return (
|
||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||
<h1>{{project_name}}</h1>
|
||
<p>Tauri v2 + React + TypeScript</p>
|
||
<button onClick={() => setCount((c) => c + 1)}>count: {count}</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|
||
"#),
|
||
("src-tauri/tauri.conf.json", r#"{
|
||
"$schema": "https://schema.tauri.app/config/2",
|
||
"productName": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"identifier": "com.example.{{project_name_snake}}",
|
||
"build": {
|
||
"beforeDevCommand": "pnpm vite",
|
||
"devUrl": "http://localhost:1420",
|
||
"beforeBuildCommand": "pnpm vite build",
|
||
"frontendDist": "../dist"
|
||
},
|
||
"app": {
|
||
"windows": [
|
||
{
|
||
"title": "{{project_name}}",
|
||
"width": 1200,
|
||
"height": 800
|
||
}
|
||
],
|
||
"security": {
|
||
"csp": null
|
||
}
|
||
},
|
||
"bundle": {
|
||
"active": true,
|
||
"targets": "all",
|
||
"icon": []
|
||
}
|
||
}
|
||
"#),
|
||
("src-tauri/Cargo.toml", r#"[package]
|
||
name = "{{project_name_snake}}"
|
||
version = "0.1.0"
|
||
description = "A Tauri App"
|
||
authors = []
|
||
edition = "2021"
|
||
|
||
[lib]
|
||
name = "{{project_name_snake}}_lib"
|
||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||
|
||
[build-dependencies]
|
||
tauri-build = { version = "2", features = [] }
|
||
|
||
[dependencies]
|
||
tauri = { version = "2", features = [] }
|
||
tauri-plugin-opener = "2"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
"#),
|
||
("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"),
|
||
("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||
|
||
fn main() {
|
||
{{project_name_snake}}_lib::run()
|
||
}
|
||
"#),
|
||
("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
tauri::Builder::default()
|
||
.plugin(tauri_plugin_opener::init())
|
||
.invoke_handler(tauri::generate_handler![])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|
||
"#),
|
||
("src-tauri/capabilities/default.json", r#"{
|
||
"$schema": "../gen/schemas/desktop-schema.json",
|
||
"identifier": "default",
|
||
"description": "Capability for the main window",
|
||
"windows": ["main"],
|
||
"permissions": [
|
||
"core:default",
|
||
"opener:default"
|
||
]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-expo",
|
||
name: "Expo",
|
||
description: "React Native 移动端 App(Android / iOS)。TypeScript + Expo SDK,适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Android","iOS"],"语言":["TypeScript"],"框架/工具":["React Native","Expo"]}"#,
|
||
init_commands: "pnpm create expo-app@latest . --template blank-typescript",
|
||
variants: Some(r#"[
|
||
{"label":"blank-typescript(推荐)","description":"最小化 TypeScript 起步,适合游戏类、工具类、独立 App","initCommands":"pnpm create expo-app@latest . --template blank-typescript"},
|
||
{"label":"tabs(底部标签导航)","description":"内置底部 Tab 栏 + Expo Router,适合多页面内容型 App","initCommands":"pnpm create expo-app@latest . --template tabs"},
|
||
{"label":"bare(裸原生配置)","description":"完整原生 Expo 配置,可深度定制原生模块,适合进阶项目","initCommands":"pnpm create expo-app@latest . --template bare-minimum"},
|
||
{"label":"默认(向导模式)","description":"运行官方交互式向导,在终端中选择模板类型","initCommands":"pnpm create expo-app@latest ."}
|
||
]"#),
|
||
files: vec![],
|
||
},
|
||
Tpl {
|
||
id: "builtin-pnpm-expo-monorepo",
|
||
name: "pnpm Monorepo + Expo",
|
||
description: "pnpm 多包 Monorepo:apps/ 内含 Expo Router (React Native) 应用,packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析(含 Windows 兼容修复),适合多 App 扩展场景。",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React Native","Expo","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
// ── monorepo 根 ──────────────────────────────────────────────
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"scripts": {
|
||
"start": "pnpm --filter app start",
|
||
"android": "pnpm --filter app android",
|
||
"ios": "pnpm --filter app ios",
|
||
"lint": "pnpm -r lint"
|
||
},
|
||
"engines": {
|
||
"node": ">=18",
|
||
"pnpm": ">=8"
|
||
}
|
||
}
|
||
"#),
|
||
("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - 'packages/*'\n"),
|
||
("tsconfig.base.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"lib": ["ES2020"],
|
||
"module": "ESNext",
|
||
"moduleResolution": "bundler",
|
||
"resolveJsonModule": true,
|
||
"allowSyntheticDefaultImports": true,
|
||
"esModuleInterop": true,
|
||
"strict": true,
|
||
"skipLibCheck": true,
|
||
"declaration": true,
|
||
"declarationMap": true,
|
||
"sourceMap": true
|
||
},
|
||
"exclude": ["node_modules", "dist", "build"]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"),
|
||
(".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),
|
||
|
||
// ── packages/types ───────────────────────────────────────────
|
||
("packages/types/package.json", r#"{
|
||
"name": "@repo/types",
|
||
"version": "0.0.1",
|
||
"private": true,
|
||
"type": "module",
|
||
"exports": {
|
||
".": {
|
||
"types": "./src/index.ts",
|
||
"import": "./src/index.ts"
|
||
}
|
||
},
|
||
"files": ["src"],
|
||
"scripts": {
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"devDependencies": {
|
||
"typescript": "^5.4.5"
|
||
}
|
||
}
|
||
"#),
|
||
("packages/types/src/index.ts", "// 在此导出跨 App 共享的类型\nexport type {}\n"),
|
||
|
||
// ── apps/app ─────────────────────────────────────────────────
|
||
("apps/app/package.json", r#"{
|
||
"name": "app",
|
||
"version": "1.0.0",
|
||
"private": true,
|
||
"main": "expo-router/entry",
|
||
"scripts": {
|
||
"start": "expo start",
|
||
"android": "expo run:android",
|
||
"ios": "expo run:ios",
|
||
"lint": "eslint . --ext .ts,.tsx"
|
||
},
|
||
"dependencies": {
|
||
"@repo/types": "workspace:*",
|
||
"expo": "~52.0.0",
|
||
"expo-router": "~4.0.0",
|
||
"expo-status-bar": "~2.0.0",
|
||
"react": "18.3.1",
|
||
"react-native": "0.76.3",
|
||
"react-native-gesture-handler": "~2.20.0",
|
||
"react-native-reanimated": "~3.16.1",
|
||
"react-native-safe-area-context": "4.12.0",
|
||
"react-native-screens": "~4.1.0"
|
||
},
|
||
"devDependencies": {
|
||
"@babel/core": "^7.25.2",
|
||
"@types/react": "~18.3.12",
|
||
"typescript": "^5.4.5"
|
||
}
|
||
}
|
||
"#),
|
||
("apps/app/app.json", r##"{
|
||
"expo": {
|
||
"name": "{{project_name}}",
|
||
"slug": "{{project_name_kebab}}",
|
||
"version": "1.0.0",
|
||
"orientation": "default",
|
||
"userInterfaceStyle": "light",
|
||
"scheme": "{{project_name_kebab}}",
|
||
"ios": {
|
||
"supportsTablet": true,
|
||
"bundleIdentifier": "com.example.{{project_name_snake}}"
|
||
},
|
||
"android": {
|
||
"adaptiveIcon": {
|
||
"backgroundColor": "#ffffff"
|
||
},
|
||
"package": "com.example.{{project_name_snake}}"
|
||
},
|
||
"plugins": ["expo-router"],
|
||
"experiments": {
|
||
"typedRoutes": true
|
||
}
|
||
}
|
||
}
|
||
"##),
|
||
("apps/app/tsconfig.json", r#"{
|
||
"extends": "expo/tsconfig.base",
|
||
"compilerOptions": {
|
||
"strict": true,
|
||
"paths": {
|
||
"@repo/types": ["../../packages/types/src/index.ts"]
|
||
}
|
||
},
|
||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||
}
|
||
"#),
|
||
("apps/app/babel.config.js", r#"module.exports = function (api) {
|
||
api.cache(true)
|
||
return {
|
||
presets: ['babel-preset-expo'],
|
||
}
|
||
}
|
||
"#),
|
||
// Metro 配置:monorepo 路径解析 + Windows release 构建兼容修复
|
||
("apps/app/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config')
|
||
const path = require('path')
|
||
|
||
const projectRoot = __dirname
|
||
const monorepoRoot = path.resolve(projectRoot, '../..')
|
||
|
||
const config = getDefaultConfig(projectRoot)
|
||
|
||
// Windows release 构建兼容:强制 unstable_serverRoot 为 projectRoot,
|
||
// 避免 Metro 与 RN Gradle 插件的路径基准不一致。
|
||
config.server = {
|
||
...config.server,
|
||
unstable_serverRoot: projectRoot,
|
||
}
|
||
|
||
// 让 Metro 监听 monorepo 根目录,确保根 node_modules 可被解析
|
||
config.watchFolders = [monorepoRoot]
|
||
|
||
// 模块解析:优先查 app 自己的 node_modules,再查 monorepo 根
|
||
config.resolver.nodeModulesPaths = [
|
||
path.resolve(projectRoot, 'node_modules'),
|
||
path.resolve(monorepoRoot, 'node_modules'),
|
||
]
|
||
|
||
module.exports = config
|
||
"#),
|
||
("apps/app/expo-env.d.ts", "/// <reference types=\"expo/types\" />\n\n// NOTE: This file should not be edited and should be in your git ignore\n"),
|
||
("apps/app/app/_layout.tsx", r#"import { Stack } from 'expo-router'
|
||
import { StatusBar } from 'expo-status-bar'
|
||
|
||
export default function RootLayout() {
|
||
return (
|
||
<>
|
||
<StatusBar style="auto" />
|
||
<Stack>
|
||
<Stack.Screen name="index" options={{ title: '{{project_name}}' }} />
|
||
</Stack>
|
||
</>
|
||
)
|
||
}
|
||
"#),
|
||
("apps/app/app/index.tsx", r#"import { View, Text, StyleSheet } from 'react-native'
|
||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||
|
||
export default function HomeScreen() {
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<View style={styles.content}>
|
||
<Text style={styles.title}>{{project_name}}</Text>
|
||
<Text style={styles.subtitle}>Expo Router + pnpm Monorepo</Text>
|
||
</View>
|
||
</SafeAreaView>
|
||
)
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: { flex: 1, backgroundColor: '#fff' },
|
||
content: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8 },
|
||
title: { fontSize: 24, fontWeight: 'bold' },
|
||
subtitle: { fontSize: 14, color: '#666' },
|
||
})
|
||
"#),
|
||
],
|
||
},
|
||
// ── P1: React + Vite SPA ─────────────────────────────────────────────
|
||
Tpl {
|
||
id: "builtin-vite-react",
|
||
name: "React + Vite SPA",
|
||
description: "纯前端 Web 应用,React 19 + TypeScript + TailwindCSS v4 + React Router v7",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite","TailwindCSS"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "tsc -b && vite build",
|
||
"preview": "vite preview"
|
||
},
|
||
"dependencies": {
|
||
"react": "^19",
|
||
"react-dom": "^19",
|
||
"react-router": "^7"
|
||
},
|
||
"devDependencies": {
|
||
"@tailwindcss/vite": "^4",
|
||
"@types/react": "^19",
|
||
"@types/react-dom": "^19",
|
||
"@vitejs/plugin-react": "^4",
|
||
"tailwindcss": "^4",
|
||
"typescript": "^5",
|
||
"vite": "^6"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
import tailwindcss from "@tailwindcss/vite";
|
||
|
||
export default defineConfig({
|
||
plugins: [react(), tailwindcss()],
|
||
});
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import { BrowserRouter } from "react-router";
|
||
import App from "./App";
|
||
import "./index.css";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<BrowserRouter>
|
||
<App />
|
||
</BrowserRouter>
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { Routes, Route } from "react-router";
|
||
import Home from "./pages/Home";
|
||
|
||
export default function App() {
|
||
return (
|
||
<Routes>
|
||
<Route path="/" element={<Home />} />
|
||
</Routes>
|
||
);
|
||
}
|
||
"#),
|
||
("src/index.css", "@import \"tailwindcss\";\n"),
|
||
("src/pages/Home.tsx", r#"export default function Home() {
|
||
return (
|
||
<main className="min-h-screen flex items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
|
||
<p className="mt-2 text-gray-500">React + Vite + TailwindCSS</p>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\n"),
|
||
],
|
||
},
|
||
// ── P1: Tauri + React + TailwindCSS ──────────────────────────────────
|
||
Tpl {
|
||
id: "builtin-tauri-react-tailwind",
|
||
name: "Tauri + React + Tailwind",
|
||
description: "桌面应用,Tauri v2 + React 19 + TypeScript + TailwindCSS v4",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","TailwindCSS","Node","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"version": "0.1.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "tauri dev",
|
||
"build": "tauri build",
|
||
"preview": "vite preview"
|
||
},
|
||
"dependencies": {
|
||
"@tauri-apps/api": "^2",
|
||
"react": "^19",
|
||
"react-dom": "^19"
|
||
},
|
||
"devDependencies": {
|
||
"@tailwindcss/vite": "^4",
|
||
"@tauri-apps/cli": "^2",
|
||
"@types/react": "^19",
|
||
"@types/react-dom": "^19",
|
||
"@vitejs/plugin-react": "^4",
|
||
"tailwindcss": "^4",
|
||
"typescript": "^5",
|
||
"vite": "^6"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
import tailwindcss from "@tailwindcss/vite";
|
||
|
||
export default defineConfig(async () => ({
|
||
plugins: [react(), tailwindcss()],
|
||
clearScreen: false,
|
||
server: {
|
||
port: 1420,
|
||
strictPort: true,
|
||
watch: {
|
||
ignored: ["**/src-tauri/**"],
|
||
},
|
||
},
|
||
}));
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
import "./index.css";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/index.css", "@import \"tailwindcss\";\n"),
|
||
("src/App.tsx", r#"import { useState } from "react";
|
||
|
||
function App() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
return (
|
||
<main className="min-h-screen flex items-center justify-center bg-gray-50">
|
||
<div className="text-center space-y-4">
|
||
<h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
|
||
<p className="text-gray-500">Tauri v2 + React + TailwindCSS</p>
|
||
<button
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||
onClick={() => setCount((c) => c + 1)}
|
||
>
|
||
count: {count}
|
||
</button>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
export default App;
|
||
"#),
|
||
("src-tauri/tauri.conf.json", r#"{
|
||
"$schema": "https://schema.tauri.app/config/2",
|
||
"productName": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"identifier": "com.example.{{project_name_snake}}",
|
||
"build": {
|
||
"beforeDevCommand": "pnpm vite",
|
||
"devUrl": "http://localhost:1420",
|
||
"beforeBuildCommand": "pnpm vite build",
|
||
"frontendDist": "../dist"
|
||
},
|
||
"app": {
|
||
"windows": [
|
||
{
|
||
"title": "{{project_name}}",
|
||
"width": 1200,
|
||
"height": 800
|
||
}
|
||
],
|
||
"security": {
|
||
"csp": null
|
||
}
|
||
},
|
||
"bundle": {
|
||
"active": true,
|
||
"targets": "all",
|
||
"icon": []
|
||
}
|
||
}
|
||
"#),
|
||
("src-tauri/Cargo.toml", r#"[package]
|
||
name = "{{project_name_snake}}"
|
||
version = "0.1.0"
|
||
description = "A Tauri App"
|
||
authors = []
|
||
edition = "2021"
|
||
|
||
[lib]
|
||
name = "{{project_name_snake}}_lib"
|
||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||
|
||
[build-dependencies]
|
||
tauri-build = { version = "2", features = [] }
|
||
|
||
[dependencies]
|
||
tauri = { version = "2", features = [] }
|
||
tauri-plugin-opener = "2"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
"#),
|
||
("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"),
|
||
("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||
|
||
fn main() {
|
||
{{project_name_snake}}_lib::run()
|
||
}
|
||
"#),
|
||
("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
tauri::Builder::default()
|
||
.plugin(tauri_plugin_opener::init())
|
||
.invoke_handler(tauri::generate_handler![])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|
||
"#),
|
||
("src-tauri/capabilities/default.json", r#"{
|
||
"$schema": "../gen/schemas/desktop-schema.json",
|
||
"identifier": "default",
|
||
"description": "Capability for the main window",
|
||
"windows": ["main"],
|
||
"permissions": [
|
||
"core:default",
|
||
"opener:default"
|
||
]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
|
||
],
|
||
},
|
||
// ── P3: Web + Mobile 共享 Monorepo ────────────────────────────────────
|
||
Tpl {
|
||
id: "builtin-web-mobile-monorepo",
|
||
name: "Web + Mobile Monorepo",
|
||
description: "跨平台代码复用:packages/core 共享业务逻辑,apps/web(Vite + Tailwind)+ apps/mobile(Expo + NativeWind)",
|
||
platform: "both",
|
||
tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React","React Native","Expo","Vite","TailwindCSS","NativeWind","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
// ── 根目录 ──────────────────────────────────────────────────────
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"private": true,
|
||
"scripts": {
|
||
"dev": "pnpm --filter web dev",
|
||
"build:web": "pnpm --filter web build",
|
||
"start:mobile": "pnpm --filter mobile start",
|
||
"android": "pnpm --filter mobile android",
|
||
"ios": "pnpm --filter mobile ios",
|
||
"typecheck": "pnpm -r typecheck"
|
||
},
|
||
"engines": {
|
||
"node": ">=18",
|
||
"pnpm": ">=8"
|
||
}
|
||
}
|
||
"#),
|
||
("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - 'packages/*'\n"),
|
||
("tsconfig.base.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"lib": ["ES2020"],
|
||
"module": "ESNext",
|
||
"moduleResolution": "bundler",
|
||
"resolveJsonModule": true,
|
||
"allowSyntheticDefaultImports": true,
|
||
"esModuleInterop": true,
|
||
"strict": true,
|
||
"skipLibCheck": true,
|
||
"declaration": true,
|
||
"declarationMap": true,
|
||
"sourceMap": true
|
||
},
|
||
"exclude": ["node_modules", "dist", "build"]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\nbuild/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"),
|
||
(".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),
|
||
|
||
// ── packages/types ──────────────────────────────────────────────
|
||
("packages/types/package.json", r#"{
|
||
"name": "@repo/types",
|
||
"version": "0.0.1",
|
||
"private": true,
|
||
"type": "module",
|
||
"exports": {
|
||
".": {
|
||
"types": "./src/index.ts",
|
||
"import": "./src/index.ts"
|
||
}
|
||
},
|
||
"scripts": {
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"devDependencies": {
|
||
"typescript": "^5"
|
||
}
|
||
}
|
||
"#),
|
||
("packages/types/tsconfig.json", r#"{
|
||
"extends": "../../tsconfig.base.json",
|
||
"compilerOptions": {
|
||
"outDir": "dist",
|
||
"rootDir": "src"
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("packages/types/src/index.ts", r#"// 在此定义跨平台共享的类型
|
||
export interface User {
|
||
id: string
|
||
name: string
|
||
email: string
|
||
}
|
||
|
||
export interface ApiResponse<T> {
|
||
data: T
|
||
error?: string
|
||
}
|
||
"#),
|
||
|
||
// ── packages/core ───────────────────────────────────────────────
|
||
("packages/core/package.json", r#"{
|
||
"name": "@repo/core",
|
||
"version": "0.0.1",
|
||
"private": true,
|
||
"type": "module",
|
||
"exports": {
|
||
".": {
|
||
"types": "./src/index.ts",
|
||
"import": "./src/index.ts"
|
||
}
|
||
},
|
||
"scripts": {
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@repo/types": "workspace:*"
|
||
},
|
||
"devDependencies": {
|
||
"typescript": "^5"
|
||
}
|
||
}
|
||
"#),
|
||
("packages/core/tsconfig.json", r#"{
|
||
"extends": "../../tsconfig.base.json",
|
||
"compilerOptions": {
|
||
"outDir": "dist",
|
||
"rootDir": "src"
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("packages/core/src/index.ts", r#"// 在此导出跨平台共享的业务逻辑(纯 TS,不依赖任何平台 API)
|
||
export * from "./api"
|
||
"#),
|
||
("packages/core/src/api.ts", r#"import type { ApiResponse, User } from "@repo/types"
|
||
|
||
const BASE_URL = ""
|
||
|
||
export async function getUser(id: string): Promise<ApiResponse<User>> {
|
||
const res = await fetch(`${BASE_URL}/api/users/${id}`)
|
||
if (!res.ok) return { data: null as unknown as User, error: res.statusText }
|
||
const data = await res.json()
|
||
return { data }
|
||
}
|
||
"#),
|
||
|
||
// ── apps/web ────────────────────────────────────────────────────
|
||
("apps/web/package.json", r#"{
|
||
"name": "web",
|
||
"version": "0.0.1",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "tsc -b && vite build",
|
||
"preview": "vite preview",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@repo/core": "workspace:*",
|
||
"@repo/types": "workspace:*",
|
||
"react": "^19",
|
||
"react-dom": "^19",
|
||
"react-router": "^7"
|
||
},
|
||
"devDependencies": {
|
||
"@tailwindcss/vite": "^4",
|
||
"@types/react": "^19",
|
||
"@types/react-dom": "^19",
|
||
"@vitejs/plugin-react": "^4",
|
||
"tailwindcss": "^4",
|
||
"typescript": "^5",
|
||
"vite": "^6"
|
||
}
|
||
}
|
||
"#),
|
||
("apps/web/tsconfig.json", r#"{
|
||
"extends": "../../tsconfig.base.json",
|
||
"compilerOptions": {
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"paths": {
|
||
"@repo/core": ["../../packages/core/src/index.ts"],
|
||
"@repo/types": ["../../packages/types/src/index.ts"]
|
||
}
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("apps/web/vite.config.ts", r#"import { defineConfig } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
import tailwindcss from "@tailwindcss/vite";
|
||
|
||
export default defineConfig({
|
||
plugins: [react(), tailwindcss()],
|
||
});
|
||
"#),
|
||
("apps/web/index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
("apps/web/src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import { BrowserRouter } from "react-router";
|
||
import App from "./App";
|
||
import "./index.css";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<BrowserRouter>
|
||
<App />
|
||
</BrowserRouter>
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("apps/web/src/index.css", "@import \"tailwindcss\";\n"),
|
||
("apps/web/src/App.tsx", r#"import { Routes, Route } from "react-router";
|
||
import Home from "./pages/Home";
|
||
|
||
export default function App() {
|
||
return (
|
||
<Routes>
|
||
<Route path="/" element={<Home />} />
|
||
</Routes>
|
||
);
|
||
}
|
||
"#),
|
||
("apps/web/src/pages/Home.tsx", r#"export default function Home() {
|
||
return (
|
||
<main className="min-h-screen flex items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
|
||
<p className="mt-2 text-gray-500">Web + Mobile Monorepo</p>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
"#),
|
||
|
||
// ── apps/mobile ─────────────────────────────────────────────────
|
||
("apps/mobile/package.json", r#"{
|
||
"name": "mobile",
|
||
"version": "1.0.0",
|
||
"private": true,
|
||
"main": "expo-router/entry",
|
||
"scripts": {
|
||
"start": "expo start",
|
||
"android": "expo run:android",
|
||
"ios": "expo run:ios",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@repo/core": "workspace:*",
|
||
"@repo/types": "workspace:*",
|
||
"expo": "~52.0.0",
|
||
"expo-router": "~4.0.0",
|
||
"expo-status-bar": "~2.0.0",
|
||
"nativewind": "^4.0.1",
|
||
"react": "18.3.1",
|
||
"react-native": "0.76.3",
|
||
"react-native-safe-area-context": "4.12.0",
|
||
"react-native-screens": "~4.1.0",
|
||
"tailwindcss": "^3.4.0"
|
||
},
|
||
"devDependencies": {
|
||
"@babel/core": "^7.25.2",
|
||
"@types/react": "~18.3.12",
|
||
"typescript": "^5.4.5"
|
||
}
|
||
}
|
||
"#),
|
||
("apps/mobile/app.json", r##"{
|
||
"expo": {
|
||
"name": "{{project_name}}",
|
||
"slug": "{{project_name_kebab}}",
|
||
"version": "1.0.0",
|
||
"scheme": "{{project_name_kebab}}",
|
||
"userInterfaceStyle": "automatic",
|
||
"ios": {
|
||
"supportsTablet": true,
|
||
"bundleIdentifier": "com.example.{{project_name_snake}}"
|
||
},
|
||
"android": {
|
||
"adaptiveIcon": {
|
||
"backgroundColor": "#ffffff"
|
||
},
|
||
"package": "com.example.{{project_name_snake}}"
|
||
},
|
||
"plugins": ["expo-router"],
|
||
"experiments": {
|
||
"typedRoutes": true
|
||
}
|
||
}
|
||
}
|
||
"##),
|
||
("apps/mobile/tsconfig.json", r#"{
|
||
"extends": "expo/tsconfig.base",
|
||
"compilerOptions": {
|
||
"strict": true,
|
||
"paths": {
|
||
"@repo/core": ["../../packages/core/src/index.ts"],
|
||
"@repo/types": ["../../packages/types/src/index.ts"]
|
||
}
|
||
},
|
||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||
}
|
||
"#),
|
||
("apps/mobile/tailwind.config.js", r#"/** @type {import('tailwindcss').Config} */
|
||
module.exports = {
|
||
content: ["./app/**/*.{ts,tsx}"],
|
||
presets: [require("nativewind/preset")],
|
||
theme: {
|
||
extend: {},
|
||
},
|
||
plugins: [],
|
||
}
|
||
"#),
|
||
("apps/mobile/global.css", "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"),
|
||
("apps/mobile/babel.config.js", r#"module.exports = function (api) {
|
||
api.cache(true)
|
||
return {
|
||
presets: ['babel-preset-expo'],
|
||
plugins: ['nativewind/babel'],
|
||
}
|
||
}
|
||
"#),
|
||
("apps/mobile/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config')
|
||
const { withNativeWind } = require('nativewind/metro')
|
||
const path = require('path')
|
||
|
||
const projectRoot = __dirname
|
||
const monorepoRoot = path.resolve(projectRoot, '../..')
|
||
|
||
const config = getDefaultConfig(projectRoot)
|
||
|
||
config.watchFolders = [monorepoRoot]
|
||
config.resolver.nodeModulesPaths = [
|
||
path.resolve(projectRoot, 'node_modules'),
|
||
path.resolve(monorepoRoot, 'node_modules'),
|
||
]
|
||
|
||
module.exports = withNativeWind(config, { input: './global.css' })
|
||
"#),
|
||
("apps/mobile/expo-env.d.ts", "/// <reference types=\"expo/types\" />\n"),
|
||
("apps/mobile/app/_layout.tsx", r#"import '../global.css'
|
||
import { Stack } from 'expo-router'
|
||
import { StatusBar } from 'expo-status-bar'
|
||
|
||
export default function RootLayout() {
|
||
return (
|
||
<>
|
||
<StatusBar style="auto" />
|
||
<Stack>
|
||
<Stack.Screen name="index" options={{ title: '{{project_name}}' }} />
|
||
</Stack>
|
||
</>
|
||
)
|
||
}
|
||
"#),
|
||
("apps/mobile/app/index.tsx", r#"import { View, Text } from 'react-native'
|
||
|
||
export default function HomeScreen() {
|
||
return (
|
||
<View className="flex-1 items-center justify-center bg-gray-50">
|
||
<Text className="text-3xl font-bold text-gray-900">{{project_name}}</Text>
|
||
<Text className="mt-2 text-gray-500">Web + Mobile Monorepo</Text>
|
||
</View>
|
||
)
|
||
}
|
||
"#),
|
||
],
|
||
},
|
||
// ── 三端体系模板(同一架构,按需起步)────────────────────────────────────
|
||
Tpl {
|
||
id: "builtin-react-web",
|
||
name: "React Web",
|
||
description: "三端体系 Web 基础。React 18 + TypeScript + Ant Design 5 + Zustand,可随时扩展为 Capacitor Android 或 Tauri Desktop。",
|
||
platform: "both",
|
||
tech_stack: r#"{"体系":["三端"],"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Ant Design","Zustand","Vite","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"web": "vite",
|
||
"build": "vite build",
|
||
"preview": "vite preview",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"antd": "^5.21.0",
|
||
"react": "^18.3.0",
|
||
"react-dom": "^18.3.0",
|
||
"zustand": "^5.0.0"
|
||
},
|
||
"devDependencies": {
|
||
"@types/react": "^18.3.0",
|
||
"@types/react-dom": "^18.3.0",
|
||
"@vitejs/plugin-react": "^4.3.0",
|
||
"typescript": "^5.5.0",
|
||
"vite": "^5.4.0"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig(({ mode }) => {
|
||
loadEnv(mode, process.cwd(), "");
|
||
return {
|
||
plugins: [react()],
|
||
base: mode === "capacitor" ? "./" : "/",
|
||
};
|
||
});
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
(".env", "VITE_PLATFORM=web\n"),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { Button } from "antd";
|
||
import { useAppStore } from "./store";
|
||
|
||
export default function App() {
|
||
const { count, increment } = useAppStore();
|
||
return (
|
||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||
<h1>{{project_name}}</h1>
|
||
<p>React 18 + Ant Design 5 + Zustand</p>
|
||
<Button type="primary" onClick={increment}>
|
||
count: {count}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
"#),
|
||
("src/store/index.ts", r#"import { create } from "zustand";
|
||
|
||
interface AppStore {
|
||
count: number;
|
||
increment: () => void;
|
||
}
|
||
|
||
export const useAppStore = create<AppStore>((set) => ({
|
||
count: 0,
|
||
increment: () => set((s) => ({ count: s.count + 1 })),
|
||
}));
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-react-capacitor",
|
||
name: "React + Capacitor",
|
||
description: "Web + Android。React 18 + Ant Design 5 + Zustand + Capacitor 8,pnpm build:cap 打包为原生 APK。",
|
||
platform: "both",
|
||
tech_stack: r#"{"体系":["三端"],"客户端":["Web","Android"],"语言":["TypeScript"],"框架/工具":["React","Ant Design","Zustand","Capacitor","Vite","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"web": "vite",
|
||
"build": "vite build",
|
||
"build:cap": "vite build --mode capacitor && npx cap sync",
|
||
"preview": "vite preview",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@capacitor/core": "^8.0.0",
|
||
"antd": "^5.21.0",
|
||
"react": "^18.3.0",
|
||
"react-dom": "^18.3.0",
|
||
"zustand": "^5.0.0"
|
||
},
|
||
"devDependencies": {
|
||
"@capacitor/android": "^8.0.0",
|
||
"@capacitor/cli": "^8.0.0",
|
||
"@types/react": "^18.3.0",
|
||
"@types/react-dom": "^18.3.0",
|
||
"@vitejs/plugin-react": "^4.3.0",
|
||
"typescript": "^5.5.0",
|
||
"vite": "^5.4.0"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig(({ mode }) => {
|
||
loadEnv(mode, process.cwd(), "");
|
||
return {
|
||
plugins: [react()],
|
||
base: mode === "capacitor" ? "./" : "/",
|
||
};
|
||
});
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src", "capacitor.config.ts"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
(".env", "VITE_PLATFORM=web\n"),
|
||
(".env.capacitor", "VITE_PLATFORM=android\n"),
|
||
("capacitor.config.ts", r#"import type { CapacitorConfig } from "@capacitor/cli";
|
||
|
||
const config: CapacitorConfig = {
|
||
appId: "com.example.{{project_name_snake}}",
|
||
appName: "{{project_name}}",
|
||
webDir: "dist",
|
||
};
|
||
|
||
export default config;
|
||
"#),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { Button } from "antd";
|
||
import { useAppStore } from "./store";
|
||
|
||
export default function App() {
|
||
const { count, increment } = useAppStore();
|
||
return (
|
||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||
<h1>{{project_name}}</h1>
|
||
<p>React 18 + Ant Design 5 + Zustand + Capacitor</p>
|
||
<Button type="primary" onClick={increment}>
|
||
count: {count}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
"#),
|
||
("src/store/index.ts", r#"import { create } from "zustand";
|
||
|
||
interface AppStore {
|
||
count: number;
|
||
increment: () => void;
|
||
}
|
||
|
||
export const useAppStore = create<AppStore>((set) => ({
|
||
count: 0,
|
||
increment: () => set((s) => ({ count: s.count + 1 })),
|
||
}));
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nandroid/\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-react-tauri-antd",
|
||
name: "React + Tauri",
|
||
description: "Web + Desktop。React 18 + Ant Design 5 + Zustand + Tauri 2,pnpm build:desktop 打包为原生桌面应用。",
|
||
platform: "both",
|
||
tech_stack: r#"{"体系":["三端"],"客户端":["Web","Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Ant Design","Zustand","Tauri","Vite","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"web": "vite",
|
||
"build": "vite build",
|
||
"build:desktop": "tauri build",
|
||
"dev:desktop": "tauri dev",
|
||
"preview": "vite preview",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@tauri-apps/api": "^2",
|
||
"antd": "^5.21.0",
|
||
"react": "^18.3.0",
|
||
"react-dom": "^18.3.0",
|
||
"zustand": "^5.0.0"
|
||
},
|
||
"devDependencies": {
|
||
"@tauri-apps/cli": "^2",
|
||
"@types/react": "^18.3.0",
|
||
"@types/react-dom": "^18.3.0",
|
||
"@vitejs/plugin-react": "^4.3.0",
|
||
"typescript": "^5.5.0",
|
||
"vite": "^5.4.0"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig(({ mode }) => {
|
||
loadEnv(mode, process.cwd(), "");
|
||
return {
|
||
plugins: [react()],
|
||
base: mode === "capacitor" ? "./" : "/",
|
||
clearScreen: false,
|
||
server: {
|
||
port: 1420,
|
||
strictPort: true,
|
||
watch: {
|
||
ignored: ["**/src-tauri/**"],
|
||
},
|
||
},
|
||
};
|
||
});
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
(".env", "VITE_PLATFORM=web\n"),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { Button } from "antd";
|
||
import { useAppStore } from "./store";
|
||
|
||
export default function App() {
|
||
const { count, increment } = useAppStore();
|
||
return (
|
||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||
<h1>{{project_name}}</h1>
|
||
<p>React 18 + Ant Design 5 + Zustand + Tauri 2</p>
|
||
<Button type="primary" onClick={increment}>
|
||
count: {count}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
"#),
|
||
("src/store/index.ts", r#"import { create } from "zustand";
|
||
|
||
interface AppStore {
|
||
count: number;
|
||
increment: () => void;
|
||
}
|
||
|
||
export const useAppStore = create<AppStore>((set) => ({
|
||
count: 0,
|
||
increment: () => set((s) => ({ count: s.count + 1 })),
|
||
}));
|
||
"#),
|
||
("src-tauri/tauri.conf.json", r#"{
|
||
"$schema": "https://schema.tauri.app/config/2",
|
||
"productName": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"identifier": "com.example.{{project_name_snake}}",
|
||
"build": {
|
||
"beforeDevCommand": "pnpm web",
|
||
"devUrl": "http://localhost:1420",
|
||
"beforeBuildCommand": "pnpm build",
|
||
"frontendDist": "../dist"
|
||
},
|
||
"app": {
|
||
"windows": [
|
||
{
|
||
"title": "{{project_name}}",
|
||
"width": 1200,
|
||
"height": 800
|
||
}
|
||
],
|
||
"security": {
|
||
"csp": null
|
||
}
|
||
},
|
||
"bundle": {
|
||
"active": true,
|
||
"targets": "all",
|
||
"icon": []
|
||
}
|
||
}
|
||
"#),
|
||
("src-tauri/Cargo.toml", r#"[package]
|
||
name = "{{project_name_snake}}"
|
||
version = "0.1.0"
|
||
description = "A Tauri App"
|
||
authors = []
|
||
edition = "2021"
|
||
|
||
[lib]
|
||
name = "{{project_name_snake}}_lib"
|
||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||
|
||
[build-dependencies]
|
||
tauri-build = { version = "2", features = [] }
|
||
|
||
[dependencies]
|
||
tauri = { version = "2", features = [] }
|
||
tauri-plugin-opener = "2"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
"#),
|
||
("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"),
|
||
("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||
|
||
fn main() {
|
||
{{project_name_snake}}_lib::run()
|
||
}
|
||
"#),
|
||
("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
tauri::Builder::default()
|
||
.plugin(tauri_plugin_opener::init())
|
||
.invoke_handler(tauri::generate_handler![])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|
||
"#),
|
||
("src-tauri/capabilities/default.json", r#"{
|
||
"$schema": "../gen/schemas/desktop-schema.json",
|
||
"identifier": "default",
|
||
"description": "Capability for the main window",
|
||
"windows": ["main"],
|
||
"permissions": [
|
||
"core:default",
|
||
"opener:default"
|
||
]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
|
||
],
|
||
},
|
||
Tpl {
|
||
id: "builtin-react-triplatform",
|
||
name: "React Tri-Platform",
|
||
description: "三端合一。同一套 React 18 + Ant Design 5 + Zustand 代码,pnpm web / build:cap / build:desktop 分别构建 Web / Android / Desktop。",
|
||
platform: "both",
|
||
tech_stack: r#"{"体系":["三端"],"客户端":["Web","Android","Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Ant Design","Zustand","Capacitor","Tauri","Vite","pnpm"]}"#,
|
||
init_commands: "pnpm install",
|
||
variants: None,
|
||
files: vec![
|
||
("package.json", r#"{
|
||
"name": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"private": true,
|
||
"type": "module",
|
||
"scripts": {
|
||
"web": "vite",
|
||
"build": "vite build",
|
||
"build:cap": "vite build --mode capacitor && npx cap sync",
|
||
"build:desktop": "tauri build",
|
||
"dev:desktop": "tauri dev",
|
||
"preview": "vite preview",
|
||
"typecheck": "tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"@capacitor/core": "^8.0.0",
|
||
"@tauri-apps/api": "^2",
|
||
"antd": "^5.21.0",
|
||
"react": "^18.3.0",
|
||
"react-dom": "^18.3.0",
|
||
"zustand": "^5.0.0"
|
||
},
|
||
"devDependencies": {
|
||
"@capacitor/android": "^8.0.0",
|
||
"@capacitor/cli": "^8.0.0",
|
||
"@tauri-apps/cli": "^2",
|
||
"@types/react": "^18.3.0",
|
||
"@types/react-dom": "^18.3.0",
|
||
"@vitejs/plugin-react": "^4.3.0",
|
||
"typescript": "^5.5.0",
|
||
"vite": "^5.4.0"
|
||
}
|
||
}
|
||
"#),
|
||
("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig(({ mode }) => {
|
||
loadEnv(mode, process.cwd(), "");
|
||
return {
|
||
plugins: [react()],
|
||
base: mode === "capacitor" ? "./" : "/",
|
||
clearScreen: false,
|
||
server: {
|
||
port: 1420,
|
||
strictPort: true,
|
||
watch: {
|
||
ignored: ["**/src-tauri/**"],
|
||
},
|
||
},
|
||
};
|
||
});
|
||
"#),
|
||
("tsconfig.json", r#"{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "react-jsx",
|
||
"strict": true
|
||
},
|
||
"include": ["src", "capacitor.config.ts"]
|
||
}
|
||
"#),
|
||
("index.html", r#"<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{project_name}}</title>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|
||
"#),
|
||
(".env", "VITE_PLATFORM=web\n"),
|
||
(".env.capacitor", "VITE_PLATFORM=android\n"),
|
||
("capacitor.config.ts", r#"import type { CapacitorConfig } from "@capacitor/cli";
|
||
|
||
const config: CapacitorConfig = {
|
||
appId: "com.example.{{project_name_snake}}",
|
||
appName: "{{project_name}}",
|
||
webDir: "dist",
|
||
};
|
||
|
||
export default config;
|
||
"#),
|
||
("src/main.tsx", r#"import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>
|
||
);
|
||
"#),
|
||
("src/App.tsx", r#"import { Button } from "antd";
|
||
import { useAppStore } from "./store";
|
||
|
||
export default function App() {
|
||
const { count, increment } = useAppStore();
|
||
return (
|
||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||
<h1>{{project_name}}</h1>
|
||
<p>React 18 + Ant Design 5 + Zustand · Web / Android / Desktop</p>
|
||
<Button type="primary" onClick={increment}>
|
||
count: {count}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
"#),
|
||
("src/store/index.ts", r#"import { create } from "zustand";
|
||
|
||
interface AppStore {
|
||
count: number;
|
||
increment: () => void;
|
||
}
|
||
|
||
export const useAppStore = create<AppStore>((set) => ({
|
||
count: 0,
|
||
increment: () => set((s) => ({ count: s.count + 1 })),
|
||
}));
|
||
"#),
|
||
("src-tauri/tauri.conf.json", r#"{
|
||
"$schema": "https://schema.tauri.app/config/2",
|
||
"productName": "{{project_name_kebab}}",
|
||
"version": "0.1.0",
|
||
"identifier": "com.example.{{project_name_snake}}",
|
||
"build": {
|
||
"beforeDevCommand": "pnpm web",
|
||
"devUrl": "http://localhost:1420",
|
||
"beforeBuildCommand": "pnpm build",
|
||
"frontendDist": "../dist"
|
||
},
|
||
"app": {
|
||
"windows": [
|
||
{
|
||
"title": "{{project_name}}",
|
||
"width": 1200,
|
||
"height": 800
|
||
}
|
||
],
|
||
"security": {
|
||
"csp": null
|
||
}
|
||
},
|
||
"bundle": {
|
||
"active": true,
|
||
"targets": "all",
|
||
"icon": []
|
||
}
|
||
}
|
||
"#),
|
||
("src-tauri/Cargo.toml", r#"[package]
|
||
name = "{{project_name_snake}}"
|
||
version = "0.1.0"
|
||
description = "A Tauri App"
|
||
authors = []
|
||
edition = "2021"
|
||
|
||
[lib]
|
||
name = "{{project_name_snake}}_lib"
|
||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||
|
||
[build-dependencies]
|
||
tauri-build = { version = "2", features = [] }
|
||
|
||
[dependencies]
|
||
tauri = { version = "2", features = [] }
|
||
tauri-plugin-opener = "2"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
"#),
|
||
("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"),
|
||
("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||
|
||
fn main() {
|
||
{{project_name_snake}}_lib::run()
|
||
}
|
||
"#),
|
||
("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
tauri::Builder::default()
|
||
.plugin(tauri_plugin_opener::init())
|
||
.invoke_handler(tauri::generate_handler![])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|
||
"#),
|
||
("src-tauri/capabilities/default.json", r#"{
|
||
"$schema": "../gen/schemas/desktop-schema.json",
|
||
"identifier": "default",
|
||
"description": "Capability for the main window",
|
||
"windows": ["main"],
|
||
"permissions": [
|
||
"core:default",
|
||
"opener:default"
|
||
]
|
||
}
|
||
"#),
|
||
(".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nandroid/\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
|
||
],
|
||
},
|
||
// ── 知识库(择升环)────────────────────────────────────────────────────
|
||
Tpl {
|
||
id: "builtin-knowledge-loop",
|
||
name: "知识库(择升环)",
|
||
description: "方法论 / 知识沉淀文档库。三层晋升结构:dialogues 对话原料 → concepts 概念卡 → CORE 稳定核心,晋升规则内置于 AGENTS.md,agent 进库即按择升环工作流运转。",
|
||
platform: "both",
|
||
tech_stack: r#"{"体系":["知识库"],"框架/工具":["Markdown","Git"]}"#,
|
||
init_commands: "git init",
|
||
variants: None,
|
||
files: vec![
|
||
("CORE.md", r#"# {{project_name}} · 核心层
|
||
|
||
> 本文件是知识库的稳定核心——只收录经过实践检验、表述已收敛的内容。
|
||
> ⚠️ 过滤铁律:全文上限 300 行。收录新内容超限时,必须合并或挤掉旧内容——
|
||
> 只升不择的核心层会失去判断力。
|
||
|
||
## 定义
|
||
|
||
<!-- 本知识库研究的核心概念,一句话可复述的稳定定义 -->
|
||
|
||
## 已验证原理
|
||
|
||
<!-- 每条原理注明来源,格式:
|
||
### 原理名
|
||
一段话陈述。
|
||
来源:concepts/<概念卡>.md · 验证案例:cases/<案例>.md
|
||
-->
|
||
|
||
## 修订记录
|
||
|
||
<!-- 日期 · 进了什么 · 挤掉/合并了什么 · 为什么 -->
|
||
"#),
|
||
("AGENTS.md", r#"# {{project_name}} — AI Agent 工作规程
|
||
|
||
本仓库是知识库,不是代码库。目录即晋升流水线:
|
||
|
||
```
|
||
dialogues/(对话原料,允许粗糙)
|
||
→ concepts/(概念卡,一事一文件)
|
||
→ CORE.md(稳定核心,上限 300 行)
|
||
```
|
||
|
||
cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。
|
||
|
||
## 每次会话必须执行
|
||
|
||
1. 先读 `CORE.md` 与本次话题相关的 `concepts/` 卡——上一轮的结论是这一轮的输入
|
||
2. 探讨结束后,把整理稿写入 `dialogues/YYYY-MM-DD-主题.md`
|
||
3. 对照晋升规则检查是否触发晋升 / 降级,触发则执行
|
||
4. git commit 收尾——本库以 git 历史作为演化台账
|
||
|
||
## 晋升规则(择)
|
||
|
||
| 从 | 到 | 条件 |
|
||
|----|----|------|
|
||
| dialogues/ | concepts/ | 同一想法在 ≥2 次对话中出现,或 ≥1 个真实案例站住 |
|
||
| concepts/ | CORE.md | 状态「稳定」+ 经 cases/ 检验 + 表述收敛为一段话 |
|
||
|
||
降级同样重要:被案例证伪的概念卡标记废弃;CORE.md 超 300 行必须挤旧。
|
||
只升不择,核心层会膨胀成噪音。
|
||
|
||
## 写作纪律
|
||
|
||
- 单一事实来源:同一结论只在一层的一个文件里详述,其他位置引用
|
||
- 概念卡必须写「边界」(什么条件下不成立)——没有边界的结论不可信
|
||
- 修改 CORE.md 必须在其修订记录写明:进了什么、挤了什么、为什么
|
||
- 案例复盘必做归因二分:执行缺口(概念没用好)修行动;
|
||
规格缺口(概念本身有漏洞)回改概念卡——后者是全库最值钱的信号
|
||
"#),
|
||
("concepts/README.md", r#"# concepts/ — 概念卡层
|
||
|
||
一个概念一个文件,文件名 kebab-case(如 `pinggu-wai-huan.md`)。
|
||
|
||
## 概念卡格式
|
||
|
||
```markdown
|
||
# 概念名
|
||
|
||
- 状态:孵化 | 稳定 | 已入核 | 已废弃
|
||
- 出处:dialogues/YYYY-MM-DD-xxx.md
|
||
- 最近验证:YYYY-MM-DD(cases/xxx.md)
|
||
|
||
## 定义
|
||
一段话说清它是什么。
|
||
|
||
## 为什么成立
|
||
支撑它的推理或证据。
|
||
|
||
## 边界
|
||
它在什么条件下不成立。
|
||
```
|
||
|
||
## 晋升与降级
|
||
|
||
- 晋升进 CORE.md 后,状态改「已入核」,详述保留在卡内,CORE 只收一段话结论
|
||
- 被案例证伪 → 状态改「已废弃」并写明证伪案例,不直接删(失败也是判断力)
|
||
"#),
|
||
("dialogues/README.md", r#"# dialogues/ — 对话沉淀层(原料)
|
||
|
||
每次探讨结束后,把整理稿存入本目录。这一层的目标是「不丢」,不是「精炼」。
|
||
|
||
- 命名:`YYYY-MM-DD-主题.md`
|
||
- 开头写三行摘要:讨论了什么 / 新想法 / 待验证
|
||
- 同一想法第二次出现时,检查是否触发晋升为概念卡
|
||
"#),
|
||
("cases/README.md", r#"# cases/ — 实例复盘层(检验)
|
||
|
||
记录概念在真实事件中的应用与翻车。案例是概念卡晋升 CORE 的检验依据。
|
||
|
||
- 命名:`YYYY-MM-DD-事件.md`
|
||
- 必写字段:用了哪个概念 / 预期 / 实际结果 / 归因(执行缺口 or 规格缺口)
|
||
- 规格缺口 → 回改概念卡;执行缺口 → 概念不动,记录教训
|
||
"#),
|
||
(".gitignore", ".DS_Store\nThumbs.db\n*.tmp\n"),
|
||
],
|
||
},
|
||
];
|
||
|
||
for t in &templates {
|
||
let _ = conn.execute(
|
||
"INSERT INTO project_templates
|
||
(id, name, description, platform, tech_stack, init_commands, is_builtin, variants)
|
||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7)
|
||
ON CONFLICT(id) DO UPDATE SET
|
||
name = excluded.name,
|
||
description = excluded.description,
|
||
tech_stack = excluded.tech_stack,
|
||
init_commands = excluded.init_commands,
|
||
variants = excluded.variants",
|
||
params![
|
||
t.id,
|
||
t.name,
|
||
if t.description.is_empty() { None } else { Some(t.description) },
|
||
t.platform,
|
||
if t.tech_stack.is_empty() { None } else { Some(t.tech_stack) },
|
||
if t.init_commands.is_empty() { None } else { Some(t.init_commands) },
|
||
t.variants,
|
||
],
|
||
);
|
||
|
||
for (path, content) in &t.files {
|
||
let file_id = format!("{}-{}", t.id, path.replace('/', "-"));
|
||
let _ = conn.execute(
|
||
"INSERT OR REPLACE INTO project_template_files (id, template_id, path, content)
|
||
VALUES (?1, ?2, ?3, ?4)",
|
||
params![file_id, t.id, path, content],
|
||
);
|
||
}
|
||
}
|
||
}
|