ReviewOS

also looking at this

craft-native/craft

feat(maps): add key-free native Android maps and shared map controls

#363
Merged chris wants to merge codex/wildloop-native-map-providers into main
28 files +1857 -481

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/scripts/compile-templates.tsmodified+8-0
Changes to packages/android/scripts/compile-templates.ts
@@ -27,6 +27,14 @@ const keepProjects = process.argv.includes('--keep')
2727const workspace = createTempDir('craft-android-templates-')
2828
2929const fixtures: CompileFixture[] = [
30 {
31 // Compile the key-free engine in both Debug and minified Release, with
32 // real MapLibre and location APIs resolved from the published SDK.
33 directoryName: 'native-maplibre',
34 name: 'CraftNativeMapLibre',
35 packageName: 'dev.craft.fixture.maplibre',
36 config: { renderer: 'native', mapProvider: 'maplibre', enableGeolocation: true },
37 },
3038 {
3139 directoryName: 'native',
3240 name: 'CraftNativeTemplate',
packages/android/src/index.tsmodified+1-0
Changes to packages/android/src/index.ts
@@ -1,4 +1,5 @@
11import { androidMapHost, type AndroidMapProvider } from './native-map'
2export type { AndroidMapProvider } from './native-map'
23/**
34 * Craft Android Builder
45 *
packages/android/src/native-map.test.tsadded+60-0
Changes to packages/android/src/native-map.test.ts
@@ -0,0 +1,60 @@
1import { afterAll, describe, expect, it } from 'bun:test'
2import { readFileSync } from 'node:fs'
3import { join } from 'node:path'
4import { init } from './index'
5import { createTempDir, removeAllTempDirs } from '../../typescript/src/temp-dir'
6
7afterAll(removeAllTempDirs)
8
9describe('native Android map provider generation', () => {
10 it('builds a MapLibre host without a Google map SDK dependency or key', async () => {
11 const output = createTempDir('craft-native-maplibre-')
12 await init({ name: 'Native maps', output, runtimeDir: null, config: { renderer: 'native', mapProvider: 'maplibre', enableGeolocation: true } })
13 const source = readFileSync(join(output, 'app/src/main/java/com/craft/native/MainActivity.kt'), 'utf8')
14 const gradle = readFileSync(join(output, 'app/build.gradle.kts'), 'utf8')
15 const manifest = readFileSync(join(output, 'app/src/main/AndroidManifest.xml'), 'utf8')
16 expect(source).toContain('MapLibre.getInstance(context)')
17 expect(source).toContain('MapView(context)')
18 expect(source).toContain('GeoJsonSource("craft-map-overlays")')
19 expect(source).toContain('iconRotationAlignment("map")')
20 expect(source).toContain('LocationComponentActivationOptions.builder(context, style)')
21 expect(source).not.toContain('com.google.android.gms.maps')
22 expect(source).not.toContain('GoogleMap')
23 expect(source).not.toContain('{{NATIVE_MAP_')
24 expect(gradle).toContain('org.maplibre.gl:android-sdk:13.7.1')
25 expect(gradle).not.toContain('play-services-maps')
26 expect(manifest).not.toContain('com.google.android.geo.API_KEY')
27 expect(JSON.parse(readFileSync(join(output, 'craft.config.json'), 'utf8')).mapProvider).toBe('maplibre')
28 // Native STX uses the Android JavaScript engine and real views, not the
29 // ts-maps DOM adapter mounted as a substitute for the native map.
30 expect(source).toContain('JavaScriptSandbox.isSupported()')
31 expect(source).not.toContain('import android.webkit.WebView')
32 })
33
34 it('keeps the existing Google host when explicitly selected', async () => {
35 const output = createTempDir('craft-native-google-map-')
36 await init({ name: 'Google maps', output, runtimeDir: null, config: { renderer: 'native', mapProvider: 'google', mapsApiKey: 'restricted-fixture-key' } })
37 const source = readFileSync(join(output, 'app/src/main/java/com/craft/native/MainActivity.kt'), 'utf8')
38 const gradle = readFileSync(join(output, 'app/build.gradle.kts'), 'utf8')
39 const manifest = readFileSync(join(output, 'app/src/main/AndroidManifest.xml'), 'utf8')
40 expect(source).toContain('com.google.android.gms.maps.GoogleMap')
41 expect(source).not.toContain('org.maplibre.android')
42 expect(gradle).toContain('com.google.android.gms:play-services-maps:18.2.0')
43 expect(gradle).not.toContain('org.maplibre.gl:android-sdk')
44 expect(manifest).toContain('android:name="com.google.android.geo.API_KEY" android:value="restricted-fixture-key"')
45 })
46
47 it('does not emit an unused Google map key for a MapLibre app', async () => {
48 const output = createTempDir('craft-maplibre-manifest-')
49 await init({ name: 'MapLibre', output, runtimeDir: null, config: { renderer: 'native', mapProvider: 'maplibre', mapsApiKey: 'unused-fixture-key' } })
50 const manifest = readFileSync(join(output, 'app/src/main/AndroidManifest.xml'), 'utf8')
51 expect(manifest).not.toContain('com.google.android.geo.API_KEY')
52 expect(manifest).not.toContain('unused-fixture-key')
53 })
54
55 it('rejects an unsupported native map provider before generating a project', async () => {
56 const output = createTempDir('craft-unknown-map-provider-')
57 // Exercise untyped JSON input, which still needs runtime validation.
58 await expect(init({ name: 'Unknown maps', output, runtimeDir: null, config: { renderer: 'native', mapProvider: 'unknown' as never } })).rejects.toThrow('Unknown Android native map provider')
59 })
60})