t3-code-android-nightly/apps/mobile/plugins/lib/addWidgetAssetCatalog.cjs
2026-07-07 02:00:10 -07:00

81 lines
3.1 KiB
JavaScript

"use strict";
// Bundle the widget asset catalog into an app-extension (widget) target.
//
// expo-widgets generates the widget target without a Resources build phase, and
// hand-adding a PBXResourcesBuildPhase does NOT get picked up — xcodebuild's
// planner never schedules `actool` for it (verified: even a full re-plan skips
// it). So instead we add a shell-script phase that runs `actool` directly and
// drops the compiled Assets.car into the extension bundle. Marked
// alwaysOutOfDate so the build system always runs it.
//
// Idempotent across re-runs. Returns true when it added the phase, false when
// it was already present. Throws when the target does not exist — that means
// this ran before expo-widgets created the target (plugin ordering bug) and
// silently skipping would ship a widget without its assets.
const PHASE_NAME = "Compile Widget Assets";
// Compiles ExpoWidgetsTarget/Assets.xcassets into the extension's resources dir.
// Uses only Xcode-provided build settings so it works for device + simulator.
const ACTOOL_SCRIPT = [
"set -e",
'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',
'if [ ! -d "$CATALOG" ]; then',
' echo "error: widget asset catalog not found at $CATALOG (expo-widgets wiped it? check plugin ordering in app.config.ts)"',
" exit 1",
"fi",
'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',
'mkdir -p "$DEST"',
'xcrun actool "$CATALOG" --compile "$DEST" --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" --output-format human-readable-text',
].join("\n");
function stripComments(map) {
const out = {};
for (const key of Object.keys(map || {})) {
if (key.endsWith("_comment")) continue;
out[key] = map[key];
}
return out;
}
function findByName(map, name) {
for (const [uuid, value] of Object.entries(stripComments(map))) {
if (value && value.name === name) return { uuid, value };
}
return null;
}
/**
* @param {import('xcode').XcodeProject} proj
* @param {{ targetName: string }} opts
*/
function addWidgetAssetCatalog(proj, opts) {
const objects = proj.hash.project.objects;
const target = findByName(objects.PBXNativeTarget, opts.targetName);
if (!target) {
throw new Error(
`addWidgetAssetCatalog: target "${opts.targetName}" not found — ` +
"withWidgetLogoAsset must be registered before expo-widgets so its " +
"xcodeproj mod runs after the widget target is created.",
);
}
const phases = target.value.buildPhases || [];
const existing = objects.PBXShellScriptBuildPhase || {};
const already = Object.entries(stripComments(existing)).some(
([uuid, value]) =>
value && value.name === `"${PHASE_NAME}"` && phases.some((p) => p.value === uuid),
);
if (already) return false;
const { uuid } = proj.addBuildPhase([], "PBXShellScriptBuildPhase", PHASE_NAME, target.uuid, {
shellPath: "/bin/sh",
shellScript: ACTOOL_SCRIPT,
});
// Always run: input-analysis is exactly what skipped the Resources phase.
objects.PBXShellScriptBuildPhase[uuid].alwaysOutOfDate = 1;
return true;
}
module.exports = { addWidgetAssetCatalog };