react-native-icon-switcher 0.1.1 → 0.1.3

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 CHANGED
@@ -184,6 +184,8 @@ const unsubscribe = await syncIcon({
184
184
  // call unsubscribe() to remove iOS AppState listener
185
185
  ```
186
186
 
187
+ `onError` receives errors from `fetchChoice` or storage, and failures from `set()` as an `IconSwitcherError` with its `code` (for example `os_rejected`, `unsupported`, or `foreground`). After `set` fails for a given icon name, that name is not retried until `retryAfterMs` elapses (default 24 hours).
188
+
187
189
  On **iOS**, the helper runs when the app is `active` and subscribes to `AppState`. On **Android**, it runs once when called (`set` may still return `pending`).
188
190
 
189
191
  ## Testing with Jest
@@ -55,7 +55,14 @@ function _isFailedBlocked(failed, choice, retryAfterMs) {
55
55
  export async function syncIcon(options) {
56
56
  const storage = options.storage ?? _createMemoryStorage();
57
57
  const retryAfterMs = options.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
58
- const onError = options.onError;
58
+ // A throwing onError must never break syncIcon or surface as an unhandled rejection.
59
+ const reportError = error => {
60
+ try {
61
+ options.onError?.(error);
62
+ } catch {
63
+ // Ignored on purpose.
64
+ }
65
+ };
59
66
  let inFlight = false;
60
67
  const run = async () => {
61
68
  if (inFlight) {
@@ -90,11 +97,12 @@ export async function syncIcon(options) {
90
97
  }
91
98
  await storage.setItem(STORAGE_KEY_APPLIED, choice);
92
99
  await storage.setItem(STORAGE_KEY_FAILED, '');
93
- } catch {
100
+ } catch (error) {
94
101
  await storage.setItem(STORAGE_KEY_FAILED, _encodeFailed(choice));
102
+ reportError(error);
95
103
  }
96
104
  } catch (error) {
97
- onError?.(error);
105
+ reportError(error);
98
106
  } finally {
99
107
  inFlight = false;
100
108
  }
@@ -1 +1 @@
1
- {"version":3,"names":["AppState","Platform","IconSwitcher","STORAGE_KEY_APPLIED","STORAGE_KEY_FAILED","DEFAULT_RETRY_AFTER_MS","_createMemoryStorage","data","Map","getItem","key","get","setItem","value","set","_encodeFailed","name","JSON","stringify","at","Date","now","_decodeFailed","raw","length","parsed","parse","_isFailedBlocked","failed","choice","retryAfterMs","syncIcon","options","storage","onError","inFlight","run","fetchChoice","list","includes","isSupported","failedRaw","current","getCurrent","result","status","error","runWhenActive","currentState","OS","subscription","addEventListener","state","remove"],"sourceRoot":"../../../src","sources":["remote/index.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["AppState","Platform","IconSwitcher","STORAGE_KEY_APPLIED","STORAGE_KEY_FAILED","DEFAULT_RETRY_AFTER_MS","_createMemoryStorage","data","Map","getItem","key","get","setItem","value","set","_encodeFailed","name","JSON","stringify","at","Date","now","_decodeFailed","raw","length","parsed","parse","_isFailedBlocked","failed","choice","retryAfterMs","syncIcon","options","storage","reportError","error","onError","inFlight","run","fetchChoice","list","includes","isSupported","failedRaw","current","getCurrent","result","status","runWhenActive","currentState","OS","subscription","addEventListener","state","remove"],"sourceRoot":"../../../src","sources":["remote/index.ts"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/remote/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA6CD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAuE5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/remote/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA6CD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CA+E5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-icon-switcher",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Switch the app icon at runtime on React Native and Expo from one config file, with the native wiring generated for you",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -93,6 +93,7 @@
93
93
  "@expo/config-plugins": "^58.0.10",
94
94
  "@expo/plist": "^0.10.1",
95
95
  "jiti": "^2.7.0",
96
+ "sax": "^1.6.1",
96
97
  "sharp": "^0.35.5",
97
98
  "xcode": "^4.0.0"
98
99
  },
@@ -111,6 +112,7 @@
111
112
  "@types/node": "^26.6.3",
112
113
  "@types/react": "^19.2.0",
113
114
  "@types/react-test-renderer": "^19",
115
+ "@types/sax": "^1.2.7",
114
116
  "del-cli": "^8.0.1",
115
117
  "eslint": "^9.39.5",
116
118
  "eslint-config-prettier": "^10.1.8",
@@ -8,13 +8,12 @@ exports.syncProject = syncProject;
8
8
  const promises_1 = __importDefault(require("node:fs/promises"));
9
9
  const node_os_1 = __importDefault(require("node:os"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
- const config_plugins_1 = require("@expo/config-plugins");
12
- const plist_1 = __importDefault(require("@expo/plist"));
13
11
  const xcode_1 = __importDefault(require("xcode"));
14
12
  const assets_1 = require("./core/assets");
15
13
  const validate_1 = require("./core/validate");
16
- const android_1 = require("./transforms/android");
14
+ const android_text_1 = require("./transforms/android-text");
17
15
  const ios_1 = require("./transforms/ios");
16
+ const plist_patch_1 = require("./transforms/plist-patch");
18
17
  const types_1 = require("./transforms/types");
19
18
  const load_config_1 = require("./load-config");
20
19
  const paths_1 = require("./paths");
@@ -85,24 +84,13 @@ async function _syncAssets(resolved, projectRoot, iosAssetCatalog, androidResDir
85
84
  }
86
85
  async function _syncAndroidManifest(resolved, manifestPath, projectRoot, check, driftedPaths) {
87
86
  const xml = await promises_1.default.readFile(manifestPath, 'utf8');
88
- const parsed = (await config_plugins_1.XML.parseXMLAsync(xml));
89
- const next = (0, android_1.applyAndroidManifest)(parsed, resolved);
90
- const nextXml = config_plugins_1.XML.format(next);
91
- const nextBuf = Buffer.from(nextXml, 'utf8');
92
- const existing = await _readFileIfExists(manifestPath);
93
- if (existing !== null && existing.equals(nextBuf)) {
94
- return _recordOutcome(projectRoot, manifestPath, 'unchanged', driftedPaths);
95
- }
96
- if (!check) {
97
- await config_plugins_1.AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, next);
98
- }
99
- return _recordOutcome(projectRoot, manifestPath, 'updated', driftedPaths);
87
+ const nextXml = (0, android_text_1.patchAndroidManifestText)(xml, resolved);
88
+ const outcome = await _applyFileChange(projectRoot, manifestPath, nextXml, check);
89
+ return _recordOutcome(projectRoot, manifestPath, outcome, driftedPaths);
100
90
  }
101
91
  async function _syncInfoPlist(resolved, infoPlistPath, projectRoot, check, driftedPaths) {
102
92
  const raw = await promises_1.default.readFile(infoPlistPath, 'utf8');
103
- const parsed = plist_1.default.parse(raw);
104
- const next = (0, ios_1.applyInfoPlist)(parsed, resolved);
105
- const nextXml = plist_1.default.build(next);
93
+ const nextXml = (0, plist_patch_1.patchInfoPlistText)(raw, resolved);
106
94
  const outcome = await _applyFileChange(projectRoot, infoPlistPath, nextXml, check);
107
95
  return _recordOutcome(projectRoot, infoPlistPath, outcome, driftedPaths);
108
96
  }
@@ -0,0 +1,48 @@
1
+ import type { ResolvedConfig } from '../types';
2
+ interface ElementSpan {
3
+ start: number;
4
+ end: number;
5
+ }
6
+ /** @internal For unit tests. */
7
+ export declare function __parseManifestStructureForTests(xml: string): ParsedManifestStructure;
8
+ export declare function patchAndroidManifestText(originalXml: string, cfg: ResolvedConfig): string;
9
+ interface LauncherFilterInfo {
10
+ activityName: string;
11
+ span: ElementSpan;
12
+ actions: string[];
13
+ categories: string[];
14
+ }
15
+ interface ParsedManifestStructure {
16
+ applicationCloseStart: number | null;
17
+ launcherIntentFilters: LauncherFilterInfo[];
18
+ markedActivityAliases: {
19
+ span: ElementSpan;
20
+ }[];
21
+ iconSwitcherMetaData: {
22
+ name: string;
23
+ span: ElementSpan;
24
+ }[];
25
+ activities: {
26
+ name: string;
27
+ hasLauncher: boolean;
28
+ }[];
29
+ defaultAliasTarget: string | null;
30
+ }
31
+ interface ApplicationIndentation {
32
+ childIndent: string;
33
+ indentStep: string;
34
+ }
35
+ /** @internal For branch coverage in unit tests. */
36
+ export declare const __androidTextTesting: {
37
+ mapIndexThroughRemovals: typeof _mapIndexThroughRemovals;
38
+ selfClosingForAttributeValue: typeof _selfClosingForAttributeValue;
39
+ detectSelfClosingSuffix: typeof _detectSelfClosingSuffix;
40
+ joinNonEmptyLines: typeof _joinNonEmptyLines;
41
+ };
42
+ /** @internal For unit tests. */
43
+ export declare function __detectApplicationIndentationForTests(xml: string, applicationCloseStart: number, eol?: string): ApplicationIndentation;
44
+ declare function _joinNonEmptyLines(lines: string[], eol: string): string;
45
+ declare function _mapIndexThroughRemovals(index: number, removals: ElementSpan[]): number;
46
+ declare function _detectSelfClosingSuffix(xml: string): string;
47
+ declare function _selfClosingForAttributeValue(value: string, style: string): string;
48
+ export {};
@@ -0,0 +1,485 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.__androidTextTesting = void 0;
7
+ exports.__parseManifestStructureForTests = __parseManifestStructureForTests;
8
+ exports.patchAndroidManifestText = patchAndroidManifestText;
9
+ exports.__detectApplicationIndentationForTests = __detectApplicationIndentationForTests;
10
+ const sax_1 = __importDefault(require("sax"));
11
+ const errors_1 = require("./errors");
12
+ const MAIN_ACTION = 'android.intent.action.MAIN';
13
+ const LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER';
14
+ const ICONKIT_ICON_META = 'com.iconswitcher.icon';
15
+ const ICONKIT_ICONS_META = 'com.iconswitcher.icons';
16
+ const ICONKIT_APPLY_ON_META = 'com.iconswitcher.applyOn';
17
+ const ICONKIT_SKIP_BRANDS_META = 'com.iconswitcher.skipBrands';
18
+ const DEFAULT_ALIAS = '.IconSwitcher_default';
19
+ const RECOGNIZED_ACTIONS = new Set([MAIN_ACTION]);
20
+ const RECOGNIZED_CATEGORIES = new Set([LAUNCHER_CATEGORY]);
21
+ /** @internal For unit tests. */
22
+ function __parseManifestStructureForTests(xml) {
23
+ return _parseManifestStructure(xml);
24
+ }
25
+ function patchAndroidManifestText(originalXml, cfg) {
26
+ var _a, _b, _c;
27
+ const eol = originalXml.includes('\r\n') ? '\r\n' : '\n';
28
+ const structure = _parseManifestStructure(originalXml);
29
+ const applicationCloseStart = (_a = structure.applicationCloseStart) !== null && _a !== void 0 ? _a : originalXml.lastIndexOf('</application>');
30
+ if (applicationCloseStart === -1) {
31
+ throw new errors_1.TransformError('AndroidManifest has no <application> element');
32
+ }
33
+ const launcherActivityName = _resolveLauncherActivityName(structure);
34
+ const { childIndent, indentStep } = _detectApplicationIndentation(originalXml, applicationCloseStart, eol);
35
+ const removals = [];
36
+ for (const filter of structure.launcherIntentFilters) {
37
+ if (filter.activityName === launcherActivityName) {
38
+ _assertRecognizedLauncherFilter(filter);
39
+ removals.push(_spanWithLeadingWhitespace(originalXml, filter.span, eol));
40
+ }
41
+ }
42
+ for (const alias of structure.markedActivityAliases) {
43
+ removals.push(_spanWithLeadingWhitespace(originalXml, alias.span, eol));
44
+ }
45
+ removals.sort((a, b) => a.start - b.start);
46
+ let insertAtAfterRemovals = -1;
47
+ if (structure.markedActivityAliases.length > 0) {
48
+ const firstAliasStart = structure.markedActivityAliases
49
+ .map((alias) => alias.span.start)
50
+ .sort((a, b) => a - b)[0];
51
+ insertAtAfterRemovals = _mapIndexThroughRemovals(_lineStartBeforeIndex(originalXml, firstAliasStart, eol), removals);
52
+ }
53
+ removals.sort((a, b) => b.start - a.start);
54
+ let xml = originalXml;
55
+ for (const span of removals) {
56
+ xml = xml.slice(0, span.start) + xml.slice(span.end);
57
+ }
58
+ const metaByName = new Map(_parseManifestStructure(xml).iconSwitcherMetaData.map((item) => [
59
+ item.name,
60
+ item.span,
61
+ ]));
62
+ const selfClosingStyle = _detectSelfClosingSuffix(originalXml);
63
+ const metaLines = _applicationMetaLines(cfg, childIndent, selfClosingStyle);
64
+ const metaReplacements = [];
65
+ const missingMeta = [];
66
+ for (const line of metaLines) {
67
+ const nameMatch = line.match(/android:name="([^"]+)"/);
68
+ const metaName = nameMatch === null || nameMatch === void 0 ? void 0 : nameMatch[1];
69
+ if (!metaName) {
70
+ continue;
71
+ }
72
+ const existing = metaByName.get(metaName);
73
+ if (existing) {
74
+ const lineBounds = _fullLineBounds(xml, existing.start, existing.end, eol);
75
+ const existingLine = xml.slice(lineBounds.start, lineBounds.end);
76
+ const existingIndent = (_c = (_b = existingLine.match(/^(\s*)/)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : '';
77
+ const lineToWrite = line.replace(/^\s+/, existingIndent);
78
+ const existingBody = existingLine.trim();
79
+ const nextBody = lineToWrite.trim();
80
+ if (existingBody === nextBody) {
81
+ continue;
82
+ }
83
+ metaReplacements.push({
84
+ start: lineBounds.start,
85
+ end: lineBounds.end,
86
+ text: lineToWrite,
87
+ });
88
+ }
89
+ else {
90
+ missingMeta.push(line);
91
+ }
92
+ }
93
+ metaReplacements.sort((a, b) => b.start - a.start);
94
+ for (const replacement of metaReplacements) {
95
+ xml =
96
+ xml.slice(0, replacement.start) +
97
+ replacement.text +
98
+ xml.slice(replacement.end);
99
+ }
100
+ const applicationCloseIndex = xml.lastIndexOf('</application>');
101
+ if (applicationCloseIndex === -1) {
102
+ throw new errors_1.TransformError('AndroidManifest has no <application> element');
103
+ }
104
+ const insertAt = insertAtAfterRemovals >= 0
105
+ ? insertAtAfterRemovals
106
+ : _lineStartBeforeIndex(xml, applicationCloseIndex, eol);
107
+ const aliasXml = _renderActivityAliases(cfg, launcherActivityName, childIndent, indentStep, eol, selfClosingStyle);
108
+ const insertBlock = _joinNonEmptyLines([...missingMeta, aliasXml], eol);
109
+ const prefix = xml.slice(0, insertAt);
110
+ const suffix = xml.slice(insertAt);
111
+ const needsNl = insertBlock.length > 0 &&
112
+ !prefix.endsWith('\n') &&
113
+ !prefix.endsWith('\r\n');
114
+ const joiner = needsNl ? eol : '';
115
+ xml = `${prefix}${joiner}${insertBlock}${insertBlock ? eol : ''}${suffix}`;
116
+ return xml;
117
+ }
118
+ function _parseManifestStructure(xml) {
119
+ const parser = sax_1.default.parser(true, { xmlns: true, position: true, trim: false });
120
+ const stack = [];
121
+ const launcherIntentFilters = [];
122
+ const markedActivityAliases = [];
123
+ const iconSwitcherMetaData = [];
124
+ const activities = [];
125
+ let currentActivity = null;
126
+ let currentActivityHasLauncher = false;
127
+ let currentAliasMarker = null;
128
+ let defaultAliasTarget = null;
129
+ let insideApplication = false;
130
+ let currentFilter = null;
131
+ parser.onopentag = (node) => {
132
+ var _a, _b;
133
+ const attrs = _normalizeAttrs(node.attributes);
134
+ const selfClosing = Boolean(node.isSelfClosing);
135
+ const frame = {
136
+ name: node.name,
137
+ start: _openTagStart(xml, parser.startTagPosition),
138
+ attrs,
139
+ };
140
+ if (!selfClosing) {
141
+ stack.push(frame);
142
+ }
143
+ if (node.name === 'application') {
144
+ insideApplication = true;
145
+ }
146
+ if (insideApplication && node.name === 'activity') {
147
+ currentActivity = (_a = attrs['android:name']) !== null && _a !== void 0 ? _a : null;
148
+ currentActivityHasLauncher = false;
149
+ }
150
+ if (node.name === 'intent-filter') {
151
+ const parent = stack[stack.length - 2];
152
+ if ((parent === null || parent === void 0 ? void 0 : parent.name) === 'activity' && currentActivity) {
153
+ currentFilter = { actions: [], categories: [] };
154
+ }
155
+ }
156
+ if (node.name === 'action' && currentFilter) {
157
+ const actionName = attrs['android:name'];
158
+ if (actionName) {
159
+ currentFilter.actions.push(actionName);
160
+ }
161
+ }
162
+ if (node.name === 'category' && currentFilter) {
163
+ const categoryName = attrs['android:name'];
164
+ if (categoryName) {
165
+ currentFilter.categories.push(categoryName);
166
+ }
167
+ }
168
+ if (insideApplication && node.name === 'activity-alias') {
169
+ currentAliasMarker = null;
170
+ const aliasName = attrs['android:name'];
171
+ const target = attrs['android:targetActivity'];
172
+ if (aliasName === DEFAULT_ALIAS && target) {
173
+ defaultAliasTarget = target;
174
+ }
175
+ }
176
+ if (node.name === 'meta-data') {
177
+ const parent = selfClosing
178
+ ? stack[stack.length - 1]
179
+ : stack[stack.length - 2];
180
+ if ((parent === null || parent === void 0 ? void 0 : parent.name) === 'activity-alias') {
181
+ const metaName = attrs['android:name'];
182
+ if (metaName === ICONKIT_ICON_META) {
183
+ currentAliasMarker = (_b = attrs['android:value']) !== null && _b !== void 0 ? _b : null;
184
+ }
185
+ }
186
+ if ((parent === null || parent === void 0 ? void 0 : parent.name) === 'application' && selfClosing) {
187
+ _recordApplicationMetaDataElement(frame, parser.position, iconSwitcherMetaData);
188
+ }
189
+ }
190
+ if (selfClosing &&
191
+ insideApplication &&
192
+ node.name === 'activity' &&
193
+ currentActivity) {
194
+ activities.push({
195
+ name: currentActivity,
196
+ hasLauncher: currentActivityHasLauncher,
197
+ });
198
+ currentActivity = null;
199
+ currentActivityHasLauncher = false;
200
+ }
201
+ };
202
+ parser.onclosetag = (name) => {
203
+ var _a;
204
+ const top = stack[stack.length - 1];
205
+ if (!top || top.name !== name) {
206
+ return;
207
+ }
208
+ const end = parser.position;
209
+ const frame = stack.pop();
210
+ if (!frame) {
211
+ return;
212
+ }
213
+ const span = { start: frame.start, end };
214
+ if (name === 'intent-filter' && currentActivity && currentFilter) {
215
+ const { actions, categories } = currentFilter;
216
+ if (actions.includes(MAIN_ACTION) &&
217
+ categories.includes(LAUNCHER_CATEGORY)) {
218
+ currentActivityHasLauncher = true;
219
+ launcherIntentFilters.push({
220
+ activityName: currentActivity,
221
+ span,
222
+ actions,
223
+ categories,
224
+ });
225
+ }
226
+ currentFilter = null;
227
+ }
228
+ if (name === 'activity') {
229
+ if (currentActivity) {
230
+ activities.push({
231
+ name: currentActivity,
232
+ hasLauncher: currentActivityHasLauncher,
233
+ });
234
+ }
235
+ currentActivity = null;
236
+ }
237
+ if (name === 'activity-alias' && insideApplication) {
238
+ if (currentAliasMarker !== null) {
239
+ markedActivityAliases.push({ span });
240
+ }
241
+ currentAliasMarker = null;
242
+ }
243
+ if (name === 'meta-data' &&
244
+ insideApplication &&
245
+ ((_a = stack[1]) === null || _a === void 0 ? void 0 : _a.name) === 'application') {
246
+ _recordApplicationMetaDataElement(frame, end, iconSwitcherMetaData);
247
+ }
248
+ };
249
+ parser.onerror = (error) => {
250
+ throw error;
251
+ };
252
+ parser.write(xml).close();
253
+ const closeIndex = xml.lastIndexOf('</application>');
254
+ return {
255
+ applicationCloseStart: closeIndex === -1 ? null : closeIndex,
256
+ launcherIntentFilters,
257
+ markedActivityAliases,
258
+ iconSwitcherMetaData,
259
+ activities,
260
+ defaultAliasTarget,
261
+ };
262
+ }
263
+ function _recordApplicationMetaDataElement(frame, end, iconSwitcherMetaData) {
264
+ const metaName = frame.attrs['android:name'];
265
+ if (metaName === ICONKIT_ICONS_META ||
266
+ metaName === ICONKIT_APPLY_ON_META ||
267
+ metaName === ICONKIT_SKIP_BRANDS_META) {
268
+ iconSwitcherMetaData.push({
269
+ name: metaName,
270
+ span: { start: frame.start, end },
271
+ });
272
+ }
273
+ }
274
+ function _openTagStart(xml, startTagPosition) {
275
+ let index = startTagPosition;
276
+ while (index > 0 && xml[index] !== '<') {
277
+ index -= 1;
278
+ }
279
+ return index;
280
+ }
281
+ function _normalizeAttrs(raw) {
282
+ const out = {};
283
+ for (const [key, value] of Object.entries(raw)) {
284
+ if (typeof value === 'string') {
285
+ out[key] = value;
286
+ continue;
287
+ }
288
+ if (value && typeof value === 'object' && 'value' in value) {
289
+ out[key] = String(value.value);
290
+ }
291
+ }
292
+ return out;
293
+ }
294
+ function _resolveLauncherActivityName(structure) {
295
+ const withLauncher = structure.activities.find((a) => a.hasLauncher);
296
+ if (withLauncher) {
297
+ return withLauncher.name;
298
+ }
299
+ const main = structure.activities.find((a) => a.name === '.MainActivity');
300
+ if (main) {
301
+ return main.name;
302
+ }
303
+ if (structure.defaultAliasTarget) {
304
+ return structure.defaultAliasTarget;
305
+ }
306
+ throw new errors_1.TransformError('Could not find launcher activity (no MAIN/LAUNCHER filter, .MainActivity, or default activity-alias)');
307
+ }
308
+ function _assertRecognizedLauncherFilter(filter) {
309
+ for (const action of filter.actions) {
310
+ if (!RECOGNIZED_ACTIONS.has(action)) {
311
+ throw new errors_1.TransformError(`Unrecognized action in launcher intent-filter: ${action}`);
312
+ }
313
+ }
314
+ for (const category of filter.categories) {
315
+ if (!RECOGNIZED_CATEGORIES.has(category)) {
316
+ throw new errors_1.TransformError(`Unrecognized category in launcher intent-filter: ${category}`);
317
+ }
318
+ }
319
+ }
320
+ function _detectApplicationIndentation(xml, applicationCloseStart, eol) {
321
+ var _a, _b, _c;
322
+ const applicationOpen = xml.indexOf('<application');
323
+ if (applicationOpen === -1) {
324
+ return { childIndent: ' ', indentStep: ' ' };
325
+ }
326
+ const applicationLineStart = _lineStartBeforeIndex(xml, applicationOpen, eol);
327
+ const lineBreak = xml.indexOf(eol, applicationOpen);
328
+ const applicationLine = xml.slice(applicationLineStart, lineBreak === -1 ? xml.length : lineBreak);
329
+ const applicationIndent = (_b = (_a = applicationLine.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
330
+ const section = xml.slice(applicationOpen, applicationCloseStart);
331
+ const childMatch = section.match(/\n(\s+)<(?:activity|activity-alias|meta-data|service|receiver|provider)\b/);
332
+ const childIndent = (_c = childMatch === null || childMatch === void 0 ? void 0 : childMatch[1]) !== null && _c !== void 0 ? _c : ' ';
333
+ const nestedMatch = section.match(/\n(\s+)<(?:intent-filter|meta-data)\b/);
334
+ let indentStep = ' ';
335
+ if ((nestedMatch === null || nestedMatch === void 0 ? void 0 : nestedMatch[1]) && nestedMatch[1].startsWith(childIndent)) {
336
+ indentStep = nestedMatch[1].slice(childIndent.length);
337
+ }
338
+ else if (childIndent.length > applicationIndent.length) {
339
+ indentStep = childIndent.slice(applicationIndent.length);
340
+ }
341
+ else if (childIndent.length >= 2) {
342
+ indentStep = childIndent.slice(0, childIndent.length / 2);
343
+ }
344
+ if (!indentStep) {
345
+ indentStep = childIndent.length >= 2 ? childIndent.slice(0, 2) : ' ';
346
+ }
347
+ return { childIndent, indentStep };
348
+ }
349
+ /** @internal For branch coverage in unit tests. */
350
+ exports.__androidTextTesting = {
351
+ mapIndexThroughRemovals: _mapIndexThroughRemovals,
352
+ selfClosingForAttributeValue: _selfClosingForAttributeValue,
353
+ detectSelfClosingSuffix: _detectSelfClosingSuffix,
354
+ joinNonEmptyLines: _joinNonEmptyLines,
355
+ };
356
+ /** @internal For unit tests. */
357
+ function __detectApplicationIndentationForTests(xml, applicationCloseStart, eol = '\n') {
358
+ return _detectApplicationIndentation(xml, applicationCloseStart, eol);
359
+ }
360
+ function _lineStartBeforeIndex(xml, index, eol) {
361
+ if (index < 0) {
362
+ return -1;
363
+ }
364
+ const lineStart = xml.lastIndexOf(eol, index - 1);
365
+ return lineStart === -1 ? 0 : lineStart + eol.length;
366
+ }
367
+ function _joinNonEmptyLines(lines, eol) {
368
+ return lines
369
+ .flatMap((block) => block.split(eol))
370
+ .filter((line) => line.trim() !== '')
371
+ .join(eol);
372
+ }
373
+ function _mapIndexThroughRemovals(index, removals) {
374
+ let adjusted = index;
375
+ for (const span of removals) {
376
+ if (span.start < index) {
377
+ adjusted -= span.end - span.start;
378
+ }
379
+ }
380
+ return adjusted;
381
+ }
382
+ function _detectSelfClosingSuffix(xml) {
383
+ const compact = /[^ ]"\/>/.test(xml);
384
+ const spaced = /" \/>/.test(xml);
385
+ if (compact && !spaced) {
386
+ return '/>';
387
+ }
388
+ return ' />';
389
+ }
390
+ function _selfClosingForAttributeValue(value, style) {
391
+ if (value === '' && style === '/>') {
392
+ return ' />';
393
+ }
394
+ return style;
395
+ }
396
+ function _fullLineBounds(xml, elementStart, elementEnd, eol) {
397
+ let start = elementStart;
398
+ const lineStart = xml.lastIndexOf(eol, start - 1);
399
+ if (lineStart !== -1) {
400
+ const prefix = xml.slice(lineStart + eol.length, start);
401
+ if (/^\s*$/.test(prefix)) {
402
+ start = lineStart + eol.length;
403
+ }
404
+ }
405
+ let end = elementEnd;
406
+ const lineEnd = xml.indexOf(eol, end);
407
+ if (lineEnd !== -1) {
408
+ end = lineEnd;
409
+ }
410
+ return { start, end };
411
+ }
412
+ function _spanWithLeadingWhitespace(xml, span, eol) {
413
+ let start = span.start;
414
+ const lineStart = xml.lastIndexOf(eol, start - 1);
415
+ if (lineStart !== -1) {
416
+ const linePrefix = xml.slice(lineStart + eol.length, start);
417
+ if (/^\s*$/.test(linePrefix)) {
418
+ start = lineStart + eol.length;
419
+ }
420
+ }
421
+ let end = span.end;
422
+ if (xml.slice(end, end + eol.length) === eol) {
423
+ end += eol.length;
424
+ }
425
+ return { start, end };
426
+ }
427
+ function _configuredAndroidIconNames(cfg) {
428
+ return cfg.icons
429
+ .filter((icon) => icon.isDefault || icon.android !== null)
430
+ .map((icon) => icon.name);
431
+ }
432
+ function _applicationMetaLines(cfg, indent, selfClosing) {
433
+ const iconsValue = _configuredAndroidIconNames(cfg).join(',');
434
+ const skipBrands = cfg.android.skipBrands.join(',');
435
+ return [
436
+ `${indent}<meta-data android:name="${ICONS_META}" android:value="${iconsValue}"${_selfClosingForAttributeValue(iconsValue, selfClosing)}`,
437
+ `${indent}<meta-data android:name="${ICONS_APPLY_ON_META}" android:value="${cfg.android.applyOn}"${_selfClosingForAttributeValue(cfg.android.applyOn, selfClosing)}`,
438
+ `${indent}<meta-data android:name="${ICONS_SKIP_BRANDS_META}" android:value="${skipBrands}"${_selfClosingForAttributeValue(skipBrands, selfClosing)}`,
439
+ ];
440
+ }
441
+ const ICONS_META = ICONKIT_ICONS_META;
442
+ const ICONS_APPLY_ON_META = ICONKIT_APPLY_ON_META;
443
+ const ICONS_SKIP_BRANDS_META = ICONKIT_SKIP_BRANDS_META;
444
+ function _renderActivityAliases(cfg, launcherActivityName, childIndent, indentStep, eol, selfClosing) {
445
+ const blocks = [];
446
+ for (const icon of cfg.icons) {
447
+ if (!icon.isDefault && icon.android === null) {
448
+ continue;
449
+ }
450
+ blocks.push(_renderSingleActivityAlias(icon.name, cfg, launcherActivityName, childIndent, indentStep, eol, selfClosing));
451
+ }
452
+ return blocks.join(eol);
453
+ }
454
+ function _renderSingleActivityAlias(iconName, cfg, launcherActivityName, childIndent, indentStep, eol, selfClosing) {
455
+ var _a;
456
+ const icon = cfg.icons.find((item) => item.name === iconName);
457
+ const isDefault = (_a = icon === null || icon === void 0 ? void 0 : icon.isDefault) !== null && _a !== void 0 ? _a : iconName === 'default';
458
+ const { icon: iconRef, roundIcon } = _mipmapRefs(iconName, isDefault);
459
+ const enabled = isDefault ? 'true' : 'false';
460
+ const aliasName = `.IconSwitcher_${iconName}`;
461
+ const inner = childIndent + indentStep;
462
+ const deepest = inner + indentStep;
463
+ return [
464
+ `${childIndent}<activity-alias android:name="${aliasName}" android:targetActivity="${launcherActivityName}" android:exported="true" android:enabled="${enabled}" android:icon="${iconRef}" android:roundIcon="${roundIcon}">`,
465
+ `${inner}<intent-filter>`,
466
+ `${deepest}<action android:name="${MAIN_ACTION}"${selfClosing}`,
467
+ `${deepest}<category android:name="${LAUNCHER_CATEGORY}"${selfClosing}`,
468
+ `${inner}</intent-filter>`,
469
+ `${inner}<meta-data android:name="${ICONS_META_ICON}" android:value="${iconName}"${_selfClosingForAttributeValue(iconName, selfClosing)}`,
470
+ `${childIndent}</activity-alias>`,
471
+ ].join(eol);
472
+ }
473
+ const ICONS_META_ICON = ICONKIT_ICON_META;
474
+ function _mipmapRefs(iconName, isDefault) {
475
+ if (isDefault) {
476
+ return {
477
+ icon: '@mipmap/ic_launcher',
478
+ roundIcon: '@mipmap/ic_launcher_round',
479
+ };
480
+ }
481
+ return {
482
+ icon: `@mipmap/ic_launcher_${iconName}`,
483
+ roundIcon: `@mipmap/ic_launcher_${iconName}_round`,
484
+ };
485
+ }
@@ -1,4 +1,7 @@
1
1
  export { applyAndroidManifest } from './android';
2
+ export { patchAndroidManifestText } from './android-text';
2
3
  export { TransformError } from './errors';
3
4
  export { applyInfoPlist, applyXcodeProject } from './ios';
5
+ export { formatPlistLikeXcode } from './plist-format';
6
+ export { patchInfoPlistText } from './plist-patch';
4
7
  export { renderTypes } from './types';
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderTypes = exports.applyXcodeProject = exports.applyInfoPlist = exports.TransformError = exports.applyAndroidManifest = void 0;
3
+ exports.renderTypes = exports.patchInfoPlistText = exports.formatPlistLikeXcode = exports.applyXcodeProject = exports.applyInfoPlist = exports.TransformError = exports.patchAndroidManifestText = exports.applyAndroidManifest = void 0;
4
4
  var android_1 = require("./android");
5
5
  Object.defineProperty(exports, "applyAndroidManifest", { enumerable: true, get: function () { return android_1.applyAndroidManifest; } });
6
+ var android_text_1 = require("./android-text");
7
+ Object.defineProperty(exports, "patchAndroidManifestText", { enumerable: true, get: function () { return android_text_1.patchAndroidManifestText; } });
6
8
  var errors_1 = require("./errors");
7
9
  Object.defineProperty(exports, "TransformError", { enumerable: true, get: function () { return errors_1.TransformError; } });
8
10
  var ios_1 = require("./ios");
9
11
  Object.defineProperty(exports, "applyInfoPlist", { enumerable: true, get: function () { return ios_1.applyInfoPlist; } });
10
12
  Object.defineProperty(exports, "applyXcodeProject", { enumerable: true, get: function () { return ios_1.applyXcodeProject; } });
13
+ var plist_format_1 = require("./plist-format");
14
+ Object.defineProperty(exports, "formatPlistLikeXcode", { enumerable: true, get: function () { return plist_format_1.formatPlistLikeXcode; } });
15
+ var plist_patch_1 = require("./plist-patch");
16
+ Object.defineProperty(exports, "patchInfoPlistText", { enumerable: true, get: function () { return plist_patch_1.patchInfoPlistText; } });
11
17
  var types_1 = require("./types");
12
18
  Object.defineProperty(exports, "renderTypes", { enumerable: true, get: function () { return types_1.renderTypes; } });
@@ -1,4 +1,5 @@
1
1
  import type { XcodeProject } from 'xcode';
2
2
  import type { ResolvedConfig } from '../types';
3
3
  export declare function applyInfoPlist(plist: Record<string, unknown>, cfg: ResolvedConfig): Record<string, unknown>;
4
+ export declare function _isLegacy01PrimaryIconBundle(value: unknown): boolean;
4
5
  export declare function applyXcodeProject(project: XcodeProject, cfg: ResolvedConfig): XcodeProject;
@@ -1,57 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyInfoPlist = applyInfoPlist;
4
+ exports._isLegacy01PrimaryIconBundle = _isLegacy01PrimaryIconBundle;
4
5
  exports.applyXcodeProject = applyXcodeProject;
5
- const PRIMARY_ICON_NAME = 'AppIcon';
6
6
  function applyInfoPlist(plist, cfg) {
7
7
  const next = { ...plist };
8
8
  next.IconSwitcherIcons = cfg.icons
9
9
  .filter((icon) => icon.isDefault || icon.ios !== null)
10
10
  .map((icon) => icon.name);
11
- next.CFBundleIcons = _ensurePrimaryIconBundle(next.CFBundleIcons, 'CFBundleIcons');
12
- if (_supportsIpad(next)) {
13
- next['CFBundleIcons~ipad'] = _mirrorIpadIcons(next.CFBundleIcons, next['CFBundleIcons~ipad']);
11
+ if (_isLegacy01PrimaryIconBundle(next.CFBundleIcons)) {
12
+ delete next.CFBundleIcons;
13
+ }
14
+ if (_isLegacy01PrimaryIconBundle(next['CFBundleIcons~ipad'])) {
15
+ delete next['CFBundleIcons~ipad'];
14
16
  }
15
17
  return next;
16
18
  }
17
- function _ensurePrimaryIconBundle(existing, label) {
18
- const bundle = existing && typeof existing === 'object' && !Array.isArray(existing)
19
- ? { ...existing }
20
- : {};
21
- const primary = bundle.CFBundlePrimaryIcon && typeof bundle.CFBundlePrimaryIcon === 'object'
22
- ? { ...bundle.CFBundlePrimaryIcon }
23
- : {};
24
- const currentName = primary.CFBundleIconName;
25
- if (currentName !== undefined && currentName !== PRIMARY_ICON_NAME) {
26
- console.warn(`[react-native-icon-switcher] ${label}.CFBundlePrimaryIcon.CFBundleIconName is "${String(currentName)}"; expected "${PRIMARY_ICON_NAME}" — leaving unchanged`);
27
- }
28
- else {
29
- primary.CFBundleIconName = PRIMARY_ICON_NAME;
19
+ function _isLegacy01PrimaryIconBundle(value) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
21
+ return false;
30
22
  }
31
- bundle.CFBundlePrimaryIcon = primary;
32
- return bundle;
33
- }
34
- function _supportsIpad(plist) {
35
- const families = plist.UIDeviceFamily;
36
- if (Array.isArray(families) &&
37
- families.some((value) => value === 2 || value === '2')) {
38
- return true;
23
+ const bundle = value;
24
+ const primary = bundle.CFBundlePrimaryIcon;
25
+ if (!primary || typeof primary !== 'object' || Array.isArray(primary)) {
26
+ return false;
39
27
  }
40
- return plist['UISupportedInterfaceOrientations~ipad'] !== undefined;
41
- }
42
- function _mirrorIpadIcons(phoneIcons, existingIpad) {
43
- const phone = phoneIcons && typeof phoneIcons === 'object' && !Array.isArray(phoneIcons)
44
- ? phoneIcons
45
- : {};
46
- const ipad = existingIpad &&
47
- typeof existingIpad === 'object' &&
48
- !Array.isArray(existingIpad)
49
- ? { ...existingIpad }
50
- : {};
51
- ipad.CFBundlePrimaryIcon = phone.CFBundlePrimaryIcon;
52
- return ipad;
28
+ const primaryIcon = primary;
29
+ const keys = Object.keys(primaryIcon);
30
+ return keys.length === 1 && primaryIcon.CFBundleIconName === 'AppIcon';
53
31
  }
54
32
  function applyXcodeProject(project, cfg) {
33
+ var _a, _b;
55
34
  const alternateNames = cfg.icons
56
35
  .filter((icon) => !icon.isDefault && icon.ios !== null)
57
36
  .map((icon) => `AppIcon-${icon.name}`)
@@ -59,10 +38,14 @@ function applyXcodeProject(project, cfg) {
59
38
  const [, nativeTarget] = _findFirstApplicationTarget(project);
60
39
  const buildConfigurations = _getBuildConfigurationsForListId(project, nativeTarget.buildConfigurationList);
61
40
  for (const [, configuration] of buildConfigurations) {
62
- configuration.buildSettings.ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES =
41
+ const settings = configuration.buildSettings;
42
+ const configName = (_b = (_a = configuration.name) !== null && _a !== void 0 ? _a : settings.PRODUCT_NAME) !== null && _b !== void 0 ? _b : 'unknown';
43
+ if (!settings.ASSETCATALOG_COMPILER_APPICON_NAME) {
44
+ console.warn(`[react-native-icon-switcher] Build configuration "${String(configName)}" has no ASSETCATALOG_COMPILER_APPICON_NAME — set the primary app icon in Xcode`);
45
+ }
46
+ settings.ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES =
63
47
  _formatAlternateAppIconNamesSetting(alternateNames);
64
- configuration.buildSettings.ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS =
65
- 'NO';
48
+ settings.ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = 'NO';
66
49
  }
67
50
  return project;
68
51
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Rewrites plist XML to match Xcode / plutil xml1 layout: root <dict> at column 0,
3
+ * one tab per nesting level, trailing newline.
4
+ */
5
+ export declare function formatPlistLikeXcode(xml: string): string;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatPlistLikeXcode = formatPlistLikeXcode;
4
+ /**
5
+ * Rewrites plist XML to match Xcode / plutil xml1 layout: root <dict> at column 0,
6
+ * one tab per nesting level, trailing newline.
7
+ */
8
+ function formatPlistLikeXcode(xml) {
9
+ const normalized = xml.replace(/\r\n/g, '\n').trimEnd();
10
+ const lines = normalized.split('\n');
11
+ const out = [];
12
+ let depth = 0;
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed) {
16
+ continue;
17
+ }
18
+ if (trimmed === '</dict>') {
19
+ depth = Math.max(0, depth - 1);
20
+ }
21
+ if (trimmed === '<dict>') {
22
+ if (depth === 0 && out.length > 0) {
23
+ out.push('<dict>');
24
+ }
25
+ else {
26
+ out.push(`${'\t'.repeat(depth)}<dict>`);
27
+ }
28
+ depth += 1;
29
+ continue;
30
+ }
31
+ if (trimmed === '</dict>') {
32
+ out.push(`${'\t'.repeat(depth)}</dict>`);
33
+ continue;
34
+ }
35
+ if (trimmed === '<plist version="1.0">') {
36
+ out.push('<plist version="1.0">');
37
+ continue;
38
+ }
39
+ if (trimmed.startsWith('<?xml') ||
40
+ trimmed.startsWith('<!DOCTYPE') ||
41
+ trimmed === '</plist>') {
42
+ out.push(trimmed);
43
+ continue;
44
+ }
45
+ out.push(`${'\t'.repeat(depth)}${trimmed}`);
46
+ }
47
+ return `${out.join('\n')}\n`;
48
+ }
@@ -0,0 +1,26 @@
1
+ import type { ResolvedConfig } from '../types';
2
+ export declare function patchInfoPlistText(originalXml: string, cfg: ResolvedConfig): string;
3
+ interface PlistIndentation {
4
+ keyIndent: string;
5
+ indentStep: string;
6
+ }
7
+ declare function _detectEol(xml: string): string;
8
+ declare function _detectPlistIndentation(xml: string, eol: string): PlistIndentation;
9
+ declare function _iconSwitcherArrayItemIndent(xml: string, eol: string): string | null;
10
+ declare function _readIconSwitcherIconsFromText(xml: string): string[];
11
+ declare function _removePlistKeyBlock(xml: string, key: string, eol: string): string;
12
+ declare function _collapseBlankLinesBeforeDictClose(xml: string, eol: string): string;
13
+ declare function _replacePlistKeyValue(xml: string, key: string, replacementBlock: string, eol: string): string;
14
+ declare function _insertBeforeRootDictClose(xml: string, block: string, _indent: string, eol: string): string;
15
+ /** @internal For branch coverage in unit tests. */
16
+ export declare const __plistPatchTesting: {
17
+ removePlistKeyBlock: typeof _removePlistKeyBlock;
18
+ detectPlistIndentation: typeof _detectPlistIndentation;
19
+ detectEol: typeof _detectEol;
20
+ insertBeforeRootDictClose: typeof _insertBeforeRootDictClose;
21
+ replacePlistKeyValue: typeof _replacePlistKeyValue;
22
+ readIconSwitcherIconsFromText: typeof _readIconSwitcherIconsFromText;
23
+ collapseBlankLinesBeforeDictClose: typeof _collapseBlankLinesBeforeDictClose;
24
+ iconSwitcherArrayItemIndent: typeof _iconSwitcherArrayItemIndent;
25
+ };
26
+ export {};
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.__plistPatchTesting = void 0;
7
+ exports.patchInfoPlistText = patchInfoPlistText;
8
+ const node_util_1 = require("node:util");
9
+ const plist_1 = __importDefault(require("@expo/plist"));
10
+ const ios_1 = require("./ios");
11
+ function patchInfoPlistText(originalXml, cfg) {
12
+ const parsed = plist_1.default.parse(originalXml);
13
+ const next = (0, ios_1.applyInfoPlist)(parsed, cfg);
14
+ if ((0, node_util_1.isDeepStrictEqual)(parsed, next)) {
15
+ return originalXml;
16
+ }
17
+ const eol = _detectEol(originalXml);
18
+ const { keyIndent, indentStep } = _detectPlistIndentation(originalXml, eol);
19
+ const icons = next.IconSwitcherIcons;
20
+ let xml = originalXml;
21
+ let changed = false;
22
+ if ((0, ios_1._isLegacy01PrimaryIconBundle)(parsed.CFBundleIcons)) {
23
+ const removed = _removePlistKeyBlock(xml, 'CFBundleIcons', eol);
24
+ if (removed !== xml) {
25
+ xml = removed;
26
+ changed = true;
27
+ }
28
+ }
29
+ if ((0, ios_1._isLegacy01PrimaryIconBundle)(parsed['CFBundleIcons~ipad'])) {
30
+ const removed = _removePlistKeyBlock(xml, 'CFBundleIcons~ipad', eol);
31
+ if (removed !== xml) {
32
+ xml = removed;
33
+ changed = true;
34
+ }
35
+ }
36
+ if (xml.includes('<key>IconSwitcherIcons</key>')) {
37
+ const existingIcons = _readIconSwitcherIconsFromText(xml);
38
+ if ((0, node_util_1.isDeepStrictEqual)(existingIcons, icons)) {
39
+ const result = changed ? xml : originalXml;
40
+ return _collapseBlankLinesBeforeDictClose(result, eol);
41
+ }
42
+ const iconsBlock = _renderIconSwitcherIcons(icons, keyIndent, indentStep, eol, _iconSwitcherArrayItemIndent(xml, eol));
43
+ xml = _replacePlistKeyValue(xml, 'IconSwitcherIcons', iconsBlock, eol);
44
+ return _collapseBlankLinesBeforeDictClose(xml, eol);
45
+ }
46
+ const iconsBlock = _renderIconSwitcherIcons(icons, keyIndent, indentStep, eol);
47
+ xml = _insertBeforeRootDictClose(xml, iconsBlock, keyIndent, eol);
48
+ return _collapseBlankLinesBeforeDictClose(xml, eol);
49
+ }
50
+ function _detectEol(xml) {
51
+ return xml.includes('\r\n') ? '\r\n' : '\n';
52
+ }
53
+ function _detectPlistIndentation(xml, eol) {
54
+ var _a, _b;
55
+ const arrayItemIndent = _iconSwitcherArrayItemIndent(xml, eol);
56
+ const keyMatch = xml.match(new RegExp(`${eol}( +)<key>[^<]+</key>`));
57
+ const tabKeyMatch = xml.match(new RegExp(`${eol}(\\t+)<key>[^<]+</key>`));
58
+ const keyIndent = (_b = (_a = keyMatch === null || keyMatch === void 0 ? void 0 : keyMatch[1]) !== null && _a !== void 0 ? _a : tabKeyMatch === null || tabKeyMatch === void 0 ? void 0 : tabKeyMatch[1]) !== null && _b !== void 0 ? _b : '\t';
59
+ const arrayStepMatch = xml.match(new RegExp(`${eol}( +)<array>${eol}( +)<string>`, 'm'));
60
+ const tabArrayStepMatch = xml.match(new RegExp(`${eol}(\\t+)<array>${eol}(\\t+)<string>`, 'm'));
61
+ let indentStep = '\t';
62
+ if (arrayStepMatch) {
63
+ const arrayIndent = arrayStepMatch[1];
64
+ const stringIndent = arrayStepMatch[2];
65
+ if (stringIndent.startsWith(arrayIndent)) {
66
+ indentStep = stringIndent.slice(arrayIndent.length);
67
+ }
68
+ }
69
+ else if (tabArrayStepMatch) {
70
+ const arrayIndent = tabArrayStepMatch[1];
71
+ const stringIndent = tabArrayStepMatch[2];
72
+ if (stringIndent.startsWith(arrayIndent)) {
73
+ indentStep = stringIndent.slice(arrayIndent.length);
74
+ }
75
+ }
76
+ else if (arrayItemIndent && arrayItemIndent.startsWith(keyIndent)) {
77
+ indentStep = arrayItemIndent.slice(keyIndent.length);
78
+ }
79
+ else if (keyIndent.length >= 2 && !keyIndent.includes('\t')) {
80
+ indentStep = keyIndent.slice(0, 2);
81
+ }
82
+ if (!indentStep) {
83
+ indentStep = keyIndent.includes('\t') ? '\t' : ' ';
84
+ }
85
+ return { keyIndent, indentStep };
86
+ }
87
+ function _iconSwitcherArrayItemIndent(xml, eol) {
88
+ const keyTag = '<key>IconSwitcherIcons</key>';
89
+ const start = xml.indexOf(keyTag);
90
+ if (start === -1) {
91
+ return null;
92
+ }
93
+ const arrayStart = xml.indexOf('<array>', start);
94
+ if (arrayStart === -1) {
95
+ return null;
96
+ }
97
+ const stringStart = xml.indexOf('<string>', arrayStart);
98
+ if (stringStart === -1) {
99
+ return null;
100
+ }
101
+ const lineStart = _lineStartBeforeIndex(xml, stringStart, eol);
102
+ const indent = xml.slice(lineStart, stringStart);
103
+ return /^\s*$/.test(indent) ? indent : null;
104
+ }
105
+ function _readIconSwitcherIconsFromText(xml) {
106
+ var _a;
107
+ const keyTag = '<key>IconSwitcherIcons</key>';
108
+ const start = xml.indexOf(keyTag);
109
+ if (start === -1) {
110
+ return [];
111
+ }
112
+ const arrayStart = xml.indexOf('<array>', start);
113
+ if (arrayStart === -1) {
114
+ return [];
115
+ }
116
+ const arrayEnd = _findBalancedTagEnd(xml, arrayStart, 'array');
117
+ const block = xml.slice(arrayStart, arrayEnd);
118
+ const names = [];
119
+ const re = /<string>([^<]*)<\/string>/g;
120
+ let match;
121
+ while ((match = re.exec(block)) !== null) {
122
+ names.push((_a = match[1]) !== null && _a !== void 0 ? _a : '');
123
+ }
124
+ return names;
125
+ }
126
+ function _renderIconSwitcherIcons(names, keyIndent, indentStep, eol, arrayItemIndent) {
127
+ const itemIndent = arrayItemIndent !== null && arrayItemIndent !== void 0 ? arrayItemIndent : `${keyIndent}${indentStep}`;
128
+ const lines = [
129
+ `${keyIndent}<key>IconSwitcherIcons</key>`,
130
+ `${keyIndent}<array>`,
131
+ ...names.map((name) => `${itemIndent}<string>${name}</string>`),
132
+ `${keyIndent}</array>`,
133
+ ];
134
+ return lines.join(eol);
135
+ }
136
+ function _lineStartBeforeIndex(xml, index, eol) {
137
+ if (index <= 0) {
138
+ return 0;
139
+ }
140
+ const lineStart = xml.lastIndexOf(eol, index - 1);
141
+ return lineStart === -1 ? 0 : lineStart + eol.length;
142
+ }
143
+ function _removePlistKeyBlock(xml, key, eol) {
144
+ const keyTag = `<key>${key}</key>`;
145
+ const start = xml.indexOf(keyTag);
146
+ if (start === -1) {
147
+ return xml;
148
+ }
149
+ const afterKey = start + keyTag.length;
150
+ const dictStart = xml.indexOf('<dict>', afterKey);
151
+ if (dictStart === -1) {
152
+ return xml;
153
+ }
154
+ let depth = 0;
155
+ let i = dictStart;
156
+ while (i < xml.length) {
157
+ if (xml.startsWith('<dict>', i)) {
158
+ depth += 1;
159
+ i += 6;
160
+ continue;
161
+ }
162
+ if (xml.startsWith('</dict>', i)) {
163
+ depth -= 1;
164
+ i += 7;
165
+ if (depth === 0) {
166
+ const end = i;
167
+ let removeStart = _lineStartBeforeIndex(xml, start, eol);
168
+ let result = xml.slice(0, removeStart) + xml.slice(end);
169
+ result = _collapseBlankLinesBeforeDictClose(result, eol);
170
+ return result;
171
+ }
172
+ continue;
173
+ }
174
+ i += 1;
175
+ }
176
+ return xml;
177
+ }
178
+ function _collapseBlankLinesBeforeDictClose(xml, eol) {
179
+ const escaped = eol.replace(/\r/g, '\\r').replace(/\n/g, '\\n');
180
+ const repeated = new RegExp(`(${escaped}[ \\t]*){2,}(?=${escaped}[ \\t]*</dict>${escaped}</plist>)`, 'g');
181
+ let result = xml.replace(repeated, eol);
182
+ const singleBlank = new RegExp(`${escaped}[ \\t]*(?=${escaped}[ \\t]*</dict>${escaped}</plist>)`, 'g');
183
+ result = result.replace(singleBlank, '');
184
+ return result;
185
+ }
186
+ function _replacePlistKeyValue(xml, key, replacementBlock, eol) {
187
+ const keyTag = `<key>${key}</key>`;
188
+ const start = xml.indexOf(keyTag);
189
+ if (start === -1) {
190
+ return xml;
191
+ }
192
+ const lineStart = _lineStartBeforeIndex(xml, start, eol);
193
+ const afterKey = start + keyTag.length;
194
+ const valueStart = xml.indexOf('<', afterKey);
195
+ if (valueStart === -1) {
196
+ return xml;
197
+ }
198
+ const valueEnd = _findBalancedTagEnd(xml, valueStart, 'array');
199
+ return xml.slice(0, lineStart) + replacementBlock + xml.slice(valueEnd);
200
+ }
201
+ function _findBalancedTagEnd(xml, openIndex, tag) {
202
+ let depth = 0;
203
+ let i = openIndex;
204
+ const open = `<${tag}>`;
205
+ const close = `</${tag}>`;
206
+ while (i < xml.length) {
207
+ if (xml.startsWith(open, i)) {
208
+ depth += 1;
209
+ i += open.length;
210
+ continue;
211
+ }
212
+ if (xml.startsWith(close, i)) {
213
+ depth -= 1;
214
+ i += close.length;
215
+ if (depth === 0) {
216
+ return i;
217
+ }
218
+ continue;
219
+ }
220
+ i += 1;
221
+ }
222
+ return xml.length;
223
+ }
224
+ function _insertBeforeRootDictClose(xml, block, _indent, eol) {
225
+ const plistClose = xml.lastIndexOf('</plist>');
226
+ if (plistClose === -1) {
227
+ return xml;
228
+ }
229
+ const beforePlist = xml.slice(0, plistClose).trimEnd();
230
+ const dictClose = beforePlist.lastIndexOf('</dict>');
231
+ if (dictClose === -1) {
232
+ return xml;
233
+ }
234
+ const prefix = xml.slice(0, dictClose);
235
+ const suffix = xml.slice(dictClose);
236
+ const needsNl = !prefix.endsWith('\n') && !prefix.endsWith('\r\n');
237
+ const joiner = needsNl ? eol : '';
238
+ return `${prefix}${joiner}${block}${eol}${suffix}`;
239
+ }
240
+ /** @internal For branch coverage in unit tests. */
241
+ exports.__plistPatchTesting = {
242
+ removePlistKeyBlock: _removePlistKeyBlock,
243
+ detectPlistIndentation: _detectPlistIndentation,
244
+ detectEol: _detectEol,
245
+ insertBeforeRootDictClose: _insertBeforeRootDictClose,
246
+ replacePlistKeyValue: _replacePlistKeyValue,
247
+ readIconSwitcherIconsFromText: _readIconSwitcherIconsFromText,
248
+ collapseBlankLinesBeforeDictClose: _collapseBlankLinesBeforeDictClose,
249
+ iconSwitcherArrayItemIndent: _iconSwitcherArrayItemIndent,
250
+ };
@@ -71,7 +71,14 @@ function _isFailedBlocked(
71
71
  export async function syncIcon(options: SyncIconOptions): Promise<() => void> {
72
72
  const storage = options.storage ?? _createMemoryStorage();
73
73
  const retryAfterMs = options.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
74
- const onError = options.onError;
74
+ // A throwing onError must never break syncIcon or surface as an unhandled rejection.
75
+ const reportError = (error: unknown): void => {
76
+ try {
77
+ options.onError?.(error);
78
+ } catch {
79
+ // Ignored on purpose.
80
+ }
81
+ };
75
82
 
76
83
  let inFlight = false;
77
84
 
@@ -111,11 +118,12 @@ export async function syncIcon(options: SyncIconOptions): Promise<() => void> {
111
118
  }
112
119
  await storage.setItem(STORAGE_KEY_APPLIED, choice);
113
120
  await storage.setItem(STORAGE_KEY_FAILED, '');
114
- } catch {
121
+ } catch (error) {
115
122
  await storage.setItem(STORAGE_KEY_FAILED, _encodeFailed(choice));
123
+ reportError(error);
116
124
  }
117
125
  } catch (error) {
118
- onError?.(error);
126
+ reportError(error);
119
127
  } finally {
120
128
  inFlight = false;
121
129
  }