220 lines
6.8 KiB
JavaScript
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)
|
|
}
|