yaver-feedback-react-native 0.9.9 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.plugin.js CHANGED
@@ -227,7 +227,9 @@ function withYaverAppDelegateHook(config) {
227
227
  // Existing consumers may already have the hot-reload hook from an older
228
228
  // SDK. Still apply newer, independently-versioned native contracts.
229
229
  if (contents.includes("YaverHotReload")) {
230
- config.modResults.contents = patchDogfoodAppShortcut(contents);
230
+ config.modResults.contents = patchDogfoodAppShortcut(
231
+ patchHotReloadBootConfirmation(contents)
232
+ );
231
233
  return config;
232
234
  }
233
235
 
@@ -273,20 +275,14 @@ function withYaverAppDelegateHook(config) {
273
275
  name: Notification.Name("YaverHotReloadBundle"),
274
276
  object: nil
275
277
  )
276
- // Crash-revert safety net: clear the boot-attempt counter once
277
- // RN renders its first frame, OR after 10 s of uptime — whichever
278
- // fires first. If neither fires (bundle crashes before render),
279
- // YaverHotReload.bundleURL() will eventually revert to the
280
- // TestFlight-installed bundle after 3 failed boots. See
281
- // YaverHotReload.swift for the full state machine.
278
+ // Crash-revert safety net: only a real first React frame confirms boot.
279
+ // Uptime is not success: JS can fail while the native process remains
280
+ // alive on its launch screen indefinitely.
282
281
  NotificationCenter.default.addObserver(
283
282
  forName: NSNotification.Name(rawValue: "RCTContentDidAppearNotification"),
284
283
  object: nil,
285
284
  queue: .main
286
285
  ) { _ in YaverHotReload.markBootSuccessful() }
287
- DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
288
- YaverHotReload.markBootSuccessful()
289
- }
290
286
  }
291
287
 
292
288
  @objc private func yaverHandleHotReload(_ notification: Notification) {
@@ -374,6 +370,23 @@ function withYaverAppDelegateHook(config) {
374
370
  });
375
371
  }
376
372
 
373
+ /** Upgrade an already-generated AppDelegate from the pre-0.9.10 boot guard.
374
+ * Older plugins treated ten seconds of process uptime as a successful React
375
+ * boot. A JS exception can leave that process alive on the native splash, so
376
+ * the timer reset the crash counter forever and prevented bundle rollback. */
377
+ function patchHotReloadBootConfirmation(contents) {
378
+ let patched = contents;
379
+ patched = patched.replace(
380
+ / \/\/ Crash-revert safety net: clear the boot-attempt counter once\n \/\/ RN renders its first frame, OR after 10 s of uptime — whichever\n \/\/ fires first\. If neither fires \(bundle crashes before render\),\n \/\/ YaverHotReload\.bundleURL\(\) will eventually revert to the\n \/\/ TestFlight-installed bundle after 3 failed boots\. See\n \/\/ YaverHotReload\.swift for the full state machine\./,
381
+ ` // Crash-revert safety net: only a real first React frame confirms boot.\n // Uptime is not success: JS can fail while the native process remains\n // alive on its launch screen indefinitely.`
382
+ );
383
+ patched = patched.replace(
384
+ / DispatchQueue\.main\.asyncAfter\(deadline: \.now\(\) \+ 10\) \{\n YaverHotReload\.markBootSuccessful\(\)\n \}\n?/g,
385
+ ""
386
+ );
387
+ return patched;
388
+ }
389
+
377
390
  /** Wire the dynamic iOS Home Screen shortcut into the SDK native module.
378
391
  * The shortcut itself is created at runtime only after backend ACL success;
379
392
  * this hook merely consumes a user-selected action on warm/cold launch. */
