authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-07 02:00:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-07 02:00:10-07:00
logbdf5aed665a91f2e1755e6a8ef72cd29627fa937
tree98b44932511d2192b2e75fd429a811dff9d2921b
parent0b62e2eb116abaaaafcb839c24accbbf5aba6a3c
signature Signed by PGP key B5690EEEBB952194

Fix Expo widget asset wiring order (#3763)


4 files changed, 29 insertions(+), 12 deletions(-)

apps/mobile/app.config.ts+6-3
...@@ -159,6 +159,12 @@ const config: ExpoConfig = {...@@ -159,6 +159,12 @@ const config: ExpoConfig = {
159 },159 },
160 ],160 ],
161 "./plugins/withIosCocoaPodsUuidCache.cjs",161 "./plugins/withIosCocoaPodsUuidCache.cjs",
162 // Must be listed BEFORE expo-widgets: same-type mods run last-registered-
163 // first, so registering earlier makes this plugin's mods run AFTER
164 // expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which
165 // would delete the asset catalog) and its xcodeproj mod creates the widget
166 // target (which must exist before the compile phase can be attached).
167 "./plugins/withWidgetLogoAsset.cjs",
162 [168 [
163 "expo-widgets",169 "expo-widgets",
164 {170 {
...@@ -178,9 +184,6 @@ const config: ExpoConfig = {...@@ -178,9 +184,6 @@ const config: ExpoConfig = {
178 ],184 ],
179 },185 },
180 ],186 ],
181 // Must run after expo-widgets so the widget target exists when it wires in
182 // the branded logo asset catalog.
183 "./plugins/withWidgetLogoAsset.cjs",
184 "./plugins/withIosSceneLifecycle.cjs",187 "./plugins/withIosSceneLifecycle.cjs",
185 "./plugins/withAndroidCleartextTraffic.cjs",188 "./plugins/withAndroidCleartextTraffic.cjs",
186 ],189 ],
apps/mobile/plugins/lib/addWidgetAssetCatalog.cjs+13-5
...@@ -9,8 +9,10 @@...@@ -9,8 +9,10 @@
9// drops the compiled Assets.car into the extension bundle. Marked9// drops the compiled Assets.car into the extension bundle. Marked
10// alwaysOutOfDate so the build system always runs it.10// alwaysOutOfDate so the build system always runs it.
11//11//
12// Idempotent across re-runs. Returns true when it added the phase, false when it12// Idempotent across re-runs. Returns true when it added the phase, false when
13// was already present or the target was not found.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.
1416
15const PHASE_NAME = "Compile Widget Assets";17const PHASE_NAME = "Compile Widget Assets";
1618
...@@ -20,8 +22,8 @@ const ACTOOL_SCRIPT = [...@@ -20,8 +22,8 @@ const ACTOOL_SCRIPT = [
20 "set -e",22 "set -e",
21 'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',23 'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',
22 'if [ ! -d "$CATALOG" ]; then',24 'if [ ! -d "$CATALOG" ]; then',
23 ' echo "warning: widget asset catalog not found at $CATALOG"',25 ' echo "error: widget asset catalog not found at $CATALOG (expo-widgets wiped it? check plugin ordering in app.config.ts)"',
24 " exit 0",26 " exit 1",
25 "fi",27 "fi",
26 'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',28 'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',
27 'mkdir -p "$DEST"',29 'mkdir -p "$DEST"',
...@@ -51,7 +53,13 @@ function findByName(map, name) {...@@ -51,7 +53,13 @@ function findByName(map, name) {
51function addWidgetAssetCatalog(proj, opts) {53function addWidgetAssetCatalog(proj, opts) {
52 const objects = proj.hash.project.objects;54 const objects = proj.hash.project.objects;
53 const target = findByName(objects.PBXNativeTarget, opts.targetName);55 const target = findByName(objects.PBXNativeTarget, opts.targetName);
54 if (!target) return false;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 }
5563
56 const phases = target.value.buildPhases || [];64 const phases = target.value.buildPhases || [];
57 const existing = objects.PBXShellScriptBuildPhase || {};65 const existing = objects.PBXShellScriptBuildPhase || {};
apps/mobile/plugins/withWidgetLogoAsset.cjs+9-3
...@@ -5,9 +5,15 @@...@@ -5,9 +5,15 @@
5// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and5// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and
6// has no asset support, so this plugin (a) writes an SVG template image set into6// has no asset support, so this plugin (a) writes an SVG template image set into
7// the generated widget asset catalog and (b) wires that catalog into the widget7// the generated widget asset catalog and (b) wires that catalog into the widget
8// target with a dedicated Resources build phase. Both steps are idempotent and8// target with an actool build phase.
9// survive `expo prebuild --clean`. Must be listed AFTER "expo-widgets" in the9//
10// plugins array so the widget target exists when this runs.10// ORDERING: must be listed BEFORE "expo-widgets" in the plugins array. Expo
11// chains same-type mods so the last-registered runs FIRST; registering this
12// plugin earlier makes its mods run AFTER expo-widgets' mods. That matters
13// twice: expo-widgets' dangerous mod rmSync's ios/ExpoWidgetsTarget/ (deleting
14// any catalog written before it), and its xcodeproj mod is what creates the
15// widget target. Listed after expo-widgets, both steps silently no-op on a
16// fresh prebuild — which is how prod build 8 shipped without the logo.
1117
12const path = require("path");18const path = require("path");
13const fs = require("fs");19const fs = require("fs");
apps/mobile/scripts/wire-widget-asset-catalog.cjs+1-1
...@@ -26,5 +26,5 @@ if (added) {...@@ -26,5 +26,5 @@ if (added) {
26 fs.writeFileSync(pbxprojPath, proj.writeSync());26 fs.writeFileSync(pbxprojPath, proj.writeSync());
27 console.log("Added widget asset-compile phase to ExpoWidgetsTarget.");27 console.log("Added widget asset-compile phase to ExpoWidgetsTarget.");
28} else {28} else {
29 console.log("No change: phase already present or target not found.");29 console.log("No change: phase already present.");
30}30}