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.
- package/README.md +23 -0
- package/TESTING.md +301 -0
- package/android/build.gradle +2 -2
- package/lib/commonjs/UsbSerial.js +58 -26
- package/lib/commonjs/UsbSerial.js.map +1 -1
- package/lib/commonjs/WebSerial.js +169 -57
- package/lib/commonjs/WebSerial.js.map +1 -1
- package/lib/commonjs/index.js +13 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/dom-exception.js +176 -0
- package/lib/commonjs/lib/dom-exception.js.map +1 -0
- package/lib/commonjs/lib/event-target.js +138 -0
- package/lib/commonjs/lib/event-target.js.map +1 -0
- package/lib/commonjs/lib/promise.js +23 -0
- package/lib/commonjs/lib/promise.js.map +1 -0
- package/lib/commonjs/testing/index.js +70 -0
- package/lib/commonjs/testing/index.js.map +1 -0
- package/lib/commonjs/testing/install.js +54 -0
- package/lib/commonjs/testing/install.js.map +1 -0
- package/lib/commonjs/testing/serial-device.js +164 -0
- package/lib/commonjs/testing/serial-device.js.map +1 -0
- package/lib/commonjs/testing/virtual-serial.js +615 -0
- package/lib/commonjs/testing/virtual-serial.js.map +1 -0
- package/lib/commonjs/transport.js +61 -0
- package/lib/commonjs/transport.js.map +1 -0
- package/lib/typescript/src/UsbSerial.d.ts +24 -67
- package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
- package/lib/typescript/src/WebSerial.d.ts +11 -2
- package/lib/typescript/src/WebSerial.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
- package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
- package/lib/typescript/src/lib/event-target.d.ts +53 -0
- package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
- package/lib/typescript/src/lib/promise.d.ts +11 -0
- package/lib/typescript/src/lib/promise.d.ts.map +1 -0
- package/lib/typescript/src/testing/index.d.ts +23 -0
- package/lib/typescript/src/testing/index.d.ts.map +1 -0
- package/lib/typescript/src/testing/install.d.ts +25 -0
- package/lib/typescript/src/testing/install.d.ts.map +1 -0
- package/lib/typescript/src/testing/serial-device.d.ts +127 -0
- package/lib/typescript/src/testing/serial-device.d.ts.map +1 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts +205 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts.map +1 -0
- package/lib/typescript/src/transport.d.ts +131 -0
- package/lib/typescript/src/transport.d.ts.map +1 -0
- package/package.json +38 -2
- package/src/UsbSerial.ts +65 -90
- package/src/WebSerial.ts +227 -88
- package/src/index.ts +2 -7
- package/src/lib/dom-exception.ts +129 -60
- package/src/lib/event-target.ts +46 -21
- package/src/lib/promise.ts +7 -7
- package/src/testing/index.ts +42 -0
- package/src/testing/install.ts +65 -0
- package/src/testing/serial-device.ts +193 -0
- package/src/testing/virtual-serial.ts +801 -0
- package/src/transport.ts +200 -0
- package/babel.config.js +0 -3
- package/biome.json +0 -35
- package/example/.watchmanconfig +0 -1
- package/example/App.tsx +0 -71
- package/example/__tests__/App.test.tsx +0 -16
- package/example/__tests__/connectEvents.test.tsx +0 -81
- package/example/__tests__/getPorts.test.tsx +0 -140
- package/example/android/app/build.gradle +0 -120
- package/example/android/app/debug.keystore +0 -0
- package/example/android/app/proguard-rules.pro +0 -10
- package/example/android/app/src/debug/AndroidManifest.xml +0 -9
- package/example/android/app/src/main/AndroidManifest.xml +0 -38
- package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
- package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
- package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/values/strings.xml +0 -3
- package/example/android/app/src/main/res/values/styles.xml +0 -9
- package/example/android/build.gradle +0 -22
- package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/example/android/gradle.properties +0 -47
- package/example/android/gradlew +0 -252
- package/example/android/gradlew.bat +0 -94
- package/example/android/settings.gradle +0 -6
- package/example/app.json +0 -4
- package/example/babel.config.js +0 -21
- package/example/biome.json +0 -47
- package/example/deploy.sh +0 -11
- package/example/index.html +0 -26
- package/example/index.js +0 -9
- package/example/index.web.js +0 -8
- package/example/jest.config.js +0 -12
- package/example/metro.config.js +0 -58
- package/example/package-lock.json +0 -14510
- package/example/package.json +0 -48
- package/example/react-native.config.js +0 -17
- package/example/src/components/AppBar.tsx +0 -73
- package/example/src/components/Menu.tsx +0 -90
- package/example/src/components/SingleChoiceDialog.tsx +0 -120
- package/example/src/screens/ConnectScreen.tsx +0 -195
- package/example/src/screens/DevicesScreen.tsx +0 -252
- package/example/src/screens/TerminalScreen.tsx +0 -572
- package/example/src/settings.ts +0 -43
- package/example/src/theme.ts +0 -19
- package/example/src/util/TextUtil.ts +0 -129
- package/example/tsconfig.json +0 -10
- package/example/vite.config.mjs +0 -55
- package/scripts/deploy-release.sh +0 -127
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -20
package/src/lib/dom-exception.ts
CHANGED
|
@@ -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
|
|
82
|
-
static readonly DOMSTRING_SIZE_ERR
|
|
83
|
-
static readonly HIERARCHY_REQUEST_ERR
|
|
84
|
-
static readonly WRONG_DOCUMENT_ERR
|
|
85
|
-
static readonly INVALID_CHARACTER_ERR
|
|
86
|
-
static readonly NO_DATA_ALLOWED_ERR
|
|
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
|
|
89
|
-
static readonly NOT_SUPPORTED_ERR
|
|
90
|
-
static readonly INUSE_ATTRIBUTE_ERR
|
|
91
|
-
static readonly INVALID_STATE_ERR
|
|
92
|
-
static readonly SYNTAX_ERR
|
|
93
|
-
static readonly INVALID_MODIFICATION_ERR
|
|
94
|
-
static readonly NAMESPACE_ERR
|
|
95
|
-
static readonly INVALID_ACCESS_ERR
|
|
96
|
-
static readonly VALIDATION_ERR
|
|
97
|
-
static readonly TYPE_MISMATCH_ERR
|
|
98
|
-
static readonly SECURITY_ERR
|
|
99
|
-
static readonly NETWORK_ERR
|
|
100
|
-
static readonly ABORT_ERR
|
|
101
|
-
static readonly URL_MISMATCH_ERR
|
|
102
|
-
static readonly QUOTA_EXCEEDED_ERR
|
|
103
|
-
static readonly TIMEOUT_ERR
|
|
104
|
-
static readonly INVALID_NODE_TYPE_ERR
|
|
105
|
-
static readonly DATA_CLONE_ERR
|
|
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 =
|
|
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
|
|
113
|
+
this.name = name;
|
|
114
114
|
this.message = message;
|
|
115
|
-
this.code
|
|
115
|
+
this.code = DOM_EXCEPTION_CODES[name] ?? 0;
|
|
116
116
|
|
|
117
117
|
// Provide a useful stack trace in V8 / SpiderMonkey
|
|
118
|
-
|
|
119
|
-
|
|
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()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
get
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
get
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
get
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
get
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
get
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
get
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
get
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
get
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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 {
|
|
161
|
-
|
|
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
|
+
};
|
package/src/lib/event-target.ts
CHANGED
|
@@ -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> = {
|
|
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
|
-
| {
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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({
|
|
156
|
+
listeners.push({target: this, listener, options});
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
dispatchEvent(event: Event): boolean {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
listeners
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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) {
|
package/src/lib/promise.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
type DeferredPromise<T> = {
|
|
2
2
|
promise: Promise<T>;
|
|
3
3
|
resolve: (value?: T) => void;
|
|
4
|
-
reject: (reason?:
|
|
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:
|
|
12
|
-
let rej:
|
|
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 {
|
|
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
|
+
}
|