react-native-icon-switcher 0.0.0-stage → 0.1.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/IconSwitcher.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +252 -2
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcher.kt +380 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherExceptions.kt +24 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherModule.kt +67 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherPackage.kt +30 -0
- package/android/src/test/AndroidManifest.xml +39 -0
- package/android/src/test/java/com/iconswitcher/IconSwitcherTest.kt +392 -0
- package/app.plugin.js +1 -0
- package/ios/ISIconSwitcher.h +49 -0
- package/ios/ISIconSwitcher.m +205 -0
- package/ios/IconSwitcher.h +5 -0
- package/ios/IconSwitcher.mm +100 -0
- package/jest/index.d.ts +31 -0
- package/jest/index.js +98 -0
- package/lib/module/NativeIconSwitcher.js +5 -0
- package/lib/module/NativeIconSwitcher.js.map +1 -0
- package/lib/module/config.js +6 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/errors.js +28 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +69 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/register.js +2 -0
- package/lib/module/register.js.map +1 -0
- package/lib/module/remote/index.js +119 -0
- package/lib/module/remote/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeIconSwitcher.d.ts +18 -0
- package/lib/typescript/src/NativeIconSwitcher.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +4 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +7 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +20 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/register.d.ts +7 -0
- package/lib/typescript/src/register.d.ts.map +1 -0
- package/lib/typescript/src/remote/index.d.ts +17 -0
- package/lib/typescript/src/remote/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +253 -4
- package/plugin/build/cli.d.ts +3 -0
- package/plugin/build/cli.js +89 -0
- package/plugin/build/core/assets.d.ts +80 -0
- package/plugin/build/core/assets.js +463 -0
- package/plugin/build/core/errors.d.ts +4 -0
- package/plugin/build/core/errors.js +11 -0
- package/plugin/build/core/validate.d.ts +6 -0
- package/plugin/build/core/validate.js +229 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +134 -0
- package/plugin/build/load-config.d.ts +3 -0
- package/plugin/build/load-config.js +50 -0
- package/plugin/build/paths.d.ts +15 -0
- package/plugin/build/paths.js +153 -0
- package/plugin/build/sync.d.ts +12 -0
- package/plugin/build/sync.js +162 -0
- package/plugin/build/transforms/android.d.ts +5 -0
- package/plugin/build/transforms/android.js +212 -0
- package/plugin/build/transforms/errors.d.ts +4 -0
- package/plugin/build/transforms/errors.js +11 -0
- package/plugin/build/transforms/index.d.ts +4 -0
- package/plugin/build/transforms/index.js +12 -0
- package/plugin/build/transforms/ios.d.ts +4 -0
- package/plugin/build/transforms/ios.js +111 -0
- package/plugin/build/transforms/sampleConfigs.d.ts +3 -0
- package/plugin/build/transforms/sampleConfigs.js +33 -0
- package/plugin/build/transforms/types.d.ts +2 -0
- package/plugin/build/transforms/types.js +14 -0
- package/plugin/build/types.d.ts +61 -0
- package/plugin/build/types.js +5 -0
- package/src/NativeIconSwitcher.ts +17 -0
- package/src/config.ts +15 -0
- package/src/errors.ts +38 -0
- package/src/index.tsx +78 -0
- package/src/register.ts +4 -0
- package/src/remote/index.ts +142 -0
- package/src/types.ts +59 -0
|
@@ -0,0 +1,229 @@
|
|
|
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.resolveConfig = resolveConfig;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
const ICON_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
11
|
+
const HEX_COLOR_RE = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
12
|
+
const RESERVED_ANDROID_NAMES = new Set([
|
|
13
|
+
'round',
|
|
14
|
+
'foreground',
|
|
15
|
+
'background',
|
|
16
|
+
'monochrome',
|
|
17
|
+
]);
|
|
18
|
+
const RESERVED_ANDROID_SUFFIXES = [
|
|
19
|
+
'_round',
|
|
20
|
+
'_foreground',
|
|
21
|
+
'_background',
|
|
22
|
+
'_monochrome',
|
|
23
|
+
];
|
|
24
|
+
function _isHexColor(value) {
|
|
25
|
+
return HEX_COLOR_RE.test(value);
|
|
26
|
+
}
|
|
27
|
+
function _normalizeIos(ios) {
|
|
28
|
+
if (ios === undefined) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (typeof ios === 'string') {
|
|
32
|
+
return { light: ios };
|
|
33
|
+
}
|
|
34
|
+
return ios;
|
|
35
|
+
}
|
|
36
|
+
function _normalizeAndroid(android) {
|
|
37
|
+
if (android === undefined) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof android === 'string') {
|
|
41
|
+
return { legacy: android };
|
|
42
|
+
}
|
|
43
|
+
return android;
|
|
44
|
+
}
|
|
45
|
+
function _splitIconSource(source) {
|
|
46
|
+
if (typeof source === 'string') {
|
|
47
|
+
return {
|
|
48
|
+
ios: { light: source },
|
|
49
|
+
android: { legacy: source },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ios: _normalizeIos(source.ios),
|
|
54
|
+
android: _normalizeAndroid(source.android),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function _resolvedImagePath(projectRoot, rel) {
|
|
58
|
+
return node_path_1.default.resolve(projectRoot, rel);
|
|
59
|
+
}
|
|
60
|
+
function _imagePathsEqual(projectRoot, a, b) {
|
|
61
|
+
return (_resolvedImagePath(projectRoot, a) === _resolvedImagePath(projectRoot, b));
|
|
62
|
+
}
|
|
63
|
+
function _comparableDefaultPaths(source) {
|
|
64
|
+
const { ios, android } = _splitIconSource(source);
|
|
65
|
+
const out = {};
|
|
66
|
+
if (ios) {
|
|
67
|
+
out.ios = ios.light;
|
|
68
|
+
}
|
|
69
|
+
if (android) {
|
|
70
|
+
out.android = 'legacy' in android ? android.legacy : android.foreground;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function _validateDefaultMatchesExpo(problems, projectRoot, cfgDefault, expoDefault) {
|
|
75
|
+
const cfgPaths = _comparableDefaultPaths(cfgDefault);
|
|
76
|
+
const expoPaths = _comparableDefaultPaths(expoDefault);
|
|
77
|
+
if (expoPaths.ios !== undefined && cfgPaths.ios !== undefined) {
|
|
78
|
+
if (!_imagePathsEqual(projectRoot, cfgPaths.ios, expoPaths.ios)) {
|
|
79
|
+
problems.push(`Default icon iOS path conflicts: icon-switcher config has "${cfgPaths.ios}", Expo \`icon\` has "${expoPaths.ios}" — remove one or point both at the same file`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (expoPaths.android !== undefined && cfgPaths.android !== undefined) {
|
|
83
|
+
if (!_imagePathsEqual(projectRoot, cfgPaths.android, expoPaths.android)) {
|
|
84
|
+
problems.push(`Default icon Android path conflicts: icon-switcher config has "${cfgPaths.android}", Expo \`icon\` / \`android.adaptiveIcon\` has "${expoPaths.android}" — remove one or point both at the same file`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function _validateAndroidResourceName(problems, name) {
|
|
89
|
+
if (RESERVED_ANDROID_NAMES.has(name)) {
|
|
90
|
+
problems.push(`Invalid icon name "${name}": reserved for Android mipmap resources (see Android resource naming rules)`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const suffix of RESERVED_ANDROID_SUFFIXES) {
|
|
94
|
+
if (name.endsWith(suffix)) {
|
|
95
|
+
problems.push(`Invalid icon name "${name}": must not end with "${suffix}" (reserved for Android mipmap resources)`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function _collectPathsFromIos(ios, out) {
|
|
101
|
+
if (!ios) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
out.push(ios.light);
|
|
105
|
+
if (ios.dark) {
|
|
106
|
+
out.push(ios.dark);
|
|
107
|
+
}
|
|
108
|
+
if (ios.tinted) {
|
|
109
|
+
out.push(ios.tinted);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function _collectPathsFromAndroid(android, out) {
|
|
113
|
+
if (!android) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if ('legacy' in android) {
|
|
117
|
+
out.push(android.legacy);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
out.push(android.foreground);
|
|
121
|
+
if (typeof android.background === 'string' &&
|
|
122
|
+
!android.background.startsWith('#')) {
|
|
123
|
+
out.push(android.background);
|
|
124
|
+
}
|
|
125
|
+
if (android.monochrome) {
|
|
126
|
+
out.push(android.monochrome);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function _validateHexField(problems, value, label) {
|
|
130
|
+
if (value === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (!_isHexColor(value)) {
|
|
134
|
+
problems.push(`${label} must be a hex colour (#RGB or #RRGGBB), got "${value}"`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function _validateFile(problems, projectRoot, rel) {
|
|
140
|
+
const abs = node_path_1.default.join(projectRoot, rel);
|
|
141
|
+
if (!node_fs_1.default.existsSync(abs)) {
|
|
142
|
+
problems.push(`Missing image file: ${rel}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function resolveConfig(cfg, ctx) {
|
|
146
|
+
var _a, _b, _c, _d, _e, _f;
|
|
147
|
+
const problems = [];
|
|
148
|
+
if (Object.keys(cfg.icons).length === 0) {
|
|
149
|
+
problems.push('At least one alternate icon must be declared in `icons`');
|
|
150
|
+
}
|
|
151
|
+
for (const name of Object.keys(cfg.icons)) {
|
|
152
|
+
if (name === 'default') {
|
|
153
|
+
problems.push('`default` is reserved and cannot be used as an alternate icon name');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!ICON_NAME_RE.test(name)) {
|
|
157
|
+
problems.push(`Invalid icon name "${name}": must match /^[a-z][a-z0-9_]*$/`);
|
|
158
|
+
}
|
|
159
|
+
_validateAndroidResourceName(problems, name);
|
|
160
|
+
}
|
|
161
|
+
const flattenOn = (_b = _validateHexField(problems, (_a = cfg.ios) === null || _a === void 0 ? void 0 : _a.flattenOn, 'ios.flattenOn')) !== null && _b !== void 0 ? _b : '#FFFFFF';
|
|
162
|
+
const applyOn = (_d = (_c = cfg.android) === null || _c === void 0 ? void 0 : _c.applyOn) !== null && _d !== void 0 ? _d : 'background';
|
|
163
|
+
if (applyOn !== 'background' && applyOn !== 'immediate') {
|
|
164
|
+
problems.push(`android.applyOn must be "background" or "immediate", got "${applyOn}"`);
|
|
165
|
+
}
|
|
166
|
+
const skipBrands = ((_f = (_e = cfg.android) === null || _e === void 0 ? void 0 : _e.skipBrands) !== null && _f !== void 0 ? _f : []).map((b) => b.toLowerCase());
|
|
167
|
+
const expoOwnsDefault = ctx.expoDefault !== undefined;
|
|
168
|
+
if (cfg.default !== undefined && ctx.expoDefault !== undefined) {
|
|
169
|
+
_validateDefaultMatchesExpo(problems, ctx.projectRoot, cfg.default, ctx.expoDefault);
|
|
170
|
+
}
|
|
171
|
+
const icons = [];
|
|
172
|
+
const defaultSplit = !expoOwnsDefault && cfg.default !== undefined
|
|
173
|
+
? _splitIconSource(cfg.default)
|
|
174
|
+
: { ios: null, android: null };
|
|
175
|
+
icons.push({
|
|
176
|
+
name: 'default',
|
|
177
|
+
isDefault: true,
|
|
178
|
+
ios: defaultSplit.ios,
|
|
179
|
+
android: defaultSplit.android,
|
|
180
|
+
});
|
|
181
|
+
for (const name of Object.keys(cfg.icons)) {
|
|
182
|
+
const source = cfg.icons[name];
|
|
183
|
+
if (source === undefined) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const split = _splitIconSource(source);
|
|
187
|
+
icons.push({
|
|
188
|
+
name,
|
|
189
|
+
isDefault: false,
|
|
190
|
+
ios: split.ios,
|
|
191
|
+
android: split.android,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const paths = [];
|
|
195
|
+
for (const icon of icons) {
|
|
196
|
+
_collectPathsFromIos(icon.ios, paths);
|
|
197
|
+
_collectPathsFromAndroid(icon.android, paths);
|
|
198
|
+
}
|
|
199
|
+
const uniquePaths = [...new Set(paths)];
|
|
200
|
+
for (const rel of uniquePaths) {
|
|
201
|
+
if (rel.startsWith('#')) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
_validateFile(problems, ctx.projectRoot, rel);
|
|
205
|
+
}
|
|
206
|
+
for (const icon of icons) {
|
|
207
|
+
const android = icon.android;
|
|
208
|
+
if (!android || 'legacy' in android) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typeof android.background === 'string' &&
|
|
212
|
+
android.background.startsWith('#')) {
|
|
213
|
+
if (!_isHexColor(android.background)) {
|
|
214
|
+
problems.push(`icons.${icon.name}.android.background must be a hex colour or image path`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (problems.length > 0) {
|
|
219
|
+
throw new errors_1.ConfigError(problems.map((p) => `- ${p}`).join('\n'));
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
icons,
|
|
223
|
+
ios: { flattenOn },
|
|
224
|
+
android: {
|
|
225
|
+
applyOn: applyOn,
|
|
226
|
+
skipBrands,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
import type { IconSwitcherConfig } from './types';
|
|
3
|
+
/** @internal Test hook for locating Images.xcassets without a full Expo iOS tree. */
|
|
4
|
+
export declare function __setGetIosSourceRootForTests(resolver: ((projectRoot: string) => string) | undefined): void;
|
|
5
|
+
declare const _default: ConfigPlugin<IconSwitcherConfig>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,134 @@
|
|
|
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.__setGetIosSourceRootForTests = __setGetIosSourceRootForTests;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
10
|
+
const assets_1 = require("./core/assets");
|
|
11
|
+
const validate_1 = require("./core/validate");
|
|
12
|
+
const android_1 = require("./transforms/android");
|
|
13
|
+
const ios_1 = require("./transforms/ios");
|
|
14
|
+
const types_1 = require("./transforms/types");
|
|
15
|
+
let _getIosSourceRootForTests;
|
|
16
|
+
/** @internal Test hook for locating Images.xcassets without a full Expo iOS tree. */
|
|
17
|
+
function __setGetIosSourceRootForTests(resolver) {
|
|
18
|
+
_getIosSourceRootForTests = resolver;
|
|
19
|
+
}
|
|
20
|
+
function _iosSourceRoot(projectRoot) {
|
|
21
|
+
if (_getIosSourceRootForTests) {
|
|
22
|
+
return _getIosSourceRootForTests(projectRoot);
|
|
23
|
+
}
|
|
24
|
+
return config_plugins_1.IOSConfig.Paths.getSourceRoot(projectRoot);
|
|
25
|
+
}
|
|
26
|
+
function _buildExpoDefault(config) {
|
|
27
|
+
var _a;
|
|
28
|
+
const icon = config.icon;
|
|
29
|
+
const adaptive = (_a = config.android) === null || _a === void 0 ? void 0 : _a.adaptiveIcon;
|
|
30
|
+
if (!icon && !adaptive) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const iosPath = typeof icon === 'string' ? icon : undefined;
|
|
34
|
+
const androidAdaptive = adaptive && typeof adaptive.foregroundImage === 'string'
|
|
35
|
+
? {
|
|
36
|
+
foreground: adaptive.foregroundImage,
|
|
37
|
+
background: (typeof adaptive.backgroundColor === 'string' &&
|
|
38
|
+
adaptive.backgroundColor) ||
|
|
39
|
+
(typeof adaptive.backgroundImage === 'string' &&
|
|
40
|
+
adaptive.backgroundImage) ||
|
|
41
|
+
'#FFFFFF',
|
|
42
|
+
}
|
|
43
|
+
: undefined;
|
|
44
|
+
if (iosPath && !androidAdaptive) {
|
|
45
|
+
return iosPath;
|
|
46
|
+
}
|
|
47
|
+
if (!iosPath && androidAdaptive) {
|
|
48
|
+
return { android: androidAdaptive };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ios: iosPath,
|
|
52
|
+
android: androidAdaptive,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function _resolve(cfg, projectRoot, expoDefault) {
|
|
56
|
+
return (0, validate_1.resolveConfig)(cfg, { projectRoot, expoDefault });
|
|
57
|
+
}
|
|
58
|
+
function _filterPlan(plan, platform) {
|
|
59
|
+
const assets = plan.assets.filter((asset) => {
|
|
60
|
+
const isIos = asset.relativePath.includes('.appiconset');
|
|
61
|
+
return platform === 'ios' ? isIos : !isIos;
|
|
62
|
+
});
|
|
63
|
+
return { ...plan, assets };
|
|
64
|
+
}
|
|
65
|
+
function _findIosAssetCatalog(projectRoot) {
|
|
66
|
+
const catalog = path_1.default.join(_iosSourceRoot(projectRoot), 'Images.xcassets');
|
|
67
|
+
if (!fs_1.default.existsSync(catalog)) {
|
|
68
|
+
throw new Error(`Could not find Images.xcassets at ${catalog}`);
|
|
69
|
+
}
|
|
70
|
+
return catalog;
|
|
71
|
+
}
|
|
72
|
+
async function _writeTypesIfChanged(projectRoot, resolved) {
|
|
73
|
+
const target = path_1.default.join(projectRoot, 'icon-switcher.d.ts');
|
|
74
|
+
const content = (0, types_1.renderTypes)(resolved);
|
|
75
|
+
let existing;
|
|
76
|
+
try {
|
|
77
|
+
existing = await fs_1.default.promises.readFile(target, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
existing = undefined;
|
|
81
|
+
}
|
|
82
|
+
if (existing !== content) {
|
|
83
|
+
await fs_1.default.promises.writeFile(target, content, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const withIconSwitcher = (config, props) => {
|
|
87
|
+
config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
88
|
+
const resolved = _resolve(props, config.modRequest.projectRoot, _buildExpoDefault(config));
|
|
89
|
+
config.modResults = (0, android_1.applyAndroidManifest)(config.modResults, resolved);
|
|
90
|
+
return config;
|
|
91
|
+
});
|
|
92
|
+
config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
|
|
93
|
+
const resolved = _resolve(props, config.modRequest.projectRoot, _buildExpoDefault(config));
|
|
94
|
+
config.modResults = (0, ios_1.applyInfoPlist)(config.modResults, resolved);
|
|
95
|
+
return config;
|
|
96
|
+
});
|
|
97
|
+
config = (0, config_plugins_1.withXcodeProject)(config, (config) => {
|
|
98
|
+
const resolved = _resolve(props, config.modRequest.projectRoot, _buildExpoDefault(config));
|
|
99
|
+
config.modResults = (0, ios_1.applyXcodeProject)(config.modResults, resolved);
|
|
100
|
+
return config;
|
|
101
|
+
});
|
|
102
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
103
|
+
'android',
|
|
104
|
+
async (config) => {
|
|
105
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
106
|
+
const resolved = _resolve(props, projectRoot, _buildExpoDefault(config));
|
|
107
|
+
const plan = _filterPlan((0, assets_1.planAssets)(resolved, projectRoot), 'android');
|
|
108
|
+
const androidResDir = path_1.default.join(config.modRequest.platformProjectRoot, 'app', 'src', 'main', 'res');
|
|
109
|
+
await (0, assets_1.writeAssets)(plan, {
|
|
110
|
+
iosAssetCatalog: path_1.default.join(projectRoot, '.noop-ios'),
|
|
111
|
+
androidResDir,
|
|
112
|
+
});
|
|
113
|
+
await _writeTypesIfChanged(projectRoot, resolved);
|
|
114
|
+
return config;
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
118
|
+
'ios',
|
|
119
|
+
async (config) => {
|
|
120
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
121
|
+
const resolved = _resolve(props, projectRoot, _buildExpoDefault(config));
|
|
122
|
+
const plan = _filterPlan((0, assets_1.planAssets)(resolved, projectRoot), 'ios');
|
|
123
|
+
const iosAssetCatalog = _findIosAssetCatalog(projectRoot);
|
|
124
|
+
await (0, assets_1.writeAssets)(plan, {
|
|
125
|
+
iosAssetCatalog,
|
|
126
|
+
androidResDir: path_1.default.join(projectRoot, '.noop-android'),
|
|
127
|
+
});
|
|
128
|
+
await _writeTypesIfChanged(projectRoot, resolved);
|
|
129
|
+
return config;
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
return config;
|
|
133
|
+
};
|
|
134
|
+
exports.default = (0, config_plugins_1.createRunOncePlugin)(withIconSwitcher, 'react-native-icon-switcher');
|
|
@@ -0,0 +1,50 @@
|
|
|
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.resolveConfigPath = resolveConfigPath;
|
|
7
|
+
exports.loadIconSwitcherConfig = loadIconSwitcherConfig;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const jiti_1 = require("jiti");
|
|
11
|
+
const paths_1 = require("./paths");
|
|
12
|
+
const CONFIG_CANDIDATES = [
|
|
13
|
+
'icon-switcher.config.ts',
|
|
14
|
+
'icon-switcher.config.js',
|
|
15
|
+
'icon-switcher.config.mjs',
|
|
16
|
+
'icon-switcher.config.cjs',
|
|
17
|
+
];
|
|
18
|
+
function resolveConfigPath(projectRoot, explicit) {
|
|
19
|
+
if (explicit) {
|
|
20
|
+
const abs = node_path_1.default.isAbsolute(explicit)
|
|
21
|
+
? explicit
|
|
22
|
+
: node_path_1.default.join(projectRoot, explicit);
|
|
23
|
+
if (!node_fs_1.default.existsSync(abs)) {
|
|
24
|
+
throw new paths_1.ProjectLayoutError(`Config file not found at ${abs}`);
|
|
25
|
+
}
|
|
26
|
+
return abs;
|
|
27
|
+
}
|
|
28
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
29
|
+
const candidate = node_path_1.default.join(projectRoot, name);
|
|
30
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new paths_1.ProjectLayoutError(`Could not find icon-switcher.config.{ts,js,mjs,cjs} in ${projectRoot}`);
|
|
35
|
+
}
|
|
36
|
+
function loadIconSwitcherConfig(configPath) {
|
|
37
|
+
const load = (0, jiti_1.createJiti)(configPath, { interopDefault: true });
|
|
38
|
+
const loaded = load(configPath);
|
|
39
|
+
let cfg = loaded &&
|
|
40
|
+
typeof loaded === 'object' &&
|
|
41
|
+
loaded !== null &&
|
|
42
|
+
'default' in loaded &&
|
|
43
|
+
loaded.default !== undefined
|
|
44
|
+
? loaded.default
|
|
45
|
+
: loaded;
|
|
46
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
47
|
+
throw new paths_1.ProjectLayoutError(`Config at ${configPath} must export a default object`);
|
|
48
|
+
}
|
|
49
|
+
return cfg;
|
|
50
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NativeProjectPaths {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
iosDir: string;
|
|
4
|
+
xcodeprojPath: string;
|
|
5
|
+
pbxprojPath: string;
|
|
6
|
+
infoPlistPath: string;
|
|
7
|
+
iosAssetCatalog: string;
|
|
8
|
+
manifestPath: string;
|
|
9
|
+
androidResDir: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class ProjectLayoutError extends Error {
|
|
12
|
+
readonly name = "ProjectLayoutError";
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function locateNativePaths(projectRoot: string): NativeProjectPaths;
|
|
@@ -0,0 +1,153 @@
|
|
|
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.ProjectLayoutError = void 0;
|
|
7
|
+
exports.locateNativePaths = locateNativePaths;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const xcode_1 = __importDefault(require("xcode"));
|
|
11
|
+
const APP_PRODUCT_TYPE = 'com.apple.product-type.application';
|
|
12
|
+
const EXCLUDED_PRODUCT_TYPES = new Set([
|
|
13
|
+
'com.apple.product-type.app-extension',
|
|
14
|
+
'com.apple.product-type.bundle.unit-test',
|
|
15
|
+
'com.apple.product-type.bundle.ui-testing',
|
|
16
|
+
]);
|
|
17
|
+
class ProjectLayoutError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ProjectLayoutError';
|
|
21
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ProjectLayoutError = ProjectLayoutError;
|
|
25
|
+
function _unquote(value) {
|
|
26
|
+
return value.replace(/^"/, '').replace(/"$/, '');
|
|
27
|
+
}
|
|
28
|
+
function _findAppTarget(project) {
|
|
29
|
+
var _a;
|
|
30
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
31
|
+
const targetRefs = project.getFirstProject().firstProject.targets;
|
|
32
|
+
const matches = [];
|
|
33
|
+
for (const ref of targetRefs) {
|
|
34
|
+
const uuid = ref.value;
|
|
35
|
+
const native = nativeTargets[uuid];
|
|
36
|
+
if (!native || uuid.endsWith('_comment')) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const productType = _unquote(String((_a = native.productType) !== null && _a !== void 0 ? _a : ''));
|
|
40
|
+
if (EXCLUDED_PRODUCT_TYPES.has(productType)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (productType === APP_PRODUCT_TYPE) {
|
|
44
|
+
matches.push({ uuid, name: String(native.name) });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (matches.length === 0) {
|
|
48
|
+
throw new ProjectLayoutError('Could not find an iOS application target (non-test, non-extension) in the Xcode project');
|
|
49
|
+
}
|
|
50
|
+
if (matches.length > 1) {
|
|
51
|
+
throw new ProjectLayoutError(`Found multiple iOS application targets (${matches.map((m) => m.name).join(', ')}); leave only one app target`);
|
|
52
|
+
}
|
|
53
|
+
return matches[0];
|
|
54
|
+
}
|
|
55
|
+
function _infoPlistRelativePath(project, targetUuid) {
|
|
56
|
+
var _a, _b;
|
|
57
|
+
const native = project.pbxNativeTargetSection()[targetUuid];
|
|
58
|
+
if (!native) {
|
|
59
|
+
throw new ProjectLayoutError('Could not read the iOS app target');
|
|
60
|
+
}
|
|
61
|
+
const configListUuid = native.buildConfigurationList;
|
|
62
|
+
const lists = project.pbxXCConfigurationList();
|
|
63
|
+
const list = lists[configListUuid];
|
|
64
|
+
if (!((_a = list === null || list === void 0 ? void 0 : list.buildConfigurations) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
65
|
+
throw new ProjectLayoutError('Could not read build configurations for the iOS app target');
|
|
66
|
+
}
|
|
67
|
+
const firstConfig = list.buildConfigurations[0];
|
|
68
|
+
if (!firstConfig) {
|
|
69
|
+
throw new ProjectLayoutError('Could not read build configurations for the iOS app target');
|
|
70
|
+
}
|
|
71
|
+
const configUuid = firstConfig.value;
|
|
72
|
+
const section = project.pbxXCBuildConfigurationSection();
|
|
73
|
+
const config = section[configUuid];
|
|
74
|
+
const infoPlist = (_b = config === null || config === void 0 ? void 0 : config.buildSettings) === null || _b === void 0 ? void 0 : _b.INFOPLIST_FILE;
|
|
75
|
+
if (infoPlist === undefined || infoPlist === '') {
|
|
76
|
+
throw new ProjectLayoutError('INFOPLIST_FILE is not set on the iOS app target build configuration');
|
|
77
|
+
}
|
|
78
|
+
return _unquote(String(infoPlist));
|
|
79
|
+
}
|
|
80
|
+
function _locateXcodeproj(iosDir) {
|
|
81
|
+
const entries = node_fs_1.default.readdirSync(iosDir);
|
|
82
|
+
const projects = entries
|
|
83
|
+
.filter((name) => name.endsWith('.xcodeproj'))
|
|
84
|
+
.map((name) => node_path_1.default.join(iosDir, name));
|
|
85
|
+
if (projects.length === 0) {
|
|
86
|
+
throw new ProjectLayoutError(`Could not find ios/*.xcodeproj under ${iosDir}`);
|
|
87
|
+
}
|
|
88
|
+
const withAppTarget = [];
|
|
89
|
+
for (const xcodeprojPath of projects) {
|
|
90
|
+
const pbxprojPath = node_path_1.default.join(xcodeprojPath, 'project.pbxproj');
|
|
91
|
+
if (!node_fs_1.default.existsSync(pbxprojPath)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const project = xcode_1.default.project(pbxprojPath);
|
|
95
|
+
project.parseSync();
|
|
96
|
+
try {
|
|
97
|
+
_findAppTarget(project);
|
|
98
|
+
withAppTarget.push(xcodeprojPath);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof ProjectLayoutError) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (withAppTarget.length === 0) {
|
|
108
|
+
throw new ProjectLayoutError(`Could not find ios/*.xcodeproj with an application target under ${iosDir}`);
|
|
109
|
+
}
|
|
110
|
+
if (withAppTarget.length > 1) {
|
|
111
|
+
throw new ProjectLayoutError(`Found multiple Xcode projects with app targets: ${withAppTarget.map((p) => node_path_1.default.basename(p)).join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
return withAppTarget[0];
|
|
114
|
+
}
|
|
115
|
+
function locateNativePaths(projectRoot) {
|
|
116
|
+
const iosDir = node_path_1.default.join(projectRoot, 'ios');
|
|
117
|
+
if (!node_fs_1.default.existsSync(iosDir)) {
|
|
118
|
+
throw new ProjectLayoutError(`Missing iOS directory at ${iosDir}`);
|
|
119
|
+
}
|
|
120
|
+
const xcodeprojPath = _locateXcodeproj(iosDir);
|
|
121
|
+
const pbxprojPath = node_path_1.default.join(xcodeprojPath, 'project.pbxproj');
|
|
122
|
+
const project = xcode_1.default.project(pbxprojPath);
|
|
123
|
+
project.parseSync();
|
|
124
|
+
const appTarget = _findAppTarget(project);
|
|
125
|
+
const infoPlistRel = _infoPlistRelativePath(project, appTarget.uuid);
|
|
126
|
+
const infoPlistPath = node_path_1.default.join(iosDir, infoPlistRel);
|
|
127
|
+
if (!node_fs_1.default.existsSync(infoPlistPath)) {
|
|
128
|
+
throw new ProjectLayoutError(`Missing Info.plist at ${infoPlistPath}`);
|
|
129
|
+
}
|
|
130
|
+
const appSourceDir = node_path_1.default.dirname(infoPlistPath);
|
|
131
|
+
const iosAssetCatalog = node_path_1.default.join(appSourceDir, 'Images.xcassets');
|
|
132
|
+
if (!node_fs_1.default.existsSync(iosAssetCatalog)) {
|
|
133
|
+
throw new ProjectLayoutError(`Missing Images.xcassets at ${iosAssetCatalog}`);
|
|
134
|
+
}
|
|
135
|
+
const manifestPath = node_path_1.default.join(projectRoot, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
136
|
+
if (!node_fs_1.default.existsSync(manifestPath)) {
|
|
137
|
+
throw new ProjectLayoutError(`Missing AndroidManifest.xml at ${manifestPath}`);
|
|
138
|
+
}
|
|
139
|
+
const androidResDir = node_path_1.default.join(projectRoot, 'android', 'app', 'src', 'main', 'res');
|
|
140
|
+
if (!node_fs_1.default.existsSync(androidResDir)) {
|
|
141
|
+
throw new ProjectLayoutError(`Missing Android res directory at ${androidResDir}`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
projectRoot,
|
|
145
|
+
iosDir,
|
|
146
|
+
xcodeprojPath,
|
|
147
|
+
pbxprojPath,
|
|
148
|
+
infoPlistPath,
|
|
149
|
+
iosAssetCatalog,
|
|
150
|
+
manifestPath,
|
|
151
|
+
androidResDir,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { ProjectLayoutError } from './paths';
|
|
2
|
+
export interface SyncOptions {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
configPath?: string;
|
|
5
|
+
check: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SyncResult {
|
|
8
|
+
updated: number;
|
|
9
|
+
unchanged: number;
|
|
10
|
+
driftedPaths: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function syncProject(opts: SyncOptions): Promise<SyncResult>;
|