feat: add V3 field fixtures and RN regression host
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-11 09:56:53 +08:00
parent 3b01275983
commit 8588e6fcb8
61 changed files with 1939 additions and 39 deletions

View File

@@ -1,15 +1,15 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
import { Camera, Map } from "@maplibre/maplibre-react-native";
import { WeatherRuntimeBridge, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge";
import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset";
import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
import { fieldDatasets, materializeV3Frame, type V3Dataset } from "./src/fieldDatasets";
import { weatherTestStyle } from "./src/weatherStyle";
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" };
const frozenStops: ColorStop[] = [
{ speedMps: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { speedMps: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 },
{ speedMps: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { speedMps: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 },
const frozenStops: FieldColorStop[] = [
{ value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { value: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 },
{ value: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { value: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 },
];
export default function App() {
@@ -17,22 +17,23 @@ export default function App() {
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
const [result, setResult] = useState("Style is loading");
const [basemap, setBasemap] = useState("LOADING");
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING" });
const [dataset, setDataset] = useState<V3Dataset>(fieldDatasets[0]);
const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE");
const [weatherVisible, setWeatherVisible] = useState(true);
const [particleVisible, setParticleVisible] = useState(true);
const [visual, setVisual] = useState(initialVisual);
const refresh = useCallback(async (tag: number) => setSnapshot(await WeatherRuntimeBridge.getState(tag)), []);
const refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []);
const attach = useCallback(async (tag: number) => {
try { await WeatherRuntimeBridge.attach(tag); await refresh(tag); setReactTag(tag); setResult("WeatherRuntime attached; style ready"); }
try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime attached; style ready"); }
catch (error) { setResult(`style/attach error: ${String(error)}`); }
}, [refresh]);
const onStyleReady = useCallback((event: unknown) => {
const tag = findNodeHandle((event as { target?: unknown }).target as never);
if (tag != null) void attach(tag);
}, [attach]);
useEffect(() => () => { if (reactTag != null) void WeatherRuntimeBridge.detach(reactTag).catch(() => undefined); }, [reactTag]);
useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]);
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
@@ -40,46 +41,47 @@ export default function App() {
catch (error) { setResult(`${name}: ${String(error)}`); }
}, [reactTag, refresh]);
const loadRealWeather = useCallback(async () => {
const loadDataset = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY", "ACCEPTED_NEXT_SNAPSHOT", "ACCEPTED_NEXT_SNAPSHOT"];
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY"];
try {
setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" });
if (await WeatherRuntimeBridge.clearWindFields(reactTag) !== "ACCEPTED") throw new Error("clearWindFields was rejected");
const localFrames = await materializeRealWeatherFrames();
setTransport("LOADING: V2 metadata + validity/scalar/vector local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING" });
if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected");
await FieldRuntime.setFieldValueSource(reactTag, dataset.fieldValueSource); await FieldRuntime.setParticleVisible(reactTag, dataset.particle); await FieldRuntime.setParticleAdvectionScale(reactTag, dataset.advectionScale);
const localFrames = await Promise.all(dataset.frames.map(materializeV3Frame));
for (let index = 0; index < localFrames.length; index += 1) {
const frame = localFrames[index]; const submit = await WeatherRuntimeBridge.submitWindField(reactTag, frame.metadataJson, frame.payloadFileUri);
const frame = localFrames[index]; const submit = await FieldRuntime.submitField(reactTag, frame.metadataJson, frame.payloadRefs);
setFrames((current) => ({ ...current, [frame.label]: submit }));
if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`);
}
const native = await WeatherRuntimeBridge.getState(reactTag); setSnapshot(native);
const native = await FieldRuntime.getState(reactTag); setSnapshot(native);
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
setTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); setResult("T0-T3 accepted in strict producer timestamp order; Native runtime READY");
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); }
}, [reactTag]);
setTransport("READY: V2 FILE_REF / FLOAT32_LE / shared validity"); setResult(`${dataset.title}: T0/T1 accepted; FieldRuntime READY`);
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD ${dataset.id}: ${String(error)}`); }
}, [reactTag, dataset]);
const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []);
const applyVisual = useCallback(() => void command("particle visual", async (tag) => {
const results = await Promise.all([
WeatherRuntimeBridge.setParticleCount(tag, Number(visual.count)), WeatherRuntimeBridge.setParticleLineWidth(tag, Number(visual.width)), WeatherRuntimeBridge.setParticleLength(tag, Number(visual.length)), WeatherRuntimeBridge.setParticleOpacity(tag, Number(visual.opacity)), WeatherRuntimeBridge.setTrailOpacity(tag, Number(visual.trailOpacity)), WeatherRuntimeBridge.setTrailFade(tag, Number(visual.trailFade)), WeatherRuntimeBridge.setWeatherColorOpacity(tag, Number(visual.weatherOpacity)),
FieldRuntime.setParticleCount(tag, Number(visual.count)), FieldRuntime.setParticleLineWidth(tag, Number(visual.width)), FieldRuntime.setParticleLength(tag, Number(visual.length)), FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)), FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), FieldRuntime.setTrailFade(tag, Number(visual.trailFade)), FieldRuntime.setFieldOpacity(tag, Number(visual.weatherOpacity)),
]);
if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED";
}), [command, visual]);
const controls = useMemo(() => [
["PLAY", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "DOUBLE")],
["FPS AUTO", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_60" as TargetFps)], ["Weather stops", (tag: number) => WeatherRuntimeBridge.setWeatherColorStops(tag, frozenStops)], ["Particle color", (tag: number) => WeatherRuntimeBridge.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => WeatherRuntimeBridge.setTrailColor(tag, 0.35, 0.85, 1, 0.8)],
["PLAY", (tag: number) => FieldRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => FieldRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "DOUBLE")],
["FPS AUTO", (tag: number) => FieldRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Field stops", (tag: number) => FieldRuntime.setFieldColorStops(tag, frozenStops)], ["Particle color", (tag: number) => FieldRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => FieldRuntime.setTrailColor(tag, 0.35, 0.85, 1, 0.8)],
] as const, []);
return <SafeAreaView style={styles.root}>
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} onDidFinishLoadingMap={() => setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View>
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
<Text>Basemap: GSI {basemap}</Text><Text>Dataset: ECMWF HRES 20260807 · 513×769 · 3.01 MiB/frame</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>REAL DATA TRANSPORT = {transport}</Text>
{realWeatherFrames.map((frame) => <Text selectable key={frame.label}>{frame.label} {frame.cycle}: {frames[frame.label]}</Text>)}
<Text selectable>{result}</Text><Button title="LOAD REAL WEATHER T0-T3" onPress={() => void loadRealWeather()} />
<Text>Basemap: GSI {basemap}</Text><Text>Dataset: {dataset.title}</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>V2 FILE TRANSPORT = {transport}</Text>
{dataset.frames.map((frame) => <Text selectable key={frame.label}>{frame.label}: {frames[frame.label]}</Text>)}
<Text selectable>{result}</Text><View style={styles.row}>{fieldDatasets.map((item) => <Button key={item.id} title={item.id} onPress={() => setDataset(item)} />)}</View><Button title="LOAD SELECTED V3 DATASET" onPress={() => void loadDataset()} />
<View style={styles.row}>{controls.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}</View>
<View style={styles.row}><Button title={`Weather ${weatherVisible ? "visible" : "hidden"}`} onPress={() => { const next = !weatherVisible; setWeatherVisible(next); void command("weather visible", (tag) => WeatherRuntimeBridge.setWeatherColorVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => WeatherRuntimeBridge.setParticleVisible(tag, next)); }} /></View>
<View style={styles.row}>{realWeatherFrames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => WeatherRuntimeBridge.setForecastTime(tag, frame.validTime))} />)}</View>
<View style={styles.row}><Button title={`Weather ${weatherVisible ? "visible" : "hidden"}`} onPress={() => { const next = !weatherVisible; setWeatherVisible(next); void command("weather visible", (tag) => FieldRuntime.setFieldVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => FieldRuntime.setParticleVisible(tag, next)); }} /></View>
<View style={styles.row}>{dataset.frames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => FieldRuntime.setTargetTime(tag, frame.validTime))} />)}</View>
<Text>Visual: count / width / length / particle opacity / trail opacity / trail fade / WeatherColor opacity</Text><View style={styles.row}>{(Object.keys(visual) as Array<keyof typeof initialVisual>).map((key) => <TextInput key={key} value={visual[key]} onChangeText={(value) => updateVisual(key, value)} keyboardType="decimal-pad" style={styles.input} />)}<Button title="Apply visual" onPress={applyVisual} /></View>
</ScrollView>
</SafeAreaView>;