@@ -820,6 +833,7 @@ const yaverFeedbackPlugin = createRunOncePlugin(
820
833
  // config-plugin seam catches template drift before a consumer discovers it in
821
834
  // an archive build.
822
835
  yaverFeedbackPlugin.__test = {
836
+ patchHotReloadBootConfirmation,
823
837
  patchDogfoodAppShortcut,
824
838
  patchDogfoodSceneDelegate,
825
839
  findAppDelegateClassClose,
@@ -17,6 +17,13 @@ export interface DogfoodProject {
17
17
  workDir: string;
18
18
  framework: string;
19
19
  lane: DogfoodLane;
20
+ /**
21
+ * Optional automatic recovery lane. The shared onboarding flow uses the
22
+ * browser lane here when a user prefers Hermes or WebRTC for a project that
23
+ * can also render in a browser. The failed preferred attempt remains in the
24
+ * live console; the fallback is never silent.
25
+ */
26
+ fallbackLane?: DogfoodLane;
20
27
  /** Optional source URL for drivers that can clone missing source. */
21
28
  repositoryUrl?: string;
22
29
  /** Optional native target from /remote-runtime/capabilities for WebRTC. */
@@ -29,12 +36,36 @@ export interface DogfoodLaneOption {
29
36
  default: boolean;
30
37
  reason?: string;
31
38
  }
39
+ export interface DogfoodLanePlan {
40
+ preferred: DogfoodLane;
41
+ /** Browser is the cheap, portable recovery lane for RN/Expo/Flutter/web. */
42
+ fallback?: DogfoodLane;
43
+ options: DogfoodLaneOption[];
44
+ }
32
45
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
33
46
  export declare function dogfoodLaneOptions(framework: string, capabilities?: {
34
47
  nativeRuntimeAvailable?: boolean;
48
+ browserRuntimeAvailable?: boolean;
35
49
  selfDevelopment?: boolean;
36
50
  }): DogfoodLaneOption[];
37
- export declare function defaultDogfoodLane(framework: string): DogfoodLane;
51
+ export declare function defaultDogfoodLane(framework: string, capabilities?: {
52
+ nativeRuntimeAvailable?: boolean;
53
+ browserRuntimeAvailable?: boolean;
54
+ selfDevelopment?: boolean;
55
+ }): DogfoodLane;
56
+ /**
57
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
58
+ *
59
+ * Browser is the onboarding default for browser-capable React Native, Expo,
60
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
61
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
62
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
63
+ */
64
+ export declare function dogfoodLanePlan(framework: string, capabilities?: {
65
+ nativeRuntimeAvailable?: boolean;
66
+ browserRuntimeAvailable?: boolean;
67
+ selfDevelopment?: boolean;
68
+ }, preferred?: DogfoodLane | null): DogfoodLanePlan;
38
69
  export interface DogfoodLogLine {
39
70
  text: string;
40
71
  at: number;
@@ -15,13 +15,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DogfoodController = exports.dogfoodLogLinesFromDevEvent = exports.DogfoodRuntimeError = void 0;
16
16
  exports.dogfoodLaneOptions = dogfoodLaneOptions;
17
17
  exports.defaultDogfoodLane = defaultDogfoodLane;
18
+ exports.dogfoodLanePlan = dogfoodLanePlan;
18
19
  exports.validateDogfoodProject = validateDogfoodProject;
19
20
  exports.runtimeLogLinesFromDevEvent = runtimeLogLinesFromDevEvent;
20
21
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
21
22
  function dogfoodLaneOptions(framework, capabilities = {}) {
22
23
  const normalized = String(framework || '').trim().toLowerCase();
23
24
  const reactNative = normalized === 'expo' || normalized === 'react-native';
24
- const browserCapable = reactNative || [
25
+ // The framework matrix is the safe default. A positively detected browser
26
+ // target may add an exception (for example SwiftWasm/Tokamak), but a missing
27
+ // browser binary must not incorrectly remove RN/Flutter's browser build lane.
28
+ const browserCapable = capabilities.browserRuntimeAvailable === true || reactNative || [
25
29
  'flutter', 'web', 'next', 'nextjs', 'vite', 'remix', 'svelte', 'vue', 'angular',
26
30
  ].includes(normalized);
27
31
  const nativeAvailable = capabilities.nativeRuntimeAvailable === true;
@@ -40,8 +44,31 @@ function dogfoodLaneOptions(framework, capabilities = {}) {
40
44
  },
41
45
  ];
42
46
  }
43
- function defaultDogfoodLane(framework) {
44
- return dogfoodLaneOptions(framework).find((option) => option.default && option.supported)?.lane || 'browser';
47
+ function defaultDogfoodLane(framework, capabilities = {}) {
48
+ const options = dogfoodLaneOptions(framework, capabilities);
49
+ return options.find((option) => option.default && option.supported)?.lane
50
+ || options.find((option) => option.supported)?.lane
51
+ || 'browser';
52
+ }
53
+ /**
54
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
55
+ *
56
+ * Browser is the onboarding default for browser-capable React Native, Expo,
57
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
58
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
59
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
60
+ */
61
+ function dogfoodLanePlan(framework, capabilities = {}, preferred) {
62
+ const options = dogfoodLaneOptions(framework, capabilities);
63
+ const supported = (lane) => !!lane && options.some((option) => option.lane === lane && option.supported);
64
+ const resolved = supported(preferred)
65
+ ? preferred
66
+ : defaultDogfoodLane(framework, capabilities);
67
+ return {
68
+ preferred: resolved,
69
+ fallback: resolved !== 'browser' && supported('browser') ? 'browser' : undefined,
70
+ options,
71
+ };
45
72
  }
46
73
  class DogfoodRuntimeError extends Error {
47
74
  constructor(failure) {
@@ -167,8 +194,8 @@ class DogfoodController {
167
194
  phase: 'preparing', attempt, project: this.project,
168
195
  message: `Preparing ${this.project.name}…`, logs: [], startedAt: Date.now(),
169
196
  });
170
- const context = {
171
- project: this.project,
197
+ const makeContext = (project) => ({
198
+ project,
172
199
  attempt,
173
200
  log: (line) => {
174
201
  if (generation !== this.generation)
@@ -195,16 +222,69 @@ class DogfoodController {
195
222
  this.cleanups.set(generation, owned);
196
223
  },
197
224
  isCurrent: () => generation === this.generation,
198
- };
225
+ });
226
+ let activeProject = this.project;
227
+ let context = makeContext(activeProject);
199
228
  try {
200
- await this.driver.prepare?.(context);
201
- if (!context.isCurrent())
202
- throw new DogfoodRuntimeError({
203
- code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
204
- remedy: 'Wait for the newer attempt.', retryable: true,
229
+ const start = async () => {
230
+ await this.driver.prepare?.(context);
231
+ if (!context.isCurrent())
232
+ throw new DogfoodRuntimeError({
233
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
234
+ remedy: 'Wait for the newer attempt.', retryable: true,
235
+ });
236
+ context.setPhase('starting', `Starting ${activeProject.name} on the ${activeProject.lane} lane…`);
237
+ return this.driver.start(context);
238
+ };
239
+ let result;
240
+ try {
241
+ result = await start();
242
+ }
243
+ catch (primaryError) {
244
+ const primaryFailure = failureFrom(primaryError);
245
+ const fallbackLane = activeProject.fallbackLane;
246
+ const mayFallback = !!fallbackLane
247
+ && fallbackLane !== activeProject.lane
248
+ && primaryFailure.retryable
249
+ && primaryFailure.code !== 'DOGFOOD_ATTEMPT_REPLACED';
250
+ if (!mayFallback)
251
+ throw primaryError;
252
+ await this.runCleanups('all', generation);
253
+ const fallbackProject = {
254
+ ...activeProject,
255
+ lane: fallbackLane,
256
+ fallbackLane: undefined,
257
+ };
258
+ const fallbackInvalid = validateDogfoodProject(fallbackProject);
259
+ if (fallbackInvalid)
260
+ throw primaryError;
261
+ const fallbackLine = {
262
+ text: `[fallback] ${activeProject.lane} failed (${primaryFailure.code}); trying ${fallbackLane}`,
263
+ at: Date.now(),
264
+ stream: 'system',
265
+ };
266
+ activeProject = fallbackProject;
267
+ this.replace({
268
+ ...this.state,
269
+ project: activeProject,
270
+ phase: 'preparing',
271
+ message: `Recovering ${activeProject.name} in the ${fallbackLane} lane…`,
272
+ logs: [...this.state.logs, fallbackLine].slice(-this.maxLogLines),
273
+ lastOutputAt: fallbackLine.at,
274
+ failure: undefined,
205
275
  });
206
- context.setPhase('starting', `Starting ${this.project.name} on the ${this.project.lane} lane…`);
207
- const result = await this.driver.start(context);
276
+ context = makeContext(activeProject);
277
+ result = await start();
278
+ result = {
279
+ ...result,
280
+ metadata: {
281
+ ...result.metadata,
282
+ preferredLane: this.project.lane,
283
+ fallbackFrom: this.project.lane,
284
+ fallbackReason: primaryFailure.code,
285
+ },
286
+ };
287
+ }
208
288
  if (!context.isCurrent()) {
209
289
  await this.runCleanups('all', generation);
210
290
  throw new DogfoodRuntimeError({
@@ -212,7 +292,7 @@ class DogfoodController {
212
292
  remedy: 'Wait for the newer attempt.', retryable: true,
213
293
  });
214
294
  }
215
- this.replace({ ...this.state, phase: 'ready', message: `${this.project.name} is ready`, result, failure: undefined });
295
+ this.replace({ ...this.state, project: activeProject, phase: 'ready', message: `${activeProject.name} is ready`, result, failure: undefined });
216
296
  return result;
217
297
  }
218
298
  catch (error) {
@@ -32,6 +32,7 @@ export declare const DogfoodStatusRail: React.FC<{
32
32
  export declare const DogfoodLanePicker: React.FC<{
33
33
  options: readonly DogfoodLaneOption[];
34
34
  selected: DogfoodLane;
35
+ fallbackLane?: DogfoodLane;
35
36
  onSelect: (lane: DogfoodLane) => void;
36
37
  colors?: Partial<DogfoodUiColors>;
37
38
  showUnsupportedReasons?: boolean;
@@ -40,6 +41,7 @@ export declare const DogfoodLanePicker: React.FC<{
40
41
  * Logs; Hermes/WebRTC use the same lifecycle and failure/remedy treatment. */
41
42
  export declare const DogfoodLiveConsole: React.FC<{
42
43
  lane: DogfoodLane;
44
+ sourceLabel?: string;
43
45
  phase: DogfoodPhase;
44
46
  message: string;
45
47
  logs: readonly DogfoodLogLine[];
@@ -54,7 +54,7 @@ const DogfoodStatusRail = ({ steps, colors: colorOverrides }) => {
54
54
  };
55
55
  exports.DogfoodStatusRail = DogfoodStatusRail;
56
56
  /** One lane selector and one default policy across Yaver, SFMG, and Talos. */
57
- const DogfoodLanePicker = ({ options, selected, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
57
+ const DogfoodLanePicker = ({ options, selected, fallbackLane, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
58
58
  const colors = resolvedColors(colorOverrides);
59
59
  return (<react_native_1.View accessibilityRole="radiogroup" accessibilityLabel="Dogfood runtime lane">
60
60
  <react_native_1.View style={styles.choiceRow}>
@@ -69,7 +69,8 @@ const DogfoodLanePicker = ({ options, selected, onSelect, colors: colorOverrides
69
69
  },
70
70
  ]}>
71
71
  <react_native_1.Text style={[styles.choiceText, { color: colors.text }, active && styles.choiceTextActive]}>
72
- {option.label}{option.default ? ' · default' : ''}
72
+ {option.label}
73
+ {active ? ' · preferred' : fallbackLane === option.lane ? ' · automatic fallback' : option.default ? ' · default' : ''}
73
74
  </react_native_1.Text>
74
75
  </react_native_1.Pressable>);
75
76
  })}
@@ -91,15 +92,17 @@ function runtimeTone(phase, colors) {
91
92
  }
92
93
  /** Shared second-stage live console. Browser lane deliberately names Browser
93
94
  * Logs; Hermes/WebRTC use the same lifecycle and failure/remedy treatment. */
94
- const DogfoodLiveConsole = ({ lane, phase, message, logs, failure, maxLines = 80, colors: colorOverrides, renderText }) => {
95
+ const DogfoodLiveConsole = ({ lane, sourceLabel, phase, message, logs, failure, maxLines = 80, colors: colorOverrides, renderText }) => {
95
96
  const colors = resolvedColors(colorOverrides);
96
97
  const text = logs.slice(-maxLines).map((line) => line.text).join('\n');
97
98
  const title = lane === 'browser' ? 'Browser Logs' : lane === 'hermes' ? 'Hermes Logs' : 'WebRTC Logs';
98
- return (<react_native_1.View style={[styles.console, { backgroundColor: colors.console, borderColor: colors.border }]} accessibilityLabel={title}>
99
+ const accessibleTitle = sourceLabel ? `${title} · ${sourceLabel}` : title;
100
+ return (<react_native_1.View style={[styles.console, { backgroundColor: colors.console, borderColor: colors.border }]} accessibilityLabel={accessibleTitle}>
99
101
  <react_native_1.View style={styles.consoleHeader}>
100
102
  <react_native_1.View style={[styles.statusDot, { backgroundColor: runtimeTone(phase, colors) }]}/>
101
103
  <react_native_1.Text style={[styles.consoleTitle, { color: colors.text }]}>{title}</react_native_1.Text>
102
104
  </react_native_1.View>
105
+ {sourceLabel ? <react_native_1.Text style={[styles.consoleSource, { color: colors.muted }]}>Source · {sourceLabel}</react_native_1.Text> : null}
103
106
  <react_native_1.Text style={[styles.consoleStatus, { color: colors.muted }]}>{message}</react_native_1.Text>
104
107
  {text ? (renderText ? renderText(text) : <react_native_1.Text selectable style={[styles.consoleText, { color: colors.text }]}>{text}</react_native_1.Text>) : (<react_native_1.Text style={[styles.consoleEmpty, { color: colors.muted }]}>Waiting for the first line from the remote PC…</react_native_1.Text>)}
105
108
  {failure ? (<react_native_1.View style={[styles.failure, { borderColor: colors.blocked }]}>
@@ -126,6 +129,7 @@ const styles = react_native_1.StyleSheet.create({
126
129
  console: { width: '100%', maxHeight: 320, overflow: 'hidden', marginTop: 10, borderWidth: 1, borderRadius: 10, padding: 11, gap: 7 },
127
130
  consoleHeader: { flexDirection: 'row', alignItems: 'center', gap: 7 },
128
131
  consoleTitle: { fontSize: 12, fontWeight: '800' },
132
+ consoleSource: { fontSize: 10, lineHeight: 14 },
129
133
  consoleStatus: { fontSize: 11, lineHeight: 16 },
130
134
  consoleText: { fontFamily: 'monospace', fontSize: 10, lineHeight: 15 },
131
135
  consoleEmpty: { fontSize: 10, fontStyle: 'italic' },