@symbiote-native/vue 0.2.0 → 0.3.1

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.
Files changed (40) hide show
  1. package/build/bootstrap.js +1 -1
  2. package/build/components/activity-indicator/index.android.js +1 -1
  3. package/build/components/activity-indicator/index.ios.js +1 -1
  4. package/build/components/activity-indicator/index.js +1 -1
  5. package/build/components/activity-indicator/shared.js +2 -2
  6. package/build/components/button.js +3 -3
  7. package/build/components/flat-list/index.js +2 -2
  8. package/build/components/image-background.js +2 -2
  9. package/build/components/image.js +2 -2
  10. package/build/components/input-accessory-view.js +1 -1
  11. package/build/components/keyboard-avoiding-view.js +1 -1
  12. package/build/components/modal.js +1 -1
  13. package/build/components/pressable.js +2 -2
  14. package/build/components/refresh-control.js +1 -1
  15. package/build/components/safe-area-view.js +1 -1
  16. package/build/components/scroll-view/index.android.js +1 -1
  17. package/build/components/scroll-view/index.ios.js +1 -1
  18. package/build/components/scroll-view/index.js +1 -1
  19. package/build/components/scroll-view/shared.js +2 -2
  20. package/build/components/scroll-view/sticky-header.js +1 -1
  21. package/build/components/section-list/index.js +2 -2
  22. package/build/components/switch/index.android.js +1 -1
  23. package/build/components/switch/index.ios.js +1 -1
  24. package/build/components/switch/index.js +1 -1
  25. package/build/components/switch/shared.js +3 -3
  26. package/build/components/text-input/index.js +3 -3
  27. package/build/components/touchable-native-feedback.js +3 -3
  28. package/build/components/touchable.js +3 -3
  29. package/build/components/virtualized-list/index.js +4 -4
  30. package/build/components/virtualized-section-list/index.js +2 -2
  31. package/build/components.js +1 -1
  32. package/build/index.js +32 -32
  33. package/build/modules/animated/create-animated-component.js +1 -1
  34. package/build/modules/animated/index.js +5 -5
  35. package/build/modules/app-registry/index.js +1 -1
  36. package/build/modules/status-bar.js +1 -1
  37. package/build/render.js +1 -1
  38. package/metro-css-parser.cjs +9 -4
  39. package/metro-vue-transformer.cjs +313 -0
  40. package/package.json +10 -6
@@ -4,7 +4,7 @@
4
4
  // header for why anything importing react-native must stay there.
5
5
  import { AppRegistry as RNAppRegistry } from 'react-native';
6
6
  import { bootstrapHost } from '@symbiote-native/components/bootstrap';
