airborne-devkit 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ import { readFileMapping } from "./file.js";
2
+ import { CreatePackageAction } from "airborne-core-cli/action";
3
+ import { writeReleaseConfig } from "./release.js";
4
+
5
+ export async function createPackageFromLocalRelease(
6
+ airborneConfig,
7
+ releaseConfig
8
+ ) {
9
+ try {
10
+ const pkg = releaseConfig.package;
11
+
12
+ if (!pkg.index?.file_path) {
13
+ throw new Error("Index file missing in package.");
14
+ }
15
+
16
+ const indexFilePath = pkg.index.file_path;
17
+ const indexMapping = await readFileMapping(
18
+ airborneConfig.directory_path,
19
+ indexFilePath,
20
+ airborneConfig.tag
21
+ );
22
+ if (!indexMapping) {
23
+ throw new Error(`Missing upload for index file: ${indexFilePath}`);
24
+ }
25
+ const index_id = indexMapping.id;
26
+
27
+ const files = [];
28
+ if (Array.isArray(pkg.important)) files.push(...pkg.important);
29
+ if (Array.isArray(pkg.lazy)) files.push(...pkg.lazy);
30
+ if (Array.isArray(releaseConfig.resources)) {
31
+ files.push(...releaseConfig.resources);
32
+ }
33
+
34
+ const file_ids = [];
35
+ for (const { file_path, _ } of files) {
36
+ const mapping = await readFileMapping(
37
+ airborneConfig.directory_path,
38
+ file_path,
39
+ airborneConfig.tag
40
+ );
41
+ if (!mapping) {
42
+ throw new Error(`Missing mapping for file: ${file_path}`);
43
+ }
44
+ file_ids.push(mapping.id);
45
+ }
46
+
47
+ // Prepare package creation options
48
+ const createPackageOptions = {
49
+ index: index_id,
50
+ organisation: airborneConfig.organisation,
51
+ application: airborneConfig.namespace,
52
+ token: airborneConfig.token,
53
+ tag: airborneConfig.tag,
54
+ files: file_ids,
55
+ };
56
+
57
+ const packg = await CreatePackageAction(null, createPackageOptions);
58
+ releaseConfig.package.version = packg.version.toString();
59
+ await writeReleaseConfig(
60
+ releaseConfig,
61
+ airborneConfig.platform,
62
+ airborneConfig.namespace,
63
+ airborneConfig.directory_path
64
+ );
65
+ } catch (err) {
66
+ console.error("Error creating package from local release:", err.message);
67
+ }
68
+ }
@@ -0,0 +1,81 @@
1
+ import readline from "readline";
2
+
3
+ function createPromptInterface() {
4
+ return readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+ }
9
+
10
+ export function promptUser(question) {
11
+ return new Promise((resolve) => {
12
+ const rl = createPromptInterface();
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolve(answer.trim());
16
+ });
17
+ });
18
+ }
19
+
20
+ export async function promptWithType(
21
+ question,
22
+ expectedType,
23
+ defaultValue,
24
+ retries = 2
25
+ ) {
26
+ for (let attempt = 0; attempt <= retries; attempt++) {
27
+ const answer = (await promptUser(question)).trim();
28
+
29
+ // 1️⃣ Use default if input is empty and default is provided
30
+ if (answer === "" && defaultValue !== undefined) {
31
+ return defaultValue;
32
+ }
33
+
34
+ let isValid = false;
35
+ let parsed = answer;
36
+
37
+ if (Array.isArray(expectedType)) {
38
+ const lower = answer.toLowerCase();
39
+ isValid = expectedType.map((v) => v.toLowerCase()).includes(lower);
40
+ if (isValid) parsed = lower;
41
+ } else {
42
+ switch (expectedType) {
43
+ case "number":
44
+ parsed = Number(answer);
45
+ isValid = answer !== "" && !isNaN(parsed); // empty string is invalid
46
+ break;
47
+ case "boolean":
48
+ if (["y", "yes", "true"].includes(answer.toLowerCase())) {
49
+ parsed = true;
50
+ isValid = true;
51
+ } else if (["n", "no", "false"].includes(answer.toLowerCase())) {
52
+ parsed = false;
53
+ isValid = true;
54
+ }
55
+ break;
56
+ case "string":
57
+ parsed = answer;
58
+ isValid = answer.length > 0; // empty string invalid if no default
59
+ break;
60
+ default:
61
+ throw new Error(`Unsupported expectedType: ${expectedType}`);
62
+ }
63
+ }
64
+
65
+ if (isValid) {
66
+ return parsed;
67
+ } else if (attempt < retries) {
68
+ const expectedMsg = Array.isArray(expectedType)
69
+ ? `one of: ${expectedType.join(", ")}`
70
+ : expectedType;
71
+ console.log(`❌ Invalid input. Expected ${expectedMsg}. Try again.`);
72
+ }
73
+ }
74
+
75
+ const expectedMsg = Array.isArray(expectedType)
76
+ ? `one of: ${expectedType.join(", ")}`
77
+ : expectedType;
78
+ throw new Error(
79
+ `Failed to provide valid ${expectedMsg} after ${retries + 1} attempts.`
80
+ );
81
+ }
@@ -0,0 +1,337 @@
1
+ import {
2
+ executeReactNativeBundleCommand,
3
+ readDirectoryRecursive,
4
+ } from "./file.js";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname } from "path";
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { promptWithType } from "./prompt.js";
10
+ import { execSync } from "child_process";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ export async function readReleaseConfig(directory_path, platform, namespace) {
16
+ let configPath;
17
+
18
+ if (platform === "android") {
19
+ configPath = path.join(
20
+ directory_path,
21
+ platform,
22
+ "app",
23
+ "src",
24
+ "main",
25
+ "assets",
26
+ namespace,
27
+ "release_config.json"
28
+ );
29
+ } else {
30
+ configPath = path.join(directory_path, platform, "release_config.json");
31
+ }
32
+
33
+ if (!fs.existsSync(configPath)) {
34
+ throw new Error(`❌ Release config not found at ${configPath}`);
35
+ }
36
+
37
+ try {
38
+ const configContent = fs.readFileSync(configPath, "utf8");
39
+ return JSON.parse(configContent);
40
+ } catch (error) {
41
+ console.error("❌ Failed to read release config:", error.message);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ export async function fillReleaseConfigOptions(options = {}) {
47
+ const questions = [
48
+ {
49
+ key: "boot_timeout",
50
+ question:
51
+ "\nPlease enter the boot timeout in milliseconds (default: 4000): ",
52
+ expectedType: "number",
53
+ defaultValue: 4000,
54
+ },
55
+ {
56
+ key: "release_config_timeout",
57
+ question:
58
+ "\nPlease enter the release config timeout in milliseconds (default: 4000): ",
59
+ expectedType: "number",
60
+ defaultValue: 4000,
61
+ },
62
+ ];
63
+
64
+ const result = { ...options };
65
+
66
+ const getNested = (obj, path) =>
67
+ path.split(".").reduce((acc, k) => (acc ? acc[k] : undefined), obj);
68
+
69
+ const setNested = (obj, path, value) => {
70
+ const parts = path.split(".");
71
+ let temp = obj;
72
+ for (let i = 0; i < parts.length - 1; i++) {
73
+ if (!temp[parts[i]]) temp[parts[i]] = {};
74
+ temp = temp[parts[i]];
75
+ }
76
+ temp[parts[parts.length - 1]] = value;
77
+ };
78
+
79
+ for (const { key, question, expectedType, defaultValue } of questions) {
80
+ let value;
81
+
82
+ if (getNested(options, key) !== undefined) {
83
+ value = getNested(options, key);
84
+ } else if (question) {
85
+ value = await promptWithType(question, expectedType, defaultValue);
86
+ }
87
+
88
+ if (value !== undefined) {
89
+ setNested(result, key, value);
90
+ }
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ export async function createLocalReleaseConfig(
97
+ airborneConfig,
98
+ options,
99
+ platform
100
+ ) {
101
+ try {
102
+ const entry_file = airborneConfig.js_entry_file || "index.js";
103
+ const index_file_path =
104
+ airborneConfig[platform].index_file_path || `index.${platform}.bundle`;
105
+ const build_folder = path.join(platform, "build", "generated", "airborne");
106
+
107
+ const fullBuildFolderPath = path.join(options.directory_path, build_folder);
108
+ if (!fs.existsSync(fullBuildFolderPath)) {
109
+ fs.mkdirSync(fullBuildFolderPath, { recursive: true });
110
+ }
111
+
112
+ 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}`;
113
+
114
+ const bundleResult = await executeReactNativeBundleCommand(command);
115
+
116
+ if (!bundleResult.success) {
117
+ throw new Error(
118
+ `React Native bundle command failed: ${bundleResult.error}`
119
+ );
120
+ }
121
+
122
+ const baseDir = path.isAbsolute(options.directory_path)
123
+ ? options.directory_path
124
+ : path.join(process.cwd(), options.directory_path);
125
+
126
+ const remotebundlePath = path.join(baseDir, build_folder);
127
+ const remotebundleContents = readDirectoryRecursive(remotebundlePath);
128
+ const filledOptions = await fillReleaseConfigOptions(options);
129
+ const releaseConfig = {
130
+ version: "",
131
+ config: {
132
+ version: "",
133
+ boot_timeout: filledOptions.boot_timeout,
134
+ release_config_timeout: filledOptions.release_config_timeout,
135
+ properties: {},
136
+ },
137
+ package: {
138
+ version: "",
139
+ prooerties: {},
140
+ index: {
141
+ file_path: airborneConfig[platform].index_file_path,
142
+ url: "",
143
+ },
144
+ important: remotebundleContents
145
+ .filter(
146
+ (item) => item.path !== airborneConfig[platform].index_file_path
147
+ )
148
+ .map((item) => {
149
+ return {
150
+ file_path: item.path,
151
+ url: "",
152
+ };
153
+ }),
154
+ lazy: [],
155
+ },
156
+ resources: [],
157
+ };
158
+
159
+ await writeReleaseConfig(
160
+ releaseConfig,
161
+ platform,
162
+ airborneConfig.namespace,
163
+ filledOptions.directory_path
164
+ );
165
+ } catch (err) {
166
+ console.error("❌ Failed to create local release config:", err.message);
167
+ }
168
+ }
169
+
170
+ export async function writeReleaseConfig(
171
+ releaseConfig,
172
+ platform,
173
+ namespace,
174
+ directory_path
175
+ ) {
176
+ try {
177
+ let configDir;
178
+ if (platform === "android") {
179
+ configDir = path.join(
180
+ directory_path,
181
+ platform,
182
+ "app",
183
+ "src",
184
+ "main",
185
+ "assets",
186
+ namespace
187
+ );
188
+ } else {
189
+ configDir = path.join(directory_path, platform);
190
+ }
191
+ if (!fs.existsSync(configDir)) {
192
+ fs.mkdirSync(configDir, {
193
+ recursive: true,
194
+ });
195
+ }
196
+ const configPath = path.join(configDir, "release_config.json");
197
+
198
+ fs.writeFileSync(
199
+ configPath,
200
+ JSON.stringify(releaseConfig, null, 2),
201
+ "utf8"
202
+ );
203
+ console.log(`✅ Release config written to ${configPath}`);
204
+
205
+ if (platform === "ios") {
206
+ console.log("Running ruby script for ios");
207
+ const rubyScriptPath = path.join(__dirname, "../", "bundleRC.rb");
208
+ const rubyCommand = `ruby "${rubyScriptPath}"`;
209
+
210
+ try {
211
+ execSync(rubyCommand, { stdio: "inherit", cwd: directory_path });
212
+ console.log("✅ Ruby script executed successfully");
213
+ } catch (error) {
214
+ console.error("❌ Ruby script execution failed:", error.message);
215
+ }
216
+ }
217
+ } catch (err) {
218
+ console.error("❌ Failed to write release config:", err.message);
219
+ }
220
+ }
221
+
222
+ export async function updateLocalReleaseConfig(
223
+ airborneConfig,
224
+ options,
225
+ platform
226
+ ) {
227
+ try {
228
+ const entry_file = airborneConfig.js_entry_file || "index.js";
229
+ const index_file_path =
230
+ airborneConfig[platform].index_file_path || `index.${platform}.bundle`;
231
+ const build_folder = path.join(platform, "build", "generated", "airborne");
232
+
233
+ const fullBuildFolderPath = path.join(options.directory_path, build_folder);
234
+ if (!fs.existsSync(fullBuildFolderPath)) {
235
+ fs.mkdirSync(fullBuildFolderPath, { recursive: true });
236
+ }
237
+
238
+ // empty the folder
239
+ fs.readdirSync(fullBuildFolderPath).forEach((file) => {
240
+ const curPath = path.join(fullBuildFolderPath, file);
241
+ if (fs.lstatSync(curPath).isDirectory()) {
242
+ fs.rmSync(curPath, { recursive: true, force: true }); // remove folder recursively
243
+ } else {
244
+ fs.unlinkSync(curPath); // remove file
245
+ }
246
+ });
247
+
248
+ 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}`;
249
+
250
+ const bundleResult = await executeReactNativeBundleCommand(command);
251
+
252
+ if (!bundleResult.success) {
253
+ throw new Error(
254
+ `React Native bundle command failed: ${bundleResult.error}`
255
+ );
256
+ }
257
+
258
+ const baseDir = path.isAbsolute(options.directory_path)
259
+ ? options.directory_path
260
+ : path.join(process.cwd(), options.directory_path);
261
+
262
+ const remotebundlePath = path.join(baseDir, build_folder);
263
+ const remotebundleContents = readDirectoryRecursive(remotebundlePath);
264
+ const existingReleaseConfig = await readReleaseConfig(
265
+ options.directory_path,
266
+ options.platform,
267
+ airborneConfig.namespace
268
+ );
269
+
270
+ const releaseConfig = {
271
+ version: existingReleaseConfig?.version || "",
272
+ config: {
273
+ version: existingReleaseConfig?.config?.version || "",
274
+ boot_timeout:
275
+ options.boot_timeout ?? existingReleaseConfig?.config?.boot_timeout,
276
+ release_config_timeout:
277
+ options.release_config_timeout ??
278
+ existingReleaseConfig?.config?.release_config_timeout,
279
+ properties: existingReleaseConfig?.config?.properties || {},
280
+ },
281
+ package: {
282
+ version: existingReleaseConfig?.package?.version || "",
283
+ properties: existingReleaseConfig?.package?.properties || {},
284
+ index: {
285
+ file_path: airborneConfig[options.platform].index_file_path,
286
+ url: existingReleaseConfig?.package?.index?.url || "",
287
+ },
288
+ important: remotebundleContents
289
+ .filter(
290
+ (item) =>
291
+ item.path !== airborneConfig[options.platform].index_file_path
292
+ )
293
+ .map((item) => ({
294
+ file_path: item.path,
295
+ url: "",
296
+ })),
297
+ lazy: existingReleaseConfig?.package?.lazy || [],
298
+ },
299
+ resources: existingReleaseConfig?.resources || [],
300
+ };
301
+
302
+ await writeReleaseConfig(
303
+ releaseConfig,
304
+ platform,
305
+ airborneConfig.namespace,
306
+ options.directory_path
307
+ );
308
+ } catch (err) {
309
+ console.error("❌ Failed to create local release config:", err.message);
310
+ }
311
+ }
312
+
313
+ export async function releaseConfigExists(directoryPath, platform, namespace) {
314
+ try {
315
+ let configPath;
316
+
317
+ if (platform === "android") {
318
+ configPath = path.join(
319
+ directoryPath,
320
+ platform,
321
+ "app",
322
+ "src",
323
+ "main",
324
+ "assets",
325
+ namespace,
326
+ "release_config.json"
327
+ );
328
+ } else {
329
+ configPath = path.join(directoryPath, platform, "release_config.json");
330
+ }
331
+
332
+ await fs.promises.access(configPath);
333
+ return true;
334
+ } catch (error) {
335
+ return false;
336
+ }
337
+ }