@symbiote-native/vue 3.5.0 → 3.6.0

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.
@@ -0,0 +1,4 @@
1
+ import { type InjectionKey } from '@vue/runtime-core';
2
+ import type { IRootTag } from '@symbiote-native/engine';
3
+ export declare const ROOT_TAG_KEY: InjectionKey<IRootTag>;
4
+ export declare function useRootTag(): IRootTag;
@@ -0,0 +1,11 @@
1
+ // The tag of the surface a component is rendered into, the Vue twin of RN's `RootTagContext`:
2
+ // `mount` provides it on the app, a composable reads it back in `setup()`
3
+ import { inject } from '@vue/runtime-core';
4
+ export const ROOT_TAG_KEY = Symbol('symbiote-root-tag');
5
+ export function useRootTag() {
6
+ const rootTag = inject(ROOT_TAG_KEY, undefined);
7
+ if (rootTag === undefined) {
8
+ throw new Error('useRootTag() needs a component mounted by mount()');
9
+ }
10
+ return rootTag;
11
+ }
package/build/index.d.ts CHANGED
@@ -60,12 +60,13 @@ export type { IComponentProvider, IAppConfig, IAppParameters, IRunnable, IHostRe
60
60
  export { useColorScheme } from './composables/use-color-scheme';
61
61
  export { useAnimatedColor, useAnimatedValue, useAnimatedValueXY, } from './composables/use-animated-value';
62
62
  export { useWindowDimensions } from './composables/use-window-dimensions';
63
+ export { useRootTag } from './composables/use-root-tag';
63
64
  export { createPermissionHook } from './composables/create-permission-hook';
64
65
  export { createResourceHook } from './composables/create-resource-hook';
65
66
  export { defineNativeViewComponent } from './composables/define-native-view-component';
66
67
  export { createEventValueHook } from './composables/create-event-value-hook';
67
68
  export { useEvent, useEventListener } from './composables/use-event';
68
- export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, TextInputState, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
69
+ export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, matchMedia, MediaQueryList, MediaQueryListEvent, setVariable, getComputedVariable, registerProperty, AppState, Keyboard, KEYBOARD_EVENT, TextInputState, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
69
70
  export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, IShareContent, IShareOptions, IShareAction, IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, IUrlEvent, II18nManagerConstants, IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, IColorSchemeName, IColorSchemePreference, IAppStateStatus, IAppStateEvent, IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, IBackPressEventName, IBackPressHandler, IPermission, IPermissionStatus, IRationale, IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, } from '@symbiote-native/engine';
70
71
  export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, } from '@symbiote-native/engine';
71
72
  export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
package/build/index.js CHANGED
@@ -54,13 +54,14 @@ export { AppRegistry, HeadlessJsTaskError, setHostRegistrar, } from './modules/a
54
54
  export { useColorScheme } from './composables/use-color-scheme.js';
55
55
  export { useAnimatedColor, useAnimatedValue, useAnimatedValueXY, } from './composables/use-animated-value.js';
56
56
  export { useWindowDimensions } from './composables/use-window-dimensions.js';
57
+ export { useRootTag } from './composables/use-root-tag.js';
57
58
  export { createPermissionHook } from './composables/create-permission-hook.js';
58
59
  export { createResourceHook } from './composables/create-resource-hook.js';
59
60
  export { defineNativeViewComponent } from './composables/define-native-view-component.js';
60
61
  export { createEventValueHook } from './composables/create-event-value-hook.js';
61
62
  export { useEvent, useEventListener } from './composables/use-event.js';
62
63
  // Imperative runtime modules: the SAME module both adapters share, re-exported from @symbiote-native/engine.
63
- export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, TextInputState, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
64
+ export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, matchMedia, MediaQueryList, MediaQueryListEvent, setVariable, getComputedVariable, registerProperty, AppState, Keyboard, KEYBOARD_EVENT, TextInputState, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
64
65
  // Re-export the framework-agnostic engine surface (pure utilities + diagnostics).
65
66
  export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, } from '@symbiote-native/engine';
66
67
  // Wired once by the app entry on a real host (like setColorProcessor): hands the engine
