@symbiote-native/cli 0.1.2 → 0.2.1
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/README.md +7 -7
- package/bundle.js +967 -160
- package/package.json +3 -2
- package/templates/README.md +6 -9
- package/templates/js/angular/README.md +2 -2
- package/templates/js/angular/package.json.fragment.json +1 -1
- package/templates/js/angular/tsconfig.angular.json +8 -14
- package/templates/js/react/package.json.fragment.json +2 -2
- package/templates/js/solid/package.json.fragment.json +2 -2
- package/templates/js/svelte/image-uris.js +3 -5
- package/templates/js/svelte/package.json.fragment.json +2 -2
- package/templates/js/vue-sfc/package.json.fragment.json +2 -2
- package/templates/js/vue-tsx/package.json.fragment.json +2 -2
- package/templates/layers/README.md +14 -24
- package/templates/layers/audio/package.json.fragment.json +5 -0
- package/templates/layers/background-fetch/package.json.fragment.json +5 -0
- package/templates/layers/background-task/package.json.fragment.json +5 -0
- package/templates/layers/file-system/package.json.fragment.json +5 -0
- package/templates/layers/location/package.json.fragment.json +5 -0
- package/templates/layers/media-library/package.json.fragment.json +5 -0
- package/templates/layers/navigation/app/svelte/image-uris.js +3 -5
- package/templates/layers/notifications/package.json.fragment.json +5 -0
- package/templates/layers/sqlite/package.json.fragment.json +5 -0
- package/templates/layers/task-manager/package.json.fragment.json +5 -0
- package/templates/native/README.md +2 -2
package/bundle.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { styleText } from "node:util";
|
|
3
4
|
import process$1, { stdin, stdout } from "node:process";
|
|
4
5
|
import l__default from "node:readline";
|
|
5
|
-
import * as fs from "node:fs";
|
|
6
|
+
import * as fs$1 from "node:fs";
|
|
6
7
|
import { readFileSync } from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
+
import * as path$1 from "node:path";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
10
12
|
//#region \0rolldown/runtime.js
|
|
11
13
|
var __create = Object.create;
|
|
12
14
|
var __defProp = Object.defineProperty;
|
|
@@ -29,6 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
31
|
value: mod,
|
|
30
32
|
enumerable: true
|
|
31
33
|
}) : target, mod));
|
|
34
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
32
35
|
//#endregion
|
|
33
36
|
//#region src/errors.ts
|
|
34
37
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -119,6 +122,32 @@ const EXPO_PACKAGE_LAYERS = [
|
|
|
119
122
|
label: "Application info",
|
|
120
123
|
symbiotePackage: "@symbiote-native/application"
|
|
121
124
|
},
|
|
125
|
+
{
|
|
126
|
+
id: "audio",
|
|
127
|
+
label: "Audio",
|
|
128
|
+
symbiotePackage: "@symbiote-native/audio",
|
|
129
|
+
optionalManifestBundles: [{
|
|
130
|
+
id: "recording",
|
|
131
|
+
label: "Background audio recording",
|
|
132
|
+
warning: "Requesting FOREGROUND_SERVICE_MICROPHONE triggers Play Console policy review — Google requires a clear, prominent in-app disclosure and justification before you can publish.",
|
|
133
|
+
nextSteps: "Pass `allowsBackgroundRecording: true` to setAudioModeAsync so recording keeps running with the app backgrounded.",
|
|
134
|
+
manifestPermissions: ["android.permission.FOREGROUND_SERVICE_MICROPHONE", "android.permission.POST_NOTIFICATIONS"],
|
|
135
|
+
manifestServices: [{
|
|
136
|
+
name: "expo.modules.audio.service.AudioRecordingService",
|
|
137
|
+
foregroundServiceType: "microphone"
|
|
138
|
+
}]
|
|
139
|
+
}]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "background-fetch",
|
|
143
|
+
label: "Background fetch",
|
|
144
|
+
symbiotePackage: "@symbiote-native/background-fetch"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "background-task",
|
|
148
|
+
label: "Background task",
|
|
149
|
+
symbiotePackage: "@symbiote-native/background-task"
|
|
150
|
+
},
|
|
122
151
|
{
|
|
123
152
|
id: "battery",
|
|
124
153
|
label: "Battery",
|
|
@@ -149,6 +178,11 @@ const EXPO_PACKAGE_LAYERS = [
|
|
|
149
178
|
label: "Device info",
|
|
150
179
|
symbiotePackage: "@symbiote-native/device"
|
|
151
180
|
},
|
|
181
|
+
{
|
|
182
|
+
id: "file-system",
|
|
183
|
+
label: "File system",
|
|
184
|
+
symbiotePackage: "@symbiote-native/file-system"
|
|
185
|
+
},
|
|
152
186
|
{
|
|
153
187
|
id: "haptics",
|
|
154
188
|
label: "Haptics",
|
|
@@ -169,11 +203,37 @@ const EXPO_PACKAGE_LAYERS = [
|
|
|
169
203
|
label: "Localization",
|
|
170
204
|
symbiotePackage: "@symbiote-native/localization"
|
|
171
205
|
},
|
|
206
|
+
{
|
|
207
|
+
id: "location",
|
|
208
|
+
label: "Location",
|
|
209
|
+
symbiotePackage: "@symbiote-native/location",
|
|
210
|
+
optionalManifestBundles: [{
|
|
211
|
+
id: "background",
|
|
212
|
+
label: "Background location tracking",
|
|
213
|
+
warning: "Requesting ACCESS_BACKGROUND_LOCATION and the location foreground-service permissions triggers Play Console policy review — Google requires a clear, prominent in-app disclosure and justification before you can publish.",
|
|
214
|
+
nextSteps: "Pass a `foregroundService` option to `startLocationUpdatesAsync` (notification title/body) so the OS keeps tracking alive outside the app. expo-location's own LocationTaskService is already declared in its AndroidManifest.xml and merges automatically — this only adds the permissions it needs.",
|
|
215
|
+
manifestPermissions: [
|
|
216
|
+
"android.permission.ACCESS_BACKGROUND_LOCATION",
|
|
217
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
218
|
+
"android.permission.FOREGROUND_SERVICE_LOCATION"
|
|
219
|
+
]
|
|
220
|
+
}]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "media-library",
|
|
224
|
+
label: "Media library",
|
|
225
|
+
symbiotePackage: "@symbiote-native/media-library"
|
|
226
|
+
},
|
|
172
227
|
{
|
|
173
228
|
id: "network",
|
|
174
229
|
label: "Network info",
|
|
175
230
|
symbiotePackage: "@symbiote-native/network"
|
|
176
231
|
},
|
|
232
|
+
{
|
|
233
|
+
id: "notifications",
|
|
234
|
+
label: "Notifications",
|
|
235
|
+
symbiotePackage: "@symbiote-native/notifications"
|
|
236
|
+
},
|
|
177
237
|
{
|
|
178
238
|
id: "screen-orientation",
|
|
179
239
|
label: "Screen orientation",
|
|
@@ -199,6 +259,11 @@ const EXPO_PACKAGE_LAYERS = [
|
|
|
199
259
|
label: "SMS",
|
|
200
260
|
symbiotePackage: "@symbiote-native/sms"
|
|
201
261
|
},
|
|
262
|
+
{
|
|
263
|
+
id: "sqlite",
|
|
264
|
+
label: "SQLite",
|
|
265
|
+
symbiotePackage: "@symbiote-native/sqlite"
|
|
266
|
+
},
|
|
202
267
|
{
|
|
203
268
|
id: "standard-web-crypto",
|
|
204
269
|
label: "Web Crypto polyfill",
|
|
@@ -214,6 +279,11 @@ const EXPO_PACKAGE_LAYERS = [
|
|
|
214
279
|
label: "System UI",
|
|
215
280
|
symbiotePackage: "@symbiote-native/system-ui"
|
|
216
281
|
},
|
|
282
|
+
{
|
|
283
|
+
id: "task-manager",
|
|
284
|
+
label: "Task manager",
|
|
285
|
+
symbiotePackage: "@symbiote-native/task-manager"
|
|
286
|
+
},
|
|
217
287
|
{
|
|
218
288
|
id: "tracking-transparency",
|
|
219
289
|
label: "Tracking transparency",
|
|
@@ -229,6 +299,10 @@ const EXPO_PACKAGE_LAYER_IDS = new Set(EXPO_PACKAGE_LAYERS.map((layer) => layer.
|
|
|
229
299
|
function isExpoPackageLayerName(value) {
|
|
230
300
|
return EXPO_PACKAGE_LAYER_IDS.has(value);
|
|
231
301
|
}
|
|
302
|
+
function expoPackagesWithOptionalManifestBundles(selected) {
|
|
303
|
+
const ids = selected instanceof Set ? selected : new Set(selected);
|
|
304
|
+
return EXPO_PACKAGE_LAYERS.filter((layer) => (layer.optionalManifestBundles?.length ?? 0) > 0 && ids.has(layer.id));
|
|
305
|
+
}
|
|
232
306
|
//#endregion
|
|
233
307
|
//#region src/utils/suggest.ts
|
|
234
308
|
function editDistance(a, b) {
|
|
@@ -336,7 +410,19 @@ function parseArgv(argv) {
|
|
|
336
410
|
const [command, ...rest] = argv;
|
|
337
411
|
if (command === void 0 || command === "--help" || command === "-h") return { kind: "help" };
|
|
338
412
|
if (command === "--version" || command === "-v") return { kind: "version" };
|
|
339
|
-
if (command
|
|
413
|
+
if (command === "grant") {
|
|
414
|
+
const [layer, ...extra] = rest;
|
|
415
|
+
if (extra.length > 0) throw new CliUsageError(`"grant" takes at most one layer — got "${layer}" and "${extra[0]}".`);
|
|
416
|
+
return {
|
|
417
|
+
kind: "grant",
|
|
418
|
+
layer
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (command !== "new" && command !== "add") throw usageErrorFor("command", command, [
|
|
422
|
+
"new",
|
|
423
|
+
"add",
|
|
424
|
+
"grant"
|
|
425
|
+
]);
|
|
340
426
|
let appName;
|
|
341
427
|
let framework;
|
|
342
428
|
let vueFlavor;
|
|
@@ -744,7 +830,7 @@ function wrapAnsi(string, columns, options) {
|
|
|
744
830
|
}
|
|
745
831
|
//#endregion
|
|
746
832
|
//#region ../../node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
|
|
747
|
-
var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
833
|
+
var import_src$1 = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
748
834
|
const ESC = "\x1B";
|
|
749
835
|
const CSI = `${ESC}[`;
|
|
750
836
|
const beep = "\x07";
|
|
@@ -973,9 +1059,9 @@ var V = class {
|
|
|
973
1059
|
escapeCodeTimeout: 50,
|
|
974
1060
|
terminal: true
|
|
975
1061
|
}), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
|
|
976
|
-
this.output.write(import_src.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
|
|
1062
|
+
this.output.write(import_src$1.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
|
|
977
1063
|
}), this.once("cancel", () => {
|
|
978
|
-
this.output.write(import_src.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
|
|
1064
|
+
this.output.write(import_src$1.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
|
|
979
1065
|
});
|
|
980
1066
|
});
|
|
981
1067
|
}
|
|
@@ -1024,7 +1110,7 @@ var V = class {
|
|
|
1024
1110
|
trim: false
|
|
1025
1111
|
}).split(`
|
|
1026
1112
|
`).length - 1;
|
|
1027
|
-
this.output.write(import_src.cursor.move(-999, t * -1));
|
|
1113
|
+
this.output.write(import_src$1.cursor.move(-999, t * -1));
|
|
1028
1114
|
}
|
|
1029
1115
|
render() {
|
|
1030
1116
|
const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
|
|
@@ -1032,7 +1118,7 @@ var V = class {
|
|
|
1032
1118
|
trim: false
|
|
1033
1119
|
});
|
|
1034
1120
|
if (t !== this._prevFrame) {
|
|
1035
|
-
if (this.state === "initial") this.output.write(import_src.cursor.hide);
|
|
1121
|
+
if (this.state === "initial") this.output.write(import_src$1.cursor.hide);
|
|
1036
1122
|
else {
|
|
1037
1123
|
const e = diffLines(this._prevFrame, t), i = getRows(this.output);
|
|
1038
1124
|
if (this.restoreCursor(), e) {
|
|
@@ -1043,18 +1129,18 @@ var V = class {
|
|
|
1043
1129
|
return;
|
|
1044
1130
|
}
|
|
1045
1131
|
if (e.lines.length === 1) {
|
|
1046
|
-
this.output.write(import_src.cursor.move(0, r - s)), this.output.write(import_src.erase.lines(1));
|
|
1132
|
+
this.output.write(import_src$1.cursor.move(0, r - s)), this.output.write(import_src$1.erase.lines(1));
|
|
1047
1133
|
const o = t.split(`
|
|
1048
1134
|
`);
|
|
1049
|
-
this.output.write(o[r]), this._prevFrame = t, this.output.write(import_src.cursor.move(0, o.length - r - 1));
|
|
1135
|
+
this.output.write(o[r]), this._prevFrame = t, this.output.write(import_src$1.cursor.move(0, o.length - r - 1));
|
|
1050
1136
|
return;
|
|
1051
1137
|
} else if (e.lines.length > 1) {
|
|
1052
1138
|
if (n < s) r = n;
|
|
1053
1139
|
else {
|
|
1054
1140
|
const h = r - s;
|
|
1055
|
-
h > 0 && this.output.write(import_src.cursor.move(0, h));
|
|
1141
|
+
h > 0 && this.output.write(import_src$1.cursor.move(0, h));
|
|
1056
1142
|
}
|
|
1057
|
-
this.output.write(import_src.erase.down());
|
|
1143
|
+
this.output.write(import_src$1.erase.down());
|
|
1058
1144
|
const f = t.split(`
|
|
1059
1145
|
`).slice(r);
|
|
1060
1146
|
this.output.write(f.join(`
|
|
@@ -1062,7 +1148,7 @@ var V = class {
|
|
|
1062
1148
|
return;
|
|
1063
1149
|
}
|
|
1064
1150
|
}
|
|
1065
|
-
this.output.write(import_src.erase.down());
|
|
1151
|
+
this.output.write(import_src$1.erase.down());
|
|
1066
1152
|
}
|
|
1067
1153
|
this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
|
|
1068
1154
|
}
|
|
@@ -1079,7 +1165,7 @@ var r = class extends V {
|
|
|
1079
1165
|
super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
|
|
1080
1166
|
this.value = this._value;
|
|
1081
1167
|
}), this.on("confirm", (i) => {
|
|
1082
|
-
this.output.write(import_src.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
|
|
1168
|
+
this.output.write(import_src$1.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
|
|
1083
1169
|
}), this.on("cursor", () => {
|
|
1084
1170
|
this.value = !this.value;
|
|
1085
1171
|
});
|
|
@@ -1255,6 +1341,11 @@ const S_RADIO_INACTIVE = unicodeOr("○", " ");
|
|
|
1255
1341
|
const S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
|
|
1256
1342
|
const S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
|
|
1257
1343
|
const S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
|
|
1344
|
+
const S_BAR_H = unicodeOr("─", "-");
|
|
1345
|
+
const S_CORNER_TOP_RIGHT = unicodeOr("╮", "+");
|
|
1346
|
+
const S_CONNECT_LEFT = unicodeOr("├", "+");
|
|
1347
|
+
const S_CORNER_BOTTOM_RIGHT = unicodeOr("╯", "+");
|
|
1348
|
+
const S_CORNER_BOTTOM_LEFT = unicodeOr("╰", "+");
|
|
1258
1349
|
const symbol = (o) => {
|
|
1259
1350
|
switch (o) {
|
|
1260
1351
|
case "initial":
|
|
@@ -1548,6 +1639,32 @@ const outro = (o = "", t) => {
|
|
|
1548
1639
|
${styleText("gray", S_BAR_END)} ` : "";
|
|
1549
1640
|
i.write(`${e}${o}
|
|
1550
1641
|
|
|
1642
|
+
`);
|
|
1643
|
+
};
|
|
1644
|
+
const W$1 = (o) => o;
|
|
1645
|
+
const C = (o, e, s) => {
|
|
1646
|
+
const a = {
|
|
1647
|
+
hard: true,
|
|
1648
|
+
trim: false
|
|
1649
|
+
}, i = wrapAnsi(o, e, a).split(`
|
|
1650
|
+
`), c = i.reduce((n, t) => Math.max(fastStringWidth(t), n), 0);
|
|
1651
|
+
return wrapAnsi(o, e - (i.map(s).reduce((n, t) => Math.max(fastStringWidth(t), n), 0) - c), a);
|
|
1652
|
+
};
|
|
1653
|
+
const note = (o = "", e = "", s) => {
|
|
1654
|
+
const a = s?.output ?? process$1.stdout, i = s?.withGuide ?? settings.withGuide, c = s?.format ?? W$1, g = [
|
|
1655
|
+
"",
|
|
1656
|
+
...C(o, getColumns(a) - 6, c).split(`
|
|
1657
|
+
`).map(c),
|
|
1658
|
+
""
|
|
1659
|
+
], n = fastStringWidth(e), t = Math.max(g.reduce((m, F) => {
|
|
1660
|
+
const O = fastStringWidth(F);
|
|
1661
|
+
return O > m ? O : m;
|
|
1662
|
+
}, 0), n) + 2, h = g.map((m) => `${styleText("gray", S_BAR)} ${m}${" ".repeat(t - fastStringWidth(m))}${styleText("gray", S_BAR)}`).join(`
|
|
1663
|
+
`), T = i ? `${styleText("gray", S_BAR)}
|
|
1664
|
+
` : "", l$1 = i ? S_CONNECT_LEFT : S_CORNER_BOTTOM_LEFT;
|
|
1665
|
+
a.write(`${T}${styleText("green", S_STEP_SUBMIT)} ${styleText("reset", e)} ${styleText("gray", S_BAR_H.repeat(Math.max(t - n - 1, 1)) + S_CORNER_TOP_RIGHT)}
|
|
1666
|
+
${h}
|
|
1667
|
+
${styleText("gray", l$1 + S_BAR_H.repeat(t + 2) + S_CORNER_BOTTOM_RIGHT)}
|
|
1551
1668
|
`);
|
|
1552
1669
|
};
|
|
1553
1670
|
const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
|
|
@@ -1668,44 +1785,44 @@ const TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
1668
1785
|
""
|
|
1669
1786
|
]);
|
|
1670
1787
|
function replaceTextContent(filePath, identity) {
|
|
1671
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
1788
|
+
const content = fs$1.readFileSync(filePath, "utf8");
|
|
1672
1789
|
const replaced = content.split("com.canary").join(identity.bundleId).split("Canary").join(identity.nativeAppName);
|
|
1673
|
-
if (replaced !== content) fs.writeFileSync(filePath, replaced);
|
|
1790
|
+
if (replaced !== content) fs$1.writeFileSync(filePath, replaced);
|
|
1674
1791
|
}
|
|
1675
1792
|
function walkTextFiles(dir, identity) {
|
|
1676
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1677
|
-
const entryPath = path.join(dir, entry.name);
|
|
1793
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
1794
|
+
const entryPath = path$1.join(dir, entry.name);
|
|
1678
1795
|
if (entry.isDirectory()) {
|
|
1679
1796
|
walkTextFiles(entryPath, identity);
|
|
1680
1797
|
continue;
|
|
1681
1798
|
}
|
|
1682
|
-
if (TEXT_FILE_EXTENSIONS.has(path.extname(entry.name))) replaceTextContent(entryPath, identity);
|
|
1799
|
+
if (TEXT_FILE_EXTENSIONS.has(path$1.extname(entry.name))) replaceTextContent(entryPath, identity);
|
|
1683
1800
|
}
|
|
1684
1801
|
}
|
|
1685
1802
|
function renameCanaryPaths(dir, nativeAppName) {
|
|
1686
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) renameCanaryPaths(path.join(dir, entry.name), nativeAppName);
|
|
1687
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1803
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) renameCanaryPaths(path$1.join(dir, entry.name), nativeAppName);
|
|
1804
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
1688
1805
|
if (!entry.name.startsWith("Canary")) continue;
|
|
1689
|
-
const from = path.join(dir, entry.name);
|
|
1690
|
-
const to = path.join(dir, entry.name.replace("Canary", nativeAppName));
|
|
1691
|
-
fs.renameSync(from, to);
|
|
1806
|
+
const from = path$1.join(dir, entry.name);
|
|
1807
|
+
const to = path$1.join(dir, entry.name.replace("Canary", nativeAppName));
|
|
1808
|
+
fs$1.renameSync(from, to);
|
|
1692
1809
|
}
|
|
1693
1810
|
}
|
|
1694
1811
|
function moveAndroidPackageDir(javaSrcDir, bundleId) {
|
|
1695
|
-
const oldDir = path.join(javaSrcDir, "com", "canary");
|
|
1696
|
-
if (!fs.existsSync(oldDir)) return;
|
|
1697
|
-
const newDir = path.join(javaSrcDir, ...bundleId.split("."));
|
|
1698
|
-
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
1699
|
-
fs.renameSync(oldDir, newDir);
|
|
1700
|
-
const oldTopDir = path.join(javaSrcDir, "com");
|
|
1701
|
-
if (fs.existsSync(oldTopDir) && fs.readdirSync(oldTopDir).length === 0) fs.rmdirSync(oldTopDir);
|
|
1812
|
+
const oldDir = path$1.join(javaSrcDir, "com", "canary");
|
|
1813
|
+
if (!fs$1.existsSync(oldDir)) return;
|
|
1814
|
+
const newDir = path$1.join(javaSrcDir, ...bundleId.split("."));
|
|
1815
|
+
fs$1.mkdirSync(path$1.dirname(newDir), { recursive: true });
|
|
1816
|
+
fs$1.renameSync(oldDir, newDir);
|
|
1817
|
+
const oldTopDir = path$1.join(javaSrcDir, "com");
|
|
1818
|
+
if (fs$1.existsSync(oldTopDir) && fs$1.readdirSync(oldTopDir).length === 0) fs$1.rmdirSync(oldTopDir);
|
|
1702
1819
|
}
|
|
1703
1820
|
function applyAppIdentity(root, identity) {
|
|
1704
1821
|
walkTextFiles(root, identity);
|
|
1705
|
-
const iosDir = path.join(root, "ios");
|
|
1706
|
-
if (fs.existsSync(iosDir)) renameCanaryPaths(iosDir, identity.nativeAppName);
|
|
1707
|
-
const androidAppSrc = path.join(root, "android", "app", "src");
|
|
1708
|
-
if (fs.existsSync(androidAppSrc)) for (const sourceSet of fs.readdirSync(androidAppSrc)) moveAndroidPackageDir(path.join(androidAppSrc, sourceSet, "java"), identity.bundleId);
|
|
1822
|
+
const iosDir = path$1.join(root, "ios");
|
|
1823
|
+
if (fs$1.existsSync(iosDir)) renameCanaryPaths(iosDir, identity.nativeAppName);
|
|
1824
|
+
const androidAppSrc = path$1.join(root, "android", "app", "src");
|
|
1825
|
+
if (fs$1.existsSync(androidAppSrc)) for (const sourceSet of fs$1.readdirSync(androidAppSrc)) moveAndroidPackageDir(path$1.join(androidAppSrc, sourceSet, "java"), identity.bundleId);
|
|
1709
1826
|
}
|
|
1710
1827
|
//#endregion
|
|
1711
1828
|
//#region src/utils/json.ts
|
|
@@ -1752,7 +1869,7 @@ function sortDependencies(packageJson) {
|
|
|
1752
1869
|
//#endregion
|
|
1753
1870
|
//#region src/utils/render-template.ts
|
|
1754
1871
|
function readJsonObject(filePath) {
|
|
1755
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1872
|
+
const parsed = JSON.parse(fs$1.readFileSync(filePath, "utf8"));
|
|
1756
1873
|
if (!isJsonObject(parsed)) throw new Error(`${filePath}: expected a JSON object`);
|
|
1757
1874
|
return parsed;
|
|
1758
1875
|
}
|
|
@@ -1782,37 +1899,37 @@ function stripTypescriptInfix(filename) {
|
|
|
1782
1899
|
}
|
|
1783
1900
|
const DEFAULT_OPTIONS = { hasTypescript: true };
|
|
1784
1901
|
function renderTemplate(src, dest, options = DEFAULT_OPTIONS) {
|
|
1785
|
-
if (fs.statSync(src).isDirectory()) {
|
|
1786
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1787
|
-
for (const entry of fs.readdirSync(src)) renderTemplate(path.resolve(src, entry), path.resolve(dest, entry), options);
|
|
1902
|
+
if (fs$1.statSync(src).isDirectory()) {
|
|
1903
|
+
fs$1.mkdirSync(dest, { recursive: true });
|
|
1904
|
+
for (const entry of fs$1.readdirSync(src)) renderTemplate(path$1.resolve(src, entry), path$1.resolve(dest, entry), options);
|
|
1788
1905
|
return;
|
|
1789
1906
|
}
|
|
1790
|
-
const filename = path.basename(src);
|
|
1907
|
+
const filename = path$1.basename(src);
|
|
1791
1908
|
if (filename === "README.md") return;
|
|
1792
1909
|
if (!options.hasTypescript && (isTypescriptOnlyFile(filename) || filename.includes(".typescript."))) return;
|
|
1793
1910
|
if (options.hasTypescript && isJavascriptOnlyFile(filename)) return;
|
|
1794
1911
|
const destFilename = filename.includes(".typescript.") ? stripTypescriptInfix(filename) : filename;
|
|
1795
|
-
const resolvedDest = path.join(path.dirname(dest), destFilename);
|
|
1912
|
+
const resolvedDest = path$1.join(path$1.dirname(dest), destFilename);
|
|
1796
1913
|
if (destFilename === "package.json.fragment.json") {
|
|
1797
|
-
const destPackageJsonPath = path.resolve(path.dirname(resolvedDest), "package.json");
|
|
1914
|
+
const destPackageJsonPath = path$1.resolve(path$1.dirname(resolvedDest), "package.json");
|
|
1798
1915
|
const fragment = readJsonObject(src);
|
|
1799
|
-
const merged = sortDependencies(deepMerge(fs.existsSync(destPackageJsonPath) ? readJsonObject(destPackageJsonPath) : {}, fragment));
|
|
1800
|
-
fs.writeFileSync(destPackageJsonPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
1916
|
+
const merged = sortDependencies(deepMerge(fs$1.existsSync(destPackageJsonPath) ? readJsonObject(destPackageJsonPath) : {}, fragment));
|
|
1917
|
+
fs$1.writeFileSync(destPackageJsonPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
1801
1918
|
return;
|
|
1802
1919
|
}
|
|
1803
|
-
if (destFilename === ".gitignore" && fs.existsSync(resolvedDest)) {
|
|
1804
|
-
const existing = fs.readFileSync(resolvedDest, "utf8");
|
|
1805
|
-
const addition = fs.readFileSync(src, "utf8");
|
|
1806
|
-
fs.writeFileSync(resolvedDest, `${existing}\n${addition}`);
|
|
1920
|
+
if (destFilename === ".gitignore" && fs$1.existsSync(resolvedDest)) {
|
|
1921
|
+
const existing = fs$1.readFileSync(resolvedDest, "utf8");
|
|
1922
|
+
const addition = fs$1.readFileSync(src, "utf8");
|
|
1923
|
+
fs$1.writeFileSync(resolvedDest, `${existing}\n${addition}`);
|
|
1807
1924
|
return;
|
|
1808
1925
|
}
|
|
1809
|
-
fs.copyFileSync(src, resolvedDest);
|
|
1926
|
+
fs$1.copyFileSync(src, resolvedDest);
|
|
1810
1927
|
}
|
|
1811
1928
|
//#endregion
|
|
1812
1929
|
//#region src/utils/apply-bootsplash-logo.ts
|
|
1813
1930
|
function applyBootsplashLogo(root, templatesRoot, framework) {
|
|
1814
|
-
const overlayRoot = path.join(templatesRoot, "bootsplash-logo", framework);
|
|
1815
|
-
if (fs.existsSync(overlayRoot)) renderTemplate(overlayRoot, root);
|
|
1931
|
+
const overlayRoot = path$1.join(templatesRoot, "bootsplash-logo", framework);
|
|
1932
|
+
if (fs$1.existsSync(overlayRoot)) renderTemplate(overlayRoot, root);
|
|
1816
1933
|
}
|
|
1817
1934
|
//#endregion
|
|
1818
1935
|
//#region src/utils/apply-expo-modules-manifest.ts
|
|
@@ -1827,54 +1944,54 @@ const EXPO_MODULES_BACKUP_ATTRS = `android:supportsRtl="true"
|
|
|
1827
1944
|
android:dataExtractionRules="@xml/secure_store_data_extraction_rules"
|
|
1828
1945
|
android:fullBackupContent="@xml/secure_store_backup_rules">`;
|
|
1829
1946
|
function applyExpoModulesAndroidManifest(root) {
|
|
1830
|
-
const manifestPath = path.join(root, "android/app/src/main/AndroidManifest.xml");
|
|
1831
|
-
const manifest = fs.readFileSync(manifestPath, "utf8");
|
|
1947
|
+
const manifestPath = path$1.join(root, "android/app/src/main/AndroidManifest.xml");
|
|
1948
|
+
const manifest = fs$1.readFileSync(manifestPath, "utf8");
|
|
1832
1949
|
const withPermissions = manifest.includes("USE_BIOMETRIC") ? manifest : manifest.replace(" <uses-permission android:name=\"android.permission.VIBRATE\" />", EXPO_MODULES_PERMISSIONS);
|
|
1833
1950
|
const withBackupAttrs = withPermissions.includes("dataExtractionRules") ? withPermissions : withPermissions.replace("android:supportsRtl=\"true\">", EXPO_MODULES_BACKUP_ATTRS);
|
|
1834
|
-
fs.writeFileSync(manifestPath, withBackupAttrs);
|
|
1951
|
+
fs$1.writeFileSync(manifestPath, withBackupAttrs);
|
|
1835
1952
|
}
|
|
1836
1953
|
//#endregion
|
|
1837
1954
|
//#region src/utils/apply-expo-modules-podfile-autolinking.ts
|
|
1838
1955
|
const AUTOLINKING_REQUIRE = "require File.join(File.dirname(`node --print \"require.resolve('expo/package.json')\"`), \"scripts/autolinking\")";
|
|
1839
1956
|
const NATIVE_MODULES_LINE = "config = use_native_modules!";
|
|
1840
1957
|
function applyExpoModulesPodfileAutolinking(root) {
|
|
1841
|
-
const podfilePath = path.join(root, "ios", "Podfile");
|
|
1842
|
-
if (!fs.existsSync(podfilePath)) return;
|
|
1843
|
-
let podfile = fs.readFileSync(podfilePath, "utf8");
|
|
1958
|
+
const podfilePath = path$1.join(root, "ios", "Podfile");
|
|
1959
|
+
if (!fs$1.existsSync(podfilePath)) return;
|
|
1960
|
+
let podfile = fs$1.readFileSync(podfilePath, "utf8");
|
|
1844
1961
|
if (podfile.includes("use_expo_modules!")) return;
|
|
1845
1962
|
if (!podfile.includes(AUTOLINKING_REQUIRE)) podfile = `${AUTOLINKING_REQUIRE}\n\n${podfile}`;
|
|
1846
1963
|
podfile = podfile.replace(NATIVE_MODULES_LINE, `${NATIVE_MODULES_LINE}\n\n use_expo_modules!`);
|
|
1847
|
-
fs.writeFileSync(podfilePath, podfile);
|
|
1964
|
+
fs$1.writeFileSync(podfilePath, podfile);
|
|
1848
1965
|
}
|
|
1849
1966
|
//#endregion
|
|
1850
1967
|
//#region src/utils/apply-expo-modules-podfile.ts
|
|
1851
1968
|
const DEFAULT_PLATFORM_LINE = "platform :ios, min_ios_version_supported.to_s";
|
|
1852
1969
|
const RAISED_PLATFORM_LINE = "platform :ios, [min_ios_version_supported.to_f, 16.4].max.to_s";
|
|
1853
1970
|
function applyExpoModulesPodfilePlatform(root) {
|
|
1854
|
-
const podfilePath = path.join(root, "ios", "Podfile");
|
|
1855
|
-
if (!fs.existsSync(podfilePath)) return;
|
|
1856
|
-
const podfile = fs.readFileSync(podfilePath, "utf8");
|
|
1971
|
+
const podfilePath = path$1.join(root, "ios", "Podfile");
|
|
1972
|
+
if (!fs$1.existsSync(podfilePath)) return;
|
|
1973
|
+
const podfile = fs$1.readFileSync(podfilePath, "utf8");
|
|
1857
1974
|
if (!podfile.includes(DEFAULT_PLATFORM_LINE)) return;
|
|
1858
|
-
fs.writeFileSync(podfilePath, podfile.replace(DEFAULT_PLATFORM_LINE, RAISED_PLATFORM_LINE));
|
|
1975
|
+
fs$1.writeFileSync(podfilePath, podfile.replace(DEFAULT_PLATFORM_LINE, RAISED_PLATFORM_LINE));
|
|
1859
1976
|
}
|
|
1860
1977
|
//#endregion
|
|
1861
1978
|
//#region src/utils/apply-expo-modules-xcode-deployment-target.ts
|
|
1862
1979
|
const MIN_EXPO_IOS_VERSION = 16.4;
|
|
1863
1980
|
const DEPLOYMENT_TARGET_LINE = /IPHONEOS_DEPLOYMENT_TARGET = (\d+(?:\.\d+)?);/g;
|
|
1864
1981
|
function findPbxprojPath(root) {
|
|
1865
|
-
const iosDir = path.join(root, "ios");
|
|
1866
|
-
if (!fs.existsSync(iosDir)) return void 0;
|
|
1867
|
-
const xcodeproj = fs.readdirSync(iosDir).find((entry) => entry.endsWith(".xcodeproj"));
|
|
1982
|
+
const iosDir = path$1.join(root, "ios");
|
|
1983
|
+
if (!fs$1.existsSync(iosDir)) return void 0;
|
|
1984
|
+
const xcodeproj = fs$1.readdirSync(iosDir).find((entry) => entry.endsWith(".xcodeproj"));
|
|
1868
1985
|
if (!xcodeproj) return void 0;
|
|
1869
|
-
const pbxprojPath = path.join(iosDir, xcodeproj, "project.pbxproj");
|
|
1870
|
-
return fs.existsSync(pbxprojPath) ? pbxprojPath : void 0;
|
|
1986
|
+
const pbxprojPath = path$1.join(iosDir, xcodeproj, "project.pbxproj");
|
|
1987
|
+
return fs$1.existsSync(pbxprojPath) ? pbxprojPath : void 0;
|
|
1871
1988
|
}
|
|
1872
1989
|
function applyExpoModulesXcodeDeploymentTarget(root) {
|
|
1873
1990
|
const pbxprojPath = findPbxprojPath(root);
|
|
1874
1991
|
if (!pbxprojPath) return;
|
|
1875
|
-
const pbxproj = fs.readFileSync(pbxprojPath, "utf8");
|
|
1992
|
+
const pbxproj = fs$1.readFileSync(pbxprojPath, "utf8");
|
|
1876
1993
|
const raised = pbxproj.replace(DEPLOYMENT_TARGET_LINE, (line, version) => Number.parseFloat(version) < MIN_EXPO_IOS_VERSION ? `IPHONEOS_DEPLOYMENT_TARGET = ${MIN_EXPO_IOS_VERSION};` : line);
|
|
1877
|
-
if (raised !== pbxproj) fs.writeFileSync(pbxprojPath, raised);
|
|
1994
|
+
if (raised !== pbxproj) fs$1.writeFileSync(pbxprojPath, raised);
|
|
1878
1995
|
}
|
|
1879
1996
|
//#endregion
|
|
1880
1997
|
//#region src/utils/apply-splash-screen-hide.ts
|
|
@@ -1891,8 +2008,8 @@ const APP_COMPONENT_FILENAMES = /* @__PURE__ */ new Set([
|
|
|
1891
2008
|
"App.svelte"
|
|
1892
2009
|
]);
|
|
1893
2010
|
function findAppComponentFile(dir) {
|
|
1894
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1895
|
-
const entryPath = path.join(dir, entry.name);
|
|
2011
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
2012
|
+
const entryPath = path$1.join(dir, entry.name);
|
|
1896
2013
|
if (entry.isDirectory()) {
|
|
1897
2014
|
if (SKIPPED_DIR_NAMES$1.has(entry.name)) continue;
|
|
1898
2015
|
const found = findAppComponentFile(entryPath);
|
|
@@ -1938,9 +2055,9 @@ function patchSvelte(source) {
|
|
|
1938
2055
|
function applySplashScreenAppHide(root, options) {
|
|
1939
2056
|
const appFilePath = findAppComponentFile(root);
|
|
1940
2057
|
if (appFilePath === void 0) return;
|
|
1941
|
-
const source = fs.readFileSync(appFilePath, "utf8");
|
|
2058
|
+
const source = fs$1.readFileSync(appFilePath, "utf8");
|
|
1942
2059
|
if (/\bhide\(\)/.test(source)) return;
|
|
1943
|
-
fs.writeFileSync(appFilePath, patch(source, options.framework, appFilePath));
|
|
2060
|
+
fs$1.writeFileSync(appFilePath, patch(source, options.framework, appFilePath));
|
|
1944
2061
|
}
|
|
1945
2062
|
function patch(source, framework, appFilePath) {
|
|
1946
2063
|
switch (framework) {
|
|
@@ -1956,16 +2073,16 @@ function patch(source, framework, appFilePath) {
|
|
|
1956
2073
|
const INLINE_STYLE_BLOCK_FRAMEWORKS = /* @__PURE__ */ new Set(["vue-sfc", "svelte"]);
|
|
1957
2074
|
const STYLE_TAG_PATTERN = /<style(?=[ >])/;
|
|
1958
2075
|
function addStyleLangAttribute(dir, lang) {
|
|
1959
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1960
|
-
const entryPath = path.join(dir, entry.name);
|
|
2076
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
2077
|
+
const entryPath = path$1.join(dir, entry.name);
|
|
1961
2078
|
if (entry.isDirectory()) {
|
|
1962
2079
|
if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
|
|
1963
2080
|
addStyleLangAttribute(entryPath, lang);
|
|
1964
2081
|
continue;
|
|
1965
2082
|
}
|
|
1966
2083
|
if (!entry.name.endsWith(".vue") && !entry.name.endsWith(".svelte")) continue;
|
|
1967
|
-
const content = fs.readFileSync(entryPath, "utf8");
|
|
1968
|
-
if (STYLE_TAG_PATTERN.test(content)) fs.writeFileSync(entryPath, content.replace(STYLE_TAG_PATTERN, `<style lang="${lang}"`));
|
|
2084
|
+
const content = fs$1.readFileSync(entryPath, "utf8");
|
|
2085
|
+
if (STYLE_TAG_PATTERN.test(content)) fs$1.writeFileSync(entryPath, content.replace(STYLE_TAG_PATTERN, `<style lang="${lang}"`));
|
|
1969
2086
|
}
|
|
1970
2087
|
}
|
|
1971
2088
|
const PREPROCESSOR_EXTENSIONS = {
|
|
@@ -1979,42 +2096,42 @@ const SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
|
1979
2096
|
"node_modules"
|
|
1980
2097
|
]);
|
|
1981
2098
|
function findAppCssFile(dir) {
|
|
1982
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2099
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
1983
2100
|
if (entry.isDirectory()) {
|
|
1984
2101
|
if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
|
|
1985
|
-
const found = findAppCssFile(path.join(dir, entry.name));
|
|
2102
|
+
const found = findAppCssFile(path$1.join(dir, entry.name));
|
|
1986
2103
|
if (found !== void 0) return found;
|
|
1987
2104
|
continue;
|
|
1988
2105
|
}
|
|
1989
|
-
if (entry.name === "App.css") return path.join(dir, entry.name);
|
|
2106
|
+
if (entry.name === "App.css") return path$1.join(dir, entry.name);
|
|
1990
2107
|
}
|
|
1991
2108
|
}
|
|
1992
2109
|
function replaceInTextFiles(dir, from, to) {
|
|
1993
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1994
|
-
const entryPath = path.join(dir, entry.name);
|
|
2110
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
2111
|
+
const entryPath = path$1.join(dir, entry.name);
|
|
1995
2112
|
if (entry.isDirectory()) {
|
|
1996
2113
|
if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
|
|
1997
2114
|
replaceInTextFiles(entryPath, from, to);
|
|
1998
2115
|
continue;
|
|
1999
2116
|
}
|
|
2000
|
-
const content = fs.readFileSync(entryPath, "utf8");
|
|
2001
|
-
if (content.includes(from)) fs.writeFileSync(entryPath, content.split(from).join(to));
|
|
2117
|
+
const content = fs$1.readFileSync(entryPath, "utf8");
|
|
2118
|
+
if (content.includes(from)) fs$1.writeFileSync(entryPath, content.split(from).join(to));
|
|
2002
2119
|
}
|
|
2003
2120
|
}
|
|
2004
2121
|
function renameStylesheetExtension(root, extension) {
|
|
2005
2122
|
const cssFile = findAppCssFile(root);
|
|
2006
2123
|
if (cssFile === void 0) return;
|
|
2007
|
-
fs.renameSync(cssFile, path.join(path.dirname(cssFile), `App.${extension}`));
|
|
2124
|
+
fs$1.renameSync(cssFile, path$1.join(path$1.dirname(cssFile), `App.${extension}`));
|
|
2008
2125
|
replaceInTextFiles(root, "'./App.css'", `'./App.${extension}'`);
|
|
2009
2126
|
}
|
|
2010
2127
|
function applyStyling(root, options) {
|
|
2011
2128
|
const { styling, jsTemplateDir, hasNavigation, hasTypescript, templatesRoot } = options;
|
|
2012
2129
|
if (styling === "css") return;
|
|
2013
2130
|
if (styling === "css-modules" || styling === "stylesheet") {
|
|
2014
|
-
renderTemplate(path.join(templatesRoot, "styling", "js", jsTemplateDir, styling), root, { hasTypescript });
|
|
2015
|
-
if (hasNavigation) renderTemplate(path.join(templatesRoot, "styling", "navigation", jsTemplateDir, styling), root, { hasTypescript });
|
|
2131
|
+
renderTemplate(path$1.join(templatesRoot, "styling", "js", jsTemplateDir, styling), root, { hasTypescript });
|
|
2132
|
+
if (hasNavigation) renderTemplate(path$1.join(templatesRoot, "styling", "navigation", jsTemplateDir, styling), root, { hasTypescript });
|
|
2016
2133
|
const orphanedCss = findAppCssFile(root);
|
|
2017
|
-
if (orphanedCss !== void 0) fs.rmSync(orphanedCss, { force: true });
|
|
2134
|
+
if (orphanedCss !== void 0) fs$1.rmSync(orphanedCss, { force: true });
|
|
2018
2135
|
return;
|
|
2019
2136
|
}
|
|
2020
2137
|
if (INLINE_STYLE_BLOCK_FRAMEWORKS.has(jsTemplateDir)) {
|
|
@@ -2053,50 +2170,50 @@ var TargetDirNotEmptyError = class extends Error {
|
|
|
2053
2170
|
}
|
|
2054
2171
|
};
|
|
2055
2172
|
function resolveTemplatesRoot() {
|
|
2056
|
-
const packageDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2057
|
-
const sibling = path.join(packageDir, "templates");
|
|
2058
|
-
return fs.existsSync(sibling) ? sibling : path.resolve(packageDir, "../templates");
|
|
2173
|
+
const packageDir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
2174
|
+
const sibling = path$1.join(packageDir, "templates");
|
|
2175
|
+
return fs$1.existsSync(sibling) ? sibling : path$1.resolve(packageDir, "../templates");
|
|
2059
2176
|
}
|
|
2060
2177
|
const templatesRoot = resolveTemplatesRoot();
|
|
2061
2178
|
function isEmptyDir(dir) {
|
|
2062
|
-
if (!fs.existsSync(dir)) return true;
|
|
2063
|
-
const entries = fs.readdirSync(dir);
|
|
2179
|
+
if (!fs$1.existsSync(dir)) return true;
|
|
2180
|
+
const entries = fs$1.readdirSync(dir);
|
|
2064
2181
|
return entries.length === 0 || entries.length === 1 && entries[0] === ".git";
|
|
2065
2182
|
}
|
|
2066
2183
|
function scaffoldApp(options) {
|
|
2067
|
-
const root = path.resolve(process.cwd(), options.appName);
|
|
2184
|
+
const root = path$1.resolve(process.cwd(), options.appName);
|
|
2068
2185
|
const jsTemplateDir = resolveJsTemplateDir(options.framework, options.vueFlavor);
|
|
2069
2186
|
if (!isEmptyDir(root)) throw new TargetDirNotEmptyError(root);
|
|
2070
|
-
fs.mkdirSync(root, { recursive: true });
|
|
2071
|
-
const packageNameSource = options.appName === "." ? path.basename(root) : options.appName;
|
|
2072
|
-
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({
|
|
2187
|
+
fs$1.mkdirSync(root, { recursive: true });
|
|
2188
|
+
const packageNameSource = options.appName === "." ? path$1.basename(root) : options.appName;
|
|
2189
|
+
fs$1.writeFileSync(path$1.join(root, "package.json"), `${JSON.stringify({
|
|
2073
2190
|
name: toValidPackageName(packageNameSource),
|
|
2074
2191
|
version: "0.0.1",
|
|
2075
2192
|
private: true
|
|
2076
2193
|
}, null, 2)}\n`);
|
|
2077
|
-
renderTemplate(path.join(templatesRoot, "native"), root, { hasTypescript: options.hasTypescript });
|
|
2078
|
-
renderTemplate(path.join(templatesRoot, "js", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
|
|
2194
|
+
renderTemplate(path$1.join(templatesRoot, "native"), root, { hasTypescript: options.hasTypescript });
|
|
2195
|
+
renderTemplate(path$1.join(templatesRoot, "js", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
|
|
2079
2196
|
applyBootsplashLogo(root, templatesRoot, options.framework);
|
|
2080
2197
|
const hasExpoModules = options.hasExpoModules || (options.expoPackages?.size ?? 0) > 0;
|
|
2081
2198
|
if (options.hasNavigation) {
|
|
2082
|
-
const navigationLayerRoot = path.join(templatesRoot, "layers", "navigation");
|
|
2083
|
-
renderTemplate(path.join(navigationLayerRoot, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2084
|
-
renderTemplate(path.join(navigationLayerRoot, "app", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
|
|
2199
|
+
const navigationLayerRoot = path$1.join(templatesRoot, "layers", "navigation");
|
|
2200
|
+
renderTemplate(path$1.join(navigationLayerRoot, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2201
|
+
renderTemplate(path$1.join(navigationLayerRoot, "app", jsTemplateDir), root, { hasTypescript: options.hasTypescript });
|
|
2085
2202
|
}
|
|
2086
|
-
if (hasExpoModules) renderTemplate(path.join(templatesRoot, "layers", "expo-modules"), root);
|
|
2203
|
+
if (hasExpoModules) renderTemplate(path$1.join(templatesRoot, "layers", "expo-modules"), root);
|
|
2087
2204
|
if (options.hasSplashScreen) {
|
|
2088
|
-
const splashScreenLayerRoot = path.join(templatesRoot, "layers", "splash-screen");
|
|
2089
|
-
renderTemplate(path.join(splashScreenLayerRoot, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2090
|
-
renderTemplate(path.join(splashScreenLayerRoot, "native"), root);
|
|
2205
|
+
const splashScreenLayerRoot = path$1.join(templatesRoot, "layers", "splash-screen");
|
|
2206
|
+
renderTemplate(path$1.join(splashScreenLayerRoot, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2207
|
+
renderTemplate(path$1.join(splashScreenLayerRoot, "native"), root);
|
|
2091
2208
|
}
|
|
2092
|
-
if (options.hasSlider) renderTemplate(path.join(templatesRoot, "layers", "slider", "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2093
|
-
for (const layerName of options.expoPackages ?? []) renderTemplate(path.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2209
|
+
if (options.hasSlider) renderTemplate(path$1.join(templatesRoot, "layers", "slider", "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2210
|
+
for (const layerName of options.expoPackages ?? []) renderTemplate(path$1.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2094
2211
|
if (options.hasTesting) {
|
|
2095
|
-
const testingLayerRoot = path.join(templatesRoot, "layers", "testing");
|
|
2212
|
+
const testingLayerRoot = path$1.join(templatesRoot, "layers", "testing");
|
|
2096
2213
|
const fragmentName = options.framework === "angular" ? "angular" : "base";
|
|
2097
|
-
renderTemplate(path.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2098
|
-
renderTemplate(path.join(testingLayerRoot, "detox.config.js"), path.join(root, "detox.config.js"));
|
|
2099
|
-
renderTemplate(path.join(testingLayerRoot, "e2e"), path.join(root, "e2e"), { hasTypescript: true });
|
|
2214
|
+
renderTemplate(path$1.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2215
|
+
renderTemplate(path$1.join(testingLayerRoot, "detox.config.js"), path$1.join(root, "detox.config.js"));
|
|
2216
|
+
renderTemplate(path$1.join(testingLayerRoot, "e2e"), path$1.join(root, "e2e"), { hasTypescript: true });
|
|
2100
2217
|
}
|
|
2101
2218
|
if (hasExpoModules) {
|
|
2102
2219
|
applyExpoModulesAndroidManifest(root);
|
|
@@ -2121,9 +2238,9 @@ function scaffoldApp(options) {
|
|
|
2121
2238
|
//#endregion
|
|
2122
2239
|
//#region src/utils/find-ios-app-dir.ts
|
|
2123
2240
|
function findIosAppDir(root) {
|
|
2124
|
-
const iosDir = path.join(root, "ios");
|
|
2125
|
-
if (!fs.existsSync(iosDir)) return void 0;
|
|
2126
|
-
for (const entry of fs.readdirSync(iosDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name !== "Pods") return path.join(iosDir, entry.name);
|
|
2241
|
+
const iosDir = path$1.join(root, "ios");
|
|
2242
|
+
if (!fs$1.existsSync(iosDir)) return void 0;
|
|
2243
|
+
for (const entry of fs$1.readdirSync(iosDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name !== "Pods") return path$1.join(iosDir, entry.name);
|
|
2127
2244
|
}
|
|
2128
2245
|
//#endregion
|
|
2129
2246
|
//#region src/utils/apply-expo-modules-plist.ts
|
|
@@ -2135,14 +2252,14 @@ const USAGE_DESCRIPTIONS = [
|
|
|
2135
2252
|
function applyExpoModulesInfoPlist(root) {
|
|
2136
2253
|
const iosAppDir = findIosAppDir(root);
|
|
2137
2254
|
if (iosAppDir === void 0) return;
|
|
2138
|
-
const plistPath = path.join(iosAppDir, "Info.plist");
|
|
2139
|
-
if (!fs.existsSync(plistPath)) return;
|
|
2140
|
-
const plist = fs.readFileSync(plistPath, "utf8");
|
|
2255
|
+
const plistPath = path$1.join(iosAppDir, "Info.plist");
|
|
2256
|
+
if (!fs$1.existsSync(plistPath)) return;
|
|
2257
|
+
const plist = fs$1.readFileSync(plistPath, "utf8");
|
|
2141
2258
|
const updated = USAGE_DESCRIPTIONS.reduce((current, [key, description]) => {
|
|
2142
2259
|
if (current.includes(`<key>${key}</key>`)) return current;
|
|
2143
2260
|
return current.replace("</dict>\n</plist>", `\t<key>${key}</key>\n\t<string>${description}</string>\n</dict>\n</plist>`);
|
|
2144
2261
|
}, plist);
|
|
2145
|
-
if (updated !== plist) fs.writeFileSync(plistPath, updated);
|
|
2262
|
+
if (updated !== plist) fs$1.writeFileSync(plistPath, updated);
|
|
2146
2263
|
}
|
|
2147
2264
|
//#endregion
|
|
2148
2265
|
//#region src/utils/apply-splash-screen-app-delegate.ts
|
|
@@ -2151,9 +2268,9 @@ const REACT_IMPORT = "import React\n";
|
|
|
2151
2268
|
function applySplashScreenAppDelegate(root) {
|
|
2152
2269
|
const iosAppDir = findIosAppDir(root);
|
|
2153
2270
|
if (iosAppDir === void 0) return;
|
|
2154
|
-
const appDelegatePath = path.join(iosAppDir, "AppDelegate.swift");
|
|
2155
|
-
if (!fs.existsSync(appDelegatePath)) return;
|
|
2156
|
-
const source = fs.readFileSync(appDelegatePath, "utf8");
|
|
2271
|
+
const appDelegatePath = path$1.join(iosAppDir, "AppDelegate.swift");
|
|
2272
|
+
if (!fs$1.existsSync(appDelegatePath)) return;
|
|
2273
|
+
const source = fs$1.readFileSync(appDelegatePath, "utf8");
|
|
2157
2274
|
if (source.includes("RNBootSplash.initWithStoryboard(")) return;
|
|
2158
2275
|
if (!source.includes(REACT_IMPORT)) return;
|
|
2159
2276
|
const updated = source.includes("import RNBootSplash") ? source : source.replace(REACT_IMPORT, `${REACT_IMPORT}import RNBootSplash\n`);
|
|
@@ -2162,13 +2279,13 @@ function applySplashScreenAppDelegate(root) {
|
|
|
2162
2279
|
}
|
|
2163
2280
|
|
|
2164
2281
|
`);
|
|
2165
|
-
fs.writeFileSync(appDelegatePath, withInit);
|
|
2282
|
+
fs$1.writeFileSync(appDelegatePath, withInit);
|
|
2166
2283
|
}
|
|
2167
2284
|
//#endregion
|
|
2168
2285
|
//#region src/utils/apply-splash-screen-main-activity.ts
|
|
2169
2286
|
function findMainActivityKt(dir) {
|
|
2170
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2171
|
-
const entryPath = path.join(dir, entry.name);
|
|
2287
|
+
for (const entry of fs$1.readdirSync(dir, { withFileTypes: true })) {
|
|
2288
|
+
const entryPath = path$1.join(dir, entry.name);
|
|
2172
2289
|
if (entry.isDirectory()) {
|
|
2173
2290
|
const found = findMainActivityKt(entryPath);
|
|
2174
2291
|
if (found !== void 0) return found;
|
|
@@ -2180,32 +2297,32 @@ function findMainActivityKt(dir) {
|
|
|
2180
2297
|
const ONCREATE_OPEN = /override fun onCreate\(savedInstanceState: Bundle\?\) \{\n/;
|
|
2181
2298
|
const REACT_ACTIVITY_IMPORT = "import com.facebook.react.ReactActivity\n";
|
|
2182
2299
|
function applySplashScreenMainActivity(root) {
|
|
2183
|
-
const javaDir = path.join(root, "android/app/src/main/java");
|
|
2184
|
-
if (!fs.existsSync(javaDir)) return;
|
|
2300
|
+
const javaDir = path$1.join(root, "android/app/src/main/java");
|
|
2301
|
+
if (!fs$1.existsSync(javaDir)) return;
|
|
2185
2302
|
const mainActivityPath = findMainActivityKt(javaDir);
|
|
2186
2303
|
if (mainActivityPath === void 0) return;
|
|
2187
|
-
const source = fs.readFileSync(mainActivityPath, "utf8");
|
|
2304
|
+
const source = fs$1.readFileSync(mainActivityPath, "utf8");
|
|
2188
2305
|
if (source.includes("RNBootSplash.init(")) return;
|
|
2189
2306
|
if (!source.includes(REACT_ACTIVITY_IMPORT)) return;
|
|
2190
2307
|
let updated = source.includes("import com.zoontek.rnbootsplash.RNBootSplash") ? source : source.replace(REACT_ACTIVITY_IMPORT, `${REACT_ACTIVITY_IMPORT}import com.zoontek.rnbootsplash.RNBootSplash\n`);
|
|
2191
2308
|
if (!updated.includes("import android.os.Bundle")) updated = updated.replace(REACT_ACTIVITY_IMPORT, `import android.os.Bundle\n${REACT_ACTIVITY_IMPORT}`);
|
|
2192
2309
|
updated = ONCREATE_OPEN.test(updated) ? updated.replace(ONCREATE_OPEN, (match) => `${match} RNBootSplash.init(this, R.style.BootTheme)\n`) : updated.replace("class MainActivity : ReactActivity() {\n", "class MainActivity : ReactActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n RNBootSplash.init(this, R.style.BootTheme)\n super.onCreate(savedInstanceState)\n }\n");
|
|
2193
|
-
fs.writeFileSync(mainActivityPath, updated);
|
|
2310
|
+
fs$1.writeFileSync(mainActivityPath, updated);
|
|
2194
2311
|
}
|
|
2195
2312
|
//#endregion
|
|
2196
2313
|
//#region src/utils/apply-splash-screen-manifest.ts
|
|
2197
2314
|
const MAIN_ACTIVITY_BLOCK = /<activity\b[^>]*android:name="\.MainActivity"[^>]*>/;
|
|
2198
2315
|
function applySplashScreenManifest(root) {
|
|
2199
|
-
const manifestPath = path.join(root, "android/app/src/main/AndroidManifest.xml");
|
|
2200
|
-
if (!fs.existsSync(manifestPath)) return;
|
|
2201
|
-
const manifest = fs.readFileSync(manifestPath, "utf8");
|
|
2316
|
+
const manifestPath = path$1.join(root, "android/app/src/main/AndroidManifest.xml");
|
|
2317
|
+
if (!fs$1.existsSync(manifestPath)) return;
|
|
2318
|
+
const manifest = fs$1.readFileSync(manifestPath, "utf8");
|
|
2202
2319
|
const match = manifest.match(MAIN_ACTIVITY_BLOCK);
|
|
2203
2320
|
if (match === null) return;
|
|
2204
2321
|
const block = match[0];
|
|
2205
2322
|
if (block.includes("@style/BootTheme")) return;
|
|
2206
2323
|
if (!block.includes("android:theme=\"@style/AppTheme\"")) return;
|
|
2207
2324
|
const updatedBlock = block.replace("android:theme=\"@style/AppTheme\"", "android:theme=\"@style/BootTheme\"");
|
|
2208
|
-
fs.writeFileSync(manifestPath, manifest.replace(block, updatedBlock));
|
|
2325
|
+
fs$1.writeFileSync(manifestPath, manifest.replace(block, updatedBlock));
|
|
2209
2326
|
}
|
|
2210
2327
|
//#endregion
|
|
2211
2328
|
//#region src/utils/apply-splash-screen-styles.ts
|
|
@@ -2216,22 +2333,22 @@ const BOOT_THEME_STYLE = ` <style name="BootTheme" parent="Theme.BootSplash">
|
|
|
2216
2333
|
</style>
|
|
2217
2334
|
`;
|
|
2218
2335
|
function applySplashScreenStyles(root) {
|
|
2219
|
-
const stylesPath = path.join(root, "android/app/src/main/res/values/styles.xml");
|
|
2220
|
-
if (!fs.existsSync(stylesPath)) return;
|
|
2221
|
-
const styles = fs.readFileSync(stylesPath, "utf8");
|
|
2336
|
+
const stylesPath = path$1.join(root, "android/app/src/main/res/values/styles.xml");
|
|
2337
|
+
if (!fs$1.existsSync(stylesPath)) return;
|
|
2338
|
+
const styles = fs$1.readFileSync(stylesPath, "utf8");
|
|
2222
2339
|
if (styles.includes("name=\"BootTheme\"")) return;
|
|
2223
2340
|
if (!styles.includes("</resources>")) return;
|
|
2224
|
-
fs.writeFileSync(stylesPath, styles.replace("</resources>", `${BOOT_THEME_STYLE}</resources>`));
|
|
2341
|
+
fs$1.writeFileSync(stylesPath, styles.replace("</resources>", `${BOOT_THEME_STYLE}</resources>`));
|
|
2225
2342
|
}
|
|
2226
2343
|
//#endregion
|
|
2227
2344
|
//#region src/add-layers.ts
|
|
2228
2345
|
function applyDependencyOnlyLayer(root, layerName) {
|
|
2229
|
-
renderTemplate(path.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2346
|
+
renderTemplate(path$1.join(templatesRoot, "layers", layerName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2230
2347
|
}
|
|
2231
2348
|
function applyExpoModulesLayer(root) {
|
|
2232
2349
|
applyDependencyOnlyLayer(root, "expo-modules");
|
|
2233
|
-
if (fs.existsSync(path.join(root, "android/app/src/main/AndroidManifest.xml"))) applyExpoModulesAndroidManifest(root);
|
|
2234
|
-
if (fs.existsSync(path.join(root, "ios"))) {
|
|
2350
|
+
if (fs$1.existsSync(path$1.join(root, "android/app/src/main/AndroidManifest.xml"))) applyExpoModulesAndroidManifest(root);
|
|
2351
|
+
if (fs$1.existsSync(path$1.join(root, "ios"))) {
|
|
2235
2352
|
applyExpoModulesInfoPlist(root);
|
|
2236
2353
|
applyExpoModulesPodfilePlatform(root);
|
|
2237
2354
|
applyExpoModulesPodfileAutolinking(root);
|
|
@@ -2248,14 +2365,14 @@ function applySplashScreenLayer(root, framework) {
|
|
|
2248
2365
|
applyBootsplashLogo(root, templatesRoot, framework);
|
|
2249
2366
|
}
|
|
2250
2367
|
async function applyTestingLayer(root, framework, confirmOverwrite) {
|
|
2251
|
-
const testingLayerRoot = path.join(templatesRoot, "layers", "testing");
|
|
2368
|
+
const testingLayerRoot = path$1.join(templatesRoot, "layers", "testing");
|
|
2252
2369
|
const fragmentName = framework === "angular" ? "angular" : "base";
|
|
2253
|
-
renderTemplate(path.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path.join(root, "package.json.fragment.json"));
|
|
2254
|
-
const detoxConfigPath = path.join(root, "detox.config.js");
|
|
2255
|
-
const e2eDirPath = path.join(root, "e2e");
|
|
2256
|
-
if ((fs.existsSync(detoxConfigPath) || fs.existsSync(e2eDirPath)) && !await confirmOverwrite()) return false;
|
|
2257
|
-
renderTemplate(path.join(testingLayerRoot, "detox.config.js"), detoxConfigPath);
|
|
2258
|
-
renderTemplate(path.join(testingLayerRoot, "e2e"), e2eDirPath, { hasTypescript: true });
|
|
2370
|
+
renderTemplate(path$1.join(testingLayerRoot, "fragment", fragmentName, "package.json.fragment.json"), path$1.join(root, "package.json.fragment.json"));
|
|
2371
|
+
const detoxConfigPath = path$1.join(root, "detox.config.js");
|
|
2372
|
+
const e2eDirPath = path$1.join(root, "e2e");
|
|
2373
|
+
if ((fs$1.existsSync(detoxConfigPath) || fs$1.existsSync(e2eDirPath)) && !await confirmOverwrite()) return false;
|
|
2374
|
+
renderTemplate(path$1.join(testingLayerRoot, "detox.config.js"), detoxConfigPath);
|
|
2375
|
+
renderTemplate(path$1.join(testingLayerRoot, "e2e"), e2eDirPath, { hasTypescript: true });
|
|
2259
2376
|
return true;
|
|
2260
2377
|
}
|
|
2261
2378
|
async function addLayersToApp(options) {
|
|
@@ -2330,6 +2447,591 @@ function getCommand(packageManager, script) {
|
|
|
2330
2447
|
return packageManager === "npm" ? `npm run ${script}` : `${packageManager} ${script}`;
|
|
2331
2448
|
}
|
|
2332
2449
|
//#endregion
|
|
2450
|
+
//#region src/grant-bundles.ts
|
|
2451
|
+
var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2452
|
+
const fs = __require("node:fs");
|
|
2453
|
+
const path = __require("node:path");
|
|
2454
|
+
const BEGIN_TAG = "SYMBIOTE-EXPO-LINK:BEGIN";
|
|
2455
|
+
const END_TAG = "SYMBIOTE-EXPO-LINK:END";
|
|
2456
|
+
const REGION_NOTE = "generated by symbiote-expo-link - do not edit, regenerated on every install";
|
|
2457
|
+
const REACT_ANDROID_ANCHOR = "implementation(\"com.facebook.react:react-android\")";
|
|
2458
|
+
const MODULES_MAP_ANCHOR = "override fun getModulesMap(): Map<Class<out Module>, String?> = mapOf(";
|
|
2459
|
+
const MODULES_PROVIDER_CLASS_ANCHOR = ": ModulesProvider {";
|
|
2460
|
+
const MANIFEST_FILENAME = "native-link.json";
|
|
2461
|
+
function isDebug() {
|
|
2462
|
+
return process.env.DEBUG === "1" || process.env.DEBUG === "true" || globalThis.__SYMBIOTE_DEBUG__ === true;
|
|
2463
|
+
}
|
|
2464
|
+
function dlog(...args) {
|
|
2465
|
+
if (isDebug()) console.log("[symbiote-expo-link]", ...args);
|
|
2466
|
+
}
|
|
2467
|
+
function warn(message) {
|
|
2468
|
+
console.warn(`[symbiote-expo-link] ${message}`);
|
|
2469
|
+
}
|
|
2470
|
+
function writeFileAtomic(filePath, content) {
|
|
2471
|
+
const tmpPath = `${filePath}.symbiote-expo-link.${process.pid}.tmp`;
|
|
2472
|
+
fs.writeFileSync(tmpPath, content);
|
|
2473
|
+
fs.renameSync(tmpPath, filePath);
|
|
2474
|
+
}
|
|
2475
|
+
function findAppRoot() {
|
|
2476
|
+
const candidates = [process.cwd(), process.env.INIT_CWD].filter(Boolean);
|
|
2477
|
+
for (const candidate of candidates) {
|
|
2478
|
+
if (fs.existsSync(path.join(candidate, "android", "app", "build.gradle"))) return candidate;
|
|
2479
|
+
if (findInfoPlistFile(candidate)) return candidate;
|
|
2480
|
+
}
|
|
2481
|
+
return null;
|
|
2482
|
+
}
|
|
2483
|
+
function findInfoPlistFile(appRoot) {
|
|
2484
|
+
const iosRoot = path.join(appRoot, "ios");
|
|
2485
|
+
if (!fs.existsSync(iosRoot)) return null;
|
|
2486
|
+
const skip = /Pods|build|DerivedData|Tests/i;
|
|
2487
|
+
const stack = [iosRoot];
|
|
2488
|
+
while (stack.length > 0) {
|
|
2489
|
+
const dir = stack.pop();
|
|
2490
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2491
|
+
if (skip.test(entry.name)) continue;
|
|
2492
|
+
const full = path.join(dir, entry.name);
|
|
2493
|
+
if (entry.isDirectory()) stack.push(full);
|
|
2494
|
+
else if (entry.name === "Info.plist") return full;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
function findMainApplicationFile(appRoot) {
|
|
2500
|
+
const javaRoot = path.join(appRoot, "android", "app", "src", "main", "java");
|
|
2501
|
+
if (!fs.existsSync(javaRoot)) return null;
|
|
2502
|
+
const stack = [javaRoot];
|
|
2503
|
+
while (stack.length > 0) {
|
|
2504
|
+
const dir = stack.pop();
|
|
2505
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2506
|
+
const full = path.join(dir, entry.name);
|
|
2507
|
+
if (entry.isDirectory()) stack.push(full);
|
|
2508
|
+
else if (entry.name === "MainApplication.kt") return full;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
function readManifest(packageDir, packageName) {
|
|
2514
|
+
const manifestPath = path.join(packageDir, MANIFEST_FILENAME);
|
|
2515
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
2516
|
+
try {
|
|
2517
|
+
return {
|
|
2518
|
+
packageName,
|
|
2519
|
+
manifest: JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
|
2520
|
+
};
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
warn(`skipping ${packageName}: its ${MANIFEST_FILENAME} is not valid JSON (${error.message})`);
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function collectManifests(appRoot) {
|
|
2527
|
+
const modulesRoot = path.join(appRoot, "node_modules");
|
|
2528
|
+
if (!fs.existsSync(modulesRoot)) {
|
|
2529
|
+
warn(`no node_modules in ${appRoot}, nothing to link`);
|
|
2530
|
+
return [];
|
|
2531
|
+
}
|
|
2532
|
+
const found = [];
|
|
2533
|
+
for (const entry of fs.readdirSync(modulesRoot, { withFileTypes: true })) {
|
|
2534
|
+
if (entry.name.startsWith(".")) continue;
|
|
2535
|
+
if (entry.name.startsWith("@")) {
|
|
2536
|
+
const scopeDir = path.join(modulesRoot, entry.name);
|
|
2537
|
+
for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
2538
|
+
const entryFound = readManifest(path.join(scopeDir, scoped.name), `${entry.name}/${scoped.name}`);
|
|
2539
|
+
if (entryFound) found.push(entryFound);
|
|
2540
|
+
}
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
const entryFound = readManifest(path.join(modulesRoot, entry.name), entry.name);
|
|
2544
|
+
if (entryFound) found.push(entryFound);
|
|
2545
|
+
}
|
|
2546
|
+
found.sort((a, b) => a.packageName < b.packageName ? -1 : a.packageName > b.packageName ? 1 : 0);
|
|
2547
|
+
dlog(`found ${found.length} linkable package(s): ${found.map((f) => f.packageName).join(", ")}`);
|
|
2548
|
+
return found;
|
|
2549
|
+
}
|
|
2550
|
+
function writeRegion({ filePath, content, name, indent, bodyLines, findAnchorIndex, anchorLabel }) {
|
|
2551
|
+
const lines = content.split("\n");
|
|
2552
|
+
const block = [
|
|
2553
|
+
`${indent}// ${BEGIN_TAG} ${name} (${REGION_NOTE})`,
|
|
2554
|
+
...bodyLines.map((line) => `${indent}${line}`),
|
|
2555
|
+
`${indent}// ${END_TAG} ${name}`
|
|
2556
|
+
];
|
|
2557
|
+
const beginIndex = lines.findIndex((line) => line.includes(`${BEGIN_TAG} ${name}`));
|
|
2558
|
+
if (beginIndex !== -1) {
|
|
2559
|
+
const endIndex = lines.findIndex((line, i) => i > beginIndex && line.includes(`${END_TAG} ${name}`));
|
|
2560
|
+
if (endIndex === -1) {
|
|
2561
|
+
warn(`${filePath} has "${BEGIN_TAG} ${name}" but no matching "${END_TAG} ${name}". Restore the end marker (or delete the whole block) and reinstall.`);
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
lines.splice(beginIndex, endIndex - beginIndex + 1, ...block);
|
|
2565
|
+
return lines.join("\n");
|
|
2566
|
+
}
|
|
2567
|
+
const anchorIndex = findAnchorIndex(lines);
|
|
2568
|
+
if (anchorIndex === -1) {
|
|
2569
|
+
warn(`cannot create the ${name} block in ${filePath}: ${anchorLabel}`);
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
lines.splice(anchorIndex + 1, 0, ...block);
|
|
2573
|
+
return lines.join("\n");
|
|
2574
|
+
}
|
|
2575
|
+
function applyIfChanged(filePath, content, nextContent) {
|
|
2576
|
+
if (nextContent === null || nextContent === content) return;
|
|
2577
|
+
writeFileAtomic(filePath, nextContent);
|
|
2578
|
+
dlog(`updated ${filePath}`);
|
|
2579
|
+
}
|
|
2580
|
+
function patchBuildGradle(appRoot, entries) {
|
|
2581
|
+
const gradlePath = path.join(appRoot, "android", "app", "build.gradle");
|
|
2582
|
+
if (!fs.existsSync(gradlePath)) {
|
|
2583
|
+
dlog("android/app/build.gradle not found, skipping");
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
const primaryProjectNames = entries.filter((entry) => entry.manifest.android && entry.manifest.android.gradleProjectName).map((entry) => entry.manifest.android.gradleProjectName);
|
|
2587
|
+
const serviceProjectNames = androidServicesOf(entries).map((service) => service.gradleProjectName);
|
|
2588
|
+
const deps = [.../* @__PURE__ */ new Set([...primaryProjectNames, ...serviceProjectNames])].sort().map((name) => `implementation project(':${name}')`);
|
|
2589
|
+
const content = fs.readFileSync(gradlePath, "utf8");
|
|
2590
|
+
applyIfChanged(gradlePath, content, writeRegion({
|
|
2591
|
+
filePath: gradlePath,
|
|
2592
|
+
content,
|
|
2593
|
+
name: "DEPENDENCIES",
|
|
2594
|
+
indent: " ",
|
|
2595
|
+
bodyLines: deps,
|
|
2596
|
+
findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(REACT_ANDROID_ANCHOR)),
|
|
2597
|
+
anchorLabel: `no \`${REACT_ANDROID_ANCHOR}\` line to anchor below`
|
|
2598
|
+
}));
|
|
2599
|
+
}
|
|
2600
|
+
function androidModulesOf(entries) {
|
|
2601
|
+
return entries.flatMap((entry) => entry.manifest.android && Array.isArray(entry.manifest.android.modules) ? entry.manifest.android.modules : []);
|
|
2602
|
+
}
|
|
2603
|
+
function androidServicesOf(entries) {
|
|
2604
|
+
return entries.flatMap((entry) => entry.manifest.android && Array.isArray(entry.manifest.android.services) ? entry.manifest.android.services : []);
|
|
2605
|
+
}
|
|
2606
|
+
function patchMainApplication(appRoot, entries) {
|
|
2607
|
+
const filePath = findMainApplicationFile(appRoot);
|
|
2608
|
+
if (!filePath) {
|
|
2609
|
+
dlog("MainApplication.kt not found, skipping");
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const modules = androidModulesOf(entries);
|
|
2613
|
+
const services = androidServicesOf(entries);
|
|
2614
|
+
const imports = [.../* @__PURE__ */ new Set([
|
|
2615
|
+
...modules.map((module$1) => `import ${module$1.importPath}`),
|
|
2616
|
+
...services.map((service) => `import ${service.importPath}`),
|
|
2617
|
+
"import expo.modules.kotlin.services.Service"
|
|
2618
|
+
])].sort();
|
|
2619
|
+
const mapEntries = [...new Set(modules.map((module$2) => `${module$2.className}::class.java to "${module$2.nativeName}",`))].sort();
|
|
2620
|
+
const serviceEntries = [...new Set(services.map((service) => ` ${service.className}::class.java,`))].sort();
|
|
2621
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
2622
|
+
const withImports = writeRegion({
|
|
2623
|
+
filePath,
|
|
2624
|
+
content,
|
|
2625
|
+
name: "IMPORTS",
|
|
2626
|
+
indent: "",
|
|
2627
|
+
bodyLines: imports,
|
|
2628
|
+
findAnchorIndex: (lines) => {
|
|
2629
|
+
let lastImport = -1;
|
|
2630
|
+
for (let i = 0; i < lines.length; i += 1) if (lines[i].trim().startsWith("import ")) lastImport = i;
|
|
2631
|
+
return lastImport;
|
|
2632
|
+
},
|
|
2633
|
+
anchorLabel: "no import statements to anchor below"
|
|
2634
|
+
});
|
|
2635
|
+
if (withImports === null) return;
|
|
2636
|
+
const withModules = writeRegion({
|
|
2637
|
+
filePath,
|
|
2638
|
+
content: withImports,
|
|
2639
|
+
name: "MODULES-MAP",
|
|
2640
|
+
indent: " ",
|
|
2641
|
+
bodyLines: mapEntries,
|
|
2642
|
+
findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(MODULES_MAP_ANCHOR)),
|
|
2643
|
+
anchorLabel: "no `getModulesMap()` declaration to anchor below"
|
|
2644
|
+
});
|
|
2645
|
+
if (withModules === null) return;
|
|
2646
|
+
applyIfChanged(filePath, content, writeRegion({
|
|
2647
|
+
filePath,
|
|
2648
|
+
content: withModules,
|
|
2649
|
+
name: "SERVICES",
|
|
2650
|
+
indent: " ",
|
|
2651
|
+
bodyLines: [
|
|
2652
|
+
"override fun getServices(): List<Class<out Service>> = listOf(",
|
|
2653
|
+
...serviceEntries,
|
|
2654
|
+
")"
|
|
2655
|
+
],
|
|
2656
|
+
findAnchorIndex: (lines) => lines.findIndex((line) => line.includes(MODULES_PROVIDER_CLASS_ANCHOR)),
|
|
2657
|
+
anchorLabel: "no `ExpoModulesProvider : ModulesProvider` class declaration to anchor below"
|
|
2658
|
+
}));
|
|
2659
|
+
}
|
|
2660
|
+
function escapeXml(value) {
|
|
2661
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2662
|
+
}
|
|
2663
|
+
function escapeXmlAttribute(value) {
|
|
2664
|
+
return escapeXml(value).replace(/"/g, """);
|
|
2665
|
+
}
|
|
2666
|
+
function findTag(content, tagName) {
|
|
2667
|
+
const match = new RegExp(`<${tagName}(?=[\\s>/])`).exec(content);
|
|
2668
|
+
if (!match) return null;
|
|
2669
|
+
let quote = null;
|
|
2670
|
+
for (let i = match.index; i < content.length; i += 1) {
|
|
2671
|
+
const char = content[i];
|
|
2672
|
+
if (quote !== null) {
|
|
2673
|
+
if (char === quote) quote = null;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
if (char === "\"" || char === "'") quote = char;
|
|
2677
|
+
else if (char === ">") return {
|
|
2678
|
+
start: match.index,
|
|
2679
|
+
end: i
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
function findApplicationTag(content) {
|
|
2685
|
+
return findTag(content, "application");
|
|
2686
|
+
}
|
|
2687
|
+
function findAllTags(content, tagName) {
|
|
2688
|
+
const tags = [];
|
|
2689
|
+
const re = new RegExp(`<${tagName}(?=[\\s>/])`, "g");
|
|
2690
|
+
let match = re.exec(content);
|
|
2691
|
+
while (match) {
|
|
2692
|
+
let quote = null;
|
|
2693
|
+
for (let i = match.index; i < content.length; i += 1) {
|
|
2694
|
+
const char = content[i];
|
|
2695
|
+
if (quote !== null) {
|
|
2696
|
+
if (char === quote) quote = null;
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
if (char === "\"" || char === "'") quote = char;
|
|
2700
|
+
else if (char === ">") {
|
|
2701
|
+
tags.push({
|
|
2702
|
+
start: match.index,
|
|
2703
|
+
end: i
|
|
2704
|
+
});
|
|
2705
|
+
re.lastIndex = i + 1;
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
match = re.exec(content);
|
|
2710
|
+
}
|
|
2711
|
+
return tags;
|
|
2712
|
+
}
|
|
2713
|
+
function findMainActivityTag(content) {
|
|
2714
|
+
for (const tag of findAllTags(content, "activity")) {
|
|
2715
|
+
const name = readXmlAttribute(content.slice(tag.start, tag.end), "android:name");
|
|
2716
|
+
if (name && /(^|\.)MainActivity$/.test(name)) return tag;
|
|
2717
|
+
}
|
|
2718
|
+
return null;
|
|
2719
|
+
}
|
|
2720
|
+
function readXmlAttribute(tagText, name) {
|
|
2721
|
+
const match = new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`).exec(tagText);
|
|
2722
|
+
return match ? match[1] : null;
|
|
2723
|
+
}
|
|
2724
|
+
function patchAndroidManifest(appRoot, entries) {
|
|
2725
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2726
|
+
for (const entry of entries) {
|
|
2727
|
+
const attributes = entry.manifest.android && entry.manifest.android.manifestApplicationAttributes;
|
|
2728
|
+
if (!attributes) continue;
|
|
2729
|
+
for (const [name, value] of Object.entries(attributes)) if (!wanted.has(name)) wanted.set(name, value);
|
|
2730
|
+
}
|
|
2731
|
+
if (wanted.size === 0) return;
|
|
2732
|
+
const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
2733
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2734
|
+
dlog("AndroidManifest.xml not found, skipping application attributes");
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
2738
|
+
let next = content;
|
|
2739
|
+
for (const [name, value] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
2740
|
+
if (!/^[\w.:-]+$/.test(name)) {
|
|
2741
|
+
warn(`skipping the manifest attribute "${name}": not a plain XML attribute name`);
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
const tag = findApplicationTag(next);
|
|
2745
|
+
if (!tag) {
|
|
2746
|
+
warn(`${manifestPath} has no <application> element, skipping ${name}`);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
const tagText = next.slice(tag.start, tag.end);
|
|
2750
|
+
const escaped = escapeXmlAttribute(value);
|
|
2751
|
+
const existing = readXmlAttribute(tagText, name);
|
|
2752
|
+
if (existing !== null) {
|
|
2753
|
+
if (existing !== escaped) warn(`${name} in ${manifestPath} reads "${existing}" but the package manifest says "${value}". Keeping the file as-is; edit it by hand if you want the manifest's.`);
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
const lastNewline = tagText.lastIndexOf("\n");
|
|
2757
|
+
const separator = lastNewline === -1 ? " " : `\n${/^[ \t]*/.exec(tagText.slice(lastNewline + 1))[0]}`;
|
|
2758
|
+
const insertAt = next[tag.end - 1] === "/" ? tag.end - 1 : tag.end;
|
|
2759
|
+
next = `${next.slice(0, insertAt)}${separator}${name}="${escaped}"${next.slice(insertAt)}`;
|
|
2760
|
+
}
|
|
2761
|
+
applyIfChanged(manifestPath, content, next);
|
|
2762
|
+
}
|
|
2763
|
+
function patchMainActivityConfigChanges(appRoot, entries) {
|
|
2764
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
2765
|
+
for (const entry of entries) {
|
|
2766
|
+
const values = entry.manifest.android && entry.manifest.android.mainActivityConfigChanges;
|
|
2767
|
+
if (!values) continue;
|
|
2768
|
+
for (const value of values) wanted.add(value);
|
|
2769
|
+
}
|
|
2770
|
+
if (wanted.size === 0) return;
|
|
2771
|
+
const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
2772
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2773
|
+
dlog("AndroidManifest.xml not found, skipping MainActivity configChanges");
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
2777
|
+
const tag = findMainActivityTag(content);
|
|
2778
|
+
if (!tag) {
|
|
2779
|
+
warn(`${manifestPath} has no MainActivity <activity> element, skipping configChanges`);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const tagText = content.slice(tag.start, tag.end);
|
|
2783
|
+
const existingValue = readXmlAttribute(tagText, "android:configChanges");
|
|
2784
|
+
const existingTokens = existingValue ? existingValue.split("|").map((token) => token.trim()).filter(Boolean) : [];
|
|
2785
|
+
const existingSet = new Set(existingTokens);
|
|
2786
|
+
const missing = [...wanted].filter((token) => !existingSet.has(token)).sort();
|
|
2787
|
+
if (missing.length === 0) return;
|
|
2788
|
+
const merged = [...existingTokens, ...missing].join("|");
|
|
2789
|
+
let next;
|
|
2790
|
+
if (existingValue !== null) {
|
|
2791
|
+
const valueMatch = /android:configChanges\s*=\s*"/.exec(tagText);
|
|
2792
|
+
const valueStart = valueMatch.index + valueMatch[0].length;
|
|
2793
|
+
const valueEnd = tagText.indexOf("\"", valueStart);
|
|
2794
|
+
const nextTagText = tagText.slice(0, valueStart) + merged + tagText.slice(valueEnd);
|
|
2795
|
+
next = content.slice(0, tag.start) + nextTagText + content.slice(tag.end);
|
|
2796
|
+
} else {
|
|
2797
|
+
const lastNewline = tagText.lastIndexOf("\n");
|
|
2798
|
+
const separator = lastNewline === -1 ? " " : `\n${/^[ \t]*/.exec(tagText.slice(lastNewline + 1))[0]}`;
|
|
2799
|
+
const selfClosing = content[tag.end - 1] === "/";
|
|
2800
|
+
const insertAt = selfClosing ? tag.end - 1 : tag.end;
|
|
2801
|
+
next = `${content.slice(0, insertAt).replace(/[ \t]+$/, "")}${separator}android:configChanges="${merged}"${selfClosing ? " " : ""}${content.slice(insertAt)}`;
|
|
2802
|
+
}
|
|
2803
|
+
applyIfChanged(manifestPath, content, next);
|
|
2804
|
+
}
|
|
2805
|
+
function readManifestPermissions(content) {
|
|
2806
|
+
return new Set([...content.matchAll(/<uses-permission\s[^>]*android:name="([^"]+)"/g)].map((match) => match[1]));
|
|
2807
|
+
}
|
|
2808
|
+
function patchAndroidManifestPermissions(appRoot, entries) {
|
|
2809
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
2810
|
+
for (const entry of entries) {
|
|
2811
|
+
const permissions = entry.manifest.android && entry.manifest.android.manifestPermissions;
|
|
2812
|
+
if (!permissions) continue;
|
|
2813
|
+
for (const permission of permissions) wanted.add(permission);
|
|
2814
|
+
}
|
|
2815
|
+
if (wanted.size === 0) return;
|
|
2816
|
+
const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
2817
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2818
|
+
dlog("AndroidManifest.xml not found, skipping permissions");
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
2822
|
+
const existing = readManifestPermissions(content);
|
|
2823
|
+
const missing = [...wanted].filter((permission) => !existing.has(permission)).sort();
|
|
2824
|
+
if (missing.length === 0) return;
|
|
2825
|
+
const tag = findTag(content, "manifest");
|
|
2826
|
+
if (!tag) {
|
|
2827
|
+
warn(`${manifestPath} has no <manifest> element, skipping permissions`);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const lines = missing.map((permission) => ` <uses-permission android:name="${permission}" />`).join("\n");
|
|
2831
|
+
applyIfChanged(manifestPath, content, `${content.slice(0, tag.end + 1)}\n${lines}${content.slice(tag.end + 1)}`);
|
|
2832
|
+
}
|
|
2833
|
+
function readManifestServiceNames(content) {
|
|
2834
|
+
return new Set([...content.matchAll(/<service\s[^>]*android:name="([^"]+)"/g)].map((match) => match[1]));
|
|
2835
|
+
}
|
|
2836
|
+
function buildServiceXml(service, indent) {
|
|
2837
|
+
const attrs = [`android:name="${escapeXmlAttribute(service.name)}"`, `android:exported="${service.exported === true ? "true" : "false"}"`];
|
|
2838
|
+
if (service.foregroundServiceType) attrs.push(`android:foregroundServiceType="${escapeXmlAttribute(service.foregroundServiceType)}"`);
|
|
2839
|
+
const openTag = `${indent}<service ${attrs.join(" ")}`;
|
|
2840
|
+
if (!service.intentFilterActions || service.intentFilterActions.length === 0) return `${openTag} />`;
|
|
2841
|
+
return `${openTag}>\n${indent} <intent-filter>\n${service.intentFilterActions.map((action) => `${indent} <action android:name="${escapeXmlAttribute(action)}" />`).join("\n")}\n${indent} </intent-filter>\n${indent}</service>`;
|
|
2842
|
+
}
|
|
2843
|
+
function patchAndroidManifestServices(appRoot, entries) {
|
|
2844
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2845
|
+
for (const entry of entries) {
|
|
2846
|
+
const services = entry.manifest.android && entry.manifest.android.manifestServices;
|
|
2847
|
+
if (!services) continue;
|
|
2848
|
+
for (const service of services) if (!wanted.has(service.name)) wanted.set(service.name, service);
|
|
2849
|
+
}
|
|
2850
|
+
if (wanted.size === 0) return;
|
|
2851
|
+
const manifestPath = path.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
2852
|
+
if (!fs.existsSync(manifestPath)) {
|
|
2853
|
+
dlog("AndroidManifest.xml not found, skipping services");
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
2857
|
+
const existing = readManifestServiceNames(content);
|
|
2858
|
+
const missing = [...wanted.values()].filter((service) => !existing.has(service.name)).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
2859
|
+
if (missing.length === 0) return;
|
|
2860
|
+
const tag = findApplicationTag(content);
|
|
2861
|
+
if (!tag) {
|
|
2862
|
+
warn(`${manifestPath} has no <application> element, skipping services`);
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
if (content[tag.end - 1] === "/") {
|
|
2866
|
+
warn(`${manifestPath}'s <application> is self-closing, skipping services`);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const lastNewline = content.lastIndexOf("\n", tag.start);
|
|
2870
|
+
const indent = `${/^[ \t]*/.exec(content.slice(lastNewline + 1, tag.start))[0]} `;
|
|
2871
|
+
const blocks = missing.map((service) => buildServiceXml(service, indent)).join("\n");
|
|
2872
|
+
const insertAt = tag.end + 1;
|
|
2873
|
+
applyIfChanged(manifestPath, content, `${content.slice(0, insertAt)}\n${blocks}${content.slice(insertAt)}`);
|
|
2874
|
+
}
|
|
2875
|
+
function readPlistString(content, key) {
|
|
2876
|
+
const keyIndex = content.indexOf(`<key>${key}</key>`);
|
|
2877
|
+
if (keyIndex === -1) return null;
|
|
2878
|
+
const match = /<string>([\s\S]*?)<\/string>/.exec(content.slice(keyIndex));
|
|
2879
|
+
return match ? match[1] : null;
|
|
2880
|
+
}
|
|
2881
|
+
function patchInfoPlist(appRoot, entries) {
|
|
2882
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2883
|
+
for (const entry of entries) {
|
|
2884
|
+
const keys = entry.manifest.ios && entry.manifest.ios.infoPlistKeys;
|
|
2885
|
+
if (!keys) continue;
|
|
2886
|
+
for (const [key, description] of Object.entries(keys)) if (!wanted.has(key)) wanted.set(key, description);
|
|
2887
|
+
}
|
|
2888
|
+
if (wanted.size === 0) return;
|
|
2889
|
+
const plistPath = findInfoPlistFile(appRoot);
|
|
2890
|
+
if (!plistPath) {
|
|
2891
|
+
dlog("Info.plist not found, skipping iOS permission strings");
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
const content = fs.readFileSync(plistPath, "utf8");
|
|
2895
|
+
let next = content;
|
|
2896
|
+
for (const [key, description] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
2897
|
+
const escaped = escapeXml(description);
|
|
2898
|
+
const existing = readPlistString(next, key);
|
|
2899
|
+
if (existing !== null) {
|
|
2900
|
+
if (existing !== escaped) warn(`${key} in ${plistPath} reads "${existing}" but the package manifest says "${description}". Keeping the file as-is; edit it by hand if you want the manifest's.`);
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
const plistCloseIndex = next.lastIndexOf("</plist>");
|
|
2904
|
+
const dictCloseIndex = next.lastIndexOf("</dict>", plistCloseIndex === -1 ? void 0 : plistCloseIndex);
|
|
2905
|
+
if (dictCloseIndex === -1) {
|
|
2906
|
+
warn(`${plistPath} has no closing </dict>, skipping ${key}`);
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
const block = `\t<key>${key}</key>\n\t<string>${escaped}</string>\n`;
|
|
2910
|
+
next = next.slice(0, dictCloseIndex) + block + next.slice(dictCloseIndex);
|
|
2911
|
+
}
|
|
2912
|
+
applyIfChanged(plistPath, content, next);
|
|
2913
|
+
}
|
|
2914
|
+
function readPlistArray(content, key) {
|
|
2915
|
+
const keyTag = `<key>${key}</key>`;
|
|
2916
|
+
const keyIndex = content.indexOf(keyTag);
|
|
2917
|
+
if (keyIndex === -1) return null;
|
|
2918
|
+
const arrayOpenIndex = content.indexOf("<array>", keyIndex + keyTag.length);
|
|
2919
|
+
const arrayCloseIndex = arrayOpenIndex === -1 ? -1 : content.indexOf("</array>", arrayOpenIndex);
|
|
2920
|
+
if (arrayOpenIndex === -1 || arrayCloseIndex === -1) return null;
|
|
2921
|
+
return {
|
|
2922
|
+
arrayCloseIndex,
|
|
2923
|
+
items: [...content.slice(arrayOpenIndex + 7, arrayCloseIndex).matchAll(/<string>([\s\S]*?)<\/string>/g)].map((match) => match[1])
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
function patchInfoPlistArrays(appRoot, entries) {
|
|
2927
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2928
|
+
for (const entry of entries) {
|
|
2929
|
+
const arrayKeys = entry.manifest.ios && entry.manifest.ios.infoPlistArrayKeys;
|
|
2930
|
+
if (!arrayKeys) continue;
|
|
2931
|
+
for (const [key, items] of Object.entries(arrayKeys)) {
|
|
2932
|
+
if (!wanted.has(key)) wanted.set(key, /* @__PURE__ */ new Set());
|
|
2933
|
+
for (const item of items) wanted.get(key).add(item);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
if (wanted.size === 0) return;
|
|
2937
|
+
const plistPath = findInfoPlistFile(appRoot);
|
|
2938
|
+
if (!plistPath) {
|
|
2939
|
+
dlog("Info.plist not found, skipping iOS array keys");
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const content = fs.readFileSync(plistPath, "utf8");
|
|
2943
|
+
let next = content;
|
|
2944
|
+
for (const [key, itemsSet] of [...wanted].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
2945
|
+
const wantedItems = [...itemsSet];
|
|
2946
|
+
const existing = readPlistArray(next, key);
|
|
2947
|
+
if (existing === null) {
|
|
2948
|
+
const plistCloseIndex = next.lastIndexOf("</plist>");
|
|
2949
|
+
const dictCloseIndex = next.lastIndexOf("</dict>", plistCloseIndex === -1 ? void 0 : plistCloseIndex);
|
|
2950
|
+
if (dictCloseIndex === -1) {
|
|
2951
|
+
warn(`${plistPath} has no closing </dict>, skipping ${key}`);
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
const block = `\t<key>${key}</key>\n\t<array>\n${wantedItems.map((item) => `\t\t<string>${escapeXml(item)}</string>`).join("\n")}\n\t</array>\n`;
|
|
2955
|
+
next = next.slice(0, dictCloseIndex) + block + next.slice(dictCloseIndex);
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
const missing = wantedItems.filter((item) => !existing.items.includes(item));
|
|
2959
|
+
if (missing.length === 0) continue;
|
|
2960
|
+
const missingLines = missing.map((item) => `\t\t<string>${escapeXml(item)}</string>\n`).join("");
|
|
2961
|
+
next = next.slice(0, existing.arrayCloseIndex) + missingLines + next.slice(existing.arrayCloseIndex);
|
|
2962
|
+
}
|
|
2963
|
+
applyIfChanged(plistPath, content, next);
|
|
2964
|
+
}
|
|
2965
|
+
function linkApp(explicitAppRoot) {
|
|
2966
|
+
const appRoot = explicitAppRoot || findAppRoot();
|
|
2967
|
+
if (!appRoot) {
|
|
2968
|
+
warn("no React Native app here (no android/app/build.gradle, no ios/**/Info.plist), skipping");
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
const entries = collectManifests(appRoot);
|
|
2972
|
+
patchBuildGradle(appRoot, entries);
|
|
2973
|
+
patchMainApplication(appRoot, entries);
|
|
2974
|
+
patchAndroidManifest(appRoot, entries);
|
|
2975
|
+
patchAndroidManifestPermissions(appRoot, entries);
|
|
2976
|
+
patchAndroidManifestServices(appRoot, entries);
|
|
2977
|
+
patchMainActivityConfigChanges(appRoot, entries);
|
|
2978
|
+
patchInfoPlist(appRoot, entries);
|
|
2979
|
+
patchInfoPlistArrays(appRoot, entries);
|
|
2980
|
+
}
|
|
2981
|
+
module.exports = {
|
|
2982
|
+
linkApp,
|
|
2983
|
+
collectManifests,
|
|
2984
|
+
findAppRoot,
|
|
2985
|
+
findInfoPlistFile,
|
|
2986
|
+
findMainApplicationFile,
|
|
2987
|
+
patchBuildGradle,
|
|
2988
|
+
patchMainApplication,
|
|
2989
|
+
patchAndroidManifest,
|
|
2990
|
+
patchAndroidManifestPermissions,
|
|
2991
|
+
patchAndroidManifestServices,
|
|
2992
|
+
patchMainActivityConfigChanges,
|
|
2993
|
+
patchInfoPlist,
|
|
2994
|
+
patchInfoPlistArrays
|
|
2995
|
+
};
|
|
2996
|
+
})))();
|
|
2997
|
+
function discoveredBundlesFromLayers(layers) {
|
|
2998
|
+
return layers.flatMap((layer) => (layer.optionalManifestBundles ?? []).map((bundle) => ({
|
|
2999
|
+
packageName: layer.symbiotePackage,
|
|
3000
|
+
bundle
|
|
3001
|
+
})));
|
|
3002
|
+
}
|
|
3003
|
+
function discoverOptionalBundles(appRoot) {
|
|
3004
|
+
const discovered = [];
|
|
3005
|
+
for (const entry of (0, import_src.collectManifests)(appRoot)) {
|
|
3006
|
+
const bundles = entry.manifest.android?.optionalManifestBundles;
|
|
3007
|
+
if (!bundles) continue;
|
|
3008
|
+
for (const bundle of bundles) discovered.push({
|
|
3009
|
+
packageName: entry.packageName,
|
|
3010
|
+
bundle
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
return discovered;
|
|
3014
|
+
}
|
|
3015
|
+
function applyBundle(appRoot, bundle) {
|
|
3016
|
+
const entries = [{
|
|
3017
|
+
packageName: `grant:${bundle.id}`,
|
|
3018
|
+
manifest: { android: {
|
|
3019
|
+
manifestPermissions: bundle.manifestPermissions,
|
|
3020
|
+
manifestServices: bundle.manifestServices
|
|
3021
|
+
} }
|
|
3022
|
+
}];
|
|
3023
|
+
(0, import_src.patchAndroidManifestPermissions)(appRoot, entries);
|
|
3024
|
+
(0, import_src.patchAndroidManifestServices)(appRoot, entries);
|
|
3025
|
+
}
|
|
3026
|
+
function packageShortName(packageName) {
|
|
3027
|
+
const slashIndex = packageName.lastIndexOf("/");
|
|
3028
|
+
return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
|
|
3029
|
+
}
|
|
3030
|
+
function filterBundlesForLayer(discovered, layer) {
|
|
3031
|
+
if (layer === void 0) return [...discovered];
|
|
3032
|
+
return discovered.filter((entry) => packageShortName(entry.packageName) === layer);
|
|
3033
|
+
}
|
|
3034
|
+
//#endregion
|
|
2333
3035
|
//#region src/detect-package-manager.ts
|
|
2334
3036
|
const USER_AGENT_PREFIXES = [
|
|
2335
3037
|
["pnpm", "pnpm"],
|
|
@@ -2433,6 +3135,15 @@ async function resolveOverwrite(targetDir, force) {
|
|
|
2433
3135
|
assertNotCancelled(answer);
|
|
2434
3136
|
return answer;
|
|
2435
3137
|
}
|
|
3138
|
+
async function resolveGitInit() {
|
|
3139
|
+
if (!isInteractive()) return false;
|
|
3140
|
+
const answer = await confirm({
|
|
3141
|
+
message: "Initialize a git repository?",
|
|
3142
|
+
initialValue: true
|
|
3143
|
+
});
|
|
3144
|
+
assertNotCancelled(answer);
|
|
3145
|
+
return answer;
|
|
3146
|
+
}
|
|
2436
3147
|
async function resolveStyling(styling) {
|
|
2437
3148
|
if (styling !== void 0) return styling;
|
|
2438
3149
|
if (!isInteractive()) return "css";
|
|
@@ -2581,6 +3292,10 @@ const ADD_LAYER_OPTIONS = [
|
|
|
2581
3292
|
hint: layer.symbiotePackage
|
|
2582
3293
|
}))
|
|
2583
3294
|
];
|
|
3295
|
+
const ADD_LAYER_NAME_SET = new Set(ADD_LAYER_OPTIONS.map((option) => option.value));
|
|
3296
|
+
function isAddLayerName(value) {
|
|
3297
|
+
return ADD_LAYER_NAME_SET.has(value);
|
|
3298
|
+
}
|
|
2584
3299
|
function explicitLayersFromFlags(flags) {
|
|
2585
3300
|
const explicit = [];
|
|
2586
3301
|
if (flags.hasNavigation) explicit.push("navigation");
|
|
@@ -2622,7 +3337,22 @@ async function resolveAddLayers(flags, alreadyAdded, hasForce = false) {
|
|
|
2622
3337
|
required: false
|
|
2623
3338
|
});
|
|
2624
3339
|
assertNotCancelled(answer);
|
|
2625
|
-
return withImpliedExpoModules(dropAlreadyAdded(answer, alreadyAdded, hasForce));
|
|
3340
|
+
return withImpliedExpoModules(dropAlreadyAdded(answer.filter(isAddLayerName), alreadyAdded, hasForce));
|
|
3341
|
+
}
|
|
3342
|
+
async function resolveGrantSelection(candidates) {
|
|
3343
|
+
if (!isInteractive()) throw new CliUsageError("Not running in an interactive terminal — \"grant\" needs a real choice for each policy-sensitive bundle, so it cannot run non-interactively.");
|
|
3344
|
+
const answer = await multiselect({
|
|
3345
|
+
message: "Which policy-sensitive capabilities should this app request?",
|
|
3346
|
+
options: candidates.map((entry, index) => ({
|
|
3347
|
+
value: String(index),
|
|
3348
|
+
label: entry.bundle.label,
|
|
3349
|
+
hint: entry.packageName
|
|
3350
|
+
})),
|
|
3351
|
+
required: false
|
|
3352
|
+
});
|
|
3353
|
+
assertNotCancelled(answer);
|
|
3354
|
+
const selectedIndexes = new Set(answer);
|
|
3355
|
+
return candidates.filter((_entry, index) => selectedIndexes.has(String(index)));
|
|
2626
3356
|
}
|
|
2627
3357
|
async function resolvePackageManager(packageManager) {
|
|
2628
3358
|
const detected = packageManager ?? detectPackageManagerFromUserAgent();
|
|
@@ -2649,6 +3379,11 @@ async function resolvePackageManager(packageManager) {
|
|
|
2649
3379
|
return answer;
|
|
2650
3380
|
}
|
|
2651
3381
|
//#endregion
|
|
3382
|
+
//#region src/commands/grant-summary.ts
|
|
3383
|
+
function printGrantedNotes(granted) {
|
|
3384
|
+
for (const entry of granted) note(`${entry.bundle.warning}\n\nNext: ${entry.bundle.nextSteps}`, entry.bundle.label);
|
|
3385
|
+
}
|
|
3386
|
+
//#endregion
|
|
2652
3387
|
//#region src/commands/add.ts
|
|
2653
3388
|
function resolveFrameworkForAdd(explicit, cwd = process.cwd()) {
|
|
2654
3389
|
const detected = detectSymbioteFrameworkFromDependencies(readCwdDependencies(cwd));
|
|
@@ -2690,20 +3425,70 @@ async function runAdd(parsed) {
|
|
|
2690
3425
|
const allSkips = [...alreadyPresentSkips, ...result.skippedLayers];
|
|
2691
3426
|
if (allSkips.length > 0) lines.push("", `Skipped (already present, left untouched): ${allSkips.join(", ")}.`);
|
|
2692
3427
|
if (result.appliedLayers.includes("navigation")) lines.push("", "Wire it up in your App entry:", import_picocolors.default.dim(` import { Stack } from '@symbiote-native/navigation/${framework}';`));
|
|
3428
|
+
const grantable = expoPackagesWithOptionalManifestBundles(new Set(result.appliedLayers));
|
|
3429
|
+
if (grantable.length > 0) {
|
|
3430
|
+
const candidates = discoveredBundlesFromLayers(grantable);
|
|
3431
|
+
let granted = [];
|
|
3432
|
+
let asked = false;
|
|
3433
|
+
try {
|
|
3434
|
+
granted = await resolveGrantSelection(candidates);
|
|
3435
|
+
asked = true;
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
if (!(error instanceof CliUsageError)) throw error;
|
|
3438
|
+
}
|
|
3439
|
+
if (granted.length > 0) {
|
|
3440
|
+
for (const entry of granted) applyBundle(cwd, entry.bundle);
|
|
3441
|
+
printGrantedNotes(granted);
|
|
3442
|
+
lines.push("", `Granted: ${granted.map((entry) => entry.bundle.label).join(", ")}.`);
|
|
3443
|
+
} else if (!asked) lines.push("", `${grantable.map((layer) => layer.label).join(", ")} also offer${grantable.length === 1 ? "s" : ""} optional, policy-sensitive Android permissions — after installing:`, ...grantable.map((layer) => import_picocolors.default.dim(` npx @symbiote-native/cli grant ${layer.id}`)));
|
|
3444
|
+
}
|
|
2693
3445
|
outro(lines.length > 0 ? lines.join("\n") : "Nothing changed.");
|
|
2694
3446
|
}
|
|
2695
3447
|
//#endregion
|
|
3448
|
+
//#region src/commands/grant.ts
|
|
3449
|
+
async function runGrant(parsed) {
|
|
3450
|
+
intro("@symbiote-native/cli grant");
|
|
3451
|
+
const appRoot = (0, import_src.findAppRoot)();
|
|
3452
|
+
if (!appRoot) throw new NotSymbioteAppError();
|
|
3453
|
+
const candidates = filterBundlesForLayer(discoverOptionalBundles(appRoot), parsed.layer);
|
|
3454
|
+
if (candidates.length === 0) {
|
|
3455
|
+
outro(parsed.layer === void 0 ? "Nothing to grant — no installed package declares an optional manifest bundle." : `Nothing to grant for "${parsed.layer}" — it declares no optional manifest bundle, or isn't installed.`);
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const selected = await resolveGrantSelection(candidates);
|
|
3459
|
+
if (selected.length === 0) {
|
|
3460
|
+
outro("Nothing granted.");
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
for (const entry of selected) applyBundle(appRoot, entry.bundle);
|
|
3464
|
+
printGrantedNotes(selected);
|
|
3465
|
+
outro(`Granted: ${selected.map((entry) => entry.bundle.label).join(", ")}.`);
|
|
3466
|
+
}
|
|
3467
|
+
//#endregion
|
|
3468
|
+
//#region src/utils/git-init.ts
|
|
3469
|
+
function tryGitInit(root) {
|
|
3470
|
+
try {
|
|
3471
|
+
execFileSync("git", ["init"], {
|
|
3472
|
+
cwd: root,
|
|
3473
|
+
stdio: "ignore"
|
|
3474
|
+
});
|
|
3475
|
+
return true;
|
|
3476
|
+
} catch {
|
|
3477
|
+
return false;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
//#endregion
|
|
2696
3481
|
//#region src/commands/new.ts
|
|
2697
3482
|
async function runNew(parsed) {
|
|
2698
3483
|
intro("@symbiote-native/cli new");
|
|
2699
3484
|
const appName = await resolveAppName(parsed.appName);
|
|
2700
|
-
const targetDir = path.resolve(process.cwd(), appName);
|
|
3485
|
+
const targetDir = path$1.resolve(process.cwd(), appName);
|
|
2701
3486
|
if (!isEmptyDir(targetDir)) {
|
|
2702
3487
|
if (!await resolveOverwrite(targetDir, parsed.hasForce)) {
|
|
2703
3488
|
cancel("Cancelled.");
|
|
2704
3489
|
process.exit(1);
|
|
2705
3490
|
}
|
|
2706
|
-
fs.rmSync(targetDir, {
|
|
3491
|
+
fs$1.rmSync(targetDir, {
|
|
2707
3492
|
recursive: true,
|
|
2708
3493
|
force: true
|
|
2709
3494
|
});
|
|
@@ -2729,23 +3514,38 @@ async function runNew(parsed) {
|
|
|
2729
3514
|
styling,
|
|
2730
3515
|
packageManager
|
|
2731
3516
|
});
|
|
2732
|
-
const hasGitDir = fs.existsSync(path.join(root, ".git"));
|
|
2733
|
-
|
|
3517
|
+
const hasGitDir = fs$1.existsSync(path$1.join(root, ".git"));
|
|
3518
|
+
const lines = [
|
|
2734
3519
|
`Scaffolded "${appName}" at ${root}.`,
|
|
2735
3520
|
"",
|
|
2736
3521
|
...appName === "." ? [] : [import_picocolors.default.bold(import_picocolors.default.green(` cd ${appName}`))],
|
|
2737
3522
|
import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "install")}`)),
|
|
2738
|
-
import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "dev")}`))
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3523
|
+
import_picocolors.default.bold(import_picocolors.default.green(` ${getCommand(packageManager, "dev")}`))
|
|
3524
|
+
];
|
|
3525
|
+
if (!hasGitDir && await resolveGitInit() && tryGitInit(root)) lines.push("", import_picocolors.default.dim("Initialized an empty git repository."));
|
|
3526
|
+
else if (!hasGitDir) lines.push("", "Not a git repository yet:", import_picocolors.default.bold(import_picocolors.default.green(" git init && git add -A && git commit -m \"initial commit\"")));
|
|
3527
|
+
const grantable = expoPackagesWithOptionalManifestBundles(expoPackages);
|
|
3528
|
+
if (grantable.length > 0) {
|
|
3529
|
+
const candidates = discoveredBundlesFromLayers(grantable);
|
|
3530
|
+
let granted = [];
|
|
3531
|
+
let asked = false;
|
|
3532
|
+
try {
|
|
3533
|
+
granted = await resolveGrantSelection(candidates);
|
|
3534
|
+
asked = true;
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
if (!(error instanceof CliUsageError)) throw error;
|
|
3537
|
+
}
|
|
3538
|
+
if (granted.length > 0) {
|
|
3539
|
+
for (const entry of granted) applyBundle(root, entry.bundle);
|
|
3540
|
+
printGrantedNotes(granted);
|
|
3541
|
+
lines.push("", `Granted: ${granted.map((entry) => entry.bundle.label).join(", ")}.`);
|
|
3542
|
+
} else if (!asked) lines.push("", `${grantable.map((layer) => layer.label).join(", ")} also offer${grantable.length === 1 ? "s" : ""} optional, policy-sensitive Android permissions — after installing:`, ...grantable.map((layer) => import_picocolors.default.dim(` npx @symbiote-native/cli grant ${layer.id}`)));
|
|
3543
|
+
}
|
|
3544
|
+
outro(lines.join("\n"));
|
|
2745
3545
|
}
|
|
2746
3546
|
//#endregion
|
|
2747
3547
|
//#region package.json
|
|
2748
|
-
var version = "0.1
|
|
3548
|
+
var version = "0.2.1";
|
|
2749
3549
|
//#endregion
|
|
2750
3550
|
//#region src/index.ts
|
|
2751
3551
|
function printHelp() {
|
|
@@ -2761,6 +3561,10 @@ Usage:
|
|
|
2761
3561
|
npx @symbiote-native/cli add [--navigation] [--expo-modules] [--testing] [--splash-screen] [--slider]
|
|
2762
3562
|
[--<expo-package> ...] [--force] [--pm npm|pnpm|yarn]
|
|
2763
3563
|
(run inside an existing @symbiote-native/* app)
|
|
3564
|
+
npx @symbiote-native/cli grant [layer]
|
|
3565
|
+
interactively opt into policy-sensitive Android permissions/services
|
|
3566
|
+
(e.g. background location, audio recording) an installed package offers;
|
|
3567
|
+
[layer] narrows to one package's short name, e.g. "grant location"
|
|
2764
3568
|
|
|
2765
3569
|
npx @symbiote-native/cli --version print the CLI version
|
|
2766
3570
|
npx @symbiote-native/cli --help print this message
|
|
@@ -2778,6 +3582,9 @@ async function main() {
|
|
|
2778
3582
|
case "add":
|
|
2779
3583
|
await runAdd(parsed);
|
|
2780
3584
|
return;
|
|
3585
|
+
case "grant":
|
|
3586
|
+
await runGrant(parsed);
|
|
3587
|
+
return;
|
|
2781
3588
|
case "help":
|
|
2782
3589
|
printHelp();
|
|
2783
3590
|
return;
|