react-native-web-serial-api 0.0.3 → 0.1.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.
Files changed (119) hide show
  1. package/README.md +23 -0
  2. package/TESTING.md +301 -0
  3. package/android/build.gradle +2 -2
  4. package/lib/commonjs/UsbSerial.js +58 -26
  5. package/lib/commonjs/UsbSerial.js.map +1 -1
  6. package/lib/commonjs/WebSerial.js +169 -57
  7. package/lib/commonjs/WebSerial.js.map +1 -1
  8. package/lib/commonjs/index.js +13 -1
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/lib/dom-exception.js +176 -0
  11. package/lib/commonjs/lib/dom-exception.js.map +1 -0
  12. package/lib/commonjs/lib/event-target.js +138 -0
  13. package/lib/commonjs/lib/event-target.js.map +1 -0
  14. package/lib/commonjs/lib/promise.js +23 -0
  15. package/lib/commonjs/lib/promise.js.map +1 -0
  16. package/lib/commonjs/testing/index.js +70 -0
  17. package/lib/commonjs/testing/index.js.map +1 -0
  18. package/lib/commonjs/testing/install.js +54 -0
  19. package/lib/commonjs/testing/install.js.map +1 -0
  20. package/lib/commonjs/testing/serial-device.js +164 -0
  21. package/lib/commonjs/testing/serial-device.js.map +1 -0
  22. package/lib/commonjs/testing/virtual-serial.js +615 -0
  23. package/lib/commonjs/testing/virtual-serial.js.map +1 -0
  24. package/lib/commonjs/transport.js +61 -0
  25. package/lib/commonjs/transport.js.map +1 -0
  26. package/lib/typescript/src/UsbSerial.d.ts +24 -67
  27. package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
  28. package/lib/typescript/src/WebSerial.d.ts +11 -2
  29. package/lib/typescript/src/WebSerial.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +2 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
  33. package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
  34. package/lib/typescript/src/lib/event-target.d.ts +53 -0
  35. package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
  36. package/lib/typescript/src/lib/promise.d.ts +11 -0
  37. package/lib/typescript/src/lib/promise.d.ts.map +1 -0
  38. package/lib/typescript/src/testing/index.d.ts +23 -0
  39. package/lib/typescript/src/testing/index.d.ts.map +1 -0
  40. package/lib/typescript/src/testing/install.d.ts +25 -0
  41. package/lib/typescript/src/testing/install.d.ts.map +1 -0
  42. package/lib/typescript/src/testing/serial-device.d.ts +127 -0
  43. package/lib/typescript/src/testing/serial-device.d.ts.map +1 -0
  44. package/lib/typescript/src/testing/virtual-serial.d.ts +205 -0
  45. package/lib/typescript/src/testing/virtual-serial.d.ts.map +1 -0
  46. package/lib/typescript/src/transport.d.ts +131 -0
  47. package/lib/typescript/src/transport.d.ts.map +1 -0
  48. package/package.json +38 -2
  49. package/src/UsbSerial.ts +65 -90
  50. package/src/WebSerial.ts +227 -88
  51. package/src/index.ts +2 -7
  52. package/src/lib/dom-exception.ts +129 -60
  53. package/src/lib/event-target.ts +46 -21
  54. package/src/lib/promise.ts +7 -7
  55. package/src/testing/index.ts +42 -0
  56. package/src/testing/install.ts +65 -0
  57. package/src/testing/serial-device.ts +193 -0
  58. package/src/testing/virtual-serial.ts +801 -0
  59. package/src/transport.ts +200 -0
  60. package/babel.config.js +0 -3
  61. package/biome.json +0 -35
  62. package/example/.watchmanconfig +0 -1
  63. package/example/App.tsx +0 -71
  64. package/example/__tests__/App.test.tsx +0 -16
  65. package/example/__tests__/connectEvents.test.tsx +0 -81
  66. package/example/__tests__/getPorts.test.tsx +0 -140
  67. package/example/android/app/build.gradle +0 -120
  68. package/example/android/app/debug.keystore +0 -0
  69. package/example/android/app/proguard-rules.pro +0 -10
  70. package/example/android/app/src/debug/AndroidManifest.xml +0 -9
  71. package/example/android/app/src/main/AndroidManifest.xml +0 -38
  72. package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
  73. package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
  74. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
  75. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  76. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  77. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  78. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  79. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  80. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  81. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  82. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  83. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  84. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  85. package/example/android/app/src/main/res/values/strings.xml +0 -3
  86. package/example/android/app/src/main/res/values/styles.xml +0 -9
  87. package/example/android/build.gradle +0 -22
  88. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  89. package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  90. package/example/android/gradle.properties +0 -47
  91. package/example/android/gradlew +0 -252
  92. package/example/android/gradlew.bat +0 -94
  93. package/example/android/settings.gradle +0 -6
  94. package/example/app.json +0 -4
  95. package/example/babel.config.js +0 -21
  96. package/example/biome.json +0 -47
  97. package/example/deploy.sh +0 -11
  98. package/example/index.html +0 -26
  99. package/example/index.js +0 -9
  100. package/example/index.web.js +0 -8
  101. package/example/jest.config.js +0 -12
  102. package/example/metro.config.js +0 -58
  103. package/example/package-lock.json +0 -14510
  104. package/example/package.json +0 -48
  105. package/example/react-native.config.js +0 -17
  106. package/example/src/components/AppBar.tsx +0 -73
  107. package/example/src/components/Menu.tsx +0 -90
  108. package/example/src/components/SingleChoiceDialog.tsx +0 -120
  109. package/example/src/screens/ConnectScreen.tsx +0 -195
  110. package/example/src/screens/DevicesScreen.tsx +0 -252
  111. package/example/src/screens/TerminalScreen.tsx +0 -572
  112. package/example/src/settings.ts +0 -43
  113. package/example/src/theme.ts +0 -19
  114. package/example/src/util/TextUtil.ts +0 -129
  115. package/example/tsconfig.json +0 -10
  116. package/example/vite.config.mjs +0 -55
  117. package/scripts/deploy-release.sh +0 -127
  118. package/tsconfig.build.json +0 -7
  119. package/tsconfig.json +0 -20
