feat(mobile): run the TypeScript backend on Node 18 in local mode (batch 7-9)
The web app migrated its server to TypeScript (server.cts) on Express 5 /
openid-client 6 / kysely, none of which ran on the old embedded Node 12.19.
Rebuild the local-mode pipeline for Node 18:
- sync-nodejs-project.sh: sync server.cts + .cts/.mts + setup/ + seed data.
- transpile-node18.js (replaces transpile-node12.js): esbuild TS->CJS in place,
keeping .cts/.mts filenames (Node 18's CJS loader loads unknown extensions via
the .js handler, so the explicit-extension require() graph keeps resolving).
- prepare-local-mode-deps.js: bundle ESM-only kysely to CJS, stub openid-client
(unused on-device), and rewrite path-to-regexp's \p{} regexes to ASCII (the
no-ICU Node 18 build can't compile unicode-property escapes).
- build-better-sqlite3-node18.sh (replaces the Node-12 build/fix scripts):
cross-compile for all three ABIs against the Node 18 ABI, injecting libnode.so
as a NEEDED entry so V8 symbols resolve at dlopen.
- main.js: entry -> server.cts; drop the now-native randomUUID/hkdfSync polyfills
(keep the Intl shim); add TMPDIR + a startup-error relay to the WebView.
- nodejs-project/package.json: server dep set matched to the web app.
Verified end-to-end on the x86_64 emulator: boot -> migrations -> seed -> login
-> create bill -> force-stop -> relaunch -> data persists. arm64/armv7 prebuilds
are built and link-verified; a physical-device run is still pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3e115dd349
commit
2c29ec45bd
|
|
@ -8,7 +8,7 @@ const path = require('path');
|
||||||
// this process by nodejs-mobile-cordova). The copy under node_modules/ is
|
// this process by nodejs-mobile-cordova). The copy under node_modules/ is
|
||||||
// whichever architecture it happened to be built for last; replace it with
|
// whichever architecture it happened to be built for last; replace it with
|
||||||
// the prebuild matching this device's actual arch before anything requires
|
// the prebuild matching this device's actual arch before anything requires
|
||||||
// better-sqlite3. See scripts/build-better-sqlite3-arm.sh.
|
// better-sqlite3. See scripts/build-better-sqlite3-node18.sh.
|
||||||
(function installBetterSqlite3Prebuild() {
|
(function installBetterSqlite3Prebuild() {
|
||||||
const archDirs = {
|
const archDirs = {
|
||||||
arm64: 'arm64-v8a',
|
arm64: 'arm64-v8a',
|
||||||
|
|
@ -31,60 +31,14 @@ const path = require('path');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Node 12.19 predates crypto.randomUUID (added in Node 14.17), used by
|
// nodejs-mobile's Node 18.20.4 build ships WITHOUT ICU (verified:
|
||||||
// services/authService.js for session token IDs.
|
// process.versions.icu === undefined, typeof Intl === 'undefined'), so `Intl`
|
||||||
const nodeCrypto = require('crypto');
|
// is entirely absent. Polyfill just the bits the server uses — en-US number
|
||||||
if (typeof nodeCrypto.randomUUID !== 'function') {
|
|
||||||
nodeCrypto.randomUUID = function () {
|
|
||||||
const b = nodeCrypto.randomBytes(16);
|
|
||||||
b[6] = (b[6] & 0x0f) | 0x40;
|
|
||||||
b[8] = (b[8] & 0x3f) | 0x80;
|
|
||||||
const hex = b.toString('hex');
|
|
||||||
return [
|
|
||||||
hex.slice(0, 8),
|
|
||||||
hex.slice(8, 12),
|
|
||||||
hex.slice(12, 16),
|
|
||||||
hex.slice(16, 20),
|
|
||||||
hex.slice(20),
|
|
||||||
].join('-');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node 12.19 predates crypto.hkdfSync (added in Node 15.0), used by
|
|
||||||
// services/encryptionService.js's deriveKey to derive the token-encryption
|
|
||||||
// key from TOKEN_ENCRYPTION_KEY. Implements HKDF-Extract/Expand (RFC 5869)
|
|
||||||
// via createHmac.
|
|
||||||
if (typeof nodeCrypto.hkdfSync !== 'function') {
|
|
||||||
nodeCrypto.hkdfSync = function (digest, ikm, salt, info, keylen) {
|
|
||||||
const hashLen = nodeCrypto.createHash(digest).digest().length;
|
|
||||||
const saltBuf = salt && salt.length ? Buffer.from(salt) : Buffer.alloc(hashLen, 0);
|
|
||||||
const ikmBuf = Buffer.isBuffer(ikm) ? ikm : Buffer.from(ikm);
|
|
||||||
const infoBuf = Buffer.isBuffer(info) ? info : Buffer.from(info || '');
|
|
||||||
|
|
||||||
const prk = nodeCrypto.createHmac(digest, saltBuf).update(ikmBuf).digest();
|
|
||||||
|
|
||||||
let t = Buffer.alloc(0);
|
|
||||||
let okm = Buffer.alloc(0);
|
|
||||||
let counter = 1;
|
|
||||||
while (okm.length < keylen) {
|
|
||||||
t = nodeCrypto
|
|
||||||
.createHmac(digest, prk)
|
|
||||||
.update(t)
|
|
||||||
.update(infoBuf)
|
|
||||||
.update(Buffer.from([counter]))
|
|
||||||
.digest();
|
|
||||||
okm = Buffer.concat([okm, t]);
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
return okm.slice(0, keylen);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodejs-mobile-cordova 0.4.3 embeds Node 12.19 built without ICU, so
|
|
||||||
// `Intl` is entirely undefined. Polyfill just the bits the server uses
|
|
||||||
// (utils/money.js, routes/dataSources.js, routes/status.js) — en-US number
|
|
||||||
// formatting with thousands separators, and a no-op DateTimeFormat whose
|
// formatting with thousands separators, and a no-op DateTimeFormat whose
|
||||||
// resolvedOptions().timeZone the callers already treat as optional.
|
// resolvedOptions().timeZone the callers already treat as optional.
|
||||||
|
//
|
||||||
|
// (crypto.randomUUID and crypto.hkdfSync are NATIVE on Node 18 — the Node-12
|
||||||
|
// shims for them are gone. See PLAN/FUTURE Batch 6.)
|
||||||
if (typeof global.Intl === 'undefined') {
|
if (typeof global.Intl === 'undefined') {
|
||||||
global.Intl = {
|
global.Intl = {
|
||||||
NumberFormat: function (_locale, options) {
|
NumberFormat: function (_locale, options) {
|
||||||
|
|
@ -114,6 +68,10 @@ if (typeof global.Intl === 'undefined') {
|
||||||
// on-device — nodejs-mobile-cordova copies nodejs-project here at runtime).
|
// on-device — nodejs-mobile-cordova copies nodejs-project here at runtime).
|
||||||
process.env.DB_PATH = path.join(__dirname, 'data', 'bills.db');
|
process.env.DB_PATH = path.join(__dirname, 'data', 'bills.db');
|
||||||
process.env.BACKUP_PATH = path.join(__dirname, 'backups');
|
process.env.BACKUP_PATH = path.join(__dirname, 'backups');
|
||||||
|
// Android has no /tmp; point os.tmpdir() at app-writable storage so the daily
|
||||||
|
// worker's temp-export cleanup doesn't error (ENOENT scandir '/tmp').
|
||||||
|
process.env.TMPDIR = path.join(__dirname, 'tmp');
|
||||||
|
try { fs.mkdirSync(process.env.TMPDIR, { recursive: true }); } catch (_) { /* best effort */ }
|
||||||
process.env.PORT = process.env.PORT || '3000';
|
process.env.PORT = process.env.PORT || '3000';
|
||||||
process.env.BIND_HOST = '127.0.0.1';
|
process.env.BIND_HOST = '127.0.0.1';
|
||||||
// The Capacitor WebView's origin (androidScheme: 'https') is cross-origin
|
// The Capacitor WebView's origin (androidScheme: 'https') is cross-origin
|
||||||
|
|
@ -123,13 +81,30 @@ process.env.CORS_ORIGIN = 'https://localhost';
|
||||||
|
|
||||||
const cordova = require('cordova-bridge');
|
const cordova = require('cordova-bridge');
|
||||||
|
|
||||||
// Wait for the WebView to hand over the device-bound encryption key (stored
|
// Surface fatal errors to logcat and to the WebView (LoadingScreen) instead of
|
||||||
// in Android Keystore / iOS Keychain — see src/crypto.ts) before starting the
|
// dying silently behind an endless "Starting…" spinner.
|
||||||
// server, so encryptionService.js picks up TOKEN_ENCRYPTION_KEY on first use.
|
function reportError(where, err) {
|
||||||
|
const msg = err && err.stack ? err.stack : String(err);
|
||||||
|
console.error('[main.js] ' + where + ':', msg);
|
||||||
|
try {
|
||||||
|
cordova.channel.post('message', { type: 'serverError', message: String((err && err.message) || err) });
|
||||||
|
} catch (_) { /* channel may not be ready */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => reportError('uncaughtException', err));
|
||||||
|
process.on('unhandledRejection', (err) => reportError('unhandledRejection', err));
|
||||||
|
|
||||||
|
// Wait for the WebView to hand over the device-bound encryption key (stored in
|
||||||
|
// Android Keystore / iOS Keychain — see src/crypto.ts) before starting the
|
||||||
|
// server, so encryptionService picks up TOKEN_ENCRYPTION_KEY on first use.
|
||||||
cordova.channel.on('message', function (msg) {
|
cordova.channel.on('message', function (msg) {
|
||||||
if (msg && msg.type === 'encryptionKey' && typeof msg.key === 'string') {
|
if (msg && msg.type === 'encryptionKey' && typeof msg.key === 'string') {
|
||||||
process.env.TOKEN_ENCRYPTION_KEY = msg.key;
|
process.env.TOKEN_ENCRYPTION_KEY = msg.key;
|
||||||
require('./server/server.js');
|
try {
|
||||||
|
require('./server/server.cts');
|
||||||
|
} catch (err) {
|
||||||
|
reportError('server failed to start', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,20 +2,21 @@
|
||||||
"name": "nodejs-project",
|
"name": "nodejs-project",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"//": "Server-side runtime deps only (client deps ship prebuilt in dist/). Versions mirror ../../../bill-tracker. openid-client is intentionally omitted — it is ESM-only and unused in single-user local mode; scripts/prepare-local-mode-deps.js installs a CJS stub. kysely is ESM-only and gets bundled to CJS by the same script.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/server": "^13.0.0",
|
"@simplewebauthn/server": "^13.0.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.4.1",
|
"express-rate-limit": "^8.4.1",
|
||||||
|
"kysely": "^0.29.3",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "^8.0.9",
|
"nodemailer": "^9.0.3",
|
||||||
"openid-client": "^5.7.1",
|
|
||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodejs-mobile-gyp": "^0.4.0"
|
"nodejs-mobile-gyp": "^0.4.0"
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,98 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Cross-compiles better-sqlite3 for arm64-v8a and armeabi-v7a (real Android
|
|
||||||
# devices) and writes the resulting better_sqlite3.node binaries into
|
|
||||||
# nodejs-assets/nodejs-project/prebuilds/<abi>/. main.js installs the one
|
|
||||||
# matching the device's actual process.arch at runtime — see main.js's
|
|
||||||
# installBetterSqlite3Prebuild().
|
|
||||||
#
|
|
||||||
# The existing x86_64 build (emulator) in
|
|
||||||
# nodejs-assets/nodejs-project/node_modules/better-sqlite3/build/Release/
|
|
||||||
# already covers nodejs-assets/nodejs-project/prebuilds/x86_64/ via
|
|
||||||
# fix-better-sqlite3-android.sh; re-copy it manually if that build changes:
|
|
||||||
# cp nodejs-assets/nodejs-project/node_modules/better-sqlite3/build/Release/better_sqlite3.node \
|
|
||||||
# nodejs-assets/nodejs-project/prebuilds/x86_64/
|
|
||||||
#
|
|
||||||
# Requires: Android NDK (ANDROID_HOME or ANDROID_NDK_HOME set), and
|
|
||||||
# `npm install` already run in nodejs-assets/nodejs-project (so
|
|
||||||
# better-sqlite3's source + nodejs-mobile-gyp are present).
|
|
||||||
#
|
|
||||||
# Usage: ./scripts/build-better-sqlite3-arm.sh
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
PKG_SRC="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/better-sqlite3"
|
|
||||||
PREBUILDS_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/prebuilds"
|
|
||||||
GYP="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/.bin/nodejs-mobile-gyp"
|
|
||||||
NODEDIR="$MOBILE_DIR/node_modules/nodejs-mobile-cordova/libs/android/libnode"
|
|
||||||
|
|
||||||
NDK="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
|
||||||
if [[ -z "$NDK" ]]; then
|
|
||||||
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
|
||||||
[[ -n "$SDK" ]] && NDK="$(ls -d "$SDK"/ndk/*/ 2>/dev/null | sort -V | tail -1)"
|
|
||||||
fi
|
|
||||||
NDK="${NDK%/}"
|
|
||||||
|
|
||||||
if [[ ! -d "$PKG_SRC" ]]; then
|
|
||||||
echo "Error: $PKG_SRC not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -z "$NDK" || ! -d "$NDK" ]]; then
|
|
||||||
echo "Error: Android NDK not found. Set ANDROID_NDK_HOME (or ANDROID_HOME with an ndk/ subdir)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -x "$GYP" ]]; then
|
|
||||||
echo "Error: $GYP not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"
|
|
||||||
API=24 # matches android/variables.gradle minSdkVersion
|
|
||||||
|
|
||||||
# Each ABI's libnode.so ships gzipped; better-sqlite3's binding.gyp (via
|
|
||||||
# nodejs-mobile's common.gypi) needs the real .so to add it as a NEEDED entry.
|
|
||||||
for abi in arm64-v8a armeabi-v7a; do
|
|
||||||
LIBNODE_DIR="$NODEDIR/bin/$abi"
|
|
||||||
if [[ ! -f "$LIBNODE_DIR/libnode.so" && -f "$LIBNODE_DIR/libnode.so.gz" ]]; then
|
|
||||||
gunzip -k "$LIBNODE_DIR/libnode.so.gz"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
build_one() {
|
|
||||||
local abi="$1" gyp_arch="$2" cc="$3" cxx="$4"
|
|
||||||
local build_dir="$MOBILE_DIR/build-output/better-sqlite3-$abi"
|
|
||||||
|
|
||||||
echo "── Building better-sqlite3 for $abi ($gyp_arch) ──"
|
|
||||||
rm -rf "$build_dir"
|
|
||||||
mkdir -p "$build_dir"
|
|
||||||
rsync -a --exclude=build --exclude=prebuilds "$PKG_SRC/" "$build_dir/"
|
|
||||||
|
|
||||||
# v8::Object::CreationContext() was removed in newer V8; nodejs-mobile's
|
|
||||||
# bundled V8 (Node 12.19) only has the old non-checked accessor. Same patch
|
|
||||||
# as fix-better-sqlite3-android.sh.
|
|
||||||
sed -i 's/obj->GetCreationContext().ToLocalChecked()/obj->CreationContext()/' \
|
|
||||||
"$build_dir/src/util/binder.cpp"
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$build_dir"
|
|
||||||
CC="$TOOLCHAIN/bin/$cc" \
|
|
||||||
CXX="$TOOLCHAIN/bin/$cxx" \
|
|
||||||
AR="$TOOLCHAIN/bin/llvm-ar" \
|
|
||||||
GYP_DEFINES="OS=android" \
|
|
||||||
"$GYP" configure --target=12.19.0 --arch="$gyp_arch" --nodedir="$NODEDIR"
|
|
||||||
|
|
||||||
CC="$TOOLCHAIN/bin/$cc" \
|
|
||||||
CXX="$TOOLCHAIN/bin/$cxx" \
|
|
||||||
AR="$TOOLCHAIN/bin/llvm-ar" \
|
|
||||||
"$GYP" build
|
|
||||||
)
|
|
||||||
|
|
||||||
mkdir -p "$PREBUILDS_DIR/$abi"
|
|
||||||
cp "$build_dir/build/Release/better_sqlite3.node" "$PREBUILDS_DIR/$abi/better_sqlite3.node"
|
|
||||||
echo "Wrote $PREBUILDS_DIR/$abi/better_sqlite3.node"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_one arm64-v8a arm64 aarch64-linux-android${API}-clang aarch64-linux-android${API}-clang++
|
|
||||||
build_one armeabi-v7a arm armv7a-linux-androideabi${API}-clang armv7a-linux-androideabi${API}-clang++
|
|
||||||
|
|
||||||
echo "Done. Verify with: file $PREBUILDS_DIR/*/better_sqlite3.node"
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cross-compiles better-sqlite3 for the embedded Node 18.20.4 (nodejs-mobile) ABI
|
||||||
|
# for all three Android ABIs and writes the resulting better_sqlite3.node binaries
|
||||||
|
# into nodejs-assets/nodejs-project/prebuilds/<abi>/. main.js installs the one
|
||||||
|
# matching the device's actual process.arch at runtime — see
|
||||||
|
# installBetterSqlite3Prebuild().
|
||||||
|
#
|
||||||
|
# Unlike the old Node-12 build, NO binder.cpp patch is needed: Node 18's V8 10.2
|
||||||
|
# still exposes the (deprecated) v8::Object::CreationContext() that better-sqlite3
|
||||||
|
# 12.x uses, so it compiles as-is (deprecation warning only).
|
||||||
|
#
|
||||||
|
# Requires: Android NDK (ANDROID_HOME or ANDROID_NDK_HOME set), the Node 18 runtime
|
||||||
|
# staged into the plugin (scripts/install-node18-runtime.sh), and `npm install`
|
||||||
|
# already run in nodejs-assets/nodejs-project.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/build-better-sqlite3-node18.sh [abi ...] (default: all three)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NODE_TARGET="18.20.4"
|
||||||
|
API=24 # matches android/variables.gradle minSdkVersion
|
||||||
|
|
||||||
|
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PKG_SRC="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/better-sqlite3"
|
||||||
|
PREBUILDS_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/prebuilds"
|
||||||
|
GYP="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/.bin/nodejs-mobile-gyp"
|
||||||
|
NODEDIR="$MOBILE_DIR/node_modules/nodejs-mobile-cordova/libs/android/libnode"
|
||||||
|
|
||||||
|
NDK="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
if [[ -z "$NDK" ]]; then
|
||||||
|
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
||||||
|
[[ -n "$SDK" ]] && NDK="$(ls -d "$SDK"/ndk/*/ 2>/dev/null | sort -V | tail -1)"
|
||||||
|
fi
|
||||||
|
NDK="${NDK%/}"
|
||||||
|
|
||||||
|
[[ -d "$PKG_SRC" ]] || { echo "Error: $PKG_SRC not found. Run npm install in nodejs-project first." >&2; exit 1; }
|
||||||
|
[[ -n "$NDK" && -d "$NDK" ]] || { echo "Error: Android NDK not found (set ANDROID_NDK_HOME or ANDROID_HOME)." >&2; exit 1; }
|
||||||
|
[[ -x "$GYP" ]] || { echo "Error: $GYP not found. Run npm install in nodejs-project first." >&2; exit 1; }
|
||||||
|
grep -q "18" "$NODEDIR/include/node/node_version.h" || { echo "Error: $NODEDIR is not the Node 18 runtime — run scripts/install-node18-runtime.sh first." >&2; exit 1; }
|
||||||
|
|
||||||
|
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"
|
||||||
|
|
||||||
|
abi_arch() { case "$1" in arm64-v8a) echo arm64;; armeabi-v7a) echo arm;; x86_64) echo x64;; *) echo "" ;; esac; }
|
||||||
|
abi_cc() { case "$1" in
|
||||||
|
arm64-v8a) echo "aarch64-linux-android${API}-clang" ;;
|
||||||
|
armeabi-v7a) echo "armv7a-linux-androideabi${API}-clang" ;;
|
||||||
|
x86_64) echo "x86_64-linux-android${API}-clang" ;;
|
||||||
|
esac; }
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local gyp_arch cc cxx
|
||||||
|
gyp_arch="$(abi_arch "$abi")"; cc="$(abi_cc "$abi")"; cxx="${cc}++"
|
||||||
|
[[ -n "$gyp_arch" ]] || { echo "Unknown ABI: $abi" >&2; exit 1; }
|
||||||
|
|
||||||
|
# better-sqlite3's binding.gyp (via nodejs-mobile's common.gypi) needs the real
|
||||||
|
# libnode.so (not the .gz) to add it as a NEEDED entry at link time.
|
||||||
|
local libnode_dir="$NODEDIR/bin/$abi"
|
||||||
|
if [[ ! -f "$libnode_dir/libnode.so" && -f "$libnode_dir/libnode.so.gz" ]]; then
|
||||||
|
gunzip -k "$libnode_dir/libnode.so.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local build_dir="$MOBILE_DIR/build-output/better-sqlite3-$abi"
|
||||||
|
echo "── Building better-sqlite3 for $abi ($gyp_arch) against Node $NODE_TARGET ──"
|
||||||
|
rm -rf "$build_dir"; mkdir -p "$build_dir"
|
||||||
|
rsync -a --exclude=build --exclude=prebuilds "$PKG_SRC/" "$build_dir/"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$build_dir"
|
||||||
|
CC="$TOOLCHAIN/bin/$cc" CXX="$TOOLCHAIN/bin/$cxx" AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||||
|
GYP_DEFINES="OS=android" "$GYP" configure \
|
||||||
|
--target="$NODE_TARGET" --arch="$gyp_arch" --nodedir="$NODEDIR"
|
||||||
|
|
||||||
|
# nodejs-mobile's common.gypi only adds libnode.so as a NEEDED entry when
|
||||||
|
# target_arch=="x86_64", but gyp reports "x64" — so the rule never fires and
|
||||||
|
# the addon can't resolve V8/Node symbols (e.g. v8::Isolate::GetCurrent) at
|
||||||
|
# dlopen time on Android. Inject the ABI's libnode.so into the link so its
|
||||||
|
# SONAME (libnode.so) is recorded as NEEDED. (patchelf isn't required.)
|
||||||
|
local mk="build/better_sqlite3.target.mk"
|
||||||
|
if [[ -f "$mk" ]] && ! grep -q 'libnode\.so' "$mk"; then
|
||||||
|
sed -i "s#^\([[:space:]]*\)-llog\$#\1-llog $libnode_dir/libnode.so#" "$mk"
|
||||||
|
grep -q 'libnode\.so' "$mk" || { echo "Error: failed to inject libnode.so into $mk" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
CC="$TOOLCHAIN/bin/$cc" CXX="$TOOLCHAIN/bin/$cxx" AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||||
|
make -C build BUILDTYPE=Release
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$PREBUILDS_DIR/$abi"
|
||||||
|
cp "$build_dir/build/Release/better_sqlite3.node" "$PREBUILDS_DIR/$abi/better_sqlite3.node"
|
||||||
|
echo "Wrote $PREBUILDS_DIR/$abi/better_sqlite3.node"
|
||||||
|
}
|
||||||
|
|
||||||
|
ABIS=("$@")
|
||||||
|
[[ ${#ABIS[@]} -eq 0 ]] && ABIS=(x86_64 arm64-v8a armeabi-v7a)
|
||||||
|
for abi in "${ABIS[@]}"; do build_one "$abi"; done
|
||||||
|
|
||||||
|
echo "Done. Verify with: file $PREBUILDS_DIR/*/better_sqlite3.node"
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Re-applies the two patches better-sqlite3 needs to load correctly under
|
|
||||||
# nodejs-mobile (Node 12.19 / Android x86_64 emulator). Required after any
|
|
||||||
# fresh `npm install` in nodejs-assets/nodejs-project, since both patches
|
|
||||||
# target files inside node_modules.
|
|
||||||
#
|
|
||||||
# Usage: ./scripts/fix-better-sqlite3-android.sh
|
|
||||||
# Run from the mobile project root, after `npm install` has already built
|
|
||||||
# better-sqlite3 once via nodejs-mobile-gyp (so build/better_sqlite3.target.mk
|
|
||||||
# and the Release/ dir already exist).
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
PKG_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/better-sqlite3"
|
|
||||||
LIBNODE="$MOBILE_DIR/android/app/libs/cdvnodejsmobile/libnode/bin/x86_64/libnode.so"
|
|
||||||
|
|
||||||
if [[ ! -d "$PKG_DIR" ]]; then
|
|
||||||
echo "Error: $PKG_DIR not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 1. v8::Object::CreationContext() was removed in newer V8; nodejs-mobile's
|
|
||||||
# bundled V8 (Node 12.19) only has the old non-checked accessor.
|
|
||||||
BINDER="$PKG_DIR/src/util/binder.cpp"
|
|
||||||
if grep -q 'GetCreationContext().ToLocalChecked()' "$BINDER"; then
|
|
||||||
sed -i 's/obj->GetCreationContext().ToLocalChecked()/obj->CreationContext()/' "$BINDER"
|
|
||||||
echo "Patched $BINDER"
|
|
||||||
else
|
|
||||||
echo "$BINDER already uses CreationContext() (no patch needed)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. nodejs-mobile-cordova's common.gypi only adds the libnode.so NEEDED entry
|
|
||||||
# when target_arch=="x86_64", but gyp reports "x64" for 64-bit x86, so the
|
|
||||||
# rule never fires. Add it to LIBS directly so the addon can resolve V8/Node
|
|
||||||
# symbols (e.g. v8::Isolate::GetCurrent) at dlopen time on Android.
|
|
||||||
TARGET_MK="$PKG_DIR/build/better_sqlite3.target.mk"
|
|
||||||
if [[ ! -f "$TARGET_MK" ]]; then
|
|
||||||
echo "Error: $TARGET_MK not found. Run npm install to build better-sqlite3 first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -q "libnode.so" "$TARGET_MK"; then
|
|
||||||
sed -i "/^\t-llog \\\\\$/a\\\\\t$LIBNODE" "$TARGET_MK"
|
|
||||||
echo "Patched $TARGET_MK"
|
|
||||||
else
|
|
||||||
echo "$TARGET_MK already references libnode.so (no patch needed)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rebuild and relink with the patched sources/Makefile.
|
|
||||||
cd "$PKG_DIR/build"
|
|
||||||
rm -f Release/better_sqlite3.node Release/obj.target/better_sqlite3.node
|
|
||||||
make BUILDTYPE=Release better_sqlite3.node
|
|
||||||
|
|
||||||
# Keep the x86_64 prebuild (emulator) in sync — main.js picks the right one
|
|
||||||
# for the device's process.arch at runtime. arm64-v8a/armeabi-v7a prebuilds
|
|
||||||
# are produced separately by build-better-sqlite3-arm.sh.
|
|
||||||
PREBUILD_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/prebuilds/x86_64"
|
|
||||||
mkdir -p "$PREBUILD_DIR"
|
|
||||||
cp "$PKG_DIR/build/Release/better_sqlite3.node" "$PREBUILD_DIR/better_sqlite3.node"
|
|
||||||
|
|
||||||
echo "Done. Verify with: readelf -d \"$PKG_DIR/build/Release/better_sqlite3.node\" | grep libnode"
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// Makes the two ESM-only server dependencies loadable by the embedded Node 18
|
||||||
|
// (nodejs-mobile) runtime, which cannot require() ESM. Run AFTER
|
||||||
|
// `npm install` inside nodejs-assets/nodejs-project (re-run after any install).
|
||||||
|
//
|
||||||
|
// • kysely (used: routes/categories.cts -> db/kysely.cts) — bundled to a
|
||||||
|
// self-contained CommonJS file; its package entry is repointed at it.
|
||||||
|
// • openid-client (UNUSED in single-user local mode, but eagerly require()'d
|
||||||
|
// at boot via auth.cts/authOidc.cts -> oidcService.cts) — replaced with a
|
||||||
|
// CommonJS stub so boot doesn't crash. Any actual use throws.
|
||||||
|
//
|
||||||
|
// Both are loaded on the web app (Node 22, native ESM); this only affects the
|
||||||
|
// on-device copy.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const esbuild = require('esbuild'); // from the mobile project's own node_modules
|
||||||
|
|
||||||
|
const PROJECT = path.join(__dirname, '..', 'nodejs-assets', 'nodejs-project');
|
||||||
|
const NM = path.join(PROJECT, 'node_modules');
|
||||||
|
|
||||||
|
function fail(msg) {
|
||||||
|
console.error('prepare-local-mode-deps: ' + msg);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── kysely → CommonJS ────────────────────────────────────────────────────────
|
||||||
|
(function bundleKysely() {
|
||||||
|
const dir = path.join(NM, 'kysely');
|
||||||
|
if (!fs.existsSync(dir)) fail('kysely not installed — run `npm install` in nodejs-project first.');
|
||||||
|
|
||||||
|
const entry = path.join(dir, 'dist', 'index.js'); // ESM entry
|
||||||
|
const outfile = path.join(dir, 'dist', 'index.cjs');
|
||||||
|
esbuild.buildSync({
|
||||||
|
entryPoints: [entry],
|
||||||
|
bundle: true,
|
||||||
|
format: 'cjs',
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node18',
|
||||||
|
outfile,
|
||||||
|
logLevel: 'silent',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pkgPath = path.join(dir, 'package.json');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
pkg.main = './dist/index.cjs';
|
||||||
|
pkg.exports = {
|
||||||
|
'.': { require: './dist/index.cjs', import: './dist/index.js', default: './dist/index.cjs' },
|
||||||
|
'./package.json': './package.json',
|
||||||
|
};
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
console.log(' kysely -> bundled to CommonJS (dist/index.cjs)');
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── openid-client → CommonJS stub ────────────────────────────────────────────
|
||||||
|
(function stubOpenidClient() {
|
||||||
|
const dir = path.join(NM, 'openid-client');
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'package.json'),
|
||||||
|
JSON.stringify({ name: 'openid-client', version: '0.0.0-local-stub', main: 'index.js' }, null, 2) + '\n',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'index.js'),
|
||||||
|
"'use strict';\n" +
|
||||||
|
'// Local-mode stub. OIDC/SSO is not available when running on-device.\n' +
|
||||||
|
'function notSupported() { throw new Error("OIDC is not available in local mode"); }\n' +
|
||||||
|
'module.exports = new Proxy({}, { get: function () { return notSupported; } });\n',
|
||||||
|
);
|
||||||
|
console.log(' openid-client -> CommonJS stub (OIDC disabled in local mode)');
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── ICU-less \p{} regex compatibility ────────────────────────────────────────
|
||||||
|
// nodejs-mobile's Node 18 ships WITHOUT ICU, so V8 cannot resolve Unicode
|
||||||
|
// property escapes (\p{...}) — any regex using them throws "Invalid property
|
||||||
|
// name" at compile time. path-to-regexp (express 5's router) uses
|
||||||
|
// \p{ID_Start}/\p{ID_Continue} at module load to validate route-param names,
|
||||||
|
// which crashes server boot. Rewrite to ASCII-safe classes; every route param
|
||||||
|
// in this app is ASCII (:id, :userId, …). (minimatch also contains \p{} but only
|
||||||
|
// as lazy POSIX-class strings that this app's globs never trigger — left as-is.)
|
||||||
|
(function patchPathToRegexp() {
|
||||||
|
const f = path.join(NM, 'path-to-regexp', 'dist', 'index.js');
|
||||||
|
if (!fs.existsSync(f)) return;
|
||||||
|
let s = fs.readFileSync(f, 'utf8');
|
||||||
|
const before = s;
|
||||||
|
s = s.replace(/\\p\{ID_Start\}/g, 'a-zA-Z').replace(/\\p\{ID_Continue\}/g, 'a-zA-Z0-9');
|
||||||
|
if (s !== before) {
|
||||||
|
fs.writeFileSync(f, s);
|
||||||
|
console.log(' path-to-regexp -> ASCII identifier regex (ICU-less \\p{} fix)');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log('prepare-local-mode-deps: done.');
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Copies the bill-tracker backend source into mobile/nodejs-assets/nodejs-project/server/
|
# Copies the bill-tracker backend into mobile/nodejs-assets/nodejs-project/server/
|
||||||
# so it can be bundled into the Android app and run by nodejs-mobile in local mode.
|
# so it can be bundled into the Android app and run by nodejs-mobile (Node 18) in
|
||||||
|
# local mode.
|
||||||
|
#
|
||||||
|
# The backend is now TypeScript (server.cts + .cts/.mts). Node 18 can't run TS, so
|
||||||
|
# this transpiles the server's own sources to CommonJS in place (keeping filenames;
|
||||||
|
# see scripts/transpile-node18.js). Dependencies run as-is on Node 18 and are NOT
|
||||||
|
# transpiled — install them separately (see the "Next" steps printed at the end).
|
||||||
#
|
#
|
||||||
# Usage: ./scripts/sync-nodejs-project.sh [--source /path/to/bill-tracker]
|
# Usage: ./scripts/sync-nodejs-project.sh [--source /path/to/bill-tracker]
|
||||||
# Run from the mobile project root.
|
# --source defaults to ../bill-tracker (sibling directory). Run from mobile root.
|
||||||
#
|
|
||||||
# --source defaults to ../bill-tracker (sibling directory).
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
# Parse --source flag
|
|
||||||
REPO_ROOT=""
|
REPO_ROOT=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -19,14 +22,13 @@ while [[ $# -gt 0 ]]; do
|
||||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$REPO_ROOT" ]]; then
|
if [[ -z "$REPO_ROOT" ]]; then
|
||||||
REPO_ROOT="$(cd "$MOBILE_DIR/../bill-tracker" && pwd)"
|
REPO_ROOT="$(cd "$MOBILE_DIR/../bill-tracker" && pwd)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$REPO_ROOT/server.js" ]]; then
|
if [[ ! -f "$REPO_ROOT/server.cts" ]]; then
|
||||||
echo "Error: Bill Tracker source not found at $REPO_ROOT" >&2
|
echo "Error: Bill Tracker source not found at $REPO_ROOT (no server.cts)." >&2
|
||||||
echo "Use --source /path/to/bill-tracker to specify the Bill Tracker project root." >&2
|
echo "Use --source /path/to/bill-tracker to specify the project root." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -36,43 +38,41 @@ echo "Syncing backend source from $REPO_ROOT -> $DEST"
|
||||||
rm -rf "$DEST"
|
rm -rf "$DEST"
|
||||||
mkdir -p "$DEST"
|
mkdir -p "$DEST"
|
||||||
|
|
||||||
cp "$REPO_ROOT/server.js" "$DEST/"
|
cp "$REPO_ROOT/server.cts" "$DEST/"
|
||||||
|
|
||||||
for dir in db routes services middleware utils workers; do
|
# Backend source trees. `types/` is type-only (erased by the transpile) but cheap
|
||||||
cp -R "$REPO_ROOT/$dir" "$DEST/$dir"
|
# insurance; `setup/` holds firstRun.cts (required by server.cts on empty DB).
|
||||||
|
for dir in db routes services middleware utils workers setup types; do
|
||||||
|
[[ -d "$REPO_ROOT/$dir" ]] && cp -R "$REPO_ROOT/$dir" "$DEST/$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
# routes/user.js requires ../scripts/seedDemoData (relative to server/).
|
# routes/user.cts requires ../scripts/seedDemoData.cts (relative to server/).
|
||||||
mkdir -p "$DEST/scripts"
|
mkdir -p "$DEST/scripts"
|
||||||
cp "$REPO_ROOT/scripts/seedDemoData.js" "$DEST/scripts/"
|
cp "$REPO_ROOT/scripts/seedDemoData.cts" "$DEST/scripts/"
|
||||||
|
|
||||||
# Drop dev SQLite DB files and node_modules if they were copied along with db/.
|
# Seed JSON read at first-run init:
|
||||||
rm -f "$DEST"/db/*.db "$DEST"/db/*.db-* "$DEST"/db/*.db-shm "$DEST"/db/*.db-wal
|
# db/data/*.json -> carried with db/ above (database.cts reads them)
|
||||||
|
# docs/top_200_*.json -> db/migrations/versionedMigrations.cts reads ../../docs/
|
||||||
|
mkdir -p "$DEST/docs"
|
||||||
|
cp "$REPO_ROOT/docs/top_200_us_subscriptions_researched_2026-06-06.json" "$DEST/docs/"
|
||||||
|
|
||||||
|
# Drop dev SQLite DBs and any stray node_modules that rode along with db/.
|
||||||
|
rm -f "$DEST"/db/*.db "$DEST"/db/*.db-* 2>/dev/null || true
|
||||||
rm -rf "$DEST/db/node_modules"
|
rm -rf "$DEST/db/node_modules"
|
||||||
|
|
||||||
# Seed-data JSON files read by db/database.js during first-run initialization.
|
# Transpile the server's TypeScript sources to CommonJS in place (Node 18).
|
||||||
mkdir -p "$DEST/docs"
|
# Runs BEFORE dist/ is copied so the browser ESM bundle isn't touched.
|
||||||
cp "$REPO_ROOT/docs/advisory_non_bill_transaction_filters_us_ms_5000.json" "$DEST/docs/"
|
node "$MOBILE_DIR/scripts/transpile-node18.js" "$DEST"
|
||||||
cp "$REPO_ROOT/docs/top_200_us_subscriptions_researched_2026-06-06.json" "$DEST/docs/"
|
|
||||||
cp "$REPO_ROOT/docs/merchant_store_match_us_nems_online_5k_v0_2.json" "$DEST/docs/"
|
|
||||||
|
|
||||||
# nodejs-mobile-cordova 0.4.3 embeds Node 12.19, which doesn't support
|
# Built frontend, served as static files by server.cts (path.join(__dirname,'dist')).
|
||||||
# optional chaining (?.) / nullish coalescing (??) used in this project and
|
|
||||||
# in some npm dependencies (e.g. express-rate-limit). Transpile server-side
|
|
||||||
# .js files down to Node 12-compatible syntax in place. Runs before the
|
|
||||||
# frontend dist/ is copied below so its browser ESM bundle isn't rewritten
|
|
||||||
# to CJS (which breaks with "module is not defined" in the WebView).
|
|
||||||
node "$MOBILE_DIR/scripts/transpile-node12.js" "$DEST"
|
|
||||||
|
|
||||||
# Built frontend, served as static files by server.js (path.join(__dirname, 'dist')).
|
|
||||||
cp -R "$REPO_ROOT/dist" "$DEST/dist"
|
cp -R "$REPO_ROOT/dist" "$DEST/dist"
|
||||||
|
|
||||||
# Also transpile already-installed dependencies, if present (run
|
cat <<EOF
|
||||||
# `npm install` in nodejs-assets/nodejs-project after this script to pick up
|
Done syncing server source.
|
||||||
# new/updated deps, then re-run this script to transpile them).
|
|
||||||
NODE_MODULES="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules"
|
|
||||||
if [ -d "$NODE_MODULES" ]; then
|
|
||||||
node "$MOBILE_DIR/scripts/transpile-node12.js" "$NODE_MODULES"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Done. Next: cd \"$MOBILE_DIR/nodejs-assets/nodejs-project\" && npm install"
|
Next (only when deps changed):
|
||||||
|
1. cd "$MOBILE_DIR/nodejs-assets/nodejs-project" && npm install
|
||||||
|
2. node "$MOBILE_DIR/scripts/prepare-local-mode-deps.js" # kysely->CJS, stub openid-client
|
||||||
|
3. "$MOBILE_DIR/scripts/install-node18-runtime.sh" # (re)apply Node 18 runtime + bridge patch
|
||||||
|
4. "$MOBILE_DIR/scripts/build-better-sqlite3-node18.sh" # better-sqlite3 for the Node 18 ABI
|
||||||
|
EOF
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
// Transpiles all .js files under a directory (in place) to syntax supported
|
|
||||||
// by nodejs-mobile-cordova's embedded Node 12.19 — primarily optional
|
|
||||||
// chaining (?.) and nullish coalescing (??) used by some npm dependencies
|
|
||||||
// and by this project's own source.
|
|
||||||
//
|
|
||||||
// Usage: node transpile-node12.js <dir> [<dir> ...]
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Resolve esbuild from this project's own node_modules (no longer parent repo)
|
|
||||||
const esbuild = require('esbuild');
|
|
||||||
|
|
||||||
const SKIP_DIRS = new Set(['.bin']);
|
|
||||||
|
|
||||||
function walk(dir, cb) {
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
if (SKIP_DIRS.has(entry.name)) continue;
|
|
||||||
const full = path.join(dir, entry.name);
|
|
||||||
if (entry.isSymbolicLink()) continue;
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walk(full, cb);
|
|
||||||
} else if (entry.name.endsWith('.js') || entry.name.endsWith('.cjs') || entry.name === 'package.json') {
|
|
||||||
cb(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
let pkgCount = 0;
|
|
||||||
for (const dir of process.argv.slice(2)) {
|
|
||||||
if (!fs.existsSync(dir)) continue;
|
|
||||||
walk(dir, (file) => {
|
|
||||||
if (path.basename(file) === 'package.json') {
|
|
||||||
// esbuild converts ESM (import/export) to CJS (module.exports) below,
|
|
||||||
// but Node still treats .js files as ES modules if the nearest
|
|
||||||
// package.json has "type": "module" — drop that so the converted
|
|
||||||
// CJS output is loaded correctly under Node 12.
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
||||||
if (pkg.type === 'module') {
|
|
||||||
delete pkg.type;
|
|
||||||
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
|
|
||||||
pkgCount++;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = fs.readFileSync(file, 'utf8');
|
|
||||||
let out;
|
|
||||||
try {
|
|
||||||
out = esbuild.transformSync(src, { target: 'node12', format: 'cjs', loader: 'js' }).code;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Skipping ${file}: ${err.message.split('\n')[0]}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Node 12 doesn't understand the "node:" builtin-module prefix
|
|
||||||
// (added in Node 14.18/16), used by some dependencies (e.g. express-rate-limit).
|
|
||||||
out = out.replace(/require\((['"])node:([a-z/_-]+)\1\)/g, "require($1$2$1)");
|
|
||||||
|
|
||||||
if (out !== src) {
|
|
||||||
fs.writeFileSync(file, out);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(`Transpiled ${count} file(s) and dropped "type": "module" from ${pkgCount} package.json file(s) for Node 12 compatibility.`);
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// Transpiles the Bill Tracker server's TypeScript sources to plain CommonJS for
|
||||||
|
// the embedded Node 18.20.4 runtime (nodejs-mobile). Node 18 runs modern JS
|
||||||
|
// syntax natively, so — unlike the old Node-12 pass — this ONLY strips
|
||||||
|
// TypeScript; it does not rewrite dependency syntax and does not touch
|
||||||
|
// node_modules (their JS runs as-is on Node 18).
|
||||||
|
//
|
||||||
|
// The server is CommonJS-with-TS: files use require()/module.exports plus
|
||||||
|
// `import type`/`as typeof import()` for types, and require() each other with
|
||||||
|
// EXPLICIT `.cts`/`.mts` extensions (115 .cts + 40 .mts call sites). We therefore
|
||||||
|
// transpile each .cts/.mts file IN PLACE, keeping its original name: esbuild
|
||||||
|
// strips the types and emits CJS, and Node 18's CommonJS loader loads unknown
|
||||||
|
// extensions (.cts/.mts) through the default `.js` handler as CommonJS — so every
|
||||||
|
// explicit-extension require() path keeps resolving with zero path rewriting.
|
||||||
|
//
|
||||||
|
// Usage: node transpile-node18.js <serverDir> [<dir> ...]
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const esbuild = require('esbuild');
|
||||||
|
|
||||||
|
const SKIP_DIRS = new Set(['.bin', 'node_modules', 'dist']);
|
||||||
|
|
||||||
|
function loaderFor(file) {
|
||||||
|
if (/\.(cts|mts|ts)$/.test(file)) return 'ts';
|
||||||
|
if (/\.(cjs|js)$/.test(file)) return 'js';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, cb) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
if (SKIP_DIRS.has(entry.name)) continue;
|
||||||
|
if (entry.isSymbolicLink()) continue;
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full, cb);
|
||||||
|
else if (loaderFor(entry.name)) cb(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
let failed = 0;
|
||||||
|
for (const dir of process.argv.slice(2)) {
|
||||||
|
if (!fs.existsSync(dir)) continue;
|
||||||
|
walk(dir, (file) => {
|
||||||
|
const src = fs.readFileSync(file, 'utf8');
|
||||||
|
let out;
|
||||||
|
try {
|
||||||
|
out = esbuild.transformSync(src, {
|
||||||
|
loader: loaderFor(file),
|
||||||
|
format: 'cjs',
|
||||||
|
target: 'node18',
|
||||||
|
sourcefile: file,
|
||||||
|
}).code;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`FAILED ${file}: ${err.message.split('\n')[0]}`);
|
||||||
|
failed++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (out !== src) {
|
||||||
|
fs.writeFileSync(file, out);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
console.error(`Transpile finished with ${failed} failure(s).`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`Transpiled ${count} file(s) to CommonJS for Node 18.`);
|
||||||
Loading…
Reference in New Issue