package/build/render.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // nativeFabricUIManager; RN's own renderer never in the path.
4
4
  import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
5
  import { createSymbioteRenderer } from './renderer/index.js';
6
+ import { ROOT_TAG_KEY } from './composables/use-root-tag.js';
6
7
  // One Vue app per surface, so a surface can be torn down (unmount) or cleanly
7
8
  // re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on
8
9
  // Fast Refresh and on lifecycle/focus changes, reusing the rootTag.
@@ -34,25 +35,9 @@ function componentStack(instance) {
34
35
  }
35
36
  return frames.length === 0 ? null : frames.join('');
36
37
  }
37
- // The app-level error seam. Without it, an unclaimed error fell through to Vue's own `logError`
38
- // (runtime-core errorHandling.ts), which does neither useful thing: in a dev bundle it RE-THROWS
39
- // out of `app.mount()`, so it escapes this function, aborts the AppRegistry runnable and leaves
40
- // the surface half-brought-up; in a release bundle it console.errors a bare error with no origin
41
- // and no component context, never touching `global.ErrorUtils`, so nothing reaches the host.
42
- // Setting a handler suppresses both branches and routes to the engine's shared channel instead —
43
- // the native redbox on a device, console.error off one.
44
- //
45
- // CAUGHT vs UNCAUGHT — the asymmetry is Vue's own, and it costs us no code. `handleError` walks
46
- // the `onErrorCaptured` chain BEFORE consulting `config.errorHandler`, and a hook returning
47
- // `false` stops propagation and returns early (runtime-core 3.5 errorHandling.ts), so a boundary
48
- // that claims the error never reaches this function at all. That is the behaviour we want and
49
- // the same split the React adapter makes by hand: writing a boundary IS the developer saying
50
- // "I am handling this", and answering it with a full-screen redbox over the fallback the app
51
- // just rendered contradicts what the app asked for.
52
- //
53
- // So do NOT "fix" this to report from inside an `onErrorCaptured` of our own — that would
54
- // re-report exactly the errors a boundary claimed. A hook that returns undefined instead of
55
- // `false` still lands here, and correctly: Vue treats that error as still propagating.
38
+ // Without it Vue's `logError` re-throws from `app.mount()` in dev and hides the error in release
39
+ // A boundary that claims an error never gets here: `onErrorCaptured` runs before `errorHandler`
40
+ // Do not report from `onErrorCaptured`, it would re-report the errors a boundary claimed
56
41
  function reportToHost(error, instance, info) {
57
42
  // `info` is Vue's own phase label ('render function', 'setup function', 'native event
58
43
  // handler'), which is the closest thing it has to React's "no error boundary" / "recovered"
@@ -82,6 +67,7 @@ export function mount(rootTag, rootComponent) {
82
67
  const renderer = createSymbioteRenderer(surface);
83
68
  const app = renderer.createApp(rootComponent);
84
69
  app.config.errorHandler = reportToHost;
70
+ app.provide(ROOT_TAG_KEY, rootTag);
85
71
  configurator?.(app);
86
72
  apps.set(rootTag, app);
87
73
  // The surface IS the Vue container: a top-level mutation routes to surface.appendChild
@@ -30,10 +30,13 @@ const {
30
30
  compileCssModule,
31
31
  compileCssToRules,
32
32
  compileScopedCss,
33
+ conditionsArgument,
33
34
  createCssMetroTransformer,
34
35
  hashFilePath,
36
+ propertiesCall,
35
37
  relabelForUpstream,
36
38
  resolveUpstreamTransformer,
39
+ serializeRules,
37
40
  } = require('@symbiote-native/css-parser');
38
41
 
39
42
  const upstreamTransformer = resolveUpstreamTransformer();
@@ -191,23 +194,27 @@ const SFC_COMPONENT_VAR = '__sfc__';
191
194
 
192
195
  // Each block restarts its rules at order 0, so a later one would tie an earlier one
193
196
  // Renumbering on append gives the file one monotonic sequence, as one stylesheet would
194
- function appendRules(target, rules) {
195
- for (const rule of rules) {
196
- target.push({ ...rule, order: target.length });
197
+ // Conditions join the file's table, so a block's condition indexes shift by the ones already there
198
+ function appendRules(sink, compiled) {
199
+ const offset = sink.conditions.length;
200
+ sink.conditions.push(...compiled.conditions);
201
+ for (const rule of compiled.rules) {
202
+ sink.rules.push({
203
+ ...withOffsetCond(rule, offset),
204
+ order: sink.rules.length,
205
+ });
206
+ }
207
+ // A `:root` block cascades in append order, the way one stylesheet would
208
+ for (const variable of compiled.rootVariables) {
209
+ sink.rootVariables.push(withOffsetCond(variable, offset));
210
+ }
211
+ for (const property of compiled.properties) {
212
+ sink.properties.push(withOffsetCond(property, offset));
197
213
  }
198
214
  }
199
215
 
200
- // `combinators` is compile-time-only, as the registry matches by token subset, so it is not shipped
201
- // Twin of css-parser's `serializeRules` (metro-css-module/index.ts) for a standalone style file
202
- function serializeRules(rules) {
203
- return JSON.stringify(
204
- rules.map(({ tokens, specificity, order, style }) => ({
205
- tokens,
206
- specificity,
207
- order,
208
- style,
209
- })),
210
- );
216
+ function withOffsetCond(item, offset) {
217
+ return item.cond === undefined ? item : { ...item, cond: item.cond + offset };
211
218
  }
212
219
 
213
220
  // A style block's `lang` names a preprocessor (`lang="scss"`), where a file has an extension
@@ -276,7 +283,7 @@ function compileSfcStyleBlock({ style, content, filename, scopeId }) {
276
283
  if (style.module) {
277
284
  const compiled = compileCssModule(content, filename);
278
285
  const bindingName = typeof style.module === 'string' ? style.module : '$style';
279
- return { rules: compiled.rules, binding: [bindingName, compiled.classMap] };
286
+ return { ...compiled, binding: [bindingName, compiled.classMap] };
280
287
  }
281
288
  if (style.scoped) {
282
289
  return compileScopedCss(content, {
@@ -284,7 +291,7 @@ function compileSfcStyleBlock({ style, content, filename, scopeId }) {
284
291
  pattern: `[local]__${scopeId}`,
285
292
  });
286
293
  }
287
- return { rules: compileCssToRules(content, { filename }).rules };
294
+ return compileCssToRules(content, { filename });
288
295
  }
289
296
 
290
297
  // Blocks cascade last-wins like CSS, carried by the rules' `order` that `appendRules` renumbers
@@ -294,9 +301,14 @@ async function compileStyles(descriptor, filename, scopeId) {
294
301
  const contents = await Promise.all(
295
302
  descriptor.styles.map(style => compileStyleBlockContent(style, filename)),
296
303
  );
297
- const rules = [];
298
- const scopedClassNames = new Map();
299
- const cssModuleBindings = new Map();
304
+ const sink = {
305
+ rules: [],
306
+ conditions: [],
307
+ rootVariables: [],
308
+ properties: [],
309
+ scopedClassNames: new Map(),
310
+ cssModuleBindings: new Map(),
311
+ };
300
312
  descriptor.styles.forEach((style, index) => {
301
313
  const block = compileSfcStyleBlock({
302
314
  style,
@@ -304,18 +316,18 @@ async function compileStyles(descriptor, filename, scopeId) {
304
316
  filename,
305
317
  scopeId,
306
318
  });
307
- appendRules(rules, block.rules);
319
+ appendRules(sink, block);
308
320
  for (const [name, renamed] of block.names ?? [])
309
- scopedClassNames.set(name, renamed);
321
+ sink.scopedClassNames.set(name, renamed);
310
322
  if (block.binding) {
311
323
  const [bindingName, classMap] = block.binding;
312
- cssModuleBindings.set(bindingName, {
313
- ...cssModuleBindings.get(bindingName),
324
+ sink.cssModuleBindings.set(bindingName, {
325
+ ...sink.cssModuleBindings.get(bindingName),
314
326
  ...classMap,
315
327
  });
316
328
  }
317
329
  });
318
- return { rules, scopedClassNames, cssModuleBindings };
330
+ return sink;
319
331
  }
320
332
 
321
333
  // Scoped-class rewriting is skipped when nothing in the file is scoped, so it adds no runtime cost
@@ -349,20 +361,37 @@ function compileComponentScript(descriptor, scopeId, styles) {
349
361
  );
350
362
  }
351
363
 
352
- function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBindings }) {
353
- const hasCssModules = cssModuleBindings.size > 0;
354
- if (rules.length === 0 && !hasCssModules) return code;
355
-
356
- // Only a scoped file needs the runtime rename helper and its per-file map
364
+ // The module text ahead of the component: the registration, the scope map and the module maps
365
+ // Only a scoped file imports the rename helper, so other files leave it out
366
+ function buildPreamble(styles) {
367
+ const {
368
+ rules,
369
+ conditions,
370
+ rootVariables,
371
+ properties,
372
+ scopedClassNames,
373
+ cssModuleBindings,
374
+ } = styles;
357
375
  const isScoped = scopedClassNames.size > 0;
358
- const engineImports = isScoped
359
- ? 'registerRules, renameClassTokens as __scopeClass'
360
- : 'registerRules';
361
-
376
+ const hasRoot = rootVariables.length > 0;
377
+ const hasProperties = properties.length > 0;
378
+ const engineImports = [
379
+ 'registerRules',
380
+ ...(hasRoot ? ['registerRootVariables'] : []),
381
+ ...(hasProperties ? ['registerProperties'] : []),
382
+ ...(isScoped ? ['renameClassTokens as __scopeClass'] : []),
383
+ ].join(', ');
384
+ const table = conditionsArgument(conditions);
362
385
  const preamble = [
363
386
  `import { ${engineImports} } from '@symbiote-native/engine';`,
364
- `registerRules(${serializeRules(rules)});`,
387
+ `registerRules(${serializeRules(rules)}${table});`,
365
388
  ];
389
+ if (hasRoot) {
390
+ preamble.push(
391
+ `registerRootVariables(${JSON.stringify(rootVariables)}${table});`,
392
+ );
393
+ }
394
+ if (hasProperties) preamble.push(propertiesCall(properties, conditions));
366
395
  if (isScoped) {
367
396
  preamble.push(
368
397
  `const __scopedClassNames = ${JSON.stringify(Object.fromEntries(scopedClassNames))};`,
@@ -373,8 +402,19 @@ function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBinding
373
402
  for (const [bindingName, classMap] of cssModuleBindings) {
374
403
  preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
375
404
  }
405
+ return preamble.join('\n');
406
+ }
407
+
408
+ function withStyleRegistration(code, styles) {
409
+ const { cssModuleBindings } = styles;
410
+ const hasCssModules = cssModuleBindings.size > 0;
411
+ const hasRegistrations =
412
+ styles.rules.length > 0 ||
413
+ styles.rootVariables.length > 0 ||
414
+ styles.properties.length > 0;
415
+ if (!hasRegistrations && !hasCssModules) return code;
376
416
 
377
- const parts = [preamble.join('\n'), code];
417
+ const parts = [buildPreamble(styles), code];
378
418
  if (hasCssModules) {
379
419
  const bindings = [...cssModuleBindings.keys()]
380
420
  .map(name => `${JSON.stringify(name)}: ${name}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/vue",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "SymbioteNative's Vue adapter — a @vue/runtime-core createRenderer driving real native iOS/Android views through the same engine as the React adapter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -76,12 +76,12 @@
76
76
  "peerDependencies": {
77
77
  "vue": ">=3.5.0",
78
78
  "react-native": ">=0.86",
79
- "@symbiote-native/engine": "^1.7.0"
79
+ "@symbiote-native/engine": "^1.8.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@babel/core": "^7.25.2",
83
- "@symbiote-native/engine": "1.7.0",
84
- "@symbiote-native/test-utils": "0.5.0"
83
+ "@symbiote-native/engine": "1.8.0",
84
+ "@symbiote-native/test-utils": "0.5.1"
85
85
  },
86
86
  "scripts": {
87
87
  "typecheck": "tsc --build",