/**
* Copyright (c) 2025 BlackBerry Limited. All Rights Reserved.
*
* BlackBerry Dynamics Capacitor Helper Utilities
*
* PURPOSE:
* This module provides utility functions used across all BlackBerry Capacitor hooks.
* It handles file operations, condition checking, logging, and common setup tasks
* required for BlackBerry Dynamics integration.
*
* KEY FUNCTIONS:
*
* 1. debugLog(message, hookName):
* - Centralized logging system for all hooks
* - Writes to both console and debug.log file
* - Includes timestamps and hook identification
*
*
* 2. replaceAndSave(filePath, replacements, options):
* - Safe file modification with backup and validation
* - Supports multiple replacements in single operation
* - Includes conditional replacement based on existing content
*
* 3. Platform-specific utilities:
* - patchCAPBridgeViewController(): iOS Capacitor bridge patching
* - addAssertDeploymentTarget(): iOS deployment target configuration
* - updateDevelopmentInfoJson(): Development tools integration
*
* USAGE:
* - Imported by bbdCapacitorInstall.js, afterUpdate.js, and other hooks
* - Provides consistent behavior across all BlackBerry setup operations
* - Enables centralized debugging and error handling
*/
import path from "path";
import fs from "fs";
import { execSync } from "child_process";
import {
registerGDStateChangeHandler,
notificationCenter,
requireHelperPhrase,
capacitorPodsHelperPhrase,
postInstallPhrase,
assertDeploymentTargetReplacePhrase,
headers,
linkerFlags,
loadWebView,
} from "./constants.js";
const projectRoot = process.env.INIT_CWD,
packageJson = JSON.parse(
fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8")
);
// Debug logging configuration - enables/disables all hook logging
const BBD_DEBUG_ENABLED = false;
/**
* Centralized debug logging function for all BlackBerry hooks
*
* @param {string} message - The debug message to log
* @param {string} hookName - Name of the hook/script calling this function
*
* Features:
* - Writes to both console and debug.log file
* - Includes ISO timestamp for precise timing analysis
* - Uses hook name as prefix for easy identification
* - Maintains consistent format across all hooks
*/
export const debugLog = (message, hookName = "HELPER") => {
if (!BBD_DEBUG_ENABLED) return;
const timestamp = new Date().toLocaleString();
const logMessage = `[DEBUG] ${timestamp} - [${hookName}] ${message}\n`;
// Log to console
console.log(`[DEBUG] ${timestamp} - [${hookName}] ${message}`);
// Log to file
const debugLogPath = path.join(projectRoot, "debug.log");
fs.appendFileSync(debugLogPath, logMessage, "utf-8");
};
const cAPBridgeViewControllerPath = path.join(
projectRoot,
"node_modules",
"@capacitor",
"ios",
"Capacitor",
"Capacitor",
"CAPBridgeViewController.swift"
);
export const getPackageNameFromAndroidManifest = (pathToAndroidManifest) => {
const androidManifestContent = fs.readFileSync(
pathToAndroidManifest,
"utf-8"
),
startIndexOfPackageString =
androidManifestContent.indexOf(
'"',
androidManifestContent.indexOf("package=")
) + 1,
endIndexOfPackageString = androidManifestContent.indexOf(
'"',
startIndexOfPackageString
);
return androidManifestContent.substring(
startIndexOfPackageString,
endIndexOfPackageString
);
};
export const addAttributeToXmlElement = (element, attributeToAdd, xml) => {
if (!xml.includes(attributeToAdd)) {
const startIndexOfElementTag = xml.indexOf("<" + element),
endIndexOfElementStartLine = xml.indexOf("\n", startIndexOfElementTag),
nextInlineAttribute = xml.substring(
startIndexOfElementTag + 1 + element.length,
endIndexOfElementStartLine
),
elementIdentationsNumber =
(startIndexOfElementTag -
xml.lastIndexOf("\n", startIndexOfElementTag)) /
4,
attributeIndentation = "\t\t".repeat(elementIdentationsNumber + 1);
xml = xml.replace(
element,
element + "\n" + attributeIndentation + attributeToAdd
);
if (nextInlineAttribute.trim()) {
xml = xml.replace(
nextInlineAttribute,
"\n" + attributeIndentation + nextInlineAttribute.trim()
);
}
return xml;
}
return xml;
};
export const updateLinkerFlags = () => {
for (const [key, value] of Object.entries(linkerFlags)) {
if ("cordova-plugin-bbd-" + key in packageJson.dependencies) {
addLinkerForBuildType("debug", value);
addLinkerForBuildType("release", value);
}
}
};
/**
* Patches Capacitor's iOS bridge controller for BlackBerry integration
*
* Modifies CAPBridgeViewController.swift to:
* - Add BlackBerry headers and imports
* - Register GD state change handlers
* - Initialize BlackBerry notification center
*
* This enables BlackBerry Dynamics to integrate with Capacitor's iOS bridge
*
* IDEMPOTENT: Safe to run multiple times - checks if patches already exist
*/
export const patchCAPBridgeViewController = () => {
if (!fs.existsSync(cAPBridgeViewControllerPath)) {
debugLog(`CAPBridgeViewController not found at: ${cAPBridgeViewControllerPath}`, "helper.js");
return;
}
const fileContent = fs.readFileSync(cAPBridgeViewControllerPath, "utf-8");
// Check if BlackBerry patches are already applied
const hasBlackBerryHeaders = fileContent.includes(headers.BlackBerry);
const hasNotificationCenter = fileContent.includes(notificationCenter);
const hasStateChangeHandler = fileContent.includes(registerGDStateChangeHandler[0]);
if (hasBlackBerryHeaders && hasNotificationCenter && hasStateChangeHandler) {
debugLog(`CAPBridgeViewController already patched - skipping`, "helper.js");
return;
}
debugLog(`Applying BlackBerry patches to CAPBridgeViewController`, "helper.js");
replaceAndSave(cAPBridgeViewControllerPath, [
[headers.Cordova, `${headers.Cordova}\n${headers.BlackBerry}`],
[loadWebView, notificationCenter],
[
`// MARK: - Initialization`,
`${registerGDStateChangeHandler.join("\n")}\n\t// MARK: - Initialization`,
],
]);
debugLog(`CAPBridgeViewController patching completed`, "helper.js");
};
export const cleanUpCAPBridgeViewController = () => {
replaceAndSave(cAPBridgeViewControllerPath, [
[`${headers.Cordova}\n${headers.BlackBerry}`, headers.Cordova],
[notificationCenter, loadWebView],
[registerGDStateChangeHandler.join("\n"), ""],
]);
};
export const addAssertDeploymentTarget = (capacitorPodFile) => {
let podFileContent = fs
.readFileSync(capacitorPodFile, { encoding: "utf-8" })
.toString();
if (podFileContent.includes("assertDeploymentTarget(installer)")) {
podFileContent = podFileContent.replace(
assertDeploymentTargetReplacePhrase,
""
);
}
if (
!podFileContent.includes(requireHelperPhrase) &&
!podFileContent.includes(postInstallPhrase)
) {
podFileContent = podFileContent + postInstallPhrase;
podFileContent = podFileContent.replace(
capacitorPodsHelperPhrase,
`${capacitorPodsHelperPhrase}\n${requireHelperPhrase}`
);
fs.writeFileSync(capacitorPodFile, podFileContent, "utf-8");
}
};
export const removeAssertDeploymentTarget = (capacitorPodFile) => {
replaceAndSave(capacitorPodFile, [
[requireHelperPhrase, ""],
[postInstallPhrase, assertDeploymentTargetReplacePhrase],
]);
};
/**
* Safe file modification utility with validation and backup
*
* @param {string} filePath - Path to file to modify
* @param {Array} collection - Array of [search, replace] pairs or regex replacements
* @param {Object} options - Configuration options
* @param {string} options.replacementTextToCheck - Text that must exist to proceed
* @param {boolean} options.revert - Whether to reverse the replacements
*
* Features:
* - Validates file exists before modification
* - Supports both string and regex replacements
* - Conditional replacement based on existing content
* - Safe error handling with detailed logging
*/
export const replaceAndSave = (
filePath,
collection,
{ replacementTextToCheck = "", revert = false } = {}
) => {
if (!fs.existsSync(filePath)) {
throw new Error(`File not exists at path ${filePath}`);
}
const encoding = { encoding: "utf8" };
let fileContent = fs.readFileSync(filePath, encoding);
if (
!replacementTextToCheck ||
(replacementTextToCheck && !fileContent.includes(replacementTextToCheck)) ||
revert
) {
for (const [target, replacement] of collection) {
fileContent = revert
? fileContent.replace(replacement, target)
: fileContent.replace(target, replacement);
}
fs.writeFileSync(filePath, fileContent, encoding);
}
};
function addLinkerForBuildType(buildType, linker) {
const xcconfigPath = path.join(
projectRoot,
"ios",
"App",
"Pods",
"Target Support Files",
"Pods-App",
"Pods-App." + buildType + ".xcconfig"
);
if (fs.existsSync(xcconfigPath)) {
replaceAndSave(xcconfigPath, [
[
'-framework "BlackBerryDynamics" ',
'-framework "BlackBerryDynamics" ' + linker,
],
]);
}
}
function getBlackBerryDynamicsPodPhrase(context) {
const [match] = context.match(
/pod 'BlackBerryDynamics', (:podspec|:path) => '(.+)'/
);
return match;
}
function addAfter(phrase, newPhrase) {
return `${phrase}\n\t${newPhrase}`;
}
/**
* Updates development tools information for BlackBerry integration
*
* Creates development-tools-info.json files in both Android and iOS projects
* containing framework version information, Ionic details, and BlackBerry SDK version.
* This information is used by BlackBerry development tools and debugging utilities.
*/
export const updateDevelopmentInfoJson = () => {
const cordovaInfoJsonPath = path.join(
projectRoot,
"node_modules",
"capacitor-plugin-bbd-base",
"assets",
"development-tools-info.json"
),
cordovaInfoJson = JSON.parse(fs.readFileSync(cordovaInfoJsonPath, "utf8")),
updatedCordovaInfo = getCordovaFrameworkInfo();
// DEVNOTE: add constant value of bbdSdkForCordovaVersion, that was already set on Jenkins job during building 'Base' plugin
updatedCordovaInfo.framework.bbdSdkForCordovaVersion =
cordovaInfoJson.framework.bbdSdkForCordovaVersion;
//create development-tools-info.json for android if platform is added
if (fs.existsSync(path.join(projectRoot, "android"))) {
const targetAndroidDirectory = path.join(
projectRoot,
"android",
"app",
"src",
"main",
"assets"
);
if (!fs.existsSync(targetAndroidDirectory)) {
fs.mkdirSync(targetAndroidDirectory);
}
const androidCordovaInfoJsonPath = path.join(
projectRoot,
"android",
"app",
"src",
"main",
"assets",
"development-tools-info.json"
);
fs.cpSync(
cordovaInfoJsonPath,
path.join(targetAndroidDirectory, "development-tools-info.json")
);
if (fs.existsSync(androidCordovaInfoJsonPath)) {
storeCordovaFrameworkInfoInJson(
updatedCordovaInfo,
androidCordovaInfoJsonPath
);
}
}
//create development-tools-info.json for ios if platform is added
if (fs.existsSync(path.join(projectRoot, "ios"))) {
const targetiOSDirectory = path.join(
projectRoot,
"ios",
"App",
"App",
"Resources"
);
if (!fs.existsSync(targetiOSDirectory)) {
fs.mkdirSync(targetiOSDirectory);
}
fs.cpSync(
cordovaInfoJsonPath,
path.join(targetiOSDirectory, "development-tools-info.json")
);
const iosCordovaInfoJsonPath = path.join(
projectRoot,
"ios",
"App",
"App",
"Resources",
"development-tools-info.json"
);
if (fs.existsSync(iosCordovaInfoJsonPath)) {
storeCordovaFrameworkInfoInJson(
updatedCordovaInfo,
iosCordovaInfoJsonPath
);
}
}
};
export const patchXCFrameworkLinkerFlagsFix = (capacitorCliUpdateJsPath) => {
debugLog("Applying complete getLinkerFlags function replacement", "bbdCapacitorInstall.js");
// Find and replace the entire getLinkerFlags function
const originalFunction = `function getLinkerFlags(config) {
var _a;
if ((_a = config.app.extConfig.ios) === null || _a === void 0 ? void 0 : _a.cordovaLinkerFlags) {
return \`\\n s.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '\${config.app.extConfig.ios.cordovaLinkerFlags.join(' ')}' }\`;
}
return '';
}`;
const blackberryFunction = `function getLinkerFlags(config) {
var _a;
let flags = '-all_load'; // BlackBerry requires -all_load
if ((_a = config.app.extConfig.ios) === null || _a === void 0 ? void 0 : _a.cordovaLinkerFlags) {
// Combine user flags with BlackBerry required flags
const userFlags = config.app.extConfig.ios.cordovaLinkerFlags.join(' ');
flags = userFlags.includes('-all_load') ? userFlags : \`\${userFlags} -all_load\`;
}
return \`\\n s.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '\${flags}' }\`;
}`;
replaceAndSave(
capacitorCliUpdateJsPath,
[
[originalFunction, blackberryFunction]
]
);
}
const getCordovaVersion = () => {
const cordovaCmd = process.env.CORDOVA_BIN || "cordova";
const osEol = "\n";
return execSync(cordovaCmd + " -v")
.toString()
.split(osEol)[0]
.trim();
};
const getIonicInfo = () => {
var IONIC_INFO_KEYS = {
ionicCli: "Ionic CLI",
capCli: "Capacitor CLI",
framework: "Ionic Framework",
};
try {
const projectPath = projectRoot;
var ionicInfoJson = execSync("ionic info --json ", {
stdio: "pipe",
cwd: projectPath,
}).toString(),
ionicInfoList = JSON.parse(ionicInfoJson),
ionicFramework = ionicInfoList.find(function (paramsObj) {
return (
paramsObj.key === IONIC_INFO_KEYS.framework ||
paramsObj.name === IONIC_INFO_KEYS.framework
);
});
if (ionicFramework) {
var ionicCliValue,
capCliValue,
ionicFrameworkValue,
ionicProjectTypeValue = execSync("ionic config get type --no-color", {
cwd: projectPath,
})
.toString()
.trim()
.replace(/[/'"]+/g, "")
.replace("\u001b[32m", "")
.replace("\u001b[39m", "");
ionicInfoList.forEach(function (ionicInfo) {
Object.keys(ionicInfo).forEach(function (key) {
switch (ionicInfo[key]) {
case IONIC_INFO_KEYS.ionicCli:
ionicCliValue = ionicInfo.value;
break;
case IONIC_INFO_KEYS.framework:
ionicFrameworkValue = ionicInfo.value;
break;
case IONIC_INFO_KEYS.capCli:
capCliValue = ionicInfo.value;
break;
default:
break;
}
});
});
}
} catch (e) {
// Ionic is not installed.
// It is optional so we shouldn't do any actions here
}
return {
cli: ionicCliValue || "not installed",
framework: ionicFrameworkValue || "not installed",
capacitor: capCliValue || "not installed",
type: ionicProjectTypeValue || "not installed",
};
};
const getCordovaFrameworkInfo = () => {
return {
framework: {
name: "Cordova",
bbdSdkForCordovaVersion: "",
version: getCordovaVersion(),
ionic: getIonicInfo(),
},
};
};
const storeCordovaFrameworkInfoInJson = (
cordovaInfoObj,
cordovaInfoJsonPath
) => {
fs.writeFileSync(
cordovaInfoJsonPath,
JSON.stringify(cordovaInfoObj, null, 2),
"utf8"
);
};