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

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/android/src/index.test.tsmodified+10-1
Changes to packages/android/src/index.test.ts
@@ -26,12 +26,21 @@ describe('Craft Android builder', () => {
2626 expect(readFileSync(join(output, 'hook.txt'), 'utf8')).toBe(`${realpathSync(output)}|android|build`)
2727 })
2828
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
2938 it('reports invalid Android postGenerate commands and non-zero exits', () => {
3039 const output = createTempDir('craft-android-post-generate-errors-')
3140 expect(() => runPostGenerateHook({ command: '' }, output)).toThrow('postGenerate.command must be a non-empty executable')
3241 expect(() => runPostGenerateHook({ command: join(output, 'missing-hook') }, output)).toThrow('postGenerate hook could not start')
3342 expect(() => runPostGenerateHook({ command: 'sh', args: ['-c', 'printf "patch failed" >&2; exit 7'] }, output))
34 .toThrow('postGenerate hook failed with exit code 7: patch failed')
43 .toThrow('postGenerate hook failed')
3544 })
3645
3746 it('generates static tab-bar selection appearance and validates its shape', async () => {
packages/android/src/index.tsmodified+2-2
Changes to packages/android/src/index.ts
@@ -403,11 +403,11 @@ export function runPostGenerateHook(
403403 })
404404 }
405405 catch (error) {
406 throw new Error(`Android postGenerate hook could not start: ${hook.command}`, { cause: error })
406 throw new Error(`Android postGenerate hook could not start in ${output}: ${hook.command}`, { cause: error })
407407 }
408408 if (result.exitCode !== 0) {
409409 const stderr = result.stderr?.toString().trim()
410 throw new Error(`Android postGenerate hook failed with exit code ${result.exitCode}${stderr ? `: ${stderr}` : ''}`)
410 throw new Error(`Android postGenerate hook failed in ${output} with exit code ${result.exitCode}${stderr ? `: ${stderr}` : ''}`)
411411 }
412412}
413413
packages/ios/src/index.test.tsmodified+23-2
Changes to packages/ios/src/index.test.ts
@@ -1,4 +1,4 @@
1import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
1import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
22import { join, resolve } from 'node:path'
33import { afterAll, describe, expect, it } from 'bun:test'
44import { addSchemeBuildTargets, addSchemeTestTargets, insertProjectTargetsBeforeSchemes } from '../scripts/insert-project-targets'
@@ -50,12 +50,33 @@ describe('Craft iOS builder', () => {
5050 expect(readFileSync(join(output, 'hook.txt'), 'utf8')).toBe(`${realpathSync(output)}|ios|build`)
5151 })
5252
53 it('runs the postGenerate hook after the iOS builder generates its Xcode project', async () => {
54 const root = createTempDir('craft-ios-post-generate-build-')
55 const output = join(root, 'app')
56 const xcodegen = join(root, 'xcodegen')
57 writeFileSync(xcodegen, '#!/bin/sh\nmkdir -p HookBuild.xcodeproj\n')
58 chmodSync(xcodegen, 0o755)
59 const previousPath = process.env.PATH
60 process.env.PATH = `${root}:${previousPath ?? ''}`
61 try {
62 await init({ name: 'Hook Build', output, runtimeDir: null, config: {
63 postGenerate: { command: 'sh', args: ['-c', 'test -d HookBuild.xcodeproj && printf "%s" "$CRAFT_GENERATE_PHASE" > hook-phase.txt'] },
64 } })
65 await build({ output, runtimeDir: null })
66 expect(readFileSync(join(output, 'hook-phase.txt'), 'utf8')).toBe('build')
67 }
68 finally {
69 if (previousPath === undefined) delete process.env.PATH
70 else process.env.PATH = previousPath
71 }
72 })
73
5374 it('reports invalid iOS postGenerate commands and non-zero exits', () => {
5475 const output = createTempDir('craft-ios-post-generate-errors-')
5576 expect(() => runPostGenerateHook({ command: '' }, output)).toThrow('postGenerate.command must be a non-empty executable')
5677 expect(() => runPostGenerateHook({ command: join(output, 'missing-hook') }, output)).toThrow('postGenerate hook could not start')
5778 expect(() => runPostGenerateHook({ command: 'sh', args: ['-c', 'printf "patch failed" >&2; exit 7'] }, output))
58 .toThrow('postGenerate hook failed with exit code 7: patch failed')
79 .toThrow('postGenerate hook failed')
5980 })
6081
6182 it('generates static tab-bar selection appearance and validates its shape', async () => {
packages/ios/src/index.tsmodified+2-2
Changes to packages/ios/src/index.ts
@@ -686,11 +686,11 @@ export function runPostGenerateHook(
686686 })
687687 }
688688 catch (error) {
689 throw new Error(`iOS postGenerate hook could not start: ${hook.command}`, { cause: error })
689 throw new Error(`iOS postGenerate hook could not start in ${output}: ${hook.command}`, { cause: error })
690690 }
691691 if (result.exitCode !== 0) {
692692 const stderr = result.stderr?.toString().trim()
693 throw new Error(`iOS postGenerate hook failed with exit code ${result.exitCode}${stderr ? `: ${stderr}` : ''}`)
693 throw new Error(`iOS postGenerate hook failed in ${output} with exit code ${result.exitCode}${stderr ? `: ${stderr}` : ''}`)
694694 }
695695}
696696