#!/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 // `/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)`); } } // 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 `` // 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`); }