2026-06-13 20:00:05 -05:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// Copies mobile/nodejs-assets/nodejs-project into the Android project's assets
|
|
|
|
|
// so the nodejs-mobile-cordova plugin can find/copy it, and generates
|
|
|
|
|
// dir.list/file.list for the plugin's runtime copy step.
|
|
|
|
|
//
|
|
|
|
|
// nodejs-mobile-cordova's build.gradle is `apply from`'d into BOTH the `app`
|
|
|
|
|
// and `capacitor-cordova-android-plugins` modules, and each checks for
|
|
|
|
|
// `<module>/src/main/assets/www/` at *configuration* time — the build fails
|
|
|
|
|
// with "couldn't find the www folder" if either is missing. So we place a
|
|
|
|
|
// copy in both.
|
|
|
|
|
//
|
|
|
|
|
// IMPORTANT: `npx cap sync` regenerates capacitor-cordova-android-plugins'
|
|
|
|
|
// assets folder (wiping anything placed here), so this script must run AFTER
|
|
|
|
|
// `npx cap sync` and the build must run with `--no-sync`
|
|
|
|
|
// (npm run android does this in the right order).
|
|
|
|
|
//
|
|
|
|
|
// Run after `./scripts/sync-nodejs-project.sh` and (optionally) `npm install`
|
|
|
|
|
// inside nodejs-assets/nodejs-project.
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const zlib = require('zlib');
|
|
|
|
|
|
|
|
|
|
const MOBILE_DIR = path.join(__dirname, '..');
|
|
|
|
|
const SRC = path.join(MOBILE_DIR, 'nodejs-assets', 'nodejs-project');
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(SRC)) {
|
|
|
|
|
console.error(`Source not found: ${SRC}\nRun ./scripts/sync-nodejs-project.sh first.`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rmrf(p) {
|
|
|
|
|
fs.rmSync(p, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function copyDir(src, dest) {
|
|
|
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
|
|
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
|
|
|
if (entry.name === '.bin') continue; // symlinks under node_modules/.bin break the asset copy
|
|
|
|
|
// Android's asset merger treats foo.js + foo.js.gz as duplicate resources.
|
|
|
|
|
if (entry.name.endsWith('.gz')) continue;
|
|
|
|
|
const srcPath = path.join(src, entry.name);
|
|
|
|
|
const destPath = path.join(dest, entry.name);
|
|
|
|
|
if (entry.isSymbolicLink()) {
|
|
|
|
|
const real = fs.realpathSync(srcPath);
|
|
|
|
|
if (fs.statSync(real).isDirectory()) copyDir(real, destPath);
|
|
|
|
|
else fs.copyFileSync(real, destPath);
|
|
|
|
|
} else if (entry.isDirectory()) {
|
|
|
|
|
copyDir(srcPath, destPath);
|
|
|
|
|
} else {
|
|
|
|
|
fs.copyFileSync(srcPath, destPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function enumerate(absDir, relDir, dirs, files) {
|
|
|
|
|
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
|
|
|
if (entry.name.startsWith('.')) continue;
|
|
|
|
|
if (entry.name.endsWith('.gz') || entry.name.endsWith('~')) continue;
|
|
|
|
|
const absPath = path.join(absDir, entry.name);
|
|
|
|
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
dirs.push(relPath);
|
|
|
|
|
enumerate(absPath, relPath, dirs, files);
|
|
|
|
|
} else {
|
|
|
|
|
files.push(relPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Patch a known issue in nodejs-mobile-cordova 0.4.3: NodeJS.java references
|
|
|
|
|
// `BuildConfig.DEBUG`, which doesn't resolve in Capacitor's cordova-plugins
|
|
|
|
|
// library module (no matching BuildConfig in that package). This file is
|
|
|
|
|
// re-copied verbatim from node_modules by `cap sync`, so patch it each time.
|
|
|
|
|
const nodeJsJava = path.join(
|
|
|
|
|
MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins', 'src', 'main', 'java',
|
|
|
|
|
'com', 'janeasystems', 'cdvnodejsmobile', 'NodeJS.java'
|
|
|
|
|
);
|
|
|
|
|
if (fs.existsSync(nodeJsJava)) {
|
|
|
|
|
const src = fs.readFileSync(nodeJsJava, 'utf8');
|
|
|
|
|
const patched = src.replace('if (BuildConfig.DEBUG) {', 'if (false) {');
|
|
|
|
|
if (patched !== src) {
|
|
|
|
|
fs.writeFileSync(nodeJsJava, patched);
|
|
|
|
|
console.log(`Patched ${path.relative(MOBILE_DIR, nodeJsJava)} (BuildConfig.DEBUG -> false)`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 13:10:14 -05:00
|
|
|
// Local mode runs the embedded Node server on http://127.0.0.1:3000, which
|
|
|
|
|
// Android's Network Security Config blocks by default (cleartext traffic).
|
|
|
|
|
// `cap sync`/`cap add android` don't know about this, so (re)write the config
|
|
|
|
|
// and wire it into AndroidManifest.xml's <application> tag each time.
|
|
|
|
|
const networkSecurityConfigXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
|
<network-security-config>
|
|
|
|
|
<!-- Local mode runs an embedded Node.js server on 127.0.0.1; the WebView
|
|
|
|
|
(https://localhost) needs to reach it over plain HTTP. -->
|
|
|
|
|
<domain-config cleartextTrafficPermitted="true">
|
|
|
|
|
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
|
|
|
<domain includeSubdomains="false">localhost</domain>
|
|
|
|
|
</domain-config>
|
|
|
|
|
</network-security-config>
|
|
|
|
|
`;
|
|
|
|
|
const networkSecurityConfigPath = path.join(
|
|
|
|
|
MOBILE_DIR, 'android', 'app', 'src', 'main', 'res', 'xml', 'network_security_config.xml'
|
|
|
|
|
);
|
|
|
|
|
fs.mkdirSync(path.dirname(networkSecurityConfigPath), { recursive: true });
|
|
|
|
|
fs.writeFileSync(networkSecurityConfigPath, networkSecurityConfigXml);
|
|
|
|
|
|
|
|
|
|
const manifestPath = path.join(MOBILE_DIR, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
|
|
|
if (fs.existsSync(manifestPath)) {
|
|
|
|
|
const src = fs.readFileSync(manifestPath, 'utf8');
|
|
|
|
|
if (!src.includes('android:networkSecurityConfig')) {
|
|
|
|
|
const patched = src.replace(
|
|
|
|
|
/(<application\b)/,
|
|
|
|
|
'$1\n android:networkSecurityConfig="@xml/network_security_config"'
|
|
|
|
|
);
|
|
|
|
|
fs.writeFileSync(manifestPath, patched);
|
|
|
|
|
console.log(`Patched ${path.relative(MOBILE_DIR, manifestPath)} (added networkSecurityConfig)`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 20:00:05 -05:00
|
|
|
// nodejs-mobile-cordova's build.gradle sets up a CMake build with
|
|
|
|
|
// `cmake.path = "libs/cdvnodejsmobile/CMakeLists.txt"` (relative to each
|
|
|
|
|
// module's projectDir) for both `app` and `capacitor-cordova-android-plugins`.
|
|
|
|
|
// `cap sync` doesn't replicate the plugin.xml `<source-file target-dir="libs/cdvnodejsmobile">`
|
|
|
|
|
// copies, so populate that directory ourselves in both modules.
|
|
|
|
|
const NODEJS_MOBILE = path.join(MOBILE_DIR, 'node_modules', 'nodejs-mobile-cordova');
|
|
|
|
|
const ABIS = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'];
|
|
|
|
|
|
|
|
|
|
function gunzipFile(src, dest) {
|
|
|
|
|
const data = fs.readFileSync(src);
|
|
|
|
|
fs.writeFileSync(dest, zlib.gunzipSync(data));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateNativeLibs(moduleDir) {
|
|
|
|
|
const dest = path.join(moduleDir, 'libs', 'cdvnodejsmobile');
|
|
|
|
|
rmrf(dest);
|
|
|
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
|
|
|
|
|
|
|
|
for (const f of ['CMakeLists.txt']) {
|
|
|
|
|
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'android', f), path.join(dest, f));
|
|
|
|
|
}
|
|
|
|
|
for (const f of ['native-lib.cpp']) {
|
|
|
|
|
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'android', 'jni', f), path.join(dest, f));
|
|
|
|
|
}
|
|
|
|
|
for (const f of ['cordova-bridge.cpp', 'cordova-bridge.h']) {
|
|
|
|
|
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'common', 'cordova-bridge', f), path.join(dest, f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const libnodeSrc = path.join(NODEJS_MOBILE, 'libs', 'android', 'libnode');
|
|
|
|
|
const libnodeDest = path.join(dest, 'libnode');
|
|
|
|
|
copyDir(path.join(libnodeSrc, 'include'), path.join(libnodeDest, 'include'));
|
|
|
|
|
for (const abi of ABIS) {
|
|
|
|
|
const binDest = path.join(libnodeDest, 'bin', abi);
|
|
|
|
|
fs.mkdirSync(binDest, { recursive: true });
|
|
|
|
|
gunzipFile(
|
|
|
|
|
path.join(libnodeSrc, 'bin', abi, 'libnode.so.gz'),
|
|
|
|
|
path.join(binDest, 'libnode.so')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Populated ${path.relative(MOBILE_DIR, dest)} (native CMake build sources + libnode)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const moduleDir of [
|
|
|
|
|
path.join(MOBILE_DIR, 'android', 'app'),
|
|
|
|
|
path.join(MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins'),
|
|
|
|
|
]) {
|
|
|
|
|
populateNativeLibs(moduleDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targets = [
|
|
|
|
|
path.join(MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins', 'src', 'main', 'assets'),
|
|
|
|
|
path.join(MOBILE_DIR, 'android', 'app', 'src', 'main', 'assets'),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// The plugin's copyNodeJSAssets() also expects a `nodejs-mobile-cordova-assets`
|
|
|
|
|
// folder at the root of the assets (builtin modules like cordova-bridge);
|
|
|
|
|
// without it, assetManager.list() returns empty and the fallback
|
|
|
|
|
// assetManager.open() throws FileNotFoundException.
|
|
|
|
|
const BUILTIN_ASSETS_SRC = path.join(NODEJS_MOBILE, 'install', 'nodejs-mobile-cordova-assets');
|
|
|
|
|
|
|
|
|
|
for (const assetsRoot of targets) {
|
|
|
|
|
const builtinDest = path.join(assetsRoot, 'nodejs-mobile-cordova-assets');
|
|
|
|
|
rmrf(builtinDest);
|
|
|
|
|
copyDir(BUILTIN_ASSETS_SRC, builtinDest);
|
|
|
|
|
|
|
|
|
|
const dest = path.join(assetsRoot, 'www', 'nodejs-project');
|
|
|
|
|
console.log(`Copying ${SRC} -> ${dest}`);
|
|
|
|
|
rmrf(dest);
|
|
|
|
|
copyDir(SRC, dest);
|
|
|
|
|
|
|
|
|
|
// dir.list/file.list, paths relative to assetsRoot (e.g. "www/nodejs-project/main.js"),
|
|
|
|
|
// matching the format produced by the cordova after_prepare hook.
|
|
|
|
|
const dirs = [];
|
|
|
|
|
const files = [];
|
|
|
|
|
enumerate(dest, 'www/nodejs-project', dirs, files);
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(assetsRoot, 'dir.list'), dirs.join('\n') + '\n');
|
|
|
|
|
fs.writeFileSync(path.join(assetsRoot, 'file.list'), files.join('\n') + '\n');
|
|
|
|
|
|
|
|
|
|
console.log(`Wrote ${dirs.length} dirs / ${files.length} files to ${path.relative(MOBILE_DIR, assetsRoot)}/{dir,file}.list`);
|
|
|
|
|
}
|