View File

@@ -8,8 +8,8 @@
"name": "maplibre-particle-rn-test-host",
"version": "0.1.0",
"dependencies": {
"@maplibre-particle/weather-runtime-bridge": "file:./modules/weather-runtime-bridge",
"@maplibre/maplibre-react-native": "11.3.6",
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
"expo": "56.0.4",
"expo-asset": "~56.0.14",
"expo-file-system": "56.0.11",
@@ -21,11 +21,22 @@
"typescript": "5.9.3"
}
},
"modules/weather-runtime-bridge": {
"name": "@maplibre-particle/weather-runtime-bridge",
"version": "0.1.0",
"../../../packages/maplibre-particle-rn": {
"name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.3",
"license": "BSD-2-Clause",
"devDependencies": {
"@maplibre/maplibre-react-native": "11.3.6"
},
"peerDependencies": {
"react-native": "0.85.2"
"@maplibre/maplibre-react-native": "11.3.6",
"react": ">=19.1.0",
"react-native": ">=0.80.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": {
@@ -1718,10 +1729,6 @@
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
"license": "BSD-2-Clause"
},
"node_modules/@maplibre-particle/weather-runtime-bridge": {
"resolved": "modules/weather-runtime-bridge",
"link": true
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "24.8.5",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.5.tgz",
@@ -1772,6 +1779,10 @@
}
}
},
"node_modules/@navsea/maplibre-particle-rn": {
"resolved": "../../../packages/maplibre-particle-rn",
"link": true
},
"node_modules/@react-native/assets-registry": {
"version": "0.85.2",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.85.2.tgz",

View File

@@ -9,7 +9,7 @@
"android": "expo run:android"
},
"dependencies": {
"@maplibre-particle/weather-runtime-bridge": "file:./modules/weather-runtime-bridge",
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
"@maplibre/maplibre-react-native": "11.3.6",
"expo": "56.0.4",
"expo-asset": "~56.0.14",

View File

@@ -0,0 +1,49 @@
import { Asset } from "expo-asset";
import type { FieldPayloadRefs } from "@navsea/maplibre-particle-rn";
export type V3Frame = Readonly<{ label: "T0" | "T1"; metadataJson: string; validTime: number; generation: number; validityModule: number; scalarModule?: number; vectorModule?: number }>;
export type V3Dataset = Readonly<{ id: string; title: string; fieldValueSource: "SCALAR" | "VECTOR_MAGNITUDE"; particle: boolean; advectionScale: number; frames: readonly V3Frame[] }>;
type Metadata = Readonly<{ validTime: number; generation: number }>;
// Static requires keep all binary planes outside JS memory and materialize cache-local file:// URIs only.
function frame(label: "T0" | "T1", metadata: Metadata, validityModule: number, scalarModule?: number, vectorModule?: number): V3Frame {
return { label, metadataJson: JSON.stringify(metadata), validTime: metadata.validTime, generation: metadata.generation, validityModule, scalarModule, vectorModule };
}
function two(id: string, title: string, fieldValueSource: V3Dataset["fieldValueSource"], particle: boolean, advectionScale: number, t0: Omit<V3Frame, "label">, t1: Omit<V3Frame, "label">): V3Dataset {
return { id, title, fieldValueSource, particle, advectionScale, frames: [{ label: "T0", ...t0 }, { label: "T1", ...t1 }] };
}
export const fieldDatasets: readonly V3Dataset[] = [
two("wind-real", "Wind(本地真实 canonical)", "VECTOR_MAGNITUDE", true, 1,
frame("T0", require("../../particle-v3-fields/wind-real/2026080700/metadata.json") as Metadata, require("../../particle-v3-fields/wind-real/2026080700/validity.u8"), undefined, require("../../particle-v3-fields/wind-real/2026080700/vector.f32")),
frame("T1", require("../../particle-v3-fields/wind-real/2026080706/metadata.json") as Metadata, require("../../particle-v3-fields/wind-real/2026080706/validity.u8"), undefined, require("../../particle-v3-fields/wind-real/2026080706/vector.f32"))),
two("rain-decoder-fixture", "Rain(decoder fallback)", "SCALAR", false, 1,
frame("T0", require("../../particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json") as Metadata, require("../../particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8"), require("../../particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32")),
frame("T1", require("../../particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json") as Metadata, require("../../particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8"), require("../../particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32"))),
two("wave-real", "Wave(本地真实 WAVEBIN1)", "SCALAR", true, 1.75,
frame("T0", require("../../particle-v3-fields/wave-real/2026090918/metadata.json") as Metadata, require("../../particle-v3-fields/wave-real/2026090918/validity.u8"), require("../../particle-v3-fields/wave-real/2026090918/scalar.f32"), require("../../particle-v3-fields/wave-real/2026090918/vector.f32")),
frame("T1", require("../../particle-v3-fields/wave-real/2026090919/metadata.json") as Metadata, require("../../particle-v3-fields/wave-real/2026090919/validity.u8"), require("../../particle-v3-fields/wave-real/2026090919/scalar.f32"), require("../../particle-v3-fields/wave-real/2026090919/vector.f32"))),
two("tide-real", "Tide/Current(本地真实 BIN)", "VECTOR_MAGNITUDE", true, 1,
frame("T0", require("../../particle-v3-fields/tide-real/2026090901/metadata.json") as Metadata, require("../../particle-v3-fields/tide-real/2026090901/validity.u8"), undefined, require("../../particle-v3-fields/tide-real/2026090901/vector.f32")),
frame("T1", require("../../particle-v3-fields/tide-real/2026090902/metadata.json") as Metadata, require("../../particle-v3-fields/tide-real/2026090902/validity.u8"), undefined, require("../../particle-v3-fields/tide-real/2026090902/vector.f32"))),
two("wave-synthetic-independence", "Wave synthetic independence", "SCALAR", true, 2.5,
frame("T0", require("../../particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json") as Metadata, require("../../particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8"), require("../../particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32"), require("../../particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32")),
frame("T1", require("../../particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json") as Metadata, require("../../particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8"), require("../../particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32"), require("../../particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32"))),
];
async function uri(module: number, label: string): Promise<string> {
const asset = Asset.fromModule(module);
await asset.downloadAsync();
if (!asset.localUri?.startsWith("file://")) throw new Error(label + " did not materialize a local file:// URI");
return asset.localUri;
}
export async function materializeV3Frame(frame: V3Frame): Promise<V3Frame & { payloadRefs: FieldPayloadRefs }> {
const [validityFileUri, scalarFileUri, vectorFileUri] = await Promise.all([
uri(frame.validityModule, frame.label + " validity"),
frame.scalarModule ? uri(frame.scalarModule, frame.label + " scalar") : Promise.resolve(undefined),
frame.vectorModule ? uri(frame.vectorModule, frame.label + " vector") : Promise.resolve(undefined),
]);
if (scalarFileUri) return { ...frame, payloadRefs: vectorFileUri ? { validityFileUri, scalarFileUri, vectorFileUri } : { validityFileUri, scalarFileUri } };
if (vectorFileUri) return { ...frame, payloadRefs: { validityFileUri, vectorFileUri } };
throw new Error(frame.label + " has neither scalar nor vector plane");
}

View File

@@ -1,5 +1,5 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": { "strict": true, "noEmit": true },
"compilerOptions": { "strict": true, "noEmit": true, "preserveSymlinks": true },
"include": ["App.tsx", "src", "modules/weather-runtime-bridge/src"]
}