ultisuite-client/mobile/scripts/new-app.mjs
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

220 lines
6.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Scaffold a suite app from the UltiMail pilot template.
*
* Usage:
* node scripts/new-app.mjs <crate> <ProductName> <identifier> <suiteApp> <startRoute> <scheme>
* node scripts/new-app.mjs --all # (re)generate every sibling app
*
* Each app is a Tauri 2 project under apps/<dir>/src-tauri that loads the same
* shared static export at its own start route, with its own URL scheme + icons.
*/
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readdirSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = join(__dirname, "..")
/** dir is the folder under apps/; crate is the Rust crate name. */
// Schemes follow the `ulti<suite>` convention from lib/platform.ts (appScheme)
// so inter-app deep links resolve consistently.
const APPS = [
{ dir: "ultimail", crate: "ultimail", product: "UltiMail", id: "space.ulti.mail", suite: "mail", route: "/mail/", scheme: "ultimail", linkPrefix: "/app/mail" },
{ dir: "ultidrive", crate: "ultidrive", product: "UltiDrive", id: "space.ulti.drive", suite: "drive", route: "/drive/", scheme: "ultidrive", linkPrefix: "/app/drive" },
{ dir: "ulticalmeet", crate: "ulticalmeet", product: "UltiCal", id: "space.ulti.cal", suite: "agenda", route: "/agenda/", scheme: "ultiagenda", linkPrefix: "/app/agenda" },
{ dir: "ultiai", crate: "ultiai", product: "UltiAI", id: "space.ulti.ai", suite: "chat", route: "/chat/", scheme: "ultichat", linkPrefix: "/app/chat" },
{ dir: "contacts", crate: "ulticontacts", product: "Contacts", id: "space.ulti.contacts", suite: "contacts", route: "/contacts/", scheme: "ulticontacts", linkPrefix: "/app/contacts" },
]
function cargoToml(a) {
return `[package]
name = "${a.crate}"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
description = "${a.product} — native shell (Tauri 2)"
[lib]
name = "${a.crate}_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { workspace = true }
[dependencies]
tauri = { workspace = true }
tauri-plugin-deep-link = { workspace = true }
tauri-plugin-notification = { workspace = true }
tauri-plugin-opener = { workspace = true }
ulti-core = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
[features]
default = []
`
}
function buildRs() {
return `fn main() {
tauri_build::build()
}
`
}
function mainRs(a) {
return `// Prevents an extra console window on Windows in release.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
${a.crate}_lib::run()
}
`
}
function libRs(a) {
return `//! ${a.product} native shell. Loads the shared static export at \`${a.route}\`
//! and wires the suite's native capabilities through \`ulti-core\` + plugins.
use tauri::Emitter;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.plugin(ulti_core::init())
.setup(|app| {
#[cfg(any(target_os = "android", target_os = "ios", desktop))]
{
use tauri_plugin_deep_link::DeepLinkExt;
let handle = app.handle().clone();
app.deep_link().on_open_url(move |event| {
let urls: Vec<String> =
event.urls().iter().map(|u| u.to_string()).collect();
let _ = handle.emit("ulti://deep-link", urls);
});
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running ${a.product}");
}
`
}
function tauriConf(a) {
return JSON.stringify(
{
$schema: "https://schema.tauri.app/config/2",
productName: a.product,
version: "0.1.0",
identifier: a.id,
build: {
frontendDist: "../../../../out",
devUrl: "http://localhost:3005",
beforeDevCommand: `bash "$ULTI_REPO_ROOT/mobile/scripts/dev-mobile-tauri.sh" ${a.suite}`,
beforeBuildCommand: `bash "$ULTI_REPO_ROOT/mobile/scripts/build-mobile.sh" ${a.suite}`,
},
app: {
windows: [
{ label: "main", title: a.product, url: a.route, width: 420, height: 900 },
],
security: { csp: null },
},
bundle: {
active: true,
targets: "all",
category: "Productivity",
icon: ["icons/icon.png"],
},
plugins: {
"deep-link": {
mobile: [{ host: "space.ulti.app", pathPrefix: [a.linkPrefix] }],
desktop: { schemes: [a.scheme] },
},
},
},
null,
2
) + "\n"
}
function capabilities() {
return JSON.stringify(
{
$schema: "../gen/schemas/desktop-schema.json",
identifier: "default",
description: "Default capabilities for the suite shell.",
windows: ["main"],
permissions: [
"core:default",
"core:window:default",
"core:event:default",
"core:webview:default",
"deep-link:default",
"notification:default",
"opener:default",
"opener:allow-open-url",
"ulti-core:default",
],
},
null,
2
) + "\n"
}
function write(path, contents) {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, contents)
}
function generate(a, { force = false } = {}) {
const base = join(ROOT, "apps", a.dir, "src-tauri")
if (a.dir === "ultimail" && !force) {
console.log("skip ultimail (pilot template — use --force to overwrite)")
return
}
write(join(base, "Cargo.toml"), cargoToml(a))
write(join(base, "build.rs"), buildRs())
write(join(base, "src", "main.rs"), mainRs(a))
write(join(base, "src", "lib.rs"), libRs(a))
write(join(base, "tauri.conf.json"), tauriConf(a))
write(join(base, "capabilities", "default.json"), capabilities())
// Icons: copy from the pilot if present.
const pilotIcons = join(ROOT, "apps", "ultimail", "src-tauri", "icons")
const iconsDir = join(base, "icons")
mkdirSync(iconsDir, { recursive: true })
if (existsSync(pilotIcons)) {
for (const f of readdirSync(pilotIcons)) {
copyFileSync(join(pilotIcons, f), join(iconsDir, f))
}
}
console.log(`generated ${a.dir} (${a.product})`)
}
const arg = process.argv[2]
if (arg === "--all") {
for (const a of APPS) generate(a)
} else if (arg) {
const [crate, product, id, suite, route, scheme] = process.argv.slice(2)
generate({
dir: crate,
crate,
product,
id,
suite,
route,
scheme,
linkPrefix: `/app/${suite}`,
}, { force: true })
} else {
console.error("usage: new-app.mjs --all | <crate> <Product> <id> <suite> <route> <scheme>")
process.exit(1)
}