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.
- package/CHANGELOG.md +59 -0
- package/README.md +289 -0
- package/package.json +51 -0
- package/src/bundleRC.rb +61 -0
- package/src/index.js +728 -0
- package/src/utils/common.js +321 -0
- package/src/utils/file.js +380 -0
- package/src/utils/package.js +68 -0
- package/src/utils/prompt.js +81 -0
- package/src/utils/release.js +337 -0
|
@@ -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
|
+
}
|