ReviewOS

also looking at this

craft-native/craft

feat: finish native customization and platform seams

#401
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
21 files +707 -17
packages/android/src/index.tsmodified+62-0
Changes to packages/android/src/index.ts
@@ -138,6 +138,10 @@ export interface CraftAndroidConfig {
138138 renderer?: 'web' | 'native'
139139 /** "system" follows the device; light/dark pin the generated host. */
140140 appearance?: 'light' | 'dark' | 'system'
141 /** Static native tab-bar styling applied before the page's own tabBar.set call. */
142 tabBar?: CraftTabBarConfig
143 /** Runs from the generated project root before Gradle compiles the app. */
144 postGenerate?: CraftPostGenerateHook
141145 version?: string
142146 versionCode?: number
143147 darkMode?: boolean
@@ -261,6 +265,21 @@ export interface CraftAndroidConfig {
261265 }
262266}
263267
268export interface CraftTabBarConfig {
269 selection?: {
270 /** The selected tab indicator's corner radius in dp; omitted keeps the default. */
271 cornerRadius?: number
272 /** The selected tab indicator fill as #RRGGBB. */
273 color?: string
274 }
275}
276
277/** A declared command Craft runs after the native project has been generated. */
278export interface CraftPostGenerateHook {
279 command: string
280 args?: string[]
281}
282
264283/** The shared health data types (the iOS builder's HealthKit names). */
265284export type HealthConnectType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate'
266285 | 'heartRateVariability' | 'bodyMass' | 'sleep' | 'mindful' | 'dietaryEnergy' | 'dietaryProtein'
@@ -356,6 +375,42 @@ export interface BuildOptions {
356375 runtimeDir?: string | null
357376}
358377
378/**
379 * Run the app's declared generator hook without invoking a shell.
380 *
381 * A hook gets the generated project as its cwd and explicit metadata so a
382 * patch can target the right platform and phase without guessing from paths.
383 */
384export function runPostGenerateHook(
385 hook: CraftPostGenerateHook | undefined,
386 output: string,
387 phase: 'build' = 'build',
388): void {
389 if (!hook) return
390 if (typeof hook.command !== 'string' || !hook.command.trim())
391 throw new Error('Android postGenerate.command must be a non-empty executable')
392 if (hook.args !== undefined && (!Array.isArray(hook.args) || hook.args.some(argument => typeof argument !== 'string')))
393 throw new Error('Android postGenerate.args must be an array of strings')
394
395 const command = [hook.command, ...(hook.args ?? [])]
396 let result: ReturnType<typeof Bun.spawnSync>
397 try {
398 result = Bun.spawnSync(command, {
399 cwd: output,
400 env: { ...process.env, CRAFT_PROJECT_ROOT: output, CRAFT_PLATFORM: 'android', CRAFT_GENERATE_PHASE: phase },
401 stdout: 'inherit',
402 stderr: 'pipe',
403 })
404 }
405 catch (error) {
406 throw new Error(`Android postGenerate hook could not start in ${output}: ${hook.command}`, { cause: error })
407 }
408 if (result.exitCode !== 0) {
409 const stderr = result.stderr?.toString().trim()
410 throw new Error(`Android postGenerate hook failed in ${output} with exit code ${result.exitCode}${stderr ? `: ${stderr}` : ''}`)
411 }
412}
413
359414export interface OpenOptions {
360415 output: string
361416}
@@ -521,6 +576,11 @@ function validateAndroidConfig(config: CraftAndroidConfig): void {
521576 if (config.appearance !== undefined && !['light', 'dark', 'system'].includes(config.appearance)) {
522577 throw new Error(`Unknown Android appearance: ${config.appearance}. Expected light, dark, or system.`)
523578 }
579 const selection = config.tabBar?.selection
580 if (selection?.cornerRadius !== undefined && (!Number.isFinite(selection.cornerRadius) || selection.cornerRadius < 0))
581 throw new Error('Android tabBar.selection.cornerRadius must be a finite non-negative number')
582 if (selection?.color !== undefined && !/^#[0-9a-f]{6}$/i.test(selection.color))
583 throw new Error('Android tabBar.selection.color must be a #RRGGBB color')
524584
525585 const packageSegments = config.packageName.split('.')
526586 if (packageSegments.length < 2
@@ -1577,6 +1637,8 @@ export async function build(options: BuildOptions): Promise<void> {
15771637
15781638 syncNativeFonts(config.nativeFonts, output)
15791639
1640 runPostGenerateHook(config.postGenerate, output)
1641
15801642 if (!compile) return
15811643
15821644 // Check if gradlew exists