first commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/** 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 { compileNativeWireFile } 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, compileNativeWireFile(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>");
|
||||
}
|
||||
Reference in New Issue
Block a user