).value as JsxElement;
+ final expr = element.children.first as JsxExpression;
+ expect(expr.expression, equals('"JSX"'));
+ });
+
+ test('transforms multiple consecutive expressions correctly', () {
+ final transformer = JsxTransformer();
+ final element = JsxElement(
+ tagName: 'div',
+ attributes: [],
+ children: [
+ JsxExpression('a'),
+ JsxExpression('b'),
+ JsxExpression('c'),
+ ],
+ isSelfClosing: false,
+ );
+
+ final result = transformer.transform(element);
+ expect(result, equals('\$div() >> [a, b, c]'));
+ });
+
+ test('transforms empty string attribute correctly', () {
+ final transformer = JsxTransformer();
+ final element = JsxElement(
+ tagName: 'div',
+ attributes: [JsxStringAttribute('className', '')],
+ children: [JsxText('Text')],
+ isSelfClosing: false,
+ );
+
+ final result = transformer.transform(element);
+ expect(result, equals("\$div(className: '') >> 'Text'"));
+ });
+
+ test('transforms unicode text correctly', () {
+ final transformer = JsxTransformer();
+ final element = JsxElement(
+ tagName: 'div',
+ attributes: [],
+ children: [JsxText('Hello 世界 🌍')],
+ isSelfClosing: false,
+ );
+
+ final result = transformer.transform(element);
+ expect(result, equals("\$div() >> 'Hello 世界 🌍'"));
+ });
+
+ test('parses closing tag with whitespace', () {
+ final parser = JsxParser('Content< / div>');
+ final result = parser.parse();
+
+ expect(result.isError, isTrue);
+ });
+
+ test('returns error for missing closing bracket on self-closing tag', () {
+ final parser = JsxParser('
).value as JsxElement;
+ expect(element.tagName, equals('br'));
+ });
+}
diff --git a/packages/dart_node_react_native/lib/dart_node_react_native.dart b/packages/dart_node_react_native/lib/dart_node_react_native.dart
index 1d66b2e..4b4f96f 100644
--- a/packages/dart_node_react_native/lib/dart_node_react_native.dart
+++ b/packages/dart_node_react_native/lib/dart_node_react_native.dart
@@ -3,4 +3,6 @@ library;
export 'src/components.dart';
export 'src/core.dart';
+export 'src/navigation_types.dart';
+export 'src/npm_component.dart';
export 'src/testing.dart';
diff --git a/packages/dart_node_react_native/lib/src/navigation_types.dart b/packages/dart_node_react_native/lib/src/navigation_types.dart
new file mode 100644
index 0000000..9c4f194
--- /dev/null
+++ b/packages/dart_node_react_native/lib/src/navigation_types.dart
@@ -0,0 +1,115 @@
+/// Navigation types for React Navigation interop.
+///
+/// These are basic extension types for working with React Navigation
+/// props passed to screen components. Use with npmComponent() for
+/// direct navigation package usage.
+library;
+
+import 'dart:js_interop';
+import 'dart:js_interop_unsafe';
+
+/// Navigation prop type (passed to screen components)
+extension type NavigationProp._(JSObject _) implements JSObject {
+ /// Navigate to a route
+ void navigate(String routeName, [Map? params]) {
+ if (params != null) {
+ _.callMethod('navigate'.toJS, routeName.toJS, params.jsify());
+ } else {
+ _.callMethod('navigate'.toJS, routeName.toJS);
+ }
+ }
+
+ /// Go back to the previous screen
+ void goBack() => _.callMethod('goBack'.toJS);
+
+ /// Push a new screen onto the stack
+ void push(String routeName, [Map? params]) {
+ if (params != null) {
+ _.callMethod('push'.toJS, routeName.toJS, params.jsify());
+ } else {
+ _.callMethod('push'.toJS, routeName.toJS);
+ }
+ }
+
+ /// Pop the current screen from the stack
+ void pop([int? count]) {
+ if (count != null) {
+ _.callMethod('pop'.toJS, count.toJS);
+ } else {
+ _.callMethod('pop'.toJS);
+ }
+ }
+
+ /// Pop to the top of the stack
+ void popToTop() => _.callMethod('popToTop'.toJS);
+
+ /// Replace the current screen
+ void replace(String routeName, [Map? params]) {
+ if (params != null) {
+ _.callMethod('replace'.toJS, routeName.toJS, params.jsify());
+ } else {
+ _.callMethod('replace'.toJS, routeName.toJS);
+ }
+ }
+
+ /// Check if can go back
+ bool canGoBack() {
+ final result = _.callMethod('canGoBack'.toJS);
+ return result?.toDart ?? false;
+ }
+
+ /// Set navigation options
+ void setOptions(Map options) =>
+ _.callMethod('setOptions'.toJS, options.jsify());
+}
+
+/// Route prop type (passed to screen components)
+extension type RouteProp._(JSObject _) implements JSObject {
+ /// Route key
+ String get key => switch (_['key']) {
+ final JSString s => s.toDart,
+ _ => '',
+ };
+
+ /// Route name
+ String get name => switch (_['name']) {
+ final JSString s => s.toDart,
+ _ => '',
+ };
+
+ /// Route params as JSObject
+ JSObject? get params => switch (_['params']) {
+ final JSObject o => o,
+ _ => null,
+ };
+
+ /// Get a typed parameter
+ T? getParam(String paramKey) {
+ final p = params;
+ if (p == null) return null;
+ final value = p[paramKey];
+ if (value == null) return null;
+ return value.dartify() as T?;
+ }
+}
+
+/// Screen component props (navigation + route)
+typedef ScreenProps = ({
+ NavigationProp navigation,
+ RouteProp route,
+});
+
+/// Extract ScreenProps from JSObject props passed to screen components.
+///
+/// Returns null if props don't contain valid navigation/route objects.
+ScreenProps? extractScreenProps(JSObject props) {
+ final nav = props['navigation'];
+ final route = props['route'];
+ return switch ((nav, route)) {
+ (final JSObject n, final JSObject r) => (
+ navigation: NavigationProp._(n),
+ route: RouteProp._(r),
+ ),
+ _ => null,
+ };
+}
diff --git a/packages/dart_node_react_native/lib/src/npm_component.dart b/packages/dart_node_react_native/lib/src/npm_component.dart
new file mode 100644
index 0000000..44eea29
--- /dev/null
+++ b/packages/dart_node_react_native/lib/src/npm_component.dart
@@ -0,0 +1,509 @@
+/// Generic npm React/React Native component wrapper.
+///
+/// Provides a flexible way to use ANY npm package's React components
+/// without needing to write manual Dart wrappers for each one.
+///
+/// ## Basic Usage
+///
+/// ```dart
+/// // Use any npm component by package name and component name
+/// final button = npmComponent(
+/// 'react-native-paper',
+/// 'Button',
+/// props: {'mode': 'contained', 'onPress': handlePress},
+/// child: 'Click Me'.toJS,
+/// );
+/// ```
+///
+/// ## Nested Components
+///
+/// For components accessed via a namespace (like Stack.Navigator):
+///
+/// ```dart
+/// final navigator = npmComponent(
+/// '@react-navigation/stack',
+/// 'createStackNavigator',
+/// );
+/// ```
+///
+/// ## Default Exports
+///
+/// For packages that use default exports:
+///
+/// ```dart
+/// final component = npmComponent(
+/// 'some-package',
+/// 'default',
+/// );
+/// ```
+library;
+
+import 'dart:js_interop';
+import 'dart:js_interop_unsafe';
+
+import 'package:dart_node_core/dart_node_core.dart';
+import 'package:dart_node_react/dart_node_react.dart';
+import 'package:nadz/nadz.dart';
+
+/// Extension type for npm component elements
+extension type NpmComponentElement._(JSObject _) implements ReactElement {
+ /// Create from a JSObject
+ factory NpmComponentElement.fromJS(JSObject js) = NpmComponentElement._;
+}
+
+/// Cache for loaded npm modules to avoid repeated require() calls
+final Map _moduleCache = {};
+
+/// Load an npm module (with caching)
+Result loadNpmModule(String packageName) {
+ // Check cache first
+ if (_moduleCache.containsKey(packageName)) {
+ return Success(_moduleCache[packageName]!);
+ }
+
+ try {
+ final module = requireModule(packageName);
+ if (module case final JSObject obj) {
+ _moduleCache[packageName] = obj;
+ return Success(obj);
+ }
+ return Error('Module $packageName did not return an object');
+ } on Object catch (e) {
+ return Error('Failed to load module $packageName: $e');
+ }
+}
+
+/// Get a component from a loaded module.
+///
+/// Handles:
+/// - Named exports: `module.ComponentName`
+/// - Default exports: `module.default` or `module.default.ComponentName`
+/// - Nested paths: `module.Stack.Navigator` via dot notation
+Result getComponentFromModule(
+ JSObject module,
+ String componentPath,
+) {
+ // Handle nested paths like "Stack.Navigator"
+ final parts = componentPath.split('.');
+ JSAny? current = module;
+
+ for (final part in parts) {
+ if (current == null) {
+ return Error('Component path $componentPath not found (null at $part)');
+ }
+
+ final currentObj = current as JSObject;
+
+ // Try direct access first
+ final direct = currentObj[part];
+ if (direct != null) {
+ current = direct;
+ continue;
+ }
+
+ // For the first part, try via default export
+ if (part == parts.first) {
+ final defaultExport = currentObj['default'];
+ if (defaultExport != null) {
+ final defaultObj = defaultExport as JSObject;
+ final viaDefault = defaultObj[part];
+ if (viaDefault != null) {
+ current = viaDefault;
+ continue;
+ }
+ // If asking for 'default' specifically, return the default export
+ if (part == 'default') {
+ current = defaultExport;
+ continue;
+ }
+ }
+ }
+
+ return Error(
+ 'Component $part not found in module (path: $componentPath)',
+ );
+ }
+
+ return switch (current) {
+ null => Error('Component $componentPath resolved to null'),
+ final JSAny c => Success(c),
+ };
+}
+
+/// Create a React element from any npm package's component.
+///
+/// [packageName] - The npm package name (e.g., 'react-native-paper')
+/// [componentPath] - The component name or path
+/// (e.g., 'Button' or 'Stack.Navigator')
+/// [props] - Optional props map
+/// [children] - Optional list of child elements
+/// [child] - Optional single child (text or element)
+///
+/// Returns [NpmComponentElement] on success, throws [StateError] on failure.
+///
+/// ## Examples
+///
+/// Basic usage:
+/// ```dart
+/// final button = npmComponent(
+/// 'react-native-paper',
+/// 'Button',
+/// props: {'mode': 'contained'},
+/// child: 'Click'.toJS,
+/// );
+/// ```
+///
+/// With children:
+/// ```dart
+/// final container = npmComponent(
+/// '@react-navigation/native',
+/// 'NavigationContainer',
+/// children: [navigator],
+/// );
+/// ```
+NpmComponentElement npmComponent(
+ String packageName,
+ String componentPath, {
+ Map? props,
+ List? children,
+ JSAny? child,
+}) {
+ final moduleResult = loadNpmModule(packageName);
+ final module = switch (moduleResult) {
+ Success(:final value) => value,
+ Error(:final error) => throw StateError(error),
+ };
+
+ final componentResult = getComponentFromModule(module, componentPath);
+ final component = switch (componentResult) {
+ Success(:final value) => value,
+ Error(:final error) => throw StateError(error),
+ };
+
+ final jsProps = (props != null) ? createProps(props) : null;
+
+ final element =
+ (children != null && children.isNotEmpty)
+ ? createElementWithChildren(component, jsProps, children)
+ : (child != null)
+ ? createElement(component, jsProps, child)
+ : createElement(component, jsProps);
+
+ return NpmComponentElement.fromJS(element);
+}
+
+/// Safe version of [npmComponent] that returns a Result instead of throwing.
+Result npmComponentSafe(
+ String packageName,
+ String componentPath, {
+ Map? props,
+ List? children,
+ JSAny? child,
+}) {
+ final moduleResult = loadNpmModule(packageName);
+ if (moduleResult case Error(:final error)) {
+ return Error(error);
+ }
+ final module = (moduleResult as Success).value;
+
+ final componentResult = getComponentFromModule(module, componentPath);
+ if (componentResult case Error(:final error)) {
+ return Error(error);
+ }
+ final component = (componentResult as Success).value;
+
+ try {
+ final jsProps = (props != null) ? createProps(props) : null;
+
+ final element =
+ (children != null && children.isNotEmpty)
+ ? createElementWithChildren(component, jsProps, children)
+ : (child != null)
+ ? createElement(component, jsProps, child)
+ : createElement(component, jsProps);
+
+ return Success(NpmComponentElement.fromJS(element));
+ } on Object catch (e) {
+ return Error('Failed to create element: $e');
+ }
+}
+
+/// Call a factory function from an npm module.
+///
+/// Useful for packages that export factory functions rather than components,
+/// like `createStackNavigator` from @react-navigation/stack.
+///
+/// ```dart
+/// final stackNav = npmFactory(
+/// '@react-navigation/stack',
+/// 'createStackNavigator',
+/// );
+/// final Stack = stackNav.call();
+/// ```
+Result npmFactory(
+ String packageName,
+ String functionPath,
+) {
+ final moduleResult = loadNpmModule(packageName);
+ if (moduleResult case Error(:final error)) {
+ return Error(error);
+ }
+ final module = (moduleResult as Success).value;
+
+ final componentResult = getComponentFromModule(module, functionPath);
+ return switch (componentResult) {
+ Success(:final value) => Success(value as T),
+ Error(:final error) => Error(error),
+ };
+}
+
+/// Clear the module cache.
+///
+/// Useful for testing or when you need to force reload modules.
+void clearNpmModuleCache() {
+ _moduleCache.clear();
+}
+
+/// Check if a module is cached.
+bool isModuleCached(String packageName) =>
+ _moduleCache.containsKey(packageName);
+
+// =============================================================================
+// TYPED EXTENSION TYPES
+// =============================================================================
+// Zero-cost wrappers over NpmComponentElement for type safety.
+// Use these when you want IDE autocomplete and type checking.
+// Start loose with npmComponent(), add types WHERE YOU NEED THEM.
+
+/// Typed element for Paper Button - zero-cost wrapper
+extension type PaperButton._(NpmComponentElement _) implements ReactElement {
+ /// Create from NpmComponentElement
+ factory PaperButton._create(NpmComponentElement e) = PaperButton._;
+}
+
+/// Typed element for Paper FAB - zero-cost wrapper
+extension type PaperFAB._(NpmComponentElement _) implements ReactElement {
+ /// Create from NpmComponentElement
+ factory PaperFAB._create(NpmComponentElement e) = PaperFAB._;
+}
+
+/// Typed element for Paper Card - zero-cost wrapper
+extension type PaperCard._(NpmComponentElement _) implements ReactElement {
+ /// Create from NpmComponentElement
+ factory PaperCard._create(NpmComponentElement e) = PaperCard._;
+}
+
+/// Typed element for Paper TextInput - zero-cost wrapper
+extension type PaperTextInput._(NpmComponentElement _) implements ReactElement {
+ /// Create from NpmComponentElement
+ factory PaperTextInput._create(NpmComponentElement e) = PaperTextInput._;
+}
+
+// =============================================================================
+// TYPED PROPS (typedef records)
+// =============================================================================
+// Named fields give full IDE autocomplete. Add only the props you use.
+
+/// Props for Paper Button
+typedef PaperButtonProps = ({
+ String? mode, // 'text' | 'outlined' | 'contained' | 'elevated'
+ bool? disabled,
+ bool? loading,
+ String? buttonColor,
+ String? textColor,
+ Map? style,
+ Map? contentStyle,
+ Map? labelStyle,
+});
+
+/// Props for Paper FAB (Floating Action Button)
+typedef PaperFABProps = ({
+ String? icon,
+ String? label,
+ bool? small,
+ bool? visible,
+ bool? loading,
+ bool? disabled,
+ String? color,
+ String? customColor,
+ Map? style,
+});
+
+/// Props for Paper Card
+typedef PaperCardProps = ({
+ String? mode, // 'elevated' | 'outlined' | 'contained'
+ Map? style,
+ Map? contentStyle,
+});
+
+/// Props for Paper TextInput
+typedef PaperTextInputProps = ({
+ String? label,
+ String? placeholder,
+ String? mode, // 'flat' | 'outlined'
+ bool? disabled,
+ bool? editable,
+ bool? secureTextEntry,
+ String? value,
+ String? activeOutlineColor,
+ String? activeUnderlineColor,
+ String? textColor,
+ Map? style,
+});
+
+// =============================================================================
+// TYPED FACTORY FUNCTIONS
+// =============================================================================
+// Build props Map and call npmComponent(). Type-safe with autocomplete!
+
+/// Create a Paper Button with full type safety.
+///
+/// ```dart
+/// final btn = paperButton(
+/// props: (mode: 'contained', disabled: false, loading: null,
+/// buttonColor: '#6200EE', textColor: null, style: null,
+/// contentStyle: null, labelStyle: null),
+/// onPress: () => print('pressed'),
+/// label: 'Click Me',
+/// );
+/// ```
+PaperButton paperButton({
+ PaperButtonProps? props,
+ void Function()? onPress,
+ String? label,
+}) {
+ final p = {};
+ if (props != null) {
+ if (props.mode != null) p['mode'] = props.mode;
+ if (props.disabled != null) p['disabled'] = props.disabled;
+ if (props.loading != null) p['loading'] = props.loading;
+ if (props.buttonColor != null) p['buttonColor'] = props.buttonColor;
+ if (props.textColor != null) p['textColor'] = props.textColor;
+ if (props.style != null) p['style'] = props.style;
+ if (props.contentStyle != null) p['contentStyle'] = props.contentStyle;
+ if (props.labelStyle != null) p['labelStyle'] = props.labelStyle;
+ }
+ if (onPress != null) p['onPress'] = onPress;
+
+ return PaperButton._create(
+ npmComponent(
+ 'react-native-paper',
+ 'Button',
+ props: p.isEmpty ? null : p,
+ child: label?.toJS,
+ ),
+ );
+}
+
+/// Create a Paper FAB with full type safety.
+///
+/// ```dart
+/// final fab = paperFAB(
+/// props: (icon: 'plus', label: null, small: false, visible: true,
+/// loading: null, disabled: null, color: null,
+/// customColor: '#6200EE', style: null),
+/// onPress: handleAdd,
+/// );
+/// ```
+PaperFAB paperFAB({
+ PaperFABProps? props,
+ void Function()? onPress,
+}) {
+ final p = {};
+ if (props != null) {
+ if (props.icon != null) p['icon'] = props.icon;
+ if (props.label != null) p['label'] = props.label;
+ if (props.small != null) p['small'] = props.small;
+ if (props.visible != null) p['visible'] = props.visible;
+ if (props.loading != null) p['loading'] = props.loading;
+ if (props.disabled != null) p['disabled'] = props.disabled;
+ if (props.color != null) p['color'] = props.color;
+ if (props.customColor != null) p['customColor'] = props.customColor;
+ if (props.style != null) p['style'] = props.style;
+ }
+ if (onPress != null) p['onPress'] = onPress;
+
+ return PaperFAB._create(
+ npmComponent('react-native-paper', 'FAB', props: p.isEmpty ? null : p),
+ );
+}
+
+/// Create a Paper Card with full type safety.
+///
+/// ```dart
+/// final card = paperCard(
+/// props: (mode: 'elevated', style: null, contentStyle: null),
+/// children: [cardTitle, cardContent, cardActions],
+/// );
+/// ```
+PaperCard paperCard({
+ PaperCardProps? props,
+ void Function()? onPress,
+ List? children,
+}) {
+ final p = {};
+ if (props != null) {
+ if (props.mode != null) p['mode'] = props.mode;
+ if (props.style != null) p['style'] = props.style;
+ if (props.contentStyle != null) p['contentStyle'] = props.contentStyle;
+ }
+ if (onPress != null) p['onPress'] = onPress;
+
+ return PaperCard._create(
+ npmComponent(
+ 'react-native-paper',
+ 'Card',
+ props: p.isEmpty ? null : p,
+ children: children,
+ ),
+ );
+}
+
+/// Create a Paper TextInput with full type safety.
+///
+/// ```dart
+/// final input = paperTextInput(
+/// props: (label: 'Email', placeholder: 'Enter email',
+/// mode: 'outlined', disabled: null, editable: null,
+/// secureTextEntry: null, value: null,
+/// activeOutlineColor: '#6200EE',
+/// activeUnderlineColor: null, textColor: null, style: null),
+/// onChangeText: (text) => setState(text),
+/// );
+/// ```
+PaperTextInput paperTextInput({
+ PaperTextInputProps? props,
+ void Function(String)? onChangeText,
+ String? value,
+}) {
+ final p = {};
+ if (props != null) {
+ if (props.label != null) p['label'] = props.label;
+ if (props.placeholder != null) p['placeholder'] = props.placeholder;
+ if (props.mode != null) p['mode'] = props.mode;
+ if (props.disabled != null) p['disabled'] = props.disabled;
+ if (props.editable != null) p['editable'] = props.editable;
+ if (props.secureTextEntry != null) {
+ p['secureTextEntry'] = props.secureTextEntry;
+ }
+ if (props.value != null) p['value'] = props.value;
+ if (props.activeOutlineColor != null) {
+ p['activeOutlineColor'] = props.activeOutlineColor;
+ }
+ if (props.activeUnderlineColor != null) {
+ p['activeUnderlineColor'] = props.activeUnderlineColor;
+ }
+ if (props.textColor != null) p['textColor'] = props.textColor;
+ if (props.style != null) p['style'] = props.style;
+ }
+ if (onChangeText != null) p['onChangeText'] = onChangeText;
+ if (value != null) p['value'] = value;
+
+ return PaperTextInput._create(
+ npmComponent(
+ 'react-native-paper',
+ 'TextInput',
+ props: p.isEmpty ? null : p,
+ ),
+ );
+}
diff --git a/packages/dart_node_react_native/pubspec.lock b/packages/dart_node_react_native/pubspec.lock
index bd25e43..33dcb9c 100644
--- a/packages/dart_node_react_native/pubspec.lock
+++ b/packages/dart_node_react_native/pubspec.lock
@@ -102,7 +102,7 @@ packages:
path: "../dart_node_coverage"
relative: true
source: path
- version: "0.1.0"
+ version: "0.9.0-beta"
dart_node_react:
dependency: "direct main"
description:
diff --git a/packages/dart_node_react_native/test/npm_component_test.dart b/packages/dart_node_react_native/test/npm_component_test.dart
new file mode 100644
index 0000000..d59abe9
--- /dev/null
+++ b/packages/dart_node_react_native/test/npm_component_test.dart
@@ -0,0 +1,168 @@
+/// Tests proving npmComponent() can use ANY npm package directly.
+///
+/// These tests demonstrate that we can drop npm packages right in
+/// and use them exactly like TypeScript - no wrapper code needed!
+@TestOn('js')
+library;
+
+import 'dart:js_interop';
+
+import 'package:dart_node_react/dart_node_react.dart';
+import 'package:dart_node_react_native/dart_node_react_native.dart';
+import 'package:nadz/nadz.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('loadNpmModule loads react successfully', () {
+ final result = loadNpmModule('react');
+ expect(result.isSuccess, isTrue);
+ });
+
+ test('loadNpmModule loads react-native successfully', () {
+ final result = loadNpmModule('react-native');
+ expect(result.isSuccess, isTrue);
+ });
+
+ test('loadNpmModule caches modules', () {
+ clearNpmModuleCache();
+ expect(isModuleCached('react'), isFalse);
+
+ loadNpmModule('react');
+ expect(isModuleCached('react'), isTrue);
+
+ // Second call uses cache
+ final result2 = loadNpmModule('react');
+ expect(result2.isSuccess, isTrue);
+ });
+
+ test('loadNpmModule returns error for nonexistent package', () {
+ final result = loadNpmModule('nonexistent-package-xyz-123');
+ expect(result.isSuccess, isFalse);
+ });
+
+ test('getComponentFromModule gets View from react-native', () {
+ final moduleResult = loadNpmModule('react-native');
+ final module = switch (moduleResult) {
+ Success(:final value) => value,
+ Error(:final error) => throw StateError(error),
+ };
+
+ final viewResult = getComponentFromModule(module, 'View');
+ expect(viewResult.isSuccess, isTrue);
+ });
+
+ test('getComponentFromModule gets Text from react-native', () {
+ final moduleResult = loadNpmModule('react-native');
+ final module = switch (moduleResult) {
+ Success(:final value) => value,
+ Error(:final error) => throw StateError(error),
+ };
+
+ final textResult = getComponentFromModule(module, 'Text');
+ expect(textResult.isSuccess, isTrue);
+ });
+
+ test('getComponentFromModule returns error for nonexistent component', () {
+ final moduleResult = loadNpmModule('react-native');
+ final module = switch (moduleResult) {
+ Success(:final value) => value,
+ Error(:final error) => throw StateError(error),
+ };
+
+ final result = getComponentFromModule(module, 'NonExistentComponent');
+ expect(result.isSuccess, isFalse);
+ });
+
+ test('npmComponent creates View element from react-native', () {
+ final element = npmComponent(
+ 'react-native',
+ 'View',
+ props: {'style': {'flex': 1}},
+ );
+ expect(element, isNotNull);
+ });
+
+ test('npmComponent creates Text element with child', () {
+ final element = npmComponent(
+ 'react-native',
+ 'Text',
+ child: 'Hello World'.toJS,
+ );
+ expect(element, isNotNull);
+ });
+
+ test('npmComponent creates element with children list', () {
+ final child1 = npmComponent('react-native', 'Text', child: 'One'.toJS);
+ final child2 = npmComponent('react-native', 'Text', child: 'Two'.toJS);
+
+ final parent = npmComponent(
+ 'react-native',
+ 'View',
+ children: [child1, child2],
+ );
+ expect(parent, isNotNull);
+ });
+
+ test('npmComponentSafe returns Success for valid component', () {
+ final result = npmComponentSafe(
+ 'react-native',
+ 'View',
+ props: {'testID': 'test-view'},
+ );
+ expect(result.isSuccess, isTrue);
+ });
+
+ test('npmComponentSafe returns Error for invalid package', () {
+ final result = npmComponentSafe(
+ 'nonexistent-package-xyz',
+ 'Component',
+ );
+ expect(result.isSuccess, isFalse);
+ });
+
+ test('npmFactory gets createElement from react', () {
+ final result = npmFactory('react', 'createElement');
+ expect(result.isSuccess, isTrue);
+ });
+
+ test('clearNpmModuleCache clears all cached modules', () {
+ loadNpmModule('react');
+ expect(isModuleCached('react'), isTrue);
+
+ clearNpmModuleCache();
+ expect(isModuleCached('react'), isFalse);
+ });
+
+ test('npmComponent works with nested props', () {
+ final element = npmComponent(
+ 'react-native',
+ 'View',
+ props: {
+ 'style': {
+ 'flex': 1,
+ 'backgroundColor': '#FFFFFF',
+ 'padding': 16,
+ 'margin': {'top': 10, 'bottom': 10},
+ },
+ 'testID': 'nested-props-view',
+ },
+ );
+ expect(element, isNotNull);
+ });
+
+ test('npmComponent works with callback props', () {
+ final element = npmComponent(
+ 'react-native',
+ 'TouchableOpacity',
+ props: {'onPress': () {}},
+ children: [npmComponent('react-native', 'Text', child: 'Press'.toJS)],
+ );
+ expect(element, isNotNull);
+ });
+
+ test('NpmComponentElement implements ReactElement', () {
+ final element = npmComponent('react-native', 'View');
+ // NpmComponentElement should be usable as ReactElement
+ expect(element, isA());
+ });
+}
diff --git a/packages/dart_node_react_native/test/react_native_test.dart b/packages/dart_node_react_native/test/react_native_test.dart
index a45328d..e3de789 100644
--- a/packages/dart_node_react_native/test/react_native_test.dart
+++ b/packages/dart_node_react_native/test/react_native_test.dart
@@ -2,10 +2,55 @@
/// Actual React Native runtime requires Expo/RN environment.
library;
+import 'dart:js_interop';
+
import 'package:dart_node_coverage/dart_node_coverage.dart';
+import 'package:dart_node_react/dart_node_react.dart' show ReactElement;
import 'package:dart_node_react_native/dart_node_react_native.dart';
import 'package:test/test.dart';
+// =============================================================================
+// TYPED WRAPPER PATTERN - demonstrates the pattern from NPM_USAGE.md
+// =============================================================================
+
+/// Step 1: Extension type - zero-cost wrapper over NpmComponentElement
+extension type TestPaperButton._(NpmComponentElement _)
+ implements ReactElement {
+ factory TestPaperButton._create(NpmComponentElement e) = TestPaperButton._;
+}
+
+/// Step 2: Props typedef - named record with typed props
+typedef TestPaperButtonProps = ({
+ String? mode,
+ bool? disabled,
+ bool? loading,
+ String? buttonColor,
+});
+
+/// Step 3: Factory function - builds props Map and calls npmComponent
+TestPaperButton testPaperButton({
+ TestPaperButtonProps? props,
+ void Function()? onPress,
+ String? label,
+}) {
+ final p = {};
+ if (props != null) {
+ if (props.mode != null) p['mode'] = props.mode;
+ if (props.disabled != null) p['disabled'] = props.disabled;
+ if (props.loading != null) p['loading'] = props.loading;
+ if (props.buttonColor != null) p['buttonColor'] = props.buttonColor;
+ }
+ if (onPress != null) p['onPress'] = onPress;
+
+ return TestPaperButton._create(
+ npmComponent('react-native-paper', 'Button', props: p, child: label?.toJS),
+ );
+}
+
+// =============================================================================
+// END TYPED WRAPPER PATTERN
+// =============================================================================
+
void main() {
setUp(initCoverage);
tearDownAll(() => writeCoverageFile('coverage/coverage.json'));
@@ -181,4 +226,377 @@ void main() {
expect(reg, isNull);
});
});
+
+ group('npm component - direct usage API', () {
+ test('loadNpmModule function exists', () {
+ expect(loadNpmModule, isA());
+ });
+
+ test('getComponentFromModule function exists', () {
+ expect(getComponentFromModule, isA());
+ });
+
+ test('npmComponent function exists', () {
+ expect(npmComponent, isA());
+ });
+
+ test('npmComponentSafe function exists', () {
+ expect(npmComponentSafe, isA());
+ });
+
+ test('npmFactory function exists', () {
+ expect(npmFactory, isA());
+ });
+
+ test('clearNpmModuleCache function exists', () {
+ expect(clearNpmModuleCache, isA());
+ });
+
+ test('isModuleCached function exists', () {
+ expect(isModuleCached, isA());
+ });
+
+ test('NpmComponentElement type exists', () {
+ NpmComponentElement? element;
+ expect(element, isNull);
+ });
+ });
+
+ group('typed extension types - type safety', () {
+ // These tests verify type hierarchy at compile-time
+ // The type assignments would fail compilation if types were wrong
+ test('NpmComponentElement implements ReactElement', () {
+ // Compile-time proof: can assign to ReactElement variable
+ const NpmComponentElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNViewElement implements ReactElement', () {
+ const RNViewElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNTextElement implements ReactElement', () {
+ const RNTextElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNTextInputElement implements ReactElement', () {
+ const RNTextInputElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNTouchableOpacityElement implements ReactElement', () {
+ const RNTouchableOpacityElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNButtonElement implements ReactElement', () {
+ const RNButtonElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNScrollViewElement implements ReactElement', () {
+ const RNScrollViewElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNSafeAreaViewElement implements ReactElement', () {
+ const RNSafeAreaViewElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNActivityIndicatorElement implements ReactElement', () {
+ const RNActivityIndicatorElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNFlatListElement implements ReactElement', () {
+ const RNFlatListElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNImageElement implements ReactElement', () {
+ const RNImageElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('RNSwitchElement implements ReactElement', () {
+ const RNSwitchElement? element = null;
+ const ReactElement? asReact = element;
+ expect(asReact, isNull);
+ });
+
+ test('typed elements assignable to List', () {
+ // Compile-time: typed elements can be added to ReactElement list
+ final elements = [];
+ expect(elements, isEmpty);
+ });
+ });
+
+ group('navigation types - type safety', () {
+ test('NavigationProp type exists', () {
+ NavigationProp? nav;
+ expect(nav, isNull);
+ });
+
+ test('RouteProp type exists', () {
+ RouteProp? route;
+ expect(route, isNull);
+ });
+
+ test('ScreenProps typedef exists', () {
+ ScreenProps? props;
+ expect(props, isNull);
+ });
+
+ test('extractScreenProps function exists', () {
+ expect(extractScreenProps, isA());
+ });
+ });
+
+ group('builder functions return typed elements', () {
+ test('view returns RNViewElement', () {
+ // Compile-time type check proves return type
+ expect(view, isA());
+ });
+
+ test('text returns RNTextElement', () {
+ expect(text, isA());
+ });
+
+ test('textInput returns RNTextInputElement', () {
+ expect(textInput, isA());
+ });
+
+ test('touchableOpacity returns RNTouchableOpacityElement', () {
+ expect(touchableOpacity, isA());
+ });
+
+ test('rnButton returns RNButtonElement', () {
+ expect(rnButton, isA());
+ });
+
+ test('scrollView returns RNScrollViewElement', () {
+ expect(scrollView, isA());
+ });
+
+ test('safeAreaView returns RNSafeAreaViewElement', () {
+ expect(safeAreaView, isA());
+ });
+
+ test('activityIndicator returns RNActivityIndicatorElement', () {
+ expect(activityIndicator, isA