@symbiote-native/vue 3.4.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();
@@ -134,7 +137,8 @@ function trimReflowedTextNodeTransform(node) {
134
137
  }
135
138
  }
136
139
 
137
- const SYMBIOTE_SOURCE_PREFIX = '@symbiote-native/';
140
+ // The scope of this package's own name, `@symbiote-native/`, so the prefix has one source
141
+ const SYMBIOTE_SOURCE_PREFIX = `${require('./package.json').name.split('/')[0]}/`;
138
142
 
139
143
  // Local names the file imports from any `@symbiote-native/*` package (`FlatList`, `Animated`)
140
144
  function symbioteImportsOf(descriptor) {
@@ -190,23 +194,27 @@ const SFC_COMPONENT_VAR = '__sfc__';
190
194
 
191
195
  // Each block restarts its rules at order 0, so a later one would tie an earlier one
192
196
  // Renumbering on append gives the file one monotonic sequence, as one stylesheet would
193
- function appendRules(target, rules) {
194
- for (const rule of rules) {
195
- 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));
196
213
  }
197
214
  }
198
215
 
199
- // `combinators` is compile-time-only, as the registry matches by token subset, so it is not shipped
200
- // Twin of css-parser's `serializeRules` (metro-css-module/index.ts) for a standalone style file
201
- function serializeRules(rules) {
202
- return JSON.stringify(
203
- rules.map(({ tokens, specificity, order, style }) => ({
204
- tokens,
205
- specificity,
206
- order,
207
- style,
208
- })),
209
- );
216
+ function withOffsetCond(item, offset) {
217
+ return item.cond === undefined ? item : { ...item, cond: item.cond + offset };
210
218
  }
211
219
 
212
220
  // A style block's `lang` names a preprocessor (`lang="scss"`), where a file has an extension
@@ -251,12 +259,19 @@ function parseDescriptor(src, filename) {
251
259
  `Vue SFC parse error in ${filename}:\n${errors.map(String).join('\n')}`,
252
260
  );
253
261
  }
254
- if (descriptor.scriptSetup == null && descriptor.script == null) {
255
- throw new Error(
256
- `Vue SFC ${filename} has no <script> / <script setup> block`,
257
- );
262
+ if (descriptor.scriptSetup != null || descriptor.script != null) {
263
+ return descriptor;
264
+ }
265
+ if (descriptor.template == null) {
266
+ throw new Error(`Vue SFC ${filename} has no <template> / <script> block`);
258
267
  }
259
- return descriptor;
268
+ // `compileScript` refuses a file without a script, yet a static template is a valid component
269
+ // A `<script setup>` appended after the source keeps every line number of the file
270
+ // It carries a comment since the parser drops a blank block, which would recurse forever
271
+ return parseDescriptor(
272
+ `${src}\n<script setup>/* static template */</script>\n`,
273
+ filename,
274
+ );
260
275
  }
261
276
 
262
277
  // `<style scoped>` and `<style module>` are one mechanism, lightningcss' CSS-Modules renaming
@@ -268,16 +283,15 @@ function compileSfcStyleBlock({ style, content, filename, scopeId }) {
268
283
  if (style.module) {
269
284
  const compiled = compileCssModule(content, filename);
270
285
  const bindingName = typeof style.module === 'string' ? style.module : '$style';
271
- return { rules: compiled.rules, binding: [bindingName, compiled.classMap] };
286
+ return { ...compiled, binding: [bindingName, compiled.classMap] };
272
287
  }
273
288
  if (style.scoped) {
274
- const compiled = compileScopedCss(content, {
289
+ return compileScopedCss(content, {
275
290
  filename,
276
291
  pattern: `[local]__${scopeId}`,
277
292
  });
278
- return { rules: compiled.rules, names: compiled.names };
279
293
  }
280
- return { rules: compileCssToRules(content, { filename }).rules };
294
+ return compileCssToRules(content, { filename });
281
295
  }
282
296
 
283
297
  // Blocks cascade last-wins like CSS, carried by the rules' `order` that `appendRules` renumbers
@@ -287,9 +301,14 @@ async function compileStyles(descriptor, filename, scopeId) {
287
301
  const contents = await Promise.all(
288
302
  descriptor.styles.map(style => compileStyleBlockContent(style, filename)),
289
303
  );
290
- const rules = [];
291
- const scopedClassNames = new Map();
292
- const cssModuleBindings = new Map();
304
+ const sink = {
305
+ rules: [],
306
+ conditions: [],
307
+ rootVariables: [],
308
+ properties: [],
309
+ scopedClassNames: new Map(),
310
+ cssModuleBindings: new Map(),
311
+ };
293
312
  descriptor.styles.forEach((style, index) => {
294
313
  const block = compileSfcStyleBlock({
295
314
  style,
@@ -297,18 +316,18 @@ async function compileStyles(descriptor, filename, scopeId) {
297
316
  filename,
298
317
  scopeId,
299
318
  });
300
- appendRules(rules, block.rules);
319
+ appendRules(sink, block);
301
320
  for (const [name, renamed] of block.names ?? [])
302
- scopedClassNames.set(name, renamed);
321
+ sink.scopedClassNames.set(name, renamed);
303
322
  if (block.binding) {
304
323
  const [bindingName, classMap] = block.binding;
305
- cssModuleBindings.set(bindingName, {
306
- ...cssModuleBindings.get(bindingName),
324
+ sink.cssModuleBindings.set(bindingName, {
325
+ ...sink.cssModuleBindings.get(bindingName),
307
326
  ...classMap,
308
327
  });
309
328
  }
310
329
  });
311
- return { rules, scopedClassNames, cssModuleBindings };
330
+ return sink;
312
331
  }
313
332
 
314
333
  // Scoped-class rewriting is skipped when nothing in the file is scoped, so it adds no runtime cost
@@ -342,20 +361,37 @@ function compileComponentScript(descriptor, scopeId, styles) {
342
361
  );
343
362
  }
344
363
 
345
- function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBindings }) {
346
- const hasCssModules = cssModuleBindings.size > 0;
347
- if (rules.length === 0 && !hasCssModules) return code;
348
-
349
- // 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;
350
375
  const isScoped = scopedClassNames.size > 0;
351
- const engineImports = isScoped
352
- ? 'registerRules, renameClassTokens as __scopeClass'
353
- : 'registerRules';
354
-
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);
355
385
  const preamble = [
356
386
  `import { ${engineImports} } from '@symbiote-native/engine';`,
357
- `registerRules(${serializeRules(rules)});`,
387
+ `registerRules(${serializeRules(rules)}${table});`,
358
388
  ];
389
+ if (hasRoot) {
390
+ preamble.push(
391
+ `registerRootVariables(${JSON.stringify(rootVariables)}${table});`,
392
+ );
393
+ }
394
+ if (hasProperties) preamble.push(propertiesCall(properties, conditions));
359
395
  if (isScoped) {
360
396
  preamble.push(
361
397
  `const __scopedClassNames = ${JSON.stringify(Object.fromEntries(scopedClassNames))};`,
@@ -366,8 +402,19 @@ function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBinding
366
402
  for (const [bindingName, classMap] of cssModuleBindings) {
367
403
  preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
368
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;
369
416
 
370
- const parts = [preamble.join('\n'), code];
417
+ const parts = [buildPreamble(styles), code];
371
418
  if (hasCssModules) {
372
419
  const bindings = [...cssModuleBindings.keys()]
373
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.4.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",