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.test.tsmodified+52-2
Changes to packages/android/src/index.test.ts
@@ -1,7 +1,7 @@
1import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
1import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
22import { join } from 'node:path'
33import { afterAll, describe, expect, it } from 'bun:test'
4import { ANDROID_MIN_SDK, build, init, installRuntime, renderAndroidDeepLinks, renderAndroidPermissions, resolveRuntimeDir, syncAndroidWebAssets, syncNativeFonts } from './index'
4import { ANDROID_MIN_SDK, build, init, installRuntime, renderAndroidDeepLinks, renderAndroidPermissions, resolveRuntimeDir, runPostGenerateHook, syncAndroidWebAssets, syncNativeFonts } from './index'
55import { createTempDir, removeAllTempDirs } from '../../typescript/src/temp-dir'
66
77// Every scratch directory this file made goes when the file is done (#357).
@@ -15,6 +15,56 @@ function generatedFiles(path: string): string[] {
1515}
1616
1717describe('Craft Android builder', () => {
18 it('runs a declared postGenerate command from the project root with metadata', async () => {
19 const output = createTempDir('craft-android-post-generate-')
20 await init({ name: 'Hook App', output, runtimeDir: null, config: {
21 postGenerate: { command: 'sh', args: ['-c', 'printf "%s|%s|%s" "$PWD" "$CRAFT_PLATFORM" "$CRAFT_GENERATE_PHASE" > hook.txt'] },
22 } })
23 const config = JSON.parse(readFileSync(join(output, 'craft.config.json'), 'utf8'))
24 expect(config.postGenerate).toEqual({ command: 'sh', args: ['-c', 'printf "%s|%s|%s" "$PWD" "$CRAFT_PLATFORM" "$CRAFT_GENERATE_PHASE" > hook.txt'] })
25 runPostGenerateHook(config.postGenerate, output)
26 expect(readFileSync(join(output, 'hook.txt'), 'utf8')).toBe(`${realpathSync(output)}|android|build`)
27 })
28
29 it('runs the postGenerate hook during the Android build before compilation', async () => {
30 const output = createTempDir('craft-android-post-generate-build-')
31 await init({ name: 'Hook Build', output, runtimeDir: null, config: {
32 postGenerate: { command: 'sh', args: ['-c', 'printf "%s" "$CRAFT_GENERATE_PHASE" > hook-phase.txt'] },
33 } })
34 await build({ output, compile: false })
35 expect(readFileSync(join(output, 'hook-phase.txt'), 'utf8')).toBe('build')
36 })
37
38 it('reports invalid Android postGenerate commands and non-zero exits', () => {
39 const output = createTempDir('craft-android-post-generate-errors-')
40 expect(() => runPostGenerateHook({ command: '' }, output)).toThrow('postGenerate.command must be a non-empty executable')
41 expect(() => runPostGenerateHook({ command: join(output, 'missing-hook') }, output)).toThrow('postGenerate hook could not start')
42 expect(() => runPostGenerateHook({ command: 'sh', args: ['-c', 'printf "patch failed" >&2; exit 7'] }, output))
43 .toThrow('postGenerate hook failed')
44 })
45
46 it('generates static tab-bar selection appearance and validates its shape', async () => {
47 const output = createTempDir('craft-android-tab-bar-appearance-')
48 await init({ name: 'Tab Bar', output, runtimeDir: null, config: {
49 renderer: 'native',
50 tabBar: { selection: { cornerRadius: 8, color: '#123456' } },
51 } })
52 const config = JSON.parse(readFileSync(join(output, 'craft.config.json'), 'utf8'))
53 const shell = readFileSync(join(output, 'app/src/main/java/com/craft/tabbar/CraftNativeShell.kt'), 'utf8')
54 const activity = readFileSync(join(output, 'app/src/main/java/com/craft/tabbar/MainActivity.kt'), 'utf8')
55 expect(config.tabBar).toEqual({ selection: { cornerRadius: 8, color: '#123456' } })
56 expect(shell).toContain('data class CraftNativeTabBarAppearance')
57 expect(shell).toContain('private fun tabCellBackground(index: Int, dark: Boolean)')
58 expect(activity).toContain('routes.tabBarAppearance')
59 await expect(init({ name: 'Invalid Tab Bar', output: join(output, 'invalid'), runtimeDir: null, config: {
60 renderer: 'native', tabBar: { selection: { cornerRadius: -1 } },
61 } })).rejects.toThrow('cornerRadius must be a finite non-negative number')
62 await expect(init({ name: 'Invalid Tab Color', output: join(output, 'invalid-color'), runtimeDir: null, config: {
63 renderer: 'native', tabBar: { selection: { color: 'red' } },
64 } })).rejects.toThrow('color must be a #RRGGBB color')
65 })
66
67
1868 it('bundles custom fonts with their licenses and removes retired fonts', async () => {
1969 const root = createTempDir('craft-native-fonts-')
2070 const font = join(root, 'Signature.ttf')