@@ -33,7 +33,7 @@ const DOM_EXCEPTION_CODES: Record<string, number> = {
33
33
  };
34
34
 
35
35
  const DOM_EXCEPTION_NAMES: Record<number, string> = Object.fromEntries(
36
- Object.entries(DOM_EXCEPTION_CODES).map(([name, code]) => [code, name])
36
+ Object.entries(DOM_EXCEPTION_CODES).map(([name, code]) => [code, name]),
37
37
  );
38
38
 
39
39
  export interface DOMExceptionConstructor {
@@ -78,74 +78,134 @@ class DOMExceptionPolyfill extends Error {
78
78
  readonly name: string;
79
79
 
80
80
  // --- Static legacy-code constants ---
81
- static readonly INDEX_SIZE_ERR = 1 as const;
82
- static readonly DOMSTRING_SIZE_ERR = 2 as const;
83
- static readonly HIERARCHY_REQUEST_ERR = 3 as const;
84
- static readonly WRONG_DOCUMENT_ERR = 4 as const;
85
- static readonly INVALID_CHARACTER_ERR = 5 as const;
86
- static readonly NO_DATA_ALLOWED_ERR = 6 as const;
81
+ static readonly INDEX_SIZE_ERR = 1 as const;
82
+ static readonly DOMSTRING_SIZE_ERR = 2 as const;
83
+ static readonly HIERARCHY_REQUEST_ERR = 3 as const;
84
+ static readonly WRONG_DOCUMENT_ERR = 4 as const;
85
+ static readonly INVALID_CHARACTER_ERR = 5 as const;
86
+ static readonly NO_DATA_ALLOWED_ERR = 6 as const;
87
87
  static readonly NO_MODIFICATION_ALLOWED_ERR = 7 as const;
88
- static readonly NOT_FOUND_ERR = 8 as const;
89
- static readonly NOT_SUPPORTED_ERR = 9 as const;
90
- static readonly INUSE_ATTRIBUTE_ERR = 10 as const;
91
- static readonly INVALID_STATE_ERR = 11 as const;
92
- static readonly SYNTAX_ERR = 12 as const;
93
- static readonly INVALID_MODIFICATION_ERR = 13 as const;
94
- static readonly NAMESPACE_ERR = 14 as const;
95
- static readonly INVALID_ACCESS_ERR = 15 as const;
96
- static readonly VALIDATION_ERR = 16 as const;
97
- static readonly TYPE_MISMATCH_ERR = 17 as const;
98
- static readonly SECURITY_ERR = 18 as const;
99
- static readonly NETWORK_ERR = 19 as const;
100
- static readonly ABORT_ERR = 20 as const;
101
- static readonly URL_MISMATCH_ERR = 21 as const;
102
- static readonly QUOTA_EXCEEDED_ERR = 22 as const;
103
- static readonly TIMEOUT_ERR = 23 as const;
104
- static readonly INVALID_NODE_TYPE_ERR = 24 as const;
105
- static readonly DATA_CLONE_ERR = 25 as const;
88
+ static readonly NOT_FOUND_ERR = 8 as const;
89
+ static readonly NOT_SUPPORTED_ERR = 9 as const;
90
+ static readonly INUSE_ATTRIBUTE_ERR = 10 as const;
91
+ static readonly INVALID_STATE_ERR = 11 as const;
92
+ static readonly SYNTAX_ERR = 12 as const;
93
+ static readonly INVALID_MODIFICATION_ERR = 13 as const;
94
+ static readonly NAMESPACE_ERR = 14 as const;
95
+ static readonly INVALID_ACCESS_ERR = 15 as const;
96
+ static readonly VALIDATION_ERR = 16 as const;
97
+ static readonly TYPE_MISMATCH_ERR = 17 as const;
98
+ static readonly SECURITY_ERR = 18 as const;
99
+ static readonly NETWORK_ERR = 19 as const;
100
+ static readonly ABORT_ERR = 20 as const;
101
+ static readonly URL_MISMATCH_ERR = 21 as const;
102
+ static readonly QUOTA_EXCEEDED_ERR = 22 as const;
103
+ static readonly TIMEOUT_ERR = 23 as const;
104
+ static readonly INVALID_NODE_TYPE_ERR = 24 as const;
105
+ static readonly DATA_CLONE_ERR = 25 as const;
106
106
 
107
- constructor(message = "", name = "Error") {
107
+ constructor(message = '', name = 'Error') {
108
108
  super(message);
109
109
 
110
110
  // Restore the correct prototype chain when transpiled to ES5
111
111
  Object.setPrototypeOf(this, new.target.prototype);
112
112
 
113
- this.name = name;
113
+ this.name = name;
114
114
  this.message = message;
115
- this.code = DOM_EXCEPTION_CODES[name] ?? 0;
115
+ this.code = DOM_EXCEPTION_CODES[name] ?? 0;
116
116
 
117
117
  // Provide a useful stack trace in V8 / SpiderMonkey
118
- if (typeof (Error as any).captureStackTrace === "function") {
119
- (Error as any).captureStackTrace(this, new.target);
118
+ const ErrorWithStack = Error as ErrorConstructor & {
119
+ captureStackTrace?: (
120
+ target: object,
121
+ constructorOpt?:
122
+ | ((...args: never[]) => unknown)
123
+ | (abstract new (
124
+ ...args: never[]
125
+ ) => unknown),
126
+ ) => void;
127
+ };
128
+ if (typeof ErrorWithStack.captureStackTrace === 'function') {
129
+ ErrorWithStack.captureStackTrace(this, new.target);
120
130
  }
121
131
  }
122
132
 
123
133
  /** Mirror static constants on the prototype (spec §3.1.2) */
124
- get INDEX_SIZE_ERR() { return 1; }
125
- get DOMSTRING_SIZE_ERR() { return 2; }
126
- get HIERARCHY_REQUEST_ERR() { return 3; }
127
- get WRONG_DOCUMENT_ERR() { return 4; }
128
- get INVALID_CHARACTER_ERR() { return 5; }
129
- get NO_DATA_ALLOWED_ERR() { return 6; }
130
- get NO_MODIFICATION_ALLOWED_ERR() { return 7; }
131
- get NOT_FOUND_ERR() { return 8; }
132
- get NOT_SUPPORTED_ERR() { return 9; }
133
- get INUSE_ATTRIBUTE_ERR() { return 10; }
134
- get INVALID_STATE_ERR() { return 11; }
135
- get SYNTAX_ERR() { return 12; }
136
- get INVALID_MODIFICATION_ERR() { return 13; }
137
- get NAMESPACE_ERR() { return 14; }
138
- get INVALID_ACCESS_ERR() { return 15; }
139
- get VALIDATION_ERR() { return 16; }
140
- get TYPE_MISMATCH_ERR() { return 17; }
141
- get SECURITY_ERR() { return 18; }
142
- get NETWORK_ERR() { return 19; }
143
- get ABORT_ERR() { return 20; }
144
- get URL_MISMATCH_ERR() { return 21; }
145
- get QUOTA_EXCEEDED_ERR() { return 22; }
146
- get TIMEOUT_ERR() { return 23; }
147
- get INVALID_NODE_TYPE_ERR() { return 24; }
148
- get DATA_CLONE_ERR() { return 25; }
134
+ get INDEX_SIZE_ERR() {
135
+ return 1;
136
+ }
137
+ get DOMSTRING_SIZE_ERR() {
138
+ return 2;
139
+ }
140
+ get HIERARCHY_REQUEST_ERR() {
141
+ return 3;
142
+ }
143
+ get WRONG_DOCUMENT_ERR() {
144
+ return 4;
145
+ }
146
+ get INVALID_CHARACTER_ERR() {
147
+ return 5;
148
+ }
149
+ get NO_DATA_ALLOWED_ERR() {
150
+ return 6;
151
+ }
152
+ get NO_MODIFICATION_ALLOWED_ERR() {
153
+ return 7;
154
+ }
155
+ get NOT_FOUND_ERR() {
156
+ return 8;
157
+ }
158
+ get NOT_SUPPORTED_ERR() {
159
+ return 9;
160
+ }
161
+ get INUSE_ATTRIBUTE_ERR() {
162
+ return 10;
163
+ }
164
+ get INVALID_STATE_ERR() {
165
+ return 11;
166
+ }
167
+ get SYNTAX_ERR() {
168
+ return 12;
169
+ }
170
+ get INVALID_MODIFICATION_ERR() {
171
+ return 13;
172
+ }
173
+ get NAMESPACE_ERR() {
174
+ return 14;
175
+ }
176
+ get INVALID_ACCESS_ERR() {
177
+ return 15;
178
+ }
179
+ get VALIDATION_ERR() {
180
+ return 16;
181
+ }
182
+ get TYPE_MISMATCH_ERR() {
183
+ return 17;
184
+ }
185
+ get SECURITY_ERR() {
186
+ return 18;
187
+ }
188
+ get NETWORK_ERR() {
189
+ return 19;
190
+ }
191
+ get ABORT_ERR() {
192
+ return 20;
193
+ }
194
+ get URL_MISMATCH_ERR() {
195
+ return 21;
196
+ }
197
+ get QUOTA_EXCEEDED_ERR() {
198
+ return 22;
199
+ }
200
+ get TIMEOUT_ERR() {
201
+ return 23;
202
+ }
203
+ get INVALID_NODE_TYPE_ERR() {
204
+ return 24;
205
+ }
206
+ get DATA_CLONE_ERR() {
207
+ return 25;
208
+ }
149
209
 
150
210
  /** Canonical string representation */
151
211
  toString(): string {
@@ -153,9 +213,18 @@ class DOMExceptionPolyfill extends Error {
153
213
  }
154
214
  }
155
215
 
156
- export const DOMExceptionImpl = (globalThis as any).DOMException
157
- ? (globalThis as any).DOMException as DOMExceptionConstructor
158
- : DOMExceptionPolyfill as DOMExceptionConstructor;
216
+ export const DOMExceptionImpl = (globalThis as Record<string, unknown>)
217
+ .DOMException
218
+ ? ((globalThis as Record<string, unknown>)
219
+ .DOMException as DOMExceptionConstructor)
220
+ : (DOMExceptionPolyfill as DOMExceptionConstructor);
159
221
 
160
- export { DOM_EXCEPTION_CODES, DOM_EXCEPTION_NAMES };
161
- export { DOMExceptionImpl as DOMException };
222
+ export {
223
+ DOM_EXCEPTION_CODES,
224
+ DOM_EXCEPTION_NAMES,
225
+ DOMExceptionImpl as DOMException,
226
+ // Exported for testing: in environments that already provide a global
227
+ // DOMException (Node, modern browsers) the export above is the native one, so
228
+ // the polyfill class would otherwise never run.
229
+ DOMExceptionPolyfill,
230
+ };
@@ -1,4 +1,3 @@
1
-
2
1
  export interface EventInit {
3
2
  bubbles?: boolean;
4
3
  cancelable?: boolean;
@@ -66,7 +65,7 @@ export class Event {
66
65
  }
67
66
  }
68
67
 
69
- type Mutable<T> = { -readonly [P in keyof T]: T[P] };
68
+ type Mutable<T> = {-readonly [P in keyof T]: T[P]};
70
69
 
71
70
  type EventListener = (event: Event) => void;
72
71
 
@@ -77,7 +76,7 @@ interface EventListenerObject {
77
76
  type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
78
77
 
79
78
  type ListenerOptions =
80
- | { once?: boolean; capture?: boolean; passive?: boolean }
79
+ | {once?: boolean; capture?: boolean; passive?: boolean}
81
80
  | boolean
82
81
  | undefined;
83
82
 
@@ -91,7 +90,23 @@ type SecretMap = Record<string, ListenerInfo[]>;
91
90
 
92
91
  const wm = new WeakMap<object, SecretMap>();
93
92
 
94
- function define<T extends object>(target: T, name: string, value: unknown): void {
93
+ // Optional event-propagation parents. An event dispatched on a child also runs
94
+ // its parent's listeners, while event.target stays the original child. This
95
+ // models the W3C "SerialPort's parent is the Serial" relationship, so a
96
+ // connect/disconnect observed on `serial` reports event.target === the
97
+ // SerialPort (per the Web Serial spec and WPT).
98
+ const eventParents = new WeakMap<EventTarget, EventTarget>();
99
+
100
+ /** Make events dispatched on `child` also bubble to `parent`'s listeners. */
101
+ export function setEventParent(child: EventTarget, parent: EventTarget): void {
102
+ eventParents.set(child, parent);
103
+ }
104
+
105
+ function define<T extends object>(
106
+ target: T,
107
+ name: string,
108
+ value: unknown,
109
+ ): void {
95
110
  Object.defineProperty(target, name, {
96
111
  configurable: true,
97
112
  writable: true,
@@ -102,13 +117,13 @@ function define<T extends object>(target: T, name: string, value: unknown): void
102
117
  function dispatch(this: Event, info: ListenerInfo): boolean {
103
118
  const options = info.options;
104
119
  const once =
105
- typeof options === "object" && options !== null ? options.once : false;
120
+ typeof options === 'object' && options !== null ? options.once : false;
106
121
 
107
122
  if (once) {
108
123
  info.target.removeEventListener(this.type, info.listener);
109
124
  }
110
125
 
111
- if (typeof info.listener === "function") {
126
+ if (typeof info.listener === 'function') {
112
127
  info.listener.call(info.target, this);
113
128
  } else {
114
129
  info.listener.handleEvent(this);
@@ -125,40 +140,50 @@ export class EventTarget {
125
140
  addEventListener(
126
141
  type: string,
127
142
  listener: EventListenerOrEventListenerObject,
128
- options?: ListenerOptions
143
+ options?: ListenerOptions,
129
144
  ): void {
130
145
  const secret = wm.get(this)!;
131
- const listeners: ListenerInfo[] = secret[type] ?? (secret[type] = []);
146
+ let listeners = secret[type];
147
+ if (!listeners) {
148
+ listeners = [];
149
+ secret[type] = listeners;
150
+ }
132
151
 
133
152
  for (let i = 0; i < listeners.length; i++) {
134
153
  if (listeners[i].listener === listener) return;
135
154
  }
136
155
 
137
- listeners.push({ target: this, listener, options });
156
+ listeners.push({target: this, listener, options});
138
157
  }
139
158
 
140
159
  dispatchEvent(event: Event): boolean {
141
- const secret = wm.get(this)!;
142
- const listeners = secret[event.type];
143
-
144
- if (listeners) {
145
- define(event, "target", this);
146
- define(event, "currentTarget", this);
147
- listeners.slice(0).some(dispatch, event);
148
- define(event, "target", null);
149
- define(event, "currentTarget", null);
160
+ // The event's target is the node it was dispatched on; it then bubbles up
161
+ // the eventParents chain (currentTarget changes, target does not).
162
+ define(event, 'target', this);
163
+ let node: EventTarget | null = this;
164
+ while (node) {
165
+ const listeners = wm.get(node)?.[event.type];
166
+ if (listeners?.length) {
167
+ define(event, 'currentTarget', node);
168
+ listeners.slice(0).some(dispatch, event);
169
+ }
170
+ if (event.cancelBubble) break;
171
+ node = eventParents.get(node) ?? null;
150
172
  }
151
-
173
+ // Per the DOM, target persists after dispatch (a re-dispatch overwrites it
174
+ // at the top of this method); only currentTarget is cleared.
175
+ define(event, 'currentTarget', null);
152
176
  return true;
153
177
  }
154
178
 
155
179
  removeEventListener(
156
180
  type: string,
157
181
  listener: EventListenerOrEventListenerObject,
158
- _options?: ListenerOptions
182
+ _options?: ListenerOptions,
159
183
  ): void {
160
184
  const secret = wm.get(this)!;
161
- const listeners: ListenerInfo[] = secret[type] ?? (secret[type] = []);
185
+ const listeners = secret[type];
186
+ if (!listeners) return;
162
187
 
163
188
  for (let i = 0; i < listeners.length; i++) {
164
189
  if (listeners[i].listener === listener) {
@@ -1,19 +1,19 @@
1
1
  type DeferredPromise<T> = {
2
2
  promise: Promise<T>;
3
3
  resolve: (value?: T) => void;
4
- reject: (reason?: any) => void;
4
+ reject: (reason?: unknown) => void;
5
5
  };
6
6
 
7
7
  /**
8
8
  * @returns An object containing a promise and its resolve/reject methods.
9
9
  */
10
10
  export function createDeferredPromise<T>(): DeferredPromise<T> {
11
- let res: any;
12
- let rej: any;
11
+ let res: unknown;
12
+ let rej: unknown;
13
13
  const promise = new Promise<T>((resolve, reject) => {
14
- res = resolve
15
- rej = reject
16
- })
14
+ res = resolve;
15
+ rej = reject;
16
+ });
17
17
 
18
- return { promise, resolve: res, reject: rej } as DeferredPromise<T>;
18
+ return {promise, resolve: res, reject: rej} as DeferredPromise<T>;
19
19
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Testing & on-device-demo entry point.
3
+ *
4
+ * Imported via the package subpath:
5
+ *
6
+ * import {VirtualSerialTransport, installSerialMock}
7
+ * from 'react-native-web-serial-api/testing';
8
+ *
9
+ * These are reusable building blocks for *consumers'* tests and demos — an
10
+ * in-memory transport and an authorable `SerialDevice` peripheral model — so
11
+ * they ship with the package. The library's own spec-compliance suite is NOT
12
+ * here: it lives in `src/__tests__/conformance-suite.ts` (test-only, excluded
13
+ * from the build and the published package). None of this is in the main bundle.
14
+ */
15
+
16
+ export type {SerialTransport} from '../transport';
17
+ // The injection seam, re-exported here for convenience so a test or the
18
+ // example's demo mode can flip the global transport from one import.
19
+ export {getUsbSerial, resetUsbSerial, setUsbSerial} from '../UsbSerial';
20
+ export type {InstallSerialMockOptions, SerialMockDevice} from './install';
21
+ // Inject a mock device set into a running app (for on-device / emulator E2E).
22
+ export {installSerialMock} from './install';
23
+ export type {
24
+ DeviceIdentity,
25
+ SerialDeviceHost,
26
+ SerialDeviceOpenOptions,
27
+ SerialHostSignals,
28
+ SerialInputSignals,
29
+ } from './serial-device';
30
+ // Author a whole simulated peripheral by extending SerialDevice.
31
+ export {
32
+ EchoDevice,
33
+ LineDevice,
34
+ SerialDevice,
35
+ SilentDevice,
36
+ } from './serial-device';
37
+ export type {
38
+ FailableOp,
39
+ VirtualDeviceOptions,
40
+ VirtualSerialOptions,
41
+ } from './virtual-serial';
42
+ export {VirtualDevice, VirtualSerialTransport} from './virtual-serial';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * installSerialMock — point the library at a virtual device set in one call.
3
+ *
4
+ * Built for E2E: call it once at app startup, behind your own env/build flag, to
5
+ * make `navigator.serial` / this library's `serial` talk to simulated
6
+ * {@link SerialDevice}s instead of real USB hardware while a test driver
7
+ * (Maestro, Detox, …) exercises the app on a device/emulator.
8
+ *
9
+ * @example
10
+ * // index.js (debug/E2E build only)
11
+ * import {installSerialMock, EchoDevice} from 'react-native-web-serial-api/testing';
12
+ * installSerialMock({
13
+ * enabled: process.env.RNWS_SERIAL_MOCK === '1',
14
+ * devices: [new EchoDevice(), new MyThermometer()],
15
+ * });
16
+ */
17
+ import {setUsbSerial} from '../UsbSerial';
18
+ import {SerialDevice} from './serial-device';
19
+ import type {
20
+ VirtualDeviceOptions,
21
+ VirtualSerialOptions,
22
+ } from './virtual-serial';
23
+ import {VirtualSerialTransport} from './virtual-serial';
24
+
25
+ /** A device to register: a SerialDevice, optionally with transport options. */
26
+ export type SerialMockDevice =
27
+ | SerialDevice
28
+ | {device: SerialDevice; options?: VirtualDeviceOptions};
29
+
30
+ export type InstallSerialMockOptions = {
31
+ /** The simulated devices to expose. */
32
+ devices: SerialMockDevice[];
33
+ /** When false, no mock is installed and `null` is returned. Defaults to true. */
34
+ enabled?: boolean;
35
+ /** Transport-level options (latency, chunkSize, …). */
36
+ transport?: VirtualSerialOptions;
37
+ };
38
+
39
+ /**
40
+ * Build a {@link VirtualSerialTransport} from `devices` and install it globally
41
+ * via {@link setUsbSerial}. SerialDevices default to granted USB permission (so
42
+ * they show up immediately); pass the `{device, options}` form to override.
43
+ * Returns the transport (handy for driving devices in-test), or `null` when
44
+ * disabled.
45
+ */
46
+ export function installSerialMock(
47
+ options: InstallSerialMockOptions,
48
+ ): VirtualSerialTransport | null {
49
+ if (options.enabled === false) return null;
50
+
51
+ const transport = new VirtualSerialTransport(options.transport);
52
+ for (const entry of options.devices) {
53
+ if (entry instanceof SerialDevice) {
54
+ transport.addDevice(entry, {hasPermission: true});
55
+ } else {
56
+ transport.addDevice(entry.device, {
57
+ hasPermission: true,
58
+ ...entry.options,
59
+ });
60
+ }
61
+ }
62
+
63
+ setUsbSerial(transport);
64
+ return transport;
65
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * SerialDevice — author a complete simulated serial peripheral.
3
+ *
4
+ * Extend the class and override the lifecycle hooks to model a real device's
5
+ * whole behaviour (firmware/protocol): react to `open()`, to bytes the host
6
+ * writes, to control-signal changes, and stream data back over time. It is
7
+ * hosted by a {@link VirtualSerialTransport} ({@link ./virtual-serial}) and is
8
+ * free of any `react-native` dependency, so the same device runs under Jest,
9
+ * in a browser, and inside a React Native app on a device/emulator (for E2E).
10
+ *
11
+ * @example
12
+ * class Thermometer extends SerialDevice {
13
+ * usbVendorId = 0x0403;
14
+ * usbProductId = 0x6001;
15
+ * #timer?: ReturnType<typeof setInterval>;
16
+ * onOpen() {
17
+ * this.#timer = setInterval(() => this.send(`${20 + Math.random()}C\r\n`), 1000);
18
+ * }
19
+ * onData(bytes) { // host wrote a command
20
+ * if (String.fromCharCode(...bytes).trim() === 'ID?') this.send('ACME-TEMP\r\n');
21
+ * }
22
+ * onClose() { clearInterval(this.#timer); }
23
+ * }
24
+ * transport.addDevice(new Thermometer(), {hasPermission: true});
25
+ */
26
+ import type {OpenOptions} from '../transport';
27
+
28
+ /** The negotiated connection parameters (native parity code: 0 none/1 odd/2 even). */
29
+ export type SerialDeviceOpenOptions = Required<OpenOptions>;
30
+
31
+ /** Device-asserted input signals — what the host reads via getSignals(). */
32
+ export type SerialInputSignals = {
33
+ dataCarrierDetect?: boolean;
34
+ clearToSend?: boolean;
35
+ ringIndicator?: boolean;
36
+ dataSetReady?: boolean;
37
+ };
38
+
39
+ /** Host-asserted output signals (DTR/RTS/break) the device observes. */
40
+ export type SerialHostSignals = {
41
+ dataTerminalReady: boolean;
42
+ requestToSend: boolean;
43
+ break: boolean;
44
+ };
45
+
46
+ /**
47
+ * The handle a {@link SerialDevice} uses to talk back to the host. Provided by
48
+ * the transport; you normally use the `protected` helpers on `SerialDevice`
49
+ * rather than this directly.
50
+ */
51
+ export interface SerialDeviceHost {
52
+ readonly deviceId: number;
53
+ readonly portNumber: number;
54
+ readonly isOpen: boolean;
55
+ readonly openOptions: SerialDeviceOpenOptions | null;
56
+ send(bytes: number[]): void;
57
+ raiseError(message: string, name?: string): void;
58
+ setSignals(signals: SerialInputSignals): void;
59
+ }
60
+
61
+ /** Coerce a payload into a byte array (string → char codes, & 0xff). */
62
+ export function toBytes(data: number[] | Uint8Array | string): number[] {
63
+ if (typeof data === 'string') {
64
+ return Array.from(data, c => c.charCodeAt(0) & 0xff);
65
+ }
66
+ if (data instanceof Uint8Array) return Array.from(data);
67
+ return data.map(n => n & 0xff);
68
+ }
69
+
70
+ /**
71
+ * Base class for a simulated serial peripheral. Override the `on*` hooks you
72
+ * care about (all default to no-ops and may be async) and use the `protected`
73
+ * helpers to drive the host.
74
+ */
75
+ export abstract class SerialDevice {
76
+ /** USB Vendor ID this device reports for enumeration. */
77
+ abstract readonly usbVendorId: number;
78
+ /** USB Product ID this device reports for enumeration. */
79
+ abstract readonly usbProductId: number;
80
+ /** Optional USB serial number. */
81
+ readonly serialNumber?: string;
82
+
83
+ #host: SerialDeviceHost | null = null;
84
+
85
+ /** @internal Bind the transport host (called by VirtualSerialTransport). */
86
+ _bind(host: SerialDeviceHost): void {
87
+ this.#host = host;
88
+ }
89
+
90
+ /** Send bytes to the host (they appear on `port.readable`). */
91
+ protected send(data: number[] | Uint8Array | string): void {
92
+ this.#host?.send(toBytes(data));
93
+ }
94
+
95
+ /**
96
+ * Raise a typed read error on the host's readable stream. `name` is the W3C
97
+ * error type, e.g. "BreakError", "BufferOverrunError", "FramingError",
98
+ * "ParityError" (defaults to "NetworkError").
99
+ */
100
+ protected raiseError(message: string, name?: string): void {
101
+ this.#host?.raiseError(message, name);
102
+ }
103
+
104
+ /** Set device-asserted input signals (DCD/CTS/RI/DSR) the host can read. */
105
+ protected setSignals(signals: SerialInputSignals): void {
106
+ this.#host?.setSignals(signals);
107
+ }
108
+
109
+ /** The parameters the host opened the port with, or null when closed. */
110
+ protected get openOptions(): SerialDeviceOpenOptions | null {
111
+ return this.#host?.openOptions ?? null;
112
+ }
113
+
114
+ protected get deviceId(): number {
115
+ return this.#host?.deviceId ?? -1;
116
+ }
117
+
118
+ protected get portNumber(): number {
119
+ return this.#host?.portNumber ?? 0;
120
+ }
121
+
122
+ /** The host opened the port. */
123
+ onOpen(_options: SerialDeviceOpenOptions): void | Promise<void> {}
124
+ /** The host wrote bytes to the device. */
125
+ onData(_data: Uint8Array): void | Promise<void> {}
126
+ /** The host changed DTR/RTS/break. */
127
+ onHostSignals(_signals: SerialHostSignals): void | Promise<void> {}
128
+ /** The host closed the port. */
129
+ onClose(): void | Promise<void> {}
130
+ }
131
+
132
+ /** Optional USB identity for the built-in devices. */
133
+ export type DeviceIdentity = {
134
+ usbVendorId?: number;
135
+ usbProductId?: number;
136
+ serialNumber?: string;
137
+ };
138
+
139
+ /** A loopback device: every byte written is echoed straight back. */
140
+ export class EchoDevice extends SerialDevice {
141
+ readonly usbVendorId: number;
142
+ readonly usbProductId: number;
143
+ readonly serialNumber?: string;
144
+
145
+ constructor(identity: DeviceIdentity = {}) {
146
+ super();
147
+ this.usbVendorId = identity.usbVendorId ?? 0x0403;
148
+ this.usbProductId = identity.usbProductId ?? 0x6001;
149
+ this.serialNumber = identity.serialNumber;
150
+ }
151
+
152
+ onData(data: Uint8Array): void {
153
+ this.send(data);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Base for a line-oriented command/response device: buffers incoming bytes and
159
+ * calls {@link onLine} for each `\n`-terminated line (trailing CR/LF stripped).
160
+ */
161
+ export abstract class LineDevice extends SerialDevice {
162
+ #buffer = '';
163
+
164
+ /** Handle one line received from the host. */
165
+ abstract onLine(line: string): void | Promise<void>;
166
+
167
+ onData(data: Uint8Array): void {
168
+ for (let i = 0; i < data.length; i++) {
169
+ this.#buffer += String.fromCharCode(data[i]);
170
+ }
171
+ let nl = this.#buffer.indexOf('\n');
172
+ while (nl >= 0) {
173
+ const line = this.#buffer.slice(0, nl).replace(/\r$/, '');
174
+ this.#buffer = this.#buffer.slice(nl + 1);
175
+ void this.onLine(line);
176
+ nl = this.#buffer.indexOf('\n');
177
+ }
178
+ }
179
+ }
180
+
181
+ /** A device that accepts writes but never sends anything back. */
182
+ export class SilentDevice extends SerialDevice {
183
+ readonly usbVendorId: number;
184
+ readonly usbProductId: number;
185
+ readonly serialNumber?: string;
186
+
187
+ constructor(identity: DeviceIdentity = {}) {
188
+ super();
189
+ this.usbVendorId = identity.usbVendorId ?? 0x0403;
190
+ this.usbProductId = identity.usbProductId ?? 0x6001;
191
+ this.serialNumber = identity.serialNumber;
192
+ }
193
+ }