nova-control-browser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +208 -0
- package/dist/nova-control-browser.d.ts +70 -0
- package/dist/nova-control-browser.js +123 -0
- package/package.json +48 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Andreas Rozek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# nova-control-browser
|
|
2
|
+
|
|
3
|
+
Browser ESM module for controlling the [Creoqode Nova DIY AI Robot](../../README.md) over USB serial via the Web Serial API.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
| requirement | details |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **Chrome or Edge 89+** | required — the Web Serial API is only available in Chromium-based browsers. Firefox and Safari do not support it. |
|
|
12
|
+
| **Node.js 22+** | required for the build toolchain only (`npm run build`). Not needed at runtime in the browser. Download from [nodejs.org](https://nodejs.org). |
|
|
13
|
+
| **Arduino sketch** | the matching `Nova_SerialController.ino` sketch must be flashed to the robot's Arduino board (baud rate 9600, 8N1). |
|
|
14
|
+
| **User gesture** | `openNova()` must be called from within a user gesture (e.g. a button click) because the browser requires a transient activation before showing the port picker. |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm add nova-control-browser
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
### Constants
|
|
29
|
+
|
|
30
|
+
| export | type | value | description |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `BaudRate` | `number` | `9600` | baud rate expected by the Arduino sketch |
|
|
33
|
+
| `HomePosition` | `ServoState` | `{ s1:90, s2:90, s3:110, s4:90, s5:95 }` | safe resting position for all five servos |
|
|
34
|
+
| `SafeRange` | `Record<ServoKey, [number, number]>` | see below | per-servo `[min, max]` in degrees |
|
|
35
|
+
|
|
36
|
+
`SafeRange` values:
|
|
37
|
+
|
|
38
|
+
| servo | pin | role | min | max |
|
|
39
|
+
| --- | --- | --- | --- | --- |
|
|
40
|
+
| `s1` | 32 | head front/back | 45 | 135 |
|
|
41
|
+
| `s2` | 34 | head CW/CCW | 10 | 170 |
|
|
42
|
+
| `s3` | 36 | head up/down | 40 | 150 |
|
|
43
|
+
| `s4` | 38 | body rotation | 30 | 180 |
|
|
44
|
+
| `s5` | 40 | secondary head | 20 | 150 |
|
|
45
|
+
|
|
46
|
+
Both `HomePosition` and `SafeRange` are frozen (`Object.isFrozen` returns `true`).
|
|
47
|
+
|
|
48
|
+
### `buildDirectPacket`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
function buildDirectPacket (State:ServoState):Uint8Array
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Assembles a 5-byte direct servo control packet from the given servo state. Each value is clamped to `SafeRange` and rounded to the nearest integer before encoding. The byte order matches the Arduino sketch:
|
|
55
|
+
|
|
56
|
+
| byte | servo |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| 0 | s4 |
|
|
59
|
+
| 1 | s3 |
|
|
60
|
+
| 2 | s2 |
|
|
61
|
+
| 3 | s1 |
|
|
62
|
+
| 4 | s5 |
|
|
63
|
+
|
|
64
|
+
### `openNova`
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
async function openNova (
|
|
68
|
+
PortOrOptions?:WebSerialPort | WebSerialPortRequestOptions
|
|
69
|
+
):Promise<NovaController>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Opens a USB serial port and returns a `NovaController`. Without an argument the browser's port picker is shown to the user. An existing `WebSerialPort` instance (e.g. from a previous session) may be passed to skip the picker. A `WebSerialPortRequestOptions` object with an optional `filters` array may be passed to narrow the picker to specific USB device IDs.
|
|
73
|
+
|
|
74
|
+
The returned promise resolves after the 2-second Arduino reset delay that follows every port open.
|
|
75
|
+
|
|
76
|
+
### `NovaController`
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
interface NovaController {
|
|
80
|
+
home ():Promise<void>
|
|
81
|
+
shiftHeadTo (Degrees:number):Promise<void>
|
|
82
|
+
rollHeadTo (Degrees:number):Promise<void>
|
|
83
|
+
pitchHeadTo (Degrees:number):Promise<void>
|
|
84
|
+
liftHeadTo (Degrees:number):Promise<void>
|
|
85
|
+
rotateBodyTo (Degrees:number):Promise<void>
|
|
86
|
+
get State ():ServoState
|
|
87
|
+
set State (Update:ServoUpdate)
|
|
88
|
+
sendServoState ():Promise<void>
|
|
89
|
+
destroy ():void
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| method / property | description |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `home()` | sends all servos to `HomePosition` |
|
|
96
|
+
| `shiftHeadTo(Degrees)` | sets s1 — head forward `> 90°`, back `< 90°` |
|
|
97
|
+
| `rollHeadTo(Degrees)` | sets s2 — head clockwise `> 90°`, counter-clockwise `< 90°` |
|
|
98
|
+
| `pitchHeadTo(Degrees)` | sets s3 — head up `> 110°`, down toward `40°` |
|
|
99
|
+
| `liftHeadTo(Degrees)` | sets s5 — secondary head up/down, range `20°`–`150°` |
|
|
100
|
+
| `rotateBodyTo(Degrees)` | sets s4 — rotates the entire body around the Z-axis |
|
|
101
|
+
| `State` (get) | returns a deep copy of the pending state if any, else the last-sent state |
|
|
102
|
+
| `State` (set) | replaces any pending entry with `Update` merged onto the *last-sent* state (not onto pending); flush with `sendServoState()` |
|
|
103
|
+
| `sendServoState()` | flushes any pending state update to the Arduino |
|
|
104
|
+
| `destroy()` | releases the stream writer lock and closes the serial port |
|
|
105
|
+
|
|
106
|
+
`State` reflects what was *sent* (or is pending to be sent) to the Arduino, not the physical servo position — there is no read-back channel in the protocol.
|
|
107
|
+
|
|
108
|
+
Sends are serialised internally: concurrent method calls and `sendServoState()` calls never overlap on the wire. Named methods such as `shiftHeadTo()` *accumulate* changes on top of whatever is already pending; the `State` setter instead *replaces* the pending entry, starting fresh from the last-sent state.
|
|
109
|
+
|
|
110
|
+
### Types
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type ServoKey = 's1' | 's2' | 's3' | 's4' | 's5'
|
|
114
|
+
type ServoState = { [K in ServoKey]:number }
|
|
115
|
+
type ServoUpdate = Partial<ServoState>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
### Basic usage
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { openNova } from 'nova-control-browser'
|
|
126
|
+
|
|
127
|
+
const Button = document.querySelector('button')!
|
|
128
|
+
|
|
129
|
+
Button.addEventListener('click', async () => {
|
|
130
|
+
// shows the browser's port picker — must be inside a user gesture
|
|
131
|
+
const Nova = await openNova()
|
|
132
|
+
|
|
133
|
+
await Nova.home()
|
|
134
|
+
await Nova.rotateBodyTo(120)
|
|
135
|
+
await Nova.shiftHeadTo(100)
|
|
136
|
+
await Nova.rollHeadTo(60)
|
|
137
|
+
|
|
138
|
+
console.log(Nova.State)
|
|
139
|
+
// → { s1:100, s2:60, s3:110, s4:120, s5:95 }
|
|
140
|
+
|
|
141
|
+
Nova.destroy()
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Two queue strategies
|
|
146
|
+
|
|
147
|
+
**Named methods accumulate** — each call merges its change on top of whatever is already pending:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// none of these three lines sends anything yet
|
|
151
|
+
Nova.shiftHeadTo(80) // pending: { ...home, s1:80 }
|
|
152
|
+
Nova.rollHeadTo(60) // pending: { ...home, s1:80, s2:60 }
|
|
153
|
+
Nova.rotateBodyTo(120) // pending: { ...home, s1:80, s2:60, s4:120 }
|
|
154
|
+
await Nova.sendServoState()
|
|
155
|
+
// sends { s1:80, s2:60, s3:110, s4:120, s5:95 }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**The `State` setter replaces** the pending entry — it starts fresh from the last-sent state, discarding any pending changes:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
Nova.State = { s4:120 }
|
|
162
|
+
Nova.State = { s1:100, s2:60 } // replaces previous; s4 reverts to last-sent
|
|
163
|
+
await Nova.sendServoState()
|
|
164
|
+
// sends { s1:100, s2:60, s3:110, s4:90, s5:95 } ← s4 is 90, not 120
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Reusing a previously selected port
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { openNova } from 'nova-control-browser'
|
|
171
|
+
|
|
172
|
+
// ask the browser for the port once (requires user gesture)
|
|
173
|
+
const [Port] = await (navigator as any).serial.getPorts()
|
|
174
|
+
|
|
175
|
+
// later — no picker shown, no gesture needed
|
|
176
|
+
const Nova = await openNova(Port)
|
|
177
|
+
await Nova.home()
|
|
178
|
+
Nova.destroy()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Filtering the port picker by USB vendor
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { openNova } from 'nova-control-browser'
|
|
185
|
+
|
|
186
|
+
Button.addEventListener('click', async () => {
|
|
187
|
+
// only show CH340/CH341 USB serial adapters (Arduino clones)
|
|
188
|
+
const Nova = await openNova({ filters: [{ usbVendorId: 0x1a86 }] })
|
|
189
|
+
await Nova.home()
|
|
190
|
+
Nova.destroy()
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Building
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm run build --workspace packages/nova-control-browser
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Output is written to `packages/nova-control-browser/dist/`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
[MIT License](../../LICENSE.md) © Andreas Rozek
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export declare const BaudRate = 9600;
|
|
2
|
+
|
|
3
|
+
/**** buildDirectPacket — 5-byte direct servo control packet ****/
|
|
4
|
+
export declare function buildDirectPacket(State: ServoState): Uint8Array;
|
|
5
|
+
|
|
6
|
+
/**** HomePosition ****/
|
|
7
|
+
export declare const HomePosition: Readonly<ServoState>;
|
|
8
|
+
|
|
9
|
+
/**** NovaController ****/
|
|
10
|
+
export declare interface NovaController {
|
|
11
|
+
home(): Promise<void>;
|
|
12
|
+
shiftHeadTo(Degrees: number): Promise<void>;
|
|
13
|
+
rollHeadTo(Degrees: number): Promise<void>;
|
|
14
|
+
pitchHeadTo(Degrees: number): Promise<void>;
|
|
15
|
+
liftHeadTo(Degrees: number): Promise<void>;
|
|
16
|
+
rotateBodyTo(Degrees: number): Promise<void>;
|
|
17
|
+
get State(): ServoState;
|
|
18
|
+
set State(Update: ServoUpdate);
|
|
19
|
+
sendServoState(): Promise<void>;
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**** NovaOptions ****/
|
|
24
|
+
export declare interface NovaOptions {
|
|
25
|
+
StepIntervalMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**** openNova — factory ****/
|
|
29
|
+
export declare function openNova(PortOrOptions?: WebSerialPort | WebSerialPortRequestOptions, Options?: NovaOptions): Promise<NovaController>;
|
|
30
|
+
|
|
31
|
+
/**** SafeRange ****/
|
|
32
|
+
export declare const SafeRange: Readonly<Record<ServoKey, [number, number]>>;
|
|
33
|
+
|
|
34
|
+
/*******************************************************************************
|
|
35
|
+
* *
|
|
36
|
+
* nova-control-browser *
|
|
37
|
+
* *
|
|
38
|
+
*******************************************************************************/
|
|
39
|
+
/**** ServoKey ****/
|
|
40
|
+
export declare type ServoKey = 's1' | 's2' | 's3' | 's4' | 's5';
|
|
41
|
+
|
|
42
|
+
/**** ServoSpeed — °/ms so that the full safe range takes exactly 1 second ****/
|
|
43
|
+
export declare const ServoSpeed: Readonly<Record<ServoKey, number>>;
|
|
44
|
+
|
|
45
|
+
/**** ServoState ****/
|
|
46
|
+
export declare type ServoState = {
|
|
47
|
+
[K in ServoKey]: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**** ServoUpdate ****/
|
|
51
|
+
export declare type ServoUpdate = Partial<ServoState>;
|
|
52
|
+
|
|
53
|
+
declare interface WebSerialPort extends EventTarget {
|
|
54
|
+
open(Options: {
|
|
55
|
+
baudRate: number;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
readonly writable: WritableStream<Uint8Array>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare interface WebSerialPortFilter {
|
|
62
|
+
usbVendorId?: number;
|
|
63
|
+
usbProductId?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare interface WebSerialPortRequestOptions {
|
|
67
|
+
filters?: WebSerialPortFilter[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/nova-control-browser.ts
|
|
2
|
+
var e = 9600, t = Object.freeze({
|
|
3
|
+
s1: 90,
|
|
4
|
+
s2: 90,
|
|
5
|
+
s3: 110,
|
|
6
|
+
s4: 90,
|
|
7
|
+
s5: 95
|
|
8
|
+
}), n = Object.freeze({
|
|
9
|
+
s1: [45, 135],
|
|
10
|
+
s2: [10, 170],
|
|
11
|
+
s3: [40, 150],
|
|
12
|
+
s4: [30, 180],
|
|
13
|
+
s5: [20, 150]
|
|
14
|
+
}), r = Object.freeze({
|
|
15
|
+
s1: (n.s1[1] - n.s1[0]) / 1e3,
|
|
16
|
+
s2: (n.s2[1] - n.s2[0]) / 1e3,
|
|
17
|
+
s3: (n.s3[1] - n.s3[0]) / 1e3,
|
|
18
|
+
s4: (n.s4[1] - n.s4[0]) / 1e3,
|
|
19
|
+
s5: (n.s5[1] - n.s5[0]) / 1e3
|
|
20
|
+
});
|
|
21
|
+
function i(e, t) {
|
|
22
|
+
let [r, i] = n[t];
|
|
23
|
+
return Math.max(r, Math.min(i, Math.round(e)));
|
|
24
|
+
}
|
|
25
|
+
function a(e) {
|
|
26
|
+
return new Uint8Array([
|
|
27
|
+
i(e.s4, "s4"),
|
|
28
|
+
i(e.s3, "s3"),
|
|
29
|
+
i(e.s2, "s2"),
|
|
30
|
+
i(e.s1, "s1"),
|
|
31
|
+
i(e.s5, "s5")
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
async function o(t) {
|
|
35
|
+
if (!("serial" in navigator)) throw Error("Nova: Web Serial API is not supported in this browser");
|
|
36
|
+
let n = navigator.serial, r = t instanceof EventTarget ? t : await n.requestPort(t ?? {});
|
|
37
|
+
await r.open({ baudRate: e });
|
|
38
|
+
let i = r.writable.getWriter();
|
|
39
|
+
await new Promise((e) => setTimeout(e, 2e3));
|
|
40
|
+
let a = !1;
|
|
41
|
+
return {
|
|
42
|
+
async write(e) {
|
|
43
|
+
a || await i.write(e);
|
|
44
|
+
},
|
|
45
|
+
destroy() {
|
|
46
|
+
if (!a) {
|
|
47
|
+
a = !0;
|
|
48
|
+
try {
|
|
49
|
+
i.releaseLock();
|
|
50
|
+
} catch {}
|
|
51
|
+
r.close().catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function s(e, n) {
|
|
57
|
+
let i = n?.StepIntervalMs ?? 20, s = await o(e), c = { ...t }, l, u = Promise.resolve();
|
|
58
|
+
function d(e) {
|
|
59
|
+
l = {
|
|
60
|
+
...l ?? c,
|
|
61
|
+
...e
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function f() {
|
|
65
|
+
let e = u;
|
|
66
|
+
u = (async () => {
|
|
67
|
+
try {
|
|
68
|
+
await e;
|
|
69
|
+
} catch {}
|
|
70
|
+
for (; l != null;) {
|
|
71
|
+
let e = l, t = !0, n = { ...c };
|
|
72
|
+
for (let a of [
|
|
73
|
+
"s1",
|
|
74
|
+
"s2",
|
|
75
|
+
"s3",
|
|
76
|
+
"s4",
|
|
77
|
+
"s5"
|
|
78
|
+
]) {
|
|
79
|
+
let o = e[a] - c[a], s = i > 0 ? r[a] * i : Infinity;
|
|
80
|
+
Math.abs(o) > s ? (n[a] = c[a] + Math.sign(o) * s, t = !1) : n[a] = e[a];
|
|
81
|
+
}
|
|
82
|
+
t && (l = void 0), c = { ...n }, await s.write(a(n)), t || await new Promise((e) => setTimeout(e, i));
|
|
83
|
+
}
|
|
84
|
+
})(), await u;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async home() {
|
|
88
|
+
d({ ...t }), await f();
|
|
89
|
+
},
|
|
90
|
+
async shiftHeadTo(e) {
|
|
91
|
+
d({ s1: e }), await f();
|
|
92
|
+
},
|
|
93
|
+
async rollHeadTo(e) {
|
|
94
|
+
d({ s2: e }), await f();
|
|
95
|
+
},
|
|
96
|
+
async pitchHeadTo(e) {
|
|
97
|
+
d({ s3: e }), await f();
|
|
98
|
+
},
|
|
99
|
+
async liftHeadTo(e) {
|
|
100
|
+
d({ s5: e }), await f();
|
|
101
|
+
},
|
|
102
|
+
async rotateBodyTo(e) {
|
|
103
|
+
d({ s4: e }), await f();
|
|
104
|
+
},
|
|
105
|
+
get State() {
|
|
106
|
+
return structuredClone(l ?? c);
|
|
107
|
+
},
|
|
108
|
+
set State(e) {
|
|
109
|
+
l = {
|
|
110
|
+
...c,
|
|
111
|
+
...e
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
async sendServoState() {
|
|
115
|
+
await f();
|
|
116
|
+
},
|
|
117
|
+
destroy() {
|
|
118
|
+
s.destroy();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { e as BaudRate, t as HomePosition, n as SafeRange, r as ServoSpeed, a as buildDirectPacket, s as openNova };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nova-control-browser",
|
|
3
|
+
"description": "Control a NOVA DIY Artificial Intelligence Robot by Creoqode from a browser",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nova",
|
|
8
|
+
"creoqode",
|
|
9
|
+
"robot",
|
|
10
|
+
"web-serial",
|
|
11
|
+
"browser"
|
|
12
|
+
],
|
|
13
|
+
"author": "Andreas Rozek",
|
|
14
|
+
"homepage": "https://github.com/rozek/nova-control#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/rozek/nova-control.git",
|
|
18
|
+
"directory": "packages/nova-control-browser"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/rozek/nova-control/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"main": "./dist/nova-control-browser.js",
|
|
25
|
+
"module": "./dist/nova-control-browser.js",
|
|
26
|
+
"types": "./dist/nova-control-browser.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/nova-control-browser.js",
|
|
30
|
+
"types": "./dist/nova-control-browser.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"happy-dom": "^20.8.3",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vite": "^8.0.0",
|
|
40
|
+
"vite-plugin-dts": "^4.5.4",
|
|
41
|
+
"vitest": "^4.1.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"test": "vitest",
|
|
46
|
+
"test:run": "vitest run"
|
|
47
|
+
}
|
|
48
|
+
}
|