airborne-devkit
Version:
Cli for Airborne
338 lines (299 loc) • 9.83 kB
JavaScript
import {
executeReactNativeBundleCommand,
readDirectoryRecursive,
} from "./file.js";
import { fileURLToPath } from "url";
import { dirname } from "path";
import fs from "fs";
import path from "path";
import { promptWithType } from "./prompt.js";
import { execSync } from "child_process";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export async function readReleaseConfig(directory_path, platform, namespace) {
let configPath;
if (platform === "android") {
configPath = path.join(
directory_path,
platform,
"app",
"src",
"main",
"assets",
namespace,
"release_config.json"
);
} else {
configPath = path.join(directory_path, platform, "release_config.json");
}
if (!fs.existsSync(configPath)) {
throw new Error(`❌ Release config not found at ${configPath}`);
}
try {
const configContent = fs.readFileSync(configPath, "utf8");
return JSON.parse(configContent);
} catch (error) {
console.error("❌ Failed to read release config:", error.message);
throw error;
}
}
export async function fillReleaseConfigOptions(options = {}) {
const questions = [
{
key: "boot_timeout",
question:
"\nPlease enter the boot timeout in milliseconds (default: 4000): ",
expectedType: "number",
defaultValue: 4000,
},
{
key: "release_config_timeout",
question:
"\nPlease enter the release config timeout in milliseconds (default: 4000): ",
expectedType: "number",
defaultValue: 4000,
},
];
const result = { ...options };
const getNested = (obj, path) =>
path.split(".").reduce((acc, k) => (acc ? acc[k] : undefined), obj);
const setNested = (obj, path, value) => {
const parts = path.split(".");
let temp = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (!temp[parts[i]]) temp[parts[i]] = {};
temp = temp[parts[i]];
}
temp[parts[parts.length - 1]] = value;
};
for (const { key, question, expectedType, defaultValue } of questions) {
let value;
if (getNested(options, key) !== undefined) {
value = getNested(options, key);
} else if (question) {
value = await promptWithType(question, expectedType, defaultValue);
}
if (value !== undefined) {
setNested(result, key, value);
}
}
return result;
}
export async function createLocalReleaseConfig(
airborneConfig,
options,
platform
) {
try {
const entry_file = airborneConfig.js_entry_file || "index.js";
const index_file_path =
airborneConfig[platform].index_file_path || `index.${platform}.bundle`;
const build_folder = path.join(platform, "build", "generated", "airborne");
const fullBuildFolderPath = path.join(options.directory_path, build_folder);
if (!fs.existsSync(fullBuildFolderPath)) {
fs.mkdirSync(fullBuildFolderPath, { recursive: true });
}
const command = `cd ${options.directory_path} && npx react-native bundle --platform ${platform} --dev false --entry-file ${entry_file} --bundle-output ${build_folder}/${index_file_path} --assets-dest ${build_folder}`;
const bundleResult = await executeReactNativeBundleCommand(command);
if (!bundleResult.success) {
throw new Error(
`React Native bundle command failed: ${bundleResult.error}`
);
}
const baseDir = path.isAbsolute(options.directory_path)
? options.directory_path
: path.join(process.cwd(), options.directory_path);
const remotebundlePath = path.join(baseDir, build_folder);
const remotebundleContents = readDirectoryRecursive(remotebundlePath);
const filledOptions = await fillReleaseConfigOptions(options);
const releaseConfig = {
version: "",
config: {
version: "",
boot_timeout: filledOptions.boot_timeout,
release_config_timeout: filledOptions.release_config_timeout,
properties: {},
},
package: {
version: "",
prooerties: {},
index: {
file_path: airborneConfig[platform].index_file_path,
url: "",
},
important: remotebundleContents
.filter(
(item) => item.path !== airborneConfig[platform].index_file_path
)
.map((item) => {
return {
file_path: item.path,
url: "",
};
}),
lazy: [],
},
resources: [],
};
await writeReleaseConfig(
releaseConfig,
platform,
airborneConfig.namespace,
filledOptions.directory_path
);
} catch (err) {
console.error("❌ Failed to create local release config:", err.message);
}
}
export async function writeReleaseConfig(
releaseConfig,
platform,
namespace,
directory_path
) {
try {
let configDir;
if (platform === "android") {
configDir = path.join(
directory_path,
platform,
"app",
"src",
"main",
"assets",
namespace
);
} else {
configDir = path.join(directory_path, platform);
}
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, {
recursive: true,
});
}
const configPath = path.join(configDir, "release_config.json");
fs.writeFileSync(
configPath,
JSON.stringify(releaseConfig, null, 2),
"utf8"
);
console.log(`✅ Release config written to ${configPath}`);
if (platform === "ios") {
console.log("Running ruby script for ios");
const rubyScriptPath = path.join(__dirname, "../", "bundleRC.rb");
const rubyCommand = `ruby "${rubyScriptPath}"`;
try {
execSync(rubyCommand, { stdio: "inherit", cwd: directory_path });
console.log("✅ Ruby script executed successfully");
} catch (error) {
console.error("❌ Ruby script execution failed:", error.message);
}
}
} catch (err) {
console.error("❌ Failed to write release config:", err.message);
}
}
export async function updateLocalReleaseConfig(
airborneConfig,
options,
platform
) {
try {
const entry_file = airborneConfig.js_entry_file || "index.js";
const index_file_path =
airborneConfig[platform].index_file_path || `index.${platform}.bundle`;
const build_folder = path.join(platform, "build", "generated", "airborne");
const fullBuildFolderPath = path.join(options.directory_path, build_folder);
if (!fs.existsSync(fullBuildFolderPath)) {
fs.mkdirSync(fullBuildFolderPath, { recursive: true });
}
// empty the folder
fs.readdirSync(fullBuildFolderPath).forEach((file) => {
const curPath = path.join(fullBuildFolderPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
fs.rmSync(curPath, { recursive: true, force: true }); // remove folder recursively
} else {
fs.unlinkSync(curPath); // remove file
}
});
const command = `cd ${options.directory_path} && npx react-native bundle --platform ${platform} --dev false --entry-file ${entry_file} --bundle-output ${build_folder}/${index_file_path} --assets-dest ${build_folder}`;
const bundleResult = await executeReactNativeBundleCommand(command);
if (!bundleResult.success) {
throw new Error(
`React Native bundle command failed: ${bundleResult.error}`
);
}
const baseDir = path.isAbsolute(options.directory_path)
? options.directory_path
: path.join(process.cwd(), options.directory_path);
const remotebundlePath = path.join(baseDir, build_folder);
const remotebundleContents = readDirectoryRecursive(remotebundlePath);
const existingReleaseConfig = await readReleaseConfig(
options.directory_path,
options.platform,
airborneConfig.namespace
);
const releaseConfig = {
version: existingReleaseConfig?.version || "",
config: {
version: existingReleaseConfig?.config?.version || "",
boot_timeout:
options.boot_timeout ?? existingReleaseConfig?.config?.boot_timeout,
release_config_timeout:
options.release_config_timeout ??
existingReleaseConfig?.config?.release_config_timeout,
properties: existingReleaseConfig?.config?.properties || {},
},
package: {
version: existingReleaseConfig?.package?.version || "",
properties: existingReleaseConfig?.package?.properties || {},
index: {
file_path: airborneConfig[options.platform].index_file_path,
url: existingReleaseConfig?.package?.index?.url || "",
},
important: remotebundleContents
.filter(
(item) =>
item.path !== airborneConfig[options.platform].index_file_path
)
.map((item) => ({
file_path: item.path,
url: "",
})),
lazy: existingReleaseConfig?.package?.lazy || [],
},
resources: existingReleaseConfig?.resources || [],
};
await writeReleaseConfig(
releaseConfig,
platform,
airborneConfig.namespace,
options.directory_path
);
} catch (err) {
console.error("❌ Failed to create local release config:", err.message);
}
}
export async function releaseConfigExists(directoryPath, platform, namespace) {
try {
let configPath;
if (platform === "android") {
configPath = path.join(
directoryPath,
platform,
"app",
"src",
"main",
"assets",
namespace,
"release_config.json"
);
} else {
configPath = path.join(directoryPath, platform, "release_config.json");
}
await fs.promises.access(configPath);
return true;
} catch (error) {
return false;
}
}