1"use strict";
2
3// Bundle the widget asset catalog into an app-extension (widget) target.
4//
5// expo-widgets generates the widget target without a Resources build phase, and
6// hand-adding a PBXResourcesBuildPhase does NOT get picked up — xcodebuild's
7// planner never schedules `actool` for it (verified: even a full re-plan skips
8// it). So instead we add a shell-script phase that runs `actool` directly and
9// drops the compiled Assets.car into the extension bundle. Marked
10// alwaysOutOfDate so the build system always runs it.
11//
12// Idempotent across re-runs. Returns true when it added the phase, false when
13// it was already present. Throws when the target does not exist — that means
14// this ran before expo-widgets created the target (plugin ordering bug) and
15// silently skipping would ship a widget without its assets.
16
17const PHASE_NAME = "Compile Widget Assets";
18
19// Compiles ExpoWidgetsTarget/Assets.xcassets into the extension's resources dir.
20// Uses only Xcode-provided build settings so it works for device + simulator.
21const ACTOOL_SCRIPT = [
22 "set -e",
23 'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',
24 'if [ ! -d "$CATALOG" ]; then',
25 ' echo "error: widget asset catalog not found at $CATALOG (expo-widgets wiped it? check plugin ordering in app.config.ts)"',
26 " exit 1",
27 "fi",
28 'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',
29 'mkdir -p "$DEST"',
30 'xcrun actool "$CATALOG" --compile "$DEST" --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" --output-format human-readable-text',
31].join("\n");
32
33function stripComments(map) {
34 const out = {};
35 for (const key of Object.keys(map || {})) {
36 if (key.endsWith("_comment")) continue;
37 out[key] = map[key];
38 }
39 return out;
40}
41
42function findByName(map, name) {
43 for (const [uuid, value] of Object.entries(stripComments(map))) {
44 if (value && value.name === name) return { uuid, value };
45 }
46 return null;
47}
48
49/**
50 * @param {import('xcode').XcodeProject} proj
51 * @param {{ targetName: string }} opts
52 */
53function addWidgetAssetCatalog(proj, opts) {
54 const objects = proj.hash.project.objects;
55 const target = findByName(objects.PBXNativeTarget, opts.targetName);
56 if (!target) {
57 throw new Error(
58 `addWidgetAssetCatalog: target "${opts.targetName}" not found — ` +
59 "withWidgetLogoAsset must be registered before expo-widgets so its " +
60 "xcodeproj mod runs after the widget target is created.",
61 );
62 }
63
64 const phases = target.value.buildPhases || [];
65 const existing = objects.PBXShellScriptBuildPhase || {};
66 const already = Object.entries(stripComments(existing)).some(
67 ([uuid, value]) =>
68 value && value.name === `"${PHASE_NAME}"` && phases.some((p) => p.value === uuid),
69 );
70 if (already) return false;
71
72 const { uuid } = proj.addBuildPhase([], "PBXShellScriptBuildPhase", PHASE_NAME, target.uuid, {
73 shellPath: "/bin/sh",
74 shellScript: ACTOOL_SCRIPT,
75 });
76 // Always run: input-analysis is exactly what skipped the Resources phase.
77 objects.PBXShellScriptBuildPhase[uuid].alwaysOutOfDate = 1;
78 return true;
79}
80
81module.exports = { addWidgetAssetCatalog };