7
- import { AppRegistry, setHostRegistrar } from './modules/app-registry';
7
+ import { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
8
8
  // Mirrors real Vue's createApp(App).mount(selector) two-step idiom.
9
9
  export function createApp(App, options = {}) {
10
10
  return {
@@ -2,7 +2,7 @@
2
2
  // ProgressBar's setStyle(); without it the view throws "setStyle() not called") and
3
3
  // `indeterminate: true`, and its default color is the theme (null). Metro picks this on an
4
4
  // Android host; no Platform.OS read. Mirrors the React adapter's Android binding. See ADR 0020.
5
- import { createActivityIndicator } from './shared';
5
+ import { createActivityIndicator } from './shared.js';
6
6
  export const ActivityIndicator = createActivityIndicator({
7
7
  // RN: `color = Platform.OS === 'ios' ? GRAY : null`. Android lets the theme color it.
8
8
  defaultColor: null,
@@ -1,7 +1,7 @@
1
1
  // ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum + a GRAY default
2
2
  // color and no extra native props. Also the base (activity-indicator.ts re-exports it) for
3
3
  // headless / web. Mirrors the React adapter's iOS binding. See ADR 0020.
4
- import { createActivityIndicator } from './shared';
4
+ import { createActivityIndicator } from './shared.js';
5
5
  // RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
6
6
  const IOS_DEFAULT_COLOR = '#999999';
7
7
  export const ActivityIndicator = createActivityIndicator({
@@ -1,4 +1,4 @@
1
1
  // Base / default ActivityIndicator. Re-exports the iOS build. Metro overrides this with
2
2
  // activity-indicator.ios.ts / .android.ts on a real host; under tsx / tsc / web the host
3
3
  // resolves here. Filename is the selector, no Platform.OS read. See ADR 0020.
4
- export * from './index.ios';
4
+ export * from './index.ios.js';
@@ -9,8 +9,8 @@
9
9
  // indeterminate) are supplied by the .ios/.android files via `IActivityIndicatorPlatform`.
10
10
  import { defineComponent } from '@vue/runtime-core';
11
11
  import { renderActivityIndicator } from '@symbiote-native/components';
12
- import { descriptorToVue } from '../../descriptor-to-vue';
13
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
12
+ import { descriptorToVue } from '../../descriptor-to-vue.js';
13
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
14
14
  function normalizeSize(size) {
15
15
  if (size === 'large')
16
16
  return 'large';
@@ -4,9 +4,9 @@
4
4
  // forwards the native-only props. The Vue twin of the React adapter's Button.
5
5
  import { defineComponent, h } from '@vue/runtime-core';
6
6
  import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonTextStyle, } from '@symbiote-native/components';
7
- import { Text } from '../components';
8
- import { TouchableOpacity } from './touchable';
9
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
7
+ import { Text } from '../components.js';
8
+ import { TouchableOpacity } from './touchable.js';
9
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
10
10
  // The props Button consumes / re-maps itself; everything else (accessibility, aria, TV-focus,
11
11
  // testID, nativeID) forwards to the TouchableOpacity. `title` becomes the Text child; onPress and
12
12
  // disabled re-map; touchSoundDisabled maps to the pressable's android_disableSound.
@@ -16,8 +16,8 @@
16
16
  import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
17
17
  import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, rowKeyExtractor, } from '@symbiote-native/components';
18
18
  import { dlog, resolveClassName, } from '@symbiote-native/engine';
19
- import { VirtualizedList } from '../virtualized-list';
20
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
19
+ import { VirtualizedList } from '../virtualized-list/index.js';
20
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
21
21
  // VirtualizedList is itself a generic component (generic construct signature), which h()'s overloads
22
22
  // can't resolve. Drive it through a loose functional-component handle (generic-component h()
23
23
  // limitation — the ItemT surface is proven at the typed FlatList boundary above).
@@ -10,8 +10,8 @@
10
10
  import { h } from '@vue/runtime-core';
11
11
  import { renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
12
12
  import { resolveClassName, } from '@symbiote-native/engine';
13
- import { descriptorToVue } from '../descriptor-to-vue';
14
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
13
+ import { descriptorToVue } from '../descriptor-to-vue.js';
14
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
15
15
  function asString(value) {
16
16
  return typeof value === 'string' ? value : undefined;
17
17
  }
@@ -13,8 +13,8 @@
13
13
  // the forward-only rest (events, blurRadius, capInsets, testID, accessibility*) is typed as the
14
14
  // a11y intersection so resolveAccessibilityProps folds aria-* into accessibility* over it.
15
15
  import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
16
- import { descriptorToVue } from '../descriptor-to-vue';
17
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
16
+ import { descriptorToVue } from '../descriptor-to-vue.js';
17
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
18
18
  export { setImageSourceResolver } from '@symbiote-native/components';
19
19
  function asString(value) {
20
20
  return typeof value === 'string' ? value : undefined;
@@ -9,7 +9,7 @@
9
9
  // so resolveAccessibilityProps lands aria-* onto the host node.
10
10
  import { h } from '@vue/runtime-core';
11
11
  import { renderInputAccessoryView, resolveAccessibilityProps, } from '@symbiote-native/components';
12
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
12
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
13
13
  function asString(value) {
14
14
  return typeof value === 'string' ? value : undefined;
15
15
  }
@@ -12,7 +12,7 @@
12
12
  import { defineComponent, h, ref, onMounted, onUnmounted } from '@vue/runtime-core';
13
13
  import { Keyboard, KEYBOARD_EVENT, dlog, } from '@symbiote-native/engine';
14
14
  import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
15
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
15
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
16
16
  // A style prop is an object (a style record) or an array of them; numbers/strings/null degrade
17
17
  // to undefined (the engine flattens what it gets). A runtime guard, not a cast.
18
18
  function isStyleProp(value) {
@@ -13,7 +13,7 @@
13
13
  import { defineComponent, h, ref, watch } from '@vue/runtime-core';
14
14
  import { createInitialModalState, modalReducer, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
15
15
  import { dlog, } from '@symbiote-native/engine';
16
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
16
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
17
17
  function asBoolean(value) {
18
18
  return typeof value === 'boolean' ? value : undefined;
19
19
  }
@@ -15,8 +15,8 @@
15
15
  import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
16
16
  import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, resolveAccessibilityProps, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
17
17
  import { measure, isSymbioteNode, } from '@symbiote-native/engine';
18
- import { View } from '../components';
19
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
18
+ import { View } from '../components.js';
19
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
20
20
  // The emits-options validator for the full press surface. Reused verbatim as the `emits` of every
21
21
  // Touchable* wrapper (they expose the same events), so the runtime list lives in exactly one place.
22
22
  export const PRESSABLE_EMITS = {
@@ -14,7 +14,7 @@
14
14
  import { defineComponent, h } from '@vue/runtime-core';
15
15
  import { dlog } from '@symbiote-native/engine';
16
16
  import { resolveAccessibilityProps, } from '@symbiote-native/components';
17
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
17
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
18
18
  // Copy every non-consumed attr into a bag typed as the a11y intersection (the accumulator is BUILT
19
19
  // at that type, a real narrowing, not a cast), then fold aria-*/role into the canonical
20
20
  // accessibility* props. Children cross via slots, never attrs. When the Android wrap re-invokes
@@ -10,7 +10,7 @@
10
10
  import { defineComponent, h } from '@vue/runtime-core';
11
11
  import { dlog } from '@symbiote-native/engine';
12
12
  import { resolveAccessibilityProps, } from '@symbiote-native/components';
13
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
13
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
14
14
  // Copy every attr into a bag typed as the a11y intersection (the accumulator is BUILT at that
15
15
  // type, a real narrowing, not a cast), then fold aria-*/role into the canonical accessibility*
16
16
  // props before they reach the host. style / onLayout / testID forward unchanged.
@@ -15,7 +15,7 @@
15
15
  import { h, isVNode } from '@vue/runtime-core';
16
16
  import { dlog } from '@symbiote-native/engine';
17
17
  import { splitLayoutProps } from '@symbiote-native/components';
18
- import { createScrollView } from './shared';
18
+ import { createScrollView } from './shared.js';
19
19
  function isRecord(value) {
20
20
  return typeof value === 'object' && value !== null && !Array.isArray(value);
21
21
  }
@@ -4,7 +4,7 @@
4
4
  // childless sibling. Also the base (scroll-view.ts re-exports it) for headless / web. Metro picks
5
5
  // this on an iOS host; no Platform.OS read. Mirrors the React adapter's iOS binding.
6
6
  import { h } from '@vue/runtime-core';
7
- import { createScrollView } from './shared';
7
+ import { createScrollView } from './shared.js';
8
8
  export const ScrollView = createScrollView({
9
9
  assemble: input => {
10
10
  // Sibling placement: RefreshControl before content (RN iOS). The node ref stays on the scroll
@@ -1,4 +1,4 @@
1
1
  // Base / default ScrollView. Re-exports the iOS build. Metro overrides this with
2
2
  // scroll-view.ios.ts / scroll-view.android.ts on a real host; under tsx / tsc / web the host
3
3
  // resolves here. Filename is the selector, no Platform.OS read. See ADR 0020 / 0024.
4
- export * from './index.ios';
4
+ export * from './index.ios.js';
@@ -21,8 +21,8 @@
21
21
  import { defineComponent, h, isVNode, markRaw, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
22
22
  import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, selectScrollIntrinsics, } from '@symbiote-native/components';
23
23
  import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, } from '@symbiote-native/engine';
24
- import { wrapStickyHeaders } from './sticky-header';
25
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
24
+ import { wrapStickyHeaders } from './sticky-header.js';
25
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
26
26
  function isHandler(value) {
27
27
  return typeof value === 'function';
28
28
  }
@@ -14,7 +14,7 @@
14
14
  import { defineComponent, h, isVNode, ref, shallowRef, watch, watchEffect, markRaw, } from '@vue/runtime-core';
15
15
  import { AnimatedValue, AnimatedInterpolation, Platform, dlog, } from '@symbiote-native/engine';
16
16
  import { computeStickyInterpolation, nextStickyHeaderY, readLayoutNumber, stickyDebounceMs, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
17
- import { Animated } from '../../modules/animated';
17
+ import { Animated } from '../../modules/animated/index.js';
18
18
  function isHandler(value) {
19
19
  return typeof value === 'function';
20
20
  }
@@ -12,8 +12,8 @@
12
12
  // synthesized events (endReached/startReached/refresh) are bridged, gated on listener presence so
13
13
  // the inner list keeps its RefreshControl / edge-reached gating.
14
14
  import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
15
- import { VirtualizedSectionList, } from '../virtualized-section-list';
16
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
15
+ import { VirtualizedSectionList, } from '../virtualized-section-list/index.js';
16
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
17
17
  // VirtualizedSectionList is a generic component (generic construct signature), which h()'s overloads
18
18
  // can't resolve. Drive it through a loose functional-component handle (generic-component h() limit).
19
19
  const VirtualizedSectionListHost = VirtualizedSectionList;
@@ -2,7 +2,7 @@
2
2
  // trackTintColor (the color for the CURRENT value), and snaps native back via the
3
3
  // `setNativeValue` command. Metro picks this on an Android host; no Platform.OS read.
4
4
  // Mirrors the React adapter's Android binding. See ADR 0020.
5
- import { createSwitch } from './shared';
5
+ import { createSwitch } from './shared.js';
6
6
  export const Switch = createSwitch({
7
7
  snapBackCommand: 'setNativeValue',
8
8
  trackColorProps: (value, trackColor) => ({
@@ -1,7 +1,7 @@
1
1
  // Switch on iOS: the native Switch takes onTintColor (ON-track) / tintColor (OFF-track) and
2
2
  // snaps native back via the `setValue` command. Also the base (switch.ts re-exports it) for
3
3
  // headless / web. Mirrors the React adapter's iOS binding. See ADR 0020.
4
- import { createSwitch } from './shared';
4
+ import { createSwitch } from './shared.js';
5
5
  export const Switch = createSwitch({
6
6
  snapBackCommand: 'setValue',
7
7
  trackColorProps: (_value, trackColor) => ({
@@ -2,4 +2,4 @@
2
2
  // switch.android.ts on a real host; under tsx / tsc / web the host resolves here. Filename is
3
3
  // the selector, no Platform.OS read. See ADR 0020. Switch is the first component to bring the
4
4
  // state half (the reducer) into the Vue adapter: render-and-state, not render-only.
5
- export * from './index.ios';
5
+ export * from './index.ios.js';
@@ -12,9 +12,9 @@
12
12
  import { defineComponent, ref, shallowRef, watch } from '@vue/runtime-core';
13
13
  import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
14
14
  import { dispatchViewCommand, isSymbioteNode, dlog, } from '@symbiote-native/engine';
15
- import { descriptorToVue } from '../../descriptor-to-vue';
16
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
17
- import { resolveModelValue, emitModelUpdate } from '../../utils/model-binding';
15
+ import { descriptorToVue } from '../../descriptor-to-vue.js';
16
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
17
+ import { resolveModelValue, emitModelUpdate } from '../../utils/model-binding.js';
18
18
  function isRecord(value) {
19
19
  return typeof value === 'object' && value !== null && !Array.isArray(value);
20
20
  }
@@ -15,9 +15,9 @@
15
15
  import { defineComponent, onBeforeUnmount, ref, shallowRef, watch } from '@vue/runtime-core';
16
16
  import { resolveAccessibilityProps, resolveTextInputProps, renderTextInput, foldText, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
17
17
  import { dispatchViewCommand, isSymbioteNode, dlog, blurTextInput, setInputFocused, setInputBlurred, whenCommitted, } from '@symbiote-native/engine';
18
- import { descriptorToVue } from '../../descriptor-to-vue';
19
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
20
- import { resolveModelValue, emitModelUpdate } from '../../utils/model-binding';
18
+ import { descriptorToVue } from '../../descriptor-to-vue.js';
19
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
20
+ import { resolveModelValue, emitModelUpdate } from '../../utils/model-binding.js';
21
21
  function isRecord(value) {
22
22
  return typeof value === 'object' && value !== null && !Array.isArray(value);
23
23
  }
@@ -6,9 +6,9 @@
6
6
  import { defineComponent, h } from '@vue/runtime-core';
7
7
  import { backgroundProps, canUseNativeForeground, rippleBackground, selectableBackground, selectableBackgroundBorderless, } from '@symbiote-native/components';
8
8
  import { dlog } from '@symbiote-native/engine';
9
- import { View } from '../components';
10
- import { Pressable, emitPressableEvents, PRESSABLE_EMITS, } from './pressable';
11
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
9
+ import { View } from '../components.js';
10
+ import { Pressable, emitPressableEvents, PRESSABLE_EMITS, } from './pressable.js';
11
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
12
12
  const HANDLED = ['background', 'useForeground'];
13
13
  function forwardAttrs(attrs) {
14
14
  const result = {};
@@ -13,9 +13,9 @@
13
13
  import { defineComponent, h } from '@vue/runtime-core';
14
14
  import { computePressOutWait, DEFAULT_ACTIVE_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_MIN_PRESS_DURATION_MS, DEFAULT_UNDERLAY_COLOR, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, } from '@symbiote-native/components';
15
15
  import { dlog, } from '@symbiote-native/engine';
16
- import { Pressable, emitPressableEvents, PRESSABLE_EMITS, } from './pressable';
17
- import { Animated } from '../modules/animated';
18
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
16
+ import { Pressable, emitPressableEvents, PRESSABLE_EMITS, } from './pressable.js';
17
+ import { Animated } from '../modules/animated/index.js';
18
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
19
19
  function numberOr(value, fallback) {
20
20
  return typeof value === 'number' ? value : fallback;
21
21
  }
@@ -27,10 +27,10 @@
27
27
  import { computed, defineComponent, getCurrentInstance, h, isVNode, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
28
28
  import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, NO_CONTENT_LENGTH_SENT, averageMeasuredLength, buildListPlan, buildOffsets, buildViewabilityPairs, computeEndReached, computeStartReached, computeViewableSet, computeWindow, diffViewable, highestMeasuredIndex, maxMinimumViewTime, offsetForIndex, readLayoutLength, readScrollOffset, throttleWindow, } from '@symbiote-native/components';
29
29
  import { dlog, } from '@symbiote-native/engine';
30
- import { ScrollView } from '../scroll-view';
31
- import { RefreshControl } from '../refresh-control';
32
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
33
- import { componentFromSlot } from '../../utils/slots-to-render-props';
30
+ import { ScrollView } from '../scroll-view/index.js';
31
+ import { RefreshControl } from '../refresh-control.js';
32
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
33
+ import { componentFromSlot } from '../../utils/slots-to-render-props.js';
34
34
  // The typed inputs VirtualizedList reads off `props`; everything else (raw scroll, keyboard*,
35
35
  // accessibility, testID, …) falls through $attrs onto the inner ScrollView. Listed for the runtime
36
36
  // `props` declaration (keyof can't derive it: the index signature widens keyof to `string`). The
@@ -18,8 +18,8 @@
18
18
  import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
19
19
  import { flattenSections, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
20
20
  import { Platform, dlog, } from '@symbiote-native/engine';
21
- import { VirtualizedList } from '../virtualized-list';
22
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
21
+ import { VirtualizedList } from '../virtualized-list/index.js';
22
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
23
23
  // VirtualizedList is a generic component (generic construct signature), which h()'s overloads can't
24
24
  // resolve. Drive it through a loose functional-component handle (generic-component h() limitation).
25
25
  const VirtualizedListHost = VirtualizedList;
@@ -9,7 +9,7 @@
9
9
  // stateful component's ref would resolve to a useless component proxy, which is why
10
10
  // createAnimatedComponent (which captures the host node via that ref) needs the fall-through.
11
11
  import { h } from '@vue/runtime-core';
12
- import { normalizeVueAttrs } from './utils/normalize-attrs';
12
+ import { normalizeVueAttrs } from './utils/normalize-attrs.js';
13
13
  function hostComponent(intrinsic, name) {
14
14
  // A functional component's ctx is Omit<SetupContext, 'expose'> (no instance to expose); let the
15
15
  // FunctionalComponent target infer the param types rather than annotate SetupContext.
package/build/index.js CHANGED
@@ -1,83 +1,83 @@
1
1
  // @symbiote-native/vue: a thin Vue 3 reconciler over @symbiote-native/engine. createRenderer maps
2
2
  // each RendererOptions call onto the engine's mutation API; all Fabric clone-on-write
3
3
  // lives in the engine, shared with every other adapter. App code names only @symbiote-native/vue.
4
- export { mount, unmount } from './render';
4
+ export { mount, unmount } from './render.js';
5
5
  // createTunnel: cross-surface content sharing (Teleport stays same-surface-only by design —
6
6
  // see create-tunnel.ts and the vue-adapter-directives skill for why).
7
- export { createTunnel } from './create-tunnel';
8
- export { View, Text } from './components';
7
+ export { createTunnel } from './create-tunnel.js';
8
+ export { View, Text } from './components.js';
9
9
  // findNodeHandle: RN's ref -> native reactTag lookup, the Vue twin of the React adapter's
10
10
  // host-instance resolution. IHostInstance is the raw engine node (Vue host refs fall
11
11
  // through to it; imperative methods live on each component's expose() handle).
12
- export { findNodeHandle } from './host-instance';
12
+ export { findNodeHandle } from './host-instance/index.js';
13
13
  // Image. Full parity with React: source/src/srcSet resolution, the width/height → style fold,
14
14
  // alt → accessibility, and the Image statics (getSize / prefetch / queryCache / …) are shared
15
15
  // verbatim from @symbiote-native/components via renderImage; Vue supplies only the functional bridge.
16
- export { Image, setImageSourceResolver } from './components/image';
16
+ export { Image, setImageSourceResolver } from './components/image.js';
17
17
  // First component off the shared @symbiote-native/components layer: render-only, drives the
18
18
  // agnostic renderActivityIndicator through descriptorToVue. Proof the layer is reusable:
19
19
  // the render fn is shared verbatim with React; Vue supplies only the bridge.
20
- export { ActivityIndicator } from './components/activity-indicator';
20
+ export { ActivityIndicator } from './components/activity-indicator/index.js';
21
21
  // First component to bring the state half into Vue (the lastNativeReport reducer + the
22
22
  // snap-back watch); render shared verbatim with React, Vue supplies the reactive lifecycle.
23
- export { Switch } from './components/switch';
23
+ export { Switch } from './components/switch/index.js';
24
24
  // ScrollView. Full parity (ADR 0024): vertical + horizontal, every pass-through prop, the
25
25
  // synthesized onContentSizeChange, the imperative handle via expose() + shallowRef, RefreshControl
26
26
  // (iOS sibling / Android wrap, Phase 2), and sticky headers (Phase 3: the scroll AnimatedValue,
27
27
  // the headerLayoutYs cross-talk, and the per-header Animated.View wrap). The pure math (intrinsics,
28
28
  // decelerationRate, content-size dedupe, the handle, the sticky interpolation) is shared verbatim
29
29
  // with React from @symbiote-native/components.
30
- export { ScrollView } from './components/scroll-view';
30
+ export { ScrollView } from './components/scroll-view/index.js';
31
31
  // Pressable family. Full parity with React (the 3-layer split): the press state machine + render
32
32
  // decisions are shared in @symbiote-native/components; Vue supplies the reactivity + descriptor bridge.
33
- export { Pressable } from './components/pressable';
34
- export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable';
35
- export { TouchableNativeFeedback } from './components/touchable-native-feedback';
36
- export { Button } from './components/button';
33
+ export { Pressable } from './components/pressable.js';
34
+ export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable.js';
35
+ export { TouchableNativeFeedback } from './components/touchable-native-feedback.js';
36
+ export { Button } from './components/button.js';
37
37
  // TextInput. Full parity with React (the 3-layer split): the value/selection folds + the
38
38
  // controlled-write predicate live in @symbiote-native/components; Vue supplies the reactivity
39
39
  // (shallowRef host node, post-flush watch) + the imperative handle via expose().
40
- export { TextInput } from './components/text-input';
40
+ export { TextInput } from './components/text-input/index.js';
41
41
  // VirtualizedList family. Full parity with React: the windowing math (visible range, cell
42
42
  // keys, viewability token diffing, edge-reached, the list plan) is shared in
43
43
  // @symbiote-native/components; Vue supplies the reactive lifecycle + the per-cell element.
44
- export { VirtualizedList } from './components/virtualized-list';
45
- export { FlatList } from './components/flat-list';
46
- export { VirtualizedSectionList } from './components/virtualized-section-list';
47
- export { SectionList } from './components/section-list';
44
+ export { VirtualizedList } from './components/virtualized-list/index.js';
45
+ export { FlatList } from './components/flat-list/index.js';
46
+ export { VirtualizedSectionList } from './components/virtualized-section-list/index.js';
47
+ export { SectionList } from './components/section-list/index.js';
48
48
  // Phase 2 (ADR 0024): SafeAreaView + RefreshControl, wired into ScrollView with the iOS-sibling /
49
49
  // Android-wrap platform split. RefreshControl hosts the wrapped scroll view via its default slot.
50
- export { SafeAreaView } from './components/safe-area-view';
51
- export { RefreshControl } from './components/refresh-control';
52
- export { descriptorToVue } from './descriptor-to-vue';
50
+ export { SafeAreaView } from './components/safe-area-view.js';
51
+ export { RefreshControl } from './components/refresh-control.js';
52
+ export { descriptorToVue } from './descriptor-to-vue.js';
53
53
  // normalizeVueAttrs: the kebab→camel attr fold every component applies at entry. Exported so an
54
54
  // external wrapper package (e.g. @symbiote-native/slider/vue over a third-party native view) can fold
55
55
  // its incoming attrs through the SAME transform rather than reimplementing it.
56
- export { normalizeVueAttrs } from './utils/normalize-attrs';
56
+ export { normalizeVueAttrs } from './utils/normalize-attrs.js';
57
57
  // resolveModelValue/emitModelUpdate: the v-model (Rule 6, vue-adapter-events skill) helper every
58
58
  // controlled-value component uses. Exported for the same external-package reason as
59
59
  // normalizeVueAttrs above (e.g. @symbiote-native/slider/vue).
60
- export { resolveModelValue, emitModelUpdate } from './utils/model-binding';
61
- export { createSymbioteRenderer } from './renderer';
60
+ export { resolveModelValue, emitModelUpdate } from './utils/model-binding.js';
61
+ export { createSymbioteRenderer } from './renderer.js';
62
62
  // Animated (ADR 0024 Phase 3a): Animated.View/Text/Image + the lazy Animated.ScrollView over the
63
63
  // Vue primitives, with the value graph / easing / drivers spread from @symbiote-native/engine. The wrap
64
64
  // mechanism (createAnimatedComponent) is the Vue twin of React's; the pure leaves live in the
65
65
  // engine.
66
- export { Animated, createAnimatedComponent } from './modules/animated';
66
+ export { Animated, createAnimatedComponent } from './modules/animated/index.js';
67
67
  // Visual components off the shared @symbiote-native/components layer (wave: simple-visual).
68
- export { ImageBackground } from './components/image-background';
69
- export { InputAccessoryView } from './components/input-accessory-view';
70
- export { Modal } from './components/modal';
68
+ export { ImageBackground } from './components/image-background.js';
69
+ export { InputAccessoryView } from './components/input-accessory-view.js';
70
+ export { Modal } from './components/modal.js';
71
71
  // KeyboardAvoidingView: full parity via the shared inset math; Vue owns the Keyboard subscription.
72
- export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
72
+ export { KeyboardAvoidingView } from './components/keyboard-avoiding-view.js';
73
73
  // StatusBar: declarative component + the shared imperative API.
74
- export { StatusBar } from './modules/status-bar';
74
+ export { StatusBar } from './modules/status-bar.js';
75
75
  // AppRegistry: RN's app entry point over `mount`. setHostRegistrar wires RN's own
76
76
  // registrar so the native Fabric host finds our runnable by app key.
77
- export { AppRegistry, setHostRegistrar } from './modules/app-registry';
77
+ export { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
78
78
  // Vue composables over the core device-state modules.
79
- export { useColorScheme } from './composables/use-color-scheme';
80
- export { useWindowDimensions } from './composables/use-window-dimensions';
79
+ export { useColorScheme } from './composables/use-color-scheme.js';
80
+ export { useWindowDimensions } from './composables/use-window-dimensions.js';
81
81
  // Imperative runtime modules: the SAME module both adapters share, re-exported from @symbiote-native/engine.
82
82
  export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
83
83
  // Re-export the framework-agnostic engine surface (pure utilities + diagnostics).
@@ -13,7 +13,7 @@
13
13
  // value.setValue / animation -> flushValue -> AnimatedProps.update() -> setNativeProps(node).
14
14
  import { defineComponent, h, onBeforeUnmount, onMounted, onUpdated, shallowRef, } from '@vue/runtime-core';
15
15
  import { AnimatedProps, attachNativeEventHandler, isNativeAnimatedAvailable, reduceProps, readPassthroughStyle, resolveHostNode, } from '@symbiote-native/engine';
16
- import { normalizeVueAttrs } from '../../utils/normalize-attrs';
16
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
17
17
  // RN's prop carrying explicit (already-rasterized) values that override the animated prop in
18
18
  // the COMMITTED props (sticky-header passthrough). Named once here so render/reconcile agree.
19
19
  const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
@@ -4,11 +4,11 @@
4
4
  // JS-driven, ADR 0016), spread in verbatim. Both halves meet here so the familiar surface,
5
5
  // `Animated.timing(new Animated.Value(0), …).start()`, works against the Vue-driven engine.
6
6
  import { AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedMock, Easing, Platform, timing, spring, decay, parallel, sequence, stagger, loop, delay, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, } from '@symbiote-native/engine';
7
- import { View, Text } from '../../components';
8
- import { Image } from '../../components/image';
9
- import { ScrollView } from '../../components/scroll-view';
10
- import { createAnimatedComponent } from './create-animated-component';
11
- export { createAnimatedComponent } from './create-animated-component';
7
+ import { View, Text } from '../../components.js';
8
+ import { Image } from '../../components/image.js';
9
+ import { ScrollView } from '../../components/scroll-view/index.js';
10
+ import { createAnimatedComponent } from './create-animated-component.js';
11
+ export { createAnimatedComponent } from './create-animated-component.js';
12
12
  // View/Text are pure host primitives; Image is the functional renderImage wrapper, and all expose
13
13
  // their host node via ref fall-through, so wrap them eagerly.
14
14
  const AnimatedView = createAnimatedComponent(View);
@@ -9,7 +9,7 @@
9
9
  // React already has.
10
10
  import { h } from '@vue/runtime-core';
11
11
  import { createAppRegistry, dlog, } from '@symbiote-native/engine';
12
- import { mount } from '../../render';
12
+ import { mount } from '../../render.js';
13
13
  function runnableFor(componentProvider, getWrapperComponentProvider) {
14
14
  return appParameters => {
15
15
  dlog(`AppRegistry: mounting on rootTag ${String(appParameters.rootTag)}`);
@@ -11,7 +11,7 @@
11
11
  // (untyped) and run through normalizeVueAttrs (kebab→camel) before each is narrowed by a guard.
12
12
  import { defineComponent, watchEffect } from '@vue/runtime-core';
13
13
  import { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, isOpaqueColorValue, } from '@symbiote-native/engine';
14
- import { normalizeVueAttrs } from '../utils/normalize-attrs';
14
+ import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
15
15
  function asBoolean(value) {
16
16
  return typeof value === 'boolean' ? value : undefined;
17
17
  }
package/build/render.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // a surface for it and let the Vue renderer drive the engine, which commits into
3
3
  // nativeFabricUIManager; RN's own renderer never in the path.
4
4
  import { createSurface, disposeRoot, dlog, } from '@symbiote-native/engine';
5
- import { createSymbioteRenderer } from './renderer';
5
+ import { createSymbioteRenderer } from './renderer.js';
6
6
  // One Vue app per surface, so a surface can be torn down (unmount) or cleanly
7
7
  // re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on
8
8
  // Fast Refresh and on lifecycle/focus changes, reusing the rootTag.
@@ -6,7 +6,12 @@
6
6
  // requiring FILE's own location, so this indirection is what actually removes the extra install
7
7
  // step, not pnpm hoisting (which does not propagate this transitively across workspace packages
8
8
  // the way a flat classic node_modules would). .cjs, not .js: this package is "type": "module",
9
- // and Metro's babelTransformerPath loading expects a require()-able module. Used by
10
- // examples/vue-sfc/metro-vue-transformer.js for BOTH its inline <style> block compilation and
11
- // standalone .css/.module.css file imports. See the symbiote-sfc-style-compiler skill.
12
- module.exports = require('@symbiote-native/css-parser');
9
+ // and Metro's babelTransformerPath loading expects a require()-able module. Used as
10
+ // vue-tsx's babelTransformerPath for standalone .css/.module.css file imports —
11
+ // metro-vue-transformer.cjs (the .vue SFC transformer) requires @symbiote-native/css-parser
12
+ // directly instead, since it needs the individual parse/compile functions, not a Metro
13
+ // transformer object. See the symbiote-sfc-style-compiler skill.
14
+ // createCssMetroTransformer is a factory, not a ready transformer — Metro's
15
+ // babelTransformerPath needs the actual {transform, getCacheKey} object it returns,
16
+ // not the css-parser package barrel.
17
+ module.exports = require('@symbiote-native/css-parser').createCssMetroTransformer();
@@ -0,0 +1,313 @@
1
+ // Metro babel transformer that teaches the bundler to read Vue SFCs (.vue). Metro has no
2
+ // Vue plugin (unplugin-vue ships vite/webpack/esbuild/rollup adapters, not Metro), so we do
3
+ // the single-pass compile here: parse the SFC, compile <script setup> + <template> into one
4
+ // component module, then hand the JS to RN's own babel transformer. This is the Metro twin
5
+ // of what @vitejs/plugin-vue does for Vite; the 'vue'→runtime-core rewrite is wolf-tui's
6
+ // pattern (a custom, non-DOM renderer needs the compiler helpers from @vue/runtime-core,
7
+ // not from vue/runtime-dom).
8
+ //
9
+ // Retargeted at @symbiote-native/vue/runtime-helpers rather than bare @vue/runtime-core: that shim
10
+ // re-exports runtime-core verbatim PLUS supplies our own `vShow` (compiled v-show imports it by
11
+ // name, and only @vue/runtime-dom's DOM-based version exists otherwise — see the
12
+ // vue-adapter-directives skill).
13
+ //
14
+ // Ships as a package-level export (`@symbiote-native/vue/metro-vue-transformer`) rather than living
15
+ // in each consuming app: a consumer's own metro.config.js just points babelTransformerPath at it —
16
+ // see the symbiote-sfc-style-compiler skill.
17
+
18
+ const { parse, compileScript } = require('@vue/compiler-sfc');
19
+ const { createCompoundExpression } = require('@vue/compiler-core');
20
+ // @symbiote-native/css-parser is a real dependency of THIS package, so requiring it directly here
21
+ // (rather than through the ./metro-css-parser public subpath, which exists for CONSUMERS) resolves
22
+ // straight from this package's own node_modules under pnpm.
23
+ const {
24
+ compile: compilePreprocessor,
25
+ compileCssFile,
26
+ globalClassNamesIn,
27
+ hashFilePath,
28
+ isStyleFile,
29
+ kebabToCamel,
30
+ parseCSS,
31
+ resolveUpstreamTransformer,
32
+ } = require('@symbiote-native/css-parser');
33
+
34
+ const upstreamTransformer = resolveUpstreamTransformer();
35
+
36
+ // Rewrites a Vue template AST so every `class`/`:class` binding on an element resolves
37
+ // against this file's scoped class names at the compiled call site, via @symbiote-native/engine's
38
+ // scopeClassName(value, localNames, scopeId) — see the symbiote-sfc-style-compiler skill for
39
+ // the full design (why AST-level, not a raw-text regex: Vue itself merges a static `class=`
40
+ // and a dynamic `:class=` on the same element into ONE codegen entry, and text substitution
41
+ // can't reproduce that merge safely; letting Vue's own transformElement do the merge on our
42
+ // already-rewritten nodes reuses that logic instead of reimplementing it).
43
+ //
44
+ // A static class="foo bar" attribute is a plain AttributeNode (prop.type === 6): its value is
45
+ // resolved to the final string directly here, at compile time — no runtime call needed for the
46
+ // purely-static case, since every token is already known. Each token is normalized kebab->camel
47
+ // FIRST (kebabToCamel) — a template may write class="section-label" (idiomatic CSS) instead of
48
+ // class="sectionLabel"; the css-parser always registers the CAMEL form, and localNames (built
49
+ // from that same registration) is camelCase-keyed, so the scoping check below only recognizes a
50
+ // token once it is in that form. The emitted string is always camelCase, suffixed or not.
51
+ //
52
+ // A dynamic :class="expr" is a `bind` DirectiveNode (prop.type === 7) targeting the `class`
53
+ // arg. Vue's own transformExpression has ALREADY run by the time our transform sees it (node
54
+ // transforms are appended after the built-in preset, per @vue/compiler-core's TransformOptions
55
+ // merge order), so prop.exp is usually a COMPOUND_EXPRESSION (type 8, a `children` array mixing
56
+ // literal source chunks with resolved identifier nodes), not a plain string — a bare identifier
57
+ // binding (`:class="dynamicClass"`) is the one case that stays a SIMPLE_EXPRESSION (type 4).
58
+ // Either way, `createCompoundExpression` wraps the ORIGINAL exp node (whichever shape it is) as
59
+ // a single child inside `scopeClassName(<original>, __localScopedClassNames, __scopeId)`, so
60
+ // codegen emits the call with the original expression source reproduced unchanged inside it —
61
+ // this needs no per-shape branching, and correctly defers even a fully opaque runtime value
62
+ // (`:class="dynamicClass"`) to scopeClassName's own runtime token-matching, so there is no
63
+ // unresolved gap for dynamic scoped classes.
64
+ function createScopeClassNodeTransform(localNames, scopeId) {
65
+ return function scopeClassNodeTransform(node) {
66
+ if (node.type !== 1 /* NodeTypes.ELEMENT */) return;
67
+
68
+ for (const prop of node.props) {
69
+ if (prop.type === 6 /* NodeTypes.ATTRIBUTE */ && prop.name === 'class' && prop.value) {
70
+ prop.value.content = prop.value.content
71
+ .split(/\s+/)
72
+ .filter(Boolean)
73
+ .map((token) => {
74
+ const camelToken = kebabToCamel(token);
75
+ return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
76
+ })
77
+ .join(' ');
78
+ continue;
79
+ }
80
+
81
+ if (
82
+ prop.type === 7 /* NodeTypes.DIRECTIVE */ &&
83
+ prop.name === 'bind' &&
84
+ prop.arg &&
85
+ prop.arg.type === 4 /* NodeTypes.SIMPLE_EXPRESSION */ &&
86
+ prop.arg.content === 'class' &&
87
+ prop.exp
88
+ ) {
89
+ prop.exp = createCompoundExpression(
90
+ ['__scopeClass(', prop.exp, ', __localScopedClassNames, __scopeId)'],
91
+ prop.exp.loc,
92
+ );
93
+ }
94
+ }
95
+ };
96
+ }
97
+
98
+ // A short, stable id per file, used as the SFC scope id (compileScript wants one regardless of
99
+ // whether the file has scoped styles) — Vue's own `data-v-hash` naming convention, built on
100
+ // @symbiote-native/css-parser's shared hashFilePath so the algorithm isn't duplicated against the
101
+ // standalone .module.css compiler's identical need (metro-css-module.ts).
102
+ function scopeIdFor(filename) {
103
+ return 'data-v-' + hashFilePath(filename);
104
+ }
105
+
106
+ // An SFC style block's `lang` attribute names a preprocessor language directly (`lang="scss"`),
107
+ // unlike a standalone file, which is identified by its extension — so this is its own small
108
+ // lookup rather than reusing detectLanguage(), which is extension-keyed.
109
+ const SFC_STYLE_LANG_TO_PREPROCESSOR = new Map([
110
+ ['scss', 'scss'],
111
+ ['sass', 'scss'],
112
+ ['less', 'less'],
113
+ ['stylus', 'stylus'],
114
+ ]);
115
+
116
+ // Reduces one <style> block down to plain CSS text. A lang-less or lang="css" block passes
117
+ // through unchanged, exactly as before. Anything outside the four recognized preprocessor langs
118
+ // (a typo, or a genuinely unsupported lang) still throws, same message as before this feature
119
+ // existed.
120
+ async function compileStyleBlockContent(style, filename) {
121
+ if (style.lang == null || style.lang === 'css') return style.content;
122
+
123
+ const preprocessorLang = SFC_STYLE_LANG_TO_PREPROCESSOR.get(style.lang);
124
+ if (!preprocessorLang) {
125
+ throw new Error(`SFC style lang="${style.lang}" not supported yet — plain CSS only`);
126
+ }
127
+
128
+ // Sass' `.sass` indented syntax and `.scss` syntax share one compiler entry point that picks
129
+ // between them off the file extension (see preprocessors.ts's compileScss) — an inline SFC
130
+ // style block has no file of its own, so a synthetic `.sass`-suffixed path is the only way to
131
+ // select the indented syntax. Every other preprocessor only uses the path for relative-import
132
+ // resolution (dirname), where the real .vue file's own path is correct as-is.
133
+ const syntheticPath = style.lang === 'sass' ? `${filename}.sass` : filename;
134
+ return compilePreprocessor(style.content, preprocessorLang, syntheticPath);
135
+ }
136
+
137
+ async function compileSfc(src, filename) {
138
+ const { descriptor, errors } = parse(src, { filename });
139
+ if (errors && errors.length > 0) {
140
+ throw new Error(`Vue SFC parse error in ${filename}:\n${errors.map(String).join('\n')}`);
141
+ }
142
+ if (descriptor.scriptSetup == null && descriptor.script == null) {
143
+ throw new Error(`Vue SFC ${filename} has no <script> / <script setup> block`);
144
+ }
145
+
146
+ const scopeId = scopeIdFor(filename);
147
+
148
+ // `descriptor.styles` is already parsed by @vue/compiler-sfc itself (one entry per <style>
149
+ // block, `.content` pre-trimmed, `.scoped` already a plain boolean flag), so there's no need
150
+ // to re-extract style blocks with a regex the way wolf-tui's Vite plugin does.
151
+ //
152
+ // A scoped block's classes get their key SUFFIXED with this file's scopeId before
153
+ // registration (`card` -> `card__data-v-xxxxxxxx`), so two components can each define their
154
+ // own `.card` without colliding in the shared global registry — mirroring what Vue's own
155
+ // `data-v-hash` attribute does for DOM targets, just as a name suffix instead of an attribute
156
+ // selector (we have neither DOM nor attribute-selector matching). An unscoped block's classes
157
+ // register exactly as before: no suffix, globally shared. `:global(...)` selectors inside a
158
+ // scoped block are the one exception — @symbiote-native/css-parser already unwraps them to their
159
+ // plain class name (`:global(.reset)` parses like `.reset`), so `globalClassNamesIn` re-scans
160
+ // the block's own raw text to find which specific keys should be exempted from suffixing.
161
+ //
162
+ // Cascade multiple blocks last-block-wins, same as CSS — this holds across scoped/unscoped
163
+ // blocks too: only each block's OWN css-parser output is scoped independently, then merged.
164
+ //
165
+ // <style module> (CSS Modules) reuses this exact suffixing machinery instead of a separate
166
+ // pipeline: `.card` still goes through parseCSS unchanged and still registers via the same
167
+ // registerStyles() call, just under a suffixed key — the only new output is a plain name->
168
+ // scopedName object (`$style` by default, or the block's `module="name"` value) emitted as a
169
+ // preamble const, so `:class="$style.card"` passes the already-scoped string straight to
170
+ // resolveClassName's existing exact-match path (no registry changes needed, see the
171
+ // symbiote-sfc-style-compiler skill). Unlike `scoped`, a module block's classes are NEVER
172
+ // auto-applied to a literal class="..." attribute — CSS Modules is opt-in per usage via
173
+ // `$style.x`, so module classes are kept out of `localScopedNames` (the nodeTransform only
174
+ // rewrites literal class strings for `scoped` blocks). The registry key gets an extra
175
+ // `module` tag (`card__module__<scopeId>`, vs scoped's plain `card__<scopeId>`) so a file
176
+ // that happens to mix `<style scoped>` and `<style module>` with the same class name can't
177
+ // collide in the shared registry.
178
+ const styles = {};
179
+ const localScopedNames = new Set();
180
+ const cssModuleBindings = new Map();
181
+
182
+ for (const style of descriptor.styles) {
183
+ // Reduces a preprocessor block down to plain CSS text BEFORE any of the scoping logic
184
+ // below runs — that logic is entirely language-agnostic, it only ever sees parseCSS's
185
+ // plain-CSS output, same as it always did for a lang-less/lang="css" block. Anything other
186
+ // than the four recognized preprocessor langs (a typo, or a genuinely unsupported lang)
187
+ // still throws, unchanged from before.
188
+ const content = await compileStyleBlockContent(style, filename);
189
+ const parsed = parseCSS(content, { filename });
190
+
191
+ if (style.module) {
192
+ const bindingName = typeof style.module === 'string' ? style.module : '$style';
193
+ // Scanned against the COMPILED content, not style.content: :global(...) isn't native
194
+ // SCSS/Less/Stylus syntax (each preprocessor passes an unrecognized selector through
195
+ // unchanged), but scanning the compiler's actual output can't drift under nesting/
196
+ // interpolation the way assuming source-and-output stay textually identical could.
197
+ const exemptFromScope = globalClassNamesIn(content);
198
+ const classMap = cssModuleBindings.get(bindingName) ?? {};
199
+ for (const [className, props] of Object.entries(parsed)) {
200
+ const isExempt = exemptFromScope.has(className);
201
+ const registeredName = isExempt ? className : `${className}__module__${scopeId}`;
202
+ classMap[className] = registeredName;
203
+ styles[registeredName] = { ...styles[registeredName], ...props };
204
+ }
205
+ cssModuleBindings.set(bindingName, classMap);
206
+ } else if (style.scoped) {
207
+ const exemptFromScope = globalClassNamesIn(content);
208
+ for (const [className, props] of Object.entries(parsed)) {
209
+ const isExempt = exemptFromScope.has(className);
210
+ const registeredName = isExempt ? className : `${className}__${scopeId}`;
211
+ if (!isExempt) localScopedNames.add(className);
212
+ styles[registeredName] = { ...styles[registeredName], ...props };
213
+ }
214
+ } else {
215
+ for (const [className, props] of Object.entries(parsed)) {
216
+ styles[className] = { ...styles[className], ...props };
217
+ }
218
+ }
219
+ }
220
+
221
+ // The nodeTransform rewrites every class/:class binding's compiled output to route through
222
+ // scopeClassName() at the scoped names this file actually defines — skipped entirely (not
223
+ // even passed to the compiler) when nothing in this file is scoped, so a .vue with only
224
+ // unscoped/no styles compiles exactly as before, zero added runtime cost or behavior change.
225
+ const templateOptions =
226
+ localScopedNames.size > 0
227
+ ? {
228
+ compilerOptions: {
229
+ nodeTransforms: [createScopeClassNodeTransform(localScopedNames, scopeId)],
230
+ },
231
+ }
232
+ : undefined;
233
+
234
+ // inlineTemplate folds the <template> render fn into setup(): one module, one `export
235
+ // default`. Only valid with <script setup>, which the canary uses.
236
+ const compiled = compileScript(descriptor, {
237
+ id: scopeId,
238
+ inlineTemplate: true,
239
+ templateOptions,
240
+ });
241
+ // Point every Vue import (the compiler's injected helpers AND the user's own
242
+ // `import { ref } from 'vue'`) at the runtime-helpers shim, which re-exports the same
243
+ // @vue/runtime-core singleton the @symbiote-native/vue adapter builds its custom renderer on, plus
244
+ // our own directive implementations. No vue/runtime-dom in a native bundle.
245
+ const code = compiled.content.replace(/from\s*(['"])vue\1/g, 'from "@symbiote-native/vue/runtime-helpers"');
246
+
247
+ if (Object.keys(styles).length === 0) return code;
248
+
249
+ // Only a scoped file needs scopeClassName + its two per-file constants — the nodeTransform
250
+ // above only ever emits calls to `__scopeClass`/`__localScopedClassNames`/`__scopeId` when
251
+ // localScopedNames is non-empty, so these stay unimported/undeclared (and absent from the
252
+ // bundle) for every non-scoped .vue file.
253
+ const engineImports =
254
+ localScopedNames.size > 0 ? 'registerStyles, scopeClassName as __scopeClass' : 'registerStyles';
255
+
256
+ const preamble = [`registerStyles(${JSON.stringify(styles)});`];
257
+ if (localScopedNames.size > 0) {
258
+ preamble.push(
259
+ `const __localScopedClassNames = new Set(${JSON.stringify([...localScopedNames])});`,
260
+ `const __scopeId = ${JSON.stringify(scopeId)};`,
261
+ );
262
+ }
263
+ // Each <style module> binding becomes a plain top-level const holding its name->scopedName
264
+ // map. Placed before the compiled `export default {...}`, so it's just a closed-over module
265
+ // scope variable inside setup() — same trick registerStyles/__scopeId already rely on — and
266
+ // usable both from the inlined template (`:class="$style.card"`) and from <script setup> code
267
+ // itself (`$style.card`), no extra wiring needed on either side.
268
+ for (const [bindingName, classMap] of cssModuleBindings) {
269
+ preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
270
+ }
271
+
272
+ return [`import { ${engineImports} } from '@symbiote-native/engine';`, ...preamble, code].join('\n') + '\n';
273
+ }
274
+
275
+ // Exported separately from `transform` so tests can assert on the compiled SFC output
276
+ // (imports, injected `registerStyles` call) without driving the full upstream RN Babel preset.
277
+ module.exports.compileSfc = compileSfc;
278
+
279
+ // Async uniformly, including the `.vue` and plain-passthrough branches that never touch a
280
+ // preprocessor: compileSfc() itself is async now (a scss/sass/less/stylus <style> block awaits
281
+ // preprocessors.ts's compile()), and Metro's own metro-transform-worker already
282
+ // `await transformer.transform(...)` before touching the result (confirmed by reading the
283
+ // installed metro-transform-worker source — see metro-transformer.ts for the fuller writeup),
284
+ // so returning a Promise here is a supported, exercised shape, not a hack. A sync fast-path for
285
+ // the no-preprocessor branches would fork this function into two shapes to save a single
286
+ // microtask on a call that only ever runs at Metro build time, content-hash-cached — not worth
287
+ // the duplication.
288
+ module.exports.transform = async function transform(params) {
289
+ if (params.filename.endsWith('.vue')) {
290
+ const code = await compileSfc(params.src, params.filename);
291
+ // Re-label as .tsx so RN's transformer strips any TS from <script setup lang="ts"> and
292
+ // processes the module exactly like app source. Metro tracks the real path separately.
293
+ return upstreamTransformer.transform({
294
+ ...params,
295
+ src: code,
296
+ filename: params.filename + '.tsx',
297
+ });
298
+ }
299
+ // A standalone style file (as opposed to a `.vue` file's own inline <style> block above) —
300
+ // the framework-agnostic path (core/css-parser's compileCssFile), usable from this example's
301
+ // .vue files exactly like from any other adapter's example. isStyleFile recognizes
302
+ // .css/.scss/.sass/.less/.styl/.stylus (+ each .module.* twin) — see the
303
+ // symbiote-sfc-style-compiler skill.
304
+ if (isStyleFile(params.filename)) {
305
+ const { code } = await compileCssFile(params.src, params.filename);
306
+ return upstreamTransformer.transform({ ...params, src: code, filename: params.filename + '.js' });
307
+ }
308
+ return upstreamTransformer.transform(params);
309
+ };
310
+
311
+ // Surface the upstream cache key so RN preset changes still bust Metro's cache. The SFC
312
+ // step itself is invalidated by `--reset-cache` (mirrors the babel DEBUG-inline note).
313
+ module.exports.getCacheKey = upstreamTransformer.getCacheKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/vue",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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
  "repository": {
6
6
  "type": "git",
@@ -29,20 +29,24 @@
29
29
  "types": "./build/runtime-helpers.d.ts",
30
30
  "default": "./build/runtime-helpers.js"
31
31
  },
32
- "./metro-css-parser": "./metro-css-parser.cjs"
32
+ "./metro-css-parser": "./metro-css-parser.cjs",
33
+ "./metro-vue-transformer": "./metro-vue-transformer.cjs"
33
34
  },
34
35
  "files": [
35
36
  "build",
36
- "metro-css-parser.cjs"
37
+ "metro-css-parser.cjs",
38
+ "metro-vue-transformer.cjs"
37
39
  ],
38
40
  "publishConfig": {
39
41
  "access": "public"
40
42
  },
41
43
  "dependencies": {
44
+ "@vue/compiler-core": "^3.5.13",
45
+ "@vue/compiler-sfc": "^3.5.13",
42
46
  "@vue/runtime-core": "^3.5.13",
43
- "@symbiote-native/css-parser": "0.1.1",
44
- "@symbiote-native/components": "0.2.0",
45
- "@symbiote-native/engine": "0.1.1"
47
+ "@symbiote-native/components": "0.2.1",
48
+ "@symbiote-native/css-parser": "0.2.0",
49
+ "@symbiote-native/engine": "0.1.2"
46
50
  },
47
51
  "peerDependencies": {
48
52
  "vue": ">=3.5.0",