210 lines
7.9 KiB
TypeScript
210 lines
7.9 KiB
TypeScript
/** Commands for maintaining the generated Capacitor package. */
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { loadAppConfig } from "@wrnexus/styles";
|
|
import { compileNativeWrnFile } from "@wrnexus/compiler";
|
|
|
|
function validPackageName(value: string): boolean {
|
|
return /^(?:@[a-z0-9._~-]+\/)?[a-z0-9._~-]+(?:@[a-zA-Z0-9._~^<>=|*+-]+)?$/.test(value);
|
|
}
|
|
|
|
function run(command: string, args: string[], cwd: string): void {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}`);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value.replace(/[&<>"']/g, (char) => {
|
|
const entities: Record<string, string> = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return entities[char]!;
|
|
});
|
|
}
|
|
|
|
async function updateMobileErrorPage(root: string, mobileDir: string): Promise<void> {
|
|
const config = await loadAppConfig(root);
|
|
const mobile = config.mobile ?? {};
|
|
const title = mobile.errorTitle ?? "Connection unavailable";
|
|
const message =
|
|
mobile.errorMessage ?? "Check your Wi-Fi and make sure the WrNexus server is running.";
|
|
const serverUrl = mobile.serverUrl ?? "http://localhost:3000";
|
|
const background = mobile.backgroundColor ?? "#0f172a";
|
|
const retryUrl = JSON.stringify(serverUrl).replaceAll("<", "\\u003c");
|
|
const page = `<!doctype html>
|
|
<html lang="en">
|
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(title)}</title>
|
|
<style>html{color-scheme:dark}body{margin:0;min-height:100vh;display:grid;place-items:center;font:16px system-ui;background:${background};color:#e2e8f0}.card{max-width:28rem;padding:2rem;text-align:center}button{padding:.8rem 1.2rem;border:0;border-radius:.75rem;background:#6366f1;color:white;font-weight:700}</style></head>
|
|
<body><main class="card"><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p><button id="retry">Try again</button></main><script>document.getElementById("retry").onclick=()=>location.replace(${retryUrl});</script></body>
|
|
</html>
|
|
`;
|
|
const path = join(mobileDir, "web", "error.html");
|
|
mkdirSync(resolve(path, ".."), { recursive: true });
|
|
writeFileSync(path, page, "utf8");
|
|
console.log(" ✓ mobile/web/error.html updated from wrnexus.config.ts");
|
|
}
|
|
|
|
function configureAndroidNetworkErrors(mobileDir: string): void {
|
|
const javaRoot = join(mobileDir, "android", "app", "src", "main", "java");
|
|
if (!existsSync(javaRoot)) return;
|
|
const file = (readdirSync(javaRoot, { recursive: true }) as string[])
|
|
.map((entry) => join(javaRoot, entry))
|
|
.find((entry) => entry.endsWith("MainActivity.java"));
|
|
if (!file) return;
|
|
const current = readFileSync(file, "utf8");
|
|
if (current.includes("WRNEXUS_NETWORK_ERROR_ONLY")) return;
|
|
const packageName = /^package\s+([\w.]+);/m.exec(current)?.[1];
|
|
if (
|
|
!packageName ||
|
|
!/public\s+class\s+MainActivity\s+extends\s+BridgeActivity\s*\{\s*\}/s.test(current)
|
|
) {
|
|
console.warn(" • MainActivity.java is customized — network-error handling was not changed");
|
|
return;
|
|
}
|
|
writeFileSync(
|
|
file,
|
|
`package ${packageName};
|
|
|
|
import android.os.Bundle;
|
|
import android.webkit.WebResourceError;
|
|
import android.webkit.WebResourceRequest;
|
|
import android.webkit.WebResourceResponse;
|
|
import android.webkit.WebView;
|
|
import com.getcapacitor.BridgeActivity;
|
|
import com.getcapacitor.BridgeWebViewClient;
|
|
|
|
// WRNEXUS_NETWORK_ERROR_ONLY
|
|
public class MainActivity extends BridgeActivity {
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
bridge.setWebViewClient(new BridgeWebViewClient(bridge) {
|
|
@Override
|
|
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
|
if (request.isForMainFrame()) view.loadUrl("file:///android_asset/public/error.html");
|
|
}
|
|
|
|
@Override
|
|
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse response) {
|
|
// Preserve WrNexus HTTP error pages (404, 500, etc.).
|
|
}
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
"utf8",
|
|
);
|
|
console.log(" ✓ Android network-only error page configured");
|
|
}
|
|
|
|
export async function runMobileCommand(
|
|
appRoot: string,
|
|
subcommand?: string,
|
|
args: string[] = [],
|
|
): Promise<void> {
|
|
const root = resolve(appRoot);
|
|
const mobileDir = join(root, "mobile");
|
|
if (!existsSync(join(mobileDir, "package.json"))) {
|
|
throw new Error("No mobile project found. Run `wrnexus generate mobile` first.");
|
|
}
|
|
const mobilePackage = JSON.parse(readFileSync(join(mobileDir, "package.json"), "utf8")) as {
|
|
wrnexus?: { mode?: "webview" | "native" };
|
|
};
|
|
const mode = mobilePackage.wrnexus?.mode ?? "webview";
|
|
|
|
if (subcommand === "compile") {
|
|
if (mode !== "native") throw new Error("`wrnexus mobile compile` requires native mode.");
|
|
const pagesDir = join(root, "app", "pages");
|
|
if (!existsSync(pagesDir)) throw new Error(`Pages directory not found: ${pagesDir}`);
|
|
let count = 0;
|
|
for (const entry of readdirSync(pagesDir, { recursive: true }) as string[]) {
|
|
if (!entry.endsWith(".wrn")) continue;
|
|
const source = join(pagesDir, entry);
|
|
const relative = entry.replace(/\.wrn$/, ".tsx");
|
|
const output = join(mobileDir, "app", relative);
|
|
mkdirSync(resolve(output, ".."), { recursive: true });
|
|
try {
|
|
writeFileSync(output, compileNativeWrnFile(readFileSync(source, "utf8")), "utf8");
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Native compilation failed for app/pages/${entry}: ${(error as Error).message}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
count++;
|
|
}
|
|
console.log(` ✓ compiled ${count} .wrn page${count === 1 ? "" : "s"} to mobile/app`);
|
|
return;
|
|
}
|
|
|
|
if (subcommand === "add") {
|
|
const packages = args.filter((arg) => !arg.startsWith("--"));
|
|
if (!packages.length || packages.some((name) => !validPackageName(name))) {
|
|
throw new Error("Usage: wrnexus mobile add <native-package...>");
|
|
}
|
|
if (mode === "native") {
|
|
run("bunx", ["expo", "install", ...packages], mobileDir);
|
|
return;
|
|
}
|
|
// The root app needs the JavaScript proxy for its future browser bundle;
|
|
// the mobile package needs the dependency so Capacitor can sync native code.
|
|
run("bun", ["add", ...packages], root);
|
|
run("bun", ["add", ...packages], mobileDir);
|
|
await updateMobileErrorPage(root, mobileDir);
|
|
run("bun", ["run", "sync"], mobileDir);
|
|
configureAndroidNetworkErrors(mobileDir);
|
|
return;
|
|
}
|
|
|
|
if (subcommand === "sync") {
|
|
if (mode === "native") {
|
|
run("bunx", ["expo", "prebuild"], mobileDir);
|
|
return;
|
|
}
|
|
await updateMobileErrorPage(root, mobileDir);
|
|
run("bun", ["run", "sync"], mobileDir);
|
|
configureAndroidNetworkErrors(mobileDir);
|
|
return;
|
|
}
|
|
|
|
if (subcommand === "assets") {
|
|
const config = await loadAppConfig(root);
|
|
if (!config.mobile?.icon) {
|
|
throw new Error("Set `mobile.icon` in wrnexus.config.ts before generating native assets.");
|
|
}
|
|
const source = resolve(root, config.mobile.icon);
|
|
if (!existsSync(source)) throw new Error(`Mobile icon not found: ${source}`);
|
|
const resources = join(mobileDir, "resources");
|
|
mkdirSync(resources, { recursive: true });
|
|
copyFileSync(source, join(resources, "icon.png"));
|
|
if (mode === "native") {
|
|
console.log(" ✓ mobile/resources/icon.png copied (reference it with `mobile.expo.icon`)");
|
|
return;
|
|
}
|
|
run("bunx", ["capacitor-assets", "generate"], mobileDir);
|
|
return;
|
|
}
|
|
|
|
throw new Error("Usage: wrnexus mobile <compile|add <package...>|sync|assets>");
|
|
}
|