/**
* Post-generation instructions for users and AI agents.
*
* Provides clear guidance on how to integrate generated assets
* into an Expo project.
*/
import type {
AssetGeneratorConfig,
AssetType,
BackgroundConfig,
ForegroundConfig,
Platform,
} from '../types'
export interface GenerationContext {
outputDir: string
platforms: Platform[]
assetTypes: AssetType[]
zipPath?: string | undefined
config?: AssetGeneratorConfig
}
export interface InstructionStep {
step: number
title: string
description: string
command?: string
files?: string[]
}
export interface Instructions {
summary: string
generationConfig?: string | undefined
steps: InstructionStep[]
expoConfigChanges?: string
notes: string[]
}
/**
* Generate human-readable instructions for integrating assets into Expo.
*/
export function generateInstructions(context: GenerationContext): Instructions {
const { outputDir, platforms, assetTypes } = context
const steps: InstructionStep[] = []
let stepNum = 1
// Step 1: Copy main icon
if (assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Copy app icon',
description: 'Copy the main icon to your Expo assets directory',
command: `cp ${outputDir}/ios/icon-1024.png ../expo/assets/images/icon.png`,
files: [`${outputDir}/ios/icon-1024.png`],
})
}
// Step 2: Copy adaptive icon (Android)
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
steps.push({
step: stepNum++,
title: 'Copy Android adaptive icon',
description: 'Copy the adaptive icon foreground for Android',
command: `cp ${outputDir}/android/mipmap-xxxhdpi/ic_launcher_foreground.png ../expo/assets/images/adaptive-icon.png`,
files: [`${outputDir}/android/mipmap-xxxhdpi/ic_launcher_foreground.png`],
})
}
// Step 3: Copy splash screen
if (assetTypes.includes('splash')) {
steps.push({
step: stepNum++,
title: 'Copy splash screen',
description:
'Copy a splash screen image (choose appropriate size for your needs)',
command: `cp ${outputDir}/ios/splash-1170x2532.png ../expo/assets/images/splash.png`,
files: [
`${outputDir}/ios/splash-*.png`,
`${outputDir}/android/drawable-*/splash.png`,
],
})
}
// Step 4: Copy favicon (Web)
if (platforms.includes('web') && assetTypes.includes('favicon')) {
steps.push({
step: stepNum++,
title: 'Copy web favicon',
description:
'Copy favicon.ico to public root and PNG favicons for web builds',
command: `cp ${outputDir}/web/favicon.ico ../public/ && cp ${outputDir}/web/favicon-*.png ../public/`,
files: [`${outputDir}/web/favicon.ico`, `${outputDir}/web/favicon-*.png`],
})
}
// Step 5: Configure iOS 18 icon variants (dark, tinted, clear)
if (platforms.includes('ios') && assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Configure iOS 18 icon variants (Xcode)',
description:
'Copy ios/dark/, ios/tinted/, ios/clear-light/, ios/clear-dark/ folders to your Xcode asset catalog for iOS 18+ icon appearances',
files: [
`${outputDir}/ios/dark/`,
`${outputDir}/ios/tinted/`,
`${outputDir}/ios/clear-light/`,
`${outputDir}/ios/clear-dark/`,
],
})
}
// Step 6: Configure Android monochrome icons
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
steps.push({
step: stepNum++,
title: 'Configure Android 13+ monochrome icons',
description:
'Add monochrome layer to your adaptive-icon XML for Material You themed icons',
files: [`${outputDir}/android/mipmap-*/ic_launcher_monochrome.png`],
})
}
// Step 7: Copy web manifest and PWA icons
if (platforms.includes('web') && assetTypes.includes('favicon')) {
steps.push({
step: stepNum++,
title: 'Copy web manifest and PWA icons',
description:
'Copy site.webmanifest and PWA icons (including maskable) to your web public folder',
files: [
`${outputDir}/web/site.webmanifest`,
`${outputDir}/web/icon-*.png`,
],
})
}
// Step: Use auto-generated iOS Contents.json
if (platforms.includes('ios') && assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Use auto-generated iOS Contents.json',
description:
'Copy ios/AppIcon.appiconset/Contents.json to your Xcode asset catalog for automatic icon configuration',
files: [`${outputDir}/ios/AppIcon.appiconset/Contents.json`],
})
}
// Step: Use auto-generated Android XML
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
steps.push({
step: stepNum++,
title: 'Use auto-generated Android adaptive icon XML',
description:
'Copy android/mipmap-anydpi-v26/*.xml to your Android res folder for adaptive icon configuration',
files: [
`${outputDir}/android/mipmap-anydpi-v26/ic_launcher.xml`,
`${outputDir}/android/mipmap-anydpi-v26/ic_launcher_round.xml`,
],
})
}
// Step: Upload store listing assets
if (assetTypes.includes('store')) {
steps.push({
step: stepNum++,
title: 'Upload store listing assets',
description:
'Upload store assets to App Store Connect and Google Play Console',
files: [
`${outputDir}/store/android/play-store-icon.png`,
`${outputDir}/store/android/feature-graphic.png`,
`${outputDir}/store/android/tv-banner.png`,
`${outputDir}/store/ios/app-store-icon.png`,
],
})
}
// Step: Configure watchOS icons
if (platforms.includes('watchos') && assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Configure watchOS icons',
description:
'Copy watchos/ folder to your Xcode asset catalog for Apple Watch icons',
files: [`${outputDir}/watchos/`],
})
}
// Step: Configure tvOS icons
if (platforms.includes('tvos') && assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Configure tvOS icons',
description:
'Copy tvos/ folder to your Xcode asset catalog. Configure layered image stack with icon-back and icon-front layers for parallax effect.',
files: [
`${outputDir}/tvos/icon-back*.png`,
`${outputDir}/tvos/icon-front*.png`,
`${outputDir}/tvos/top-shelf*.png`,
],
})
}
// Step: Configure visionOS icons
if (platforms.includes('visionos') && assetTypes.includes('icon')) {
steps.push({
step: stepNum++,
title: 'Configure visionOS icons',
description:
'Copy visionos/ folder to your Xcode asset catalog. Configure 3D layered icon with icon-back and icon-front layers.',
files: [
`${outputDir}/visionos/icon-1024.png`,
`${outputDir}/visionos/icon-back.png`,
`${outputDir}/visionos/icon-front.png`,
],
})
}
// Step: Rebuild native projects
steps.push({
step: stepNum++,
title: 'Rebuild native projects',
description: 'Regenerate native iOS/Android projects with new assets',
command: 'cd ../expo && npx expo prebuild --clean',
})
// Generate expo config example
const expoConfigChanges = generateExpoConfigExample(platforms, assetTypes)
// Notes
const notes: string[] = [
'The 1024x1024 icon is used as source; Expo generates all required sizes',
'For production, ensure icon has no transparency (iOS requirement)',
'Android adaptive icons should have content within the safe zone (66% center)',
]
// iOS 18 notes
if (platforms.includes('ios') && assetTypes.includes('icon')) {
notes.push(
'iOS 18+ supports 5 icon appearances: default, dark, tinted, clear-light, clear-dark',
)
notes.push(
'Tinted icons use white foreground; system applies wallpaper tint color',
)
notes.push(
'Clear icons have semi-transparent backgrounds for light/dark modes',
)
}
// Android 13+ notes
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
notes.push(
'Android 13+ themed icons require a monochrome layer in adaptive-icon XML',
)
notes.push(
'ic_launcher.xml and ic_launcher_round.xml are auto-generated with monochrome layer',
)
}
// watchOS notes
if (platforms.includes('watchos') && assetTypes.includes('icon')) {
notes.push('watchOS icons use circular mask with 80% safe zone')
notes.push('watchOS icons are generated at @2x scale for all sizes')
}
// tvOS notes
if (platforms.includes('tvos') && assetTypes.includes('icon')) {
notes.push(
'tvOS uses layered icons (back/front) for parallax effect on Apple TV',
)
notes.push('Top shelf images are shown when app is focused on home screen')
}
// visionOS notes
if (platforms.includes('visionos') && assetTypes.includes('icon')) {
notes.push('visionOS icons use circular mask with 80% safe zone')
notes.push('visionOS supports optional 3D layered icons (back/front)')
}
// Store listing notes
if (assetTypes.includes('store')) {
notes.push('Play Store icon: 512x512 PNG, required for Google Play')
notes.push(
'Feature graphic: 1024x500 PNG, displayed on Play Store app page',
)
notes.push('TV banner: 1280x720 PNG, for Android TV apps on Play Store')
}
if (context.zipPath) {
notes.push(`Full asset archive available at: ${context.zipPath}`)
}
// Generate config summary if available
const generationConfig = context.config
? formatGenerationConfig(context.config)
: undefined
return {
summary: `Generated assets for ${platforms.join(', ')} (${assetTypes.join(', ')})`,
generationConfig,
steps,
expoConfigChanges,
notes,
}
}
/**
* Format background configuration for display.
*/
function formatBackgroundConfig(bg: BackgroundConfig): string[] {
const lines: string[] = []
if (bg.type === 'color' && bg.color) {
lines.push(`Type: Solid color`)
lines.push(`Color: ${bg.color.color}`)
} else if (bg.type === 'gradient' && bg.gradient) {
lines.push(`Type: ${bg.gradient.type} gradient`)
lines.push(`Colors: ${bg.gradient.colors.join(' → ')}`)
if (bg.gradient.angle !== undefined) {
lines.push(`Angle: ${bg.gradient.angle}°`)
}
} else if (bg.type === 'image' && bg.imagePath) {
lines.push(`Type: Image`)
lines.push(`Path: ${bg.imagePath}`)
}
return lines
}
/**
* Format foreground configuration for display.
*/
function formatForegroundConfig(fg: ForegroundConfig): string[] {
const lines: string[] = []
if (fg.type === 'text') {
lines.push(`Type: Text`)
lines.push(`Text: "${fg.text}"`)
lines.push(`Font: ${fg.fontFamily}`)
lines.push(`Font source: ${fg.fontSource}`)
lines.push(`Color: ${fg.color}`)
if (fg.fontSize) {
lines.push(`Font size: ${fg.fontSize}px`)
}
} else if (fg.type === 'svg') {
lines.push(`Type: SVG`)
lines.push(`Path: ${fg.svgPath}`)
if (fg.color) {
lines.push(`Color override: ${fg.color}`)
}
} else if (fg.type === 'image') {
lines.push(`Type: Image`)
lines.push(`Path: ${fg.imagePath}`)
}
return lines
}
/**
* Format the complete generation configuration for display.
*/
function formatGenerationConfig(config: AssetGeneratorConfig): string {
const lines: string[] = []
lines.push(`App name: ${config.appName}`)
lines.push(`Platforms: ${config.platforms.join(', ')}`)
lines.push(`Asset types: ${config.assetTypes.join(', ')}`)
lines.push(`Icon scale: ${((config.iconScale ?? 0.7) * 100).toFixed(0)}%`)
lines.push(
`Splash scale: ${((config.splashScale ?? 0.25) * 100).toFixed(0)}%`,
)
lines.push('')
lines.push('Background:')
for (const line of formatBackgroundConfig(config.background)) {
lines.push(` ${line}`)
}
lines.push('')
lines.push('Foreground:')
for (const line of formatForegroundConfig(config.foreground)) {
lines.push(` ${line}`)
}
return lines.join('\n')
}
/**
* Generate example app.config.ts changes.
*/
function generateExpoConfigExample(
platforms: Platform[],
assetTypes: AssetType[],
): string {
const lines: string[] = ['// app.config.ts asset configuration example:', '']
if (assetTypes.includes('icon')) {
lines.push('// iOS icon (in expo.ios)')
lines.push("icon: './assets/images/icon.png',")
lines.push('')
}
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
lines.push('// Android adaptive icon (in expo.android)')
lines.push('adaptiveIcon: {')
lines.push(" foregroundImage: './assets/images/adaptive-icon.png',")
lines.push(
" monochromeImage: './assets/images/adaptive-icon-monochrome.png', // Android 13+ themed icons",
)
lines.push(" backgroundColor: '#FFFFFF', // or your background color")
lines.push('},')
lines.push('')
}
if (assetTypes.includes('splash')) {
lines.push('// Splash screen (in expo.plugins)')
lines.push('[')
lines.push(" 'expo-splash-screen',")
lines.push(' {')
lines.push(" backgroundColor: '#FFFFFF',")
lines.push(" image: './assets/images/splash.png',")
lines.push(' imageWidth: 200,')
lines.push(' },')
lines.push('],')
lines.push('')
}
if (platforms.includes('web') && assetTypes.includes('favicon')) {
lines.push('// Web favicon (in expo.web)')
lines.push("favicon: './assets/images/favicon.png',")
lines.push('')
lines.push(
'// ─── Modern Favicon Setup (HTML head) ────────────────────────',
)
lines.push(
'// Place favicon.ico in your public root (browsers check /favicon.ico automatically)',
)
lines.push('//')
lines.push(
'// ',
)
lines.push(
'// ',
)
lines.push(
'// ',
)
lines.push('// ')
lines.push('')
}
// iOS 18 Xcode asset catalog configuration
if (platforms.includes('ios') && assetTypes.includes('icon')) {
lines.push('')
lines.push(
'// ─── iOS 18 Icon Variants (Xcode Asset Catalog) ───────────────',
)
lines.push(
'// For native iOS projects, configure AppIcon.appiconset/Contents.json:',
)
lines.push('//')
lines.push('// Add appearances for dark, tinted, and clear variants:')
lines.push('// {')
lines.push('// "images": [')
lines.push(
'// { "filename": "icon-60@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" },',
)
lines.push(
'// { "appearances": [{ "appearance": "luminosity", "value": "dark" }],',
)
lines.push(
'// "filename": "dark/icon-60@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" },',
)
lines.push(
'// { "appearances": [{ "appearance": "luminosity", "value": "tinted" }],',
)
lines.push(
'// "filename": "tinted/icon-60@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" }',
)
lines.push('// ]')
lines.push('// }')
lines.push('')
}
// Android monochrome icon configuration
if (platforms.includes('android') && assetTypes.includes('adaptive')) {
lines.push('')
lines.push(
'// ─── Android 13+ Themed Icons (Native) ────────────────────────',
)
lines.push(
'// For native Android projects, update res/mipmap-anydpi-v26/ic_launcher.xml:',
)
lines.push('//')
lines.push(
'// ',
)
lines.push(
'// ',
)
lines.push(
'// ',
)
lines.push(
'// ',
)
lines.push('// ')
lines.push('')
}
return lines.join('\n')
}
/**
* Format instructions for console output (human-readable).
*/
export function formatInstructionsText(instructions: Instructions): string {
const lines: string[] = []
lines.push('')
lines.push('═══════════════════════════════════════════════════════════════')
lines.push(' GENERATION INFO')
lines.push('═══════════════════════════════════════════════════════════════')
lines.push('')
lines.push(instructions.summary)
lines.push('')
if (instructions.generationConfig) {
lines.push(
'───────────────────────────────────────────────────────────────',
)
lines.push(' CONFIGURATION')
lines.push(
'───────────────────────────────────────────────────────────────',
)
lines.push('')
lines.push(instructions.generationConfig)
lines.push('')
}
lines.push('───────────────────────────────────────────────────────────────')
lines.push(' INTEGRATION STEPS')
lines.push('───────────────────────────────────────────────────────────────')
lines.push('')
for (const step of instructions.steps) {
lines.push(` ${step.step}. ${step.title}`)
lines.push(` ${step.description}`)
if (step.command) {
lines.push(` $ ${step.command}`)
}
lines.push('')
}
if (instructions.expoConfigChanges) {
lines.push(
'───────────────────────────────────────────────────────────────',
)
lines.push(' EXPO CONFIG EXAMPLE')
lines.push(
'───────────────────────────────────────────────────────────────',
)
lines.push('')
lines.push(instructions.expoConfigChanges)
lines.push('')
}
lines.push('───────────────────────────────────────────────────────────────')
lines.push(' NOTES')
lines.push('───────────────────────────────────────────────────────────────')
lines.push('')
for (const note of instructions.notes) {
lines.push(` • ${note}`)
}
lines.push('')
lines.push('═══════════════════════════════════════════════════════════════')
return lines.join('\n')
}
/**
* Format instructions for JSON output (AI agents).
*/
export function formatInstructionsJson(instructions: Instructions): object {
return {
summary: instructions.summary,
steps: instructions.steps,
expoConfig: instructions.expoConfigChanges,
notes: instructions.notes,
aiInstructions: [
'Copy the generated assets to the Expo project assets/images/ directory',
'Use the largest resolution icons as source (Expo auto-generates sizes)',
'Run "npx expo prebuild --clean" after updating assets',
'Verify icons render correctly on both iOS and Android simulators',
],
}
}