@cosyte/mllp 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/CHANGELOG.md +529 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/ack-from-hl7/index.cjs +417 -0
- package/dist/ack-from-hl7/index.cjs.map +1 -0
- package/dist/ack-from-hl7/index.d.cts +425 -0
- package/dist/ack-from-hl7/index.d.ts +425 -0
- package/dist/ack-from-hl7/index.mjs +400 -0
- package/dist/ack-from-hl7/index.mjs.map +1 -0
- package/dist/index-DjGQJtYE.d.cts +83 -0
- package/dist/index-DjGQJtYE.d.ts +83 -0
- package/dist/index.cjs +3455 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3112 -0
- package/dist/index.d.ts +3112 -0
- package/dist/index.mjs +3428 -0
- package/dist/index.mjs.map +1 -0
- package/dist/testing/index.cjs +243 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +174 -0
- package/dist/testing/index.d.ts +174 -0
- package/dist/testing/index.mjs +241 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// src/testing/in-memory-transport.ts
|
|
2
|
+
var InMemoryTransport = class _InMemoryTransport {
|
|
3
|
+
_peer = null;
|
|
4
|
+
_onDataFn = null;
|
|
5
|
+
_onConnectFn = null;
|
|
6
|
+
_onCloseFn = null;
|
|
7
|
+
_onErrorFn = null;
|
|
8
|
+
_destroyed = false;
|
|
9
|
+
_paused = false;
|
|
10
|
+
_splitBytes = 0;
|
|
11
|
+
_pendingChunks = [];
|
|
12
|
+
_writeDepth = 0;
|
|
13
|
+
constructor() {
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create two connected InMemoryTransport ends.
|
|
17
|
+
*
|
|
18
|
+
* Writes to `a` deliver to `b`'s `onData` handler synchronously (and vice versa).
|
|
19
|
+
* Call `a.simulateConnect()` after `pair()` to fire the `onConnect` handler,
|
|
20
|
+
* simulating TCP connect completion.
|
|
21
|
+
*
|
|
22
|
+
* @returns Tuple `[a, b]` of two connected transports.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const [a, b] = InMemoryTransport.pair();
|
|
27
|
+
* b.onData((chunk) => b.write(chunk)); // echo server
|
|
28
|
+
* a.write(Buffer.from([0x0b, 0x41, 0x1c, 0x0d]));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
static pair() {
|
|
32
|
+
const a = new _InMemoryTransport();
|
|
33
|
+
const b = new _InMemoryTransport();
|
|
34
|
+
a._peer = b;
|
|
35
|
+
b._peer = a;
|
|
36
|
+
return [a, b];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write `buf` to the peer transport.
|
|
40
|
+
*
|
|
41
|
+
* If the peer is paused, the buffer is copied and queued — returns `false`
|
|
42
|
+
* (backpressure signal). Otherwise, the peer's `onData` handler fires
|
|
43
|
+
* synchronously before this method returns.
|
|
44
|
+
*
|
|
45
|
+
* @returns `true` if delivered immediately; `false` if queued or transport is destroyed.
|
|
46
|
+
*/
|
|
47
|
+
write(buf) {
|
|
48
|
+
if (this._destroyed) return false;
|
|
49
|
+
const peer = this._peer;
|
|
50
|
+
if (peer === null || peer._destroyed) return false;
|
|
51
|
+
if (peer._paused) {
|
|
52
|
+
peer._pendingChunks.push(Buffer.from(buf));
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
peer._deliverChunk(buf);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initiate graceful close — fires `onClose` on this end and on the peer,
|
|
60
|
+
* simulating a TCP FIN exchange.
|
|
61
|
+
*/
|
|
62
|
+
close() {
|
|
63
|
+
if (this._destroyed) return;
|
|
64
|
+
this._destroyed = true;
|
|
65
|
+
if (this._onCloseFn !== null) {
|
|
66
|
+
try {
|
|
67
|
+
this._onCloseFn();
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const peer = this._peer;
|
|
72
|
+
if (peer !== null && !peer._destroyed) {
|
|
73
|
+
peer._destroyed = true;
|
|
74
|
+
if (peer._onCloseFn !== null) {
|
|
75
|
+
try {
|
|
76
|
+
peer._onCloseFn();
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Abruptly destroy this transport end.
|
|
84
|
+
*
|
|
85
|
+
* Fires `onError(reason)` then `onClose` on this end only. Does NOT affect
|
|
86
|
+
* the peer (simulates one-sided abrupt disconnect, e.g., ECONNRESET on
|
|
87
|
+
* just one socket).
|
|
88
|
+
*
|
|
89
|
+
* @param reason - Optional error; defaults to `new Error('InMemoryTransport destroyed')`.
|
|
90
|
+
*/
|
|
91
|
+
destroy(reason) {
|
|
92
|
+
if (this._destroyed) return;
|
|
93
|
+
this._destroyed = true;
|
|
94
|
+
const err = reason ?? new Error("InMemoryTransport destroyed");
|
|
95
|
+
if (this._onErrorFn !== null) {
|
|
96
|
+
try {
|
|
97
|
+
this._onErrorFn(err);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (this._onCloseFn !== null) {
|
|
102
|
+
try {
|
|
103
|
+
this._onCloseFn();
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Register the data handler. Replaces any previously registered handler.
|
|
110
|
+
*
|
|
111
|
+
* @param fn - Called with each chunk delivered by the peer.
|
|
112
|
+
*/
|
|
113
|
+
onData(fn) {
|
|
114
|
+
this._onDataFn = fn;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Register the connect handler. Replaces any previously registered handler.
|
|
118
|
+
* Use `simulateConnect()` to trigger this in tests.
|
|
119
|
+
*
|
|
120
|
+
* @param fn - Called when `simulateConnect()` is invoked on this end.
|
|
121
|
+
*/
|
|
122
|
+
onConnect(fn) {
|
|
123
|
+
this._onConnectFn = fn;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Register the close handler. Replaces any previously registered handler.
|
|
127
|
+
*
|
|
128
|
+
* @param fn - Called when `close()` or `destroy()` is invoked on this end,
|
|
129
|
+
* or when the peer calls `close()`.
|
|
130
|
+
*/
|
|
131
|
+
onClose(fn) {
|
|
132
|
+
this._onCloseFn = fn;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Register the error handler. Replaces any previously registered handler.
|
|
136
|
+
*
|
|
137
|
+
* @param fn - Called when `destroy(reason)` is invoked on this end.
|
|
138
|
+
*/
|
|
139
|
+
onError(fn) {
|
|
140
|
+
this._onErrorFn = fn;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Simulate chunked TCP reads.
|
|
144
|
+
*
|
|
145
|
+
* After calling `split(n)`, any `write()` directed at this transport will be
|
|
146
|
+
* delivered to the `onData` handler in chunks of `n` bytes rather than
|
|
147
|
+
* all at once. Useful for verifying that a `FrameReader` reassembles frames
|
|
148
|
+
* correctly across chunk boundaries.
|
|
149
|
+
*
|
|
150
|
+
* @param bytesPerChunk - Chunk size in bytes. Pass `0` to disable chunking.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const [a, b] = InMemoryTransport.pair();
|
|
155
|
+
* b.split(1); // b receives one byte at a time
|
|
156
|
+
* a.write(Buffer.from([0x0b, 0x41, 0x1c, 0x0d])); // fires b.onData 4 times
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
split(bytesPerChunk) {
|
|
160
|
+
this._splitBytes = bytesPerChunk;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Pause delivery.
|
|
164
|
+
*
|
|
165
|
+
* While paused, writes directed at this transport are queued internally and
|
|
166
|
+
* the `onData` handler is NOT called. The queued chunks are flushed on `resume()`.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* b.pause();
|
|
171
|
+
* a.write(buf); // queued, b.onData not called
|
|
172
|
+
* b.resume(); // flushes queue synchronously
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
pause() {
|
|
176
|
+
this._paused = true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resume delivery — flushes all queued chunks to the `onData` handler,
|
|
180
|
+
* synchronously, in the order they were written.
|
|
181
|
+
*/
|
|
182
|
+
resume() {
|
|
183
|
+
this._paused = false;
|
|
184
|
+
const pending = this._pendingChunks.splice(0);
|
|
185
|
+
for (const chunk of pending) {
|
|
186
|
+
this._deliverChunk(chunk);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Fire the `onConnect` handler on this end.
|
|
191
|
+
*
|
|
192
|
+
* Call after `pair()` to simulate TCP connect completion. In production code,
|
|
193
|
+
* `NetTransport` fires the handler automatically when the socket emits `'connect'`.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* const [a] = InMemoryTransport.pair();
|
|
198
|
+
* a.onConnect(() => console.log('connected'));
|
|
199
|
+
* a.simulateConnect(); // fires immediately
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
simulateConnect() {
|
|
203
|
+
if (this._onConnectFn !== null) {
|
|
204
|
+
try {
|
|
205
|
+
this._onConnectFn();
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
_deliverChunk(buf) {
|
|
211
|
+
if (this._onDataFn === null) return;
|
|
212
|
+
if (this._writeDepth > 0) {
|
|
213
|
+
throw new Error("InMemoryTransport: re-entrant write detected");
|
|
214
|
+
}
|
|
215
|
+
if (this._splitBytes > 0) {
|
|
216
|
+
let offset = 0;
|
|
217
|
+
while (offset < buf.length) {
|
|
218
|
+
const end = Math.min(offset + this._splitBytes, buf.length);
|
|
219
|
+
const chunk = buf.subarray(offset, end);
|
|
220
|
+
this._writeDepth++;
|
|
221
|
+
try {
|
|
222
|
+
this._onDataFn(chunk);
|
|
223
|
+
} finally {
|
|
224
|
+
this._writeDepth--;
|
|
225
|
+
}
|
|
226
|
+
offset = end;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
this._writeDepth++;
|
|
230
|
+
try {
|
|
231
|
+
this._onDataFn(buf);
|
|
232
|
+
} finally {
|
|
233
|
+
this._writeDepth--;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export { InMemoryTransport };
|
|
240
|
+
//# sourceMappingURL=index.mjs.map
|
|
241
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/in-memory-transport.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * InMemoryTransport — deterministic, socket-free Transport for tests.\n *\n * Two connected ends are created via `InMemoryTransport.pair()`.\n * Writes to end A fire end B's registered `onData` handler **synchronously**\n * before `write()` returns — no async ceremony, no `await` in tests.\n *\n * @example\n * ```typescript\n * import { InMemoryTransport } from '@cosyte/mllp/testing';\n * const [a, b] = InMemoryTransport.pair();\n * b.onData((chunk) => console.log('received:', chunk));\n * a.write(Buffer.from([0x0b, 0x41, 0x1c, 0x0d]));\n * // b's onData fired synchronously — log printed before this line\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { Transport } from \"../transport/index.js\";\n\n/**\n * Deterministic in-memory transport for socket-free tests.\n *\n * Create connected pairs with `InMemoryTransport.pair()`. Writes to one end\n * deliver synchronously to the other end's `onData` handler — no async ceremony,\n * no `await` needed in tests.\n *\n * Supports `split(n)` for chunked-read simulation, `pause()`/`resume()` for\n * backpressure simulation, and `destroy(reason)` for abrupt-disconnect simulation.\n *\n * @example\n * ```typescript\n * const [client, server] = InMemoryTransport.pair();\n * server.onData((chunk) => server.write(chunk)); // echo\n * client.onData((echo) => console.log('echo:', echo));\n * client.write(Buffer.from('hello'));\n * ```\n */\nexport class InMemoryTransport implements Transport {\n private _peer: InMemoryTransport | null = null;\n\n private _onDataFn: ((chunk: Buffer) => void) | null = null;\n private _onConnectFn: (() => void) | null = null;\n private _onCloseFn: (() => void) | null = null;\n private _onErrorFn: ((err: Error) => void) | null = null;\n\n private _destroyed = false;\n private _paused = false;\n private _splitBytes = 0;\n private _pendingChunks: Buffer[] = [];\n private _writeDepth = 0;\n\n private constructor() {}\n\n /**\n * Create two connected InMemoryTransport ends.\n *\n * Writes to `a` deliver to `b`'s `onData` handler synchronously (and vice versa).\n * Call `a.simulateConnect()` after `pair()` to fire the `onConnect` handler,\n * simulating TCP connect completion.\n *\n * @returns Tuple `[a, b]` of two connected transports.\n *\n * @example\n * ```typescript\n * const [a, b] = InMemoryTransport.pair();\n * b.onData((chunk) => b.write(chunk)); // echo server\n * a.write(Buffer.from([0x0b, 0x41, 0x1c, 0x0d]));\n * ```\n */\n static pair(): [InMemoryTransport, InMemoryTransport] {\n const a = new InMemoryTransport();\n const b = new InMemoryTransport();\n a._peer = b;\n b._peer = a;\n return [a, b];\n }\n\n /**\n * Write `buf` to the peer transport.\n *\n * If the peer is paused, the buffer is copied and queued — returns `false`\n * (backpressure signal). Otherwise, the peer's `onData` handler fires\n * synchronously before this method returns.\n *\n * @returns `true` if delivered immediately; `false` if queued or transport is destroyed.\n */\n write(buf: Buffer): boolean {\n if (this._destroyed) return false;\n const peer = this._peer;\n if (peer === null || peer._destroyed) return false;\n\n if (peer._paused) {\n // Queue a copy — caller may reuse buf after write() returns (T-03-02-02)\n peer._pendingChunks.push(Buffer.from(buf));\n return false;\n }\n\n peer._deliverChunk(buf);\n return true;\n }\n\n /**\n * Initiate graceful close — fires `onClose` on this end and on the peer,\n * simulating a TCP FIN exchange.\n */\n close(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n // Fire onClose on this end\n if (this._onCloseFn !== null) {\n try {\n this._onCloseFn();\n } catch {\n /* swallow — test handlers must not break transport logic */\n }\n }\n // Simulate peer receiving FIN — fire peer's onClose\n const peer = this._peer;\n if (peer !== null && !peer._destroyed) {\n peer._destroyed = true;\n if (peer._onCloseFn !== null) {\n try {\n peer._onCloseFn();\n } catch {\n /* swallow */\n }\n }\n }\n }\n\n /**\n * Abruptly destroy this transport end.\n *\n * Fires `onError(reason)` then `onClose` on this end only. Does NOT affect\n * the peer (simulates one-sided abrupt disconnect, e.g., ECONNRESET on\n * just one socket).\n *\n * @param reason - Optional error; defaults to `new Error('InMemoryTransport destroyed')`.\n */\n destroy(reason?: Error): void {\n if (this._destroyed) return;\n this._destroyed = true;\n const err = reason ?? new Error(\"InMemoryTransport destroyed\");\n if (this._onErrorFn !== null) {\n try {\n this._onErrorFn(err);\n } catch {\n /* swallow */\n }\n }\n if (this._onCloseFn !== null) {\n try {\n this._onCloseFn();\n } catch {\n /* swallow */\n }\n }\n }\n\n /**\n * Register the data handler. Replaces any previously registered handler.\n *\n * @param fn - Called with each chunk delivered by the peer.\n */\n onData(fn: (chunk: Buffer) => void): void {\n this._onDataFn = fn;\n }\n\n /**\n * Register the connect handler. Replaces any previously registered handler.\n * Use `simulateConnect()` to trigger this in tests.\n *\n * @param fn - Called when `simulateConnect()` is invoked on this end.\n */\n onConnect(fn: () => void): void {\n this._onConnectFn = fn;\n }\n\n /**\n * Register the close handler. Replaces any previously registered handler.\n *\n * @param fn - Called when `close()` or `destroy()` is invoked on this end,\n * or when the peer calls `close()`.\n */\n onClose(fn: () => void): void {\n this._onCloseFn = fn;\n }\n\n /**\n * Register the error handler. Replaces any previously registered handler.\n *\n * @param fn - Called when `destroy(reason)` is invoked on this end.\n */\n onError(fn: (err: Error) => void): void {\n this._onErrorFn = fn;\n }\n\n /**\n * Simulate chunked TCP reads.\n *\n * After calling `split(n)`, any `write()` directed at this transport will be\n * delivered to the `onData` handler in chunks of `n` bytes rather than\n * all at once. Useful for verifying that a `FrameReader` reassembles frames\n * correctly across chunk boundaries.\n *\n * @param bytesPerChunk - Chunk size in bytes. Pass `0` to disable chunking.\n *\n * @example\n * ```typescript\n * const [a, b] = InMemoryTransport.pair();\n * b.split(1); // b receives one byte at a time\n * a.write(Buffer.from([0x0b, 0x41, 0x1c, 0x0d])); // fires b.onData 4 times\n * ```\n */\n split(bytesPerChunk: number): void {\n this._splitBytes = bytesPerChunk;\n }\n\n /**\n * Pause delivery.\n *\n * While paused, writes directed at this transport are queued internally and\n * the `onData` handler is NOT called. The queued chunks are flushed on `resume()`.\n *\n * @example\n * ```typescript\n * b.pause();\n * a.write(buf); // queued, b.onData not called\n * b.resume(); // flushes queue synchronously\n * ```\n */\n pause(): void {\n this._paused = true;\n }\n\n /**\n * Resume delivery — flushes all queued chunks to the `onData` handler,\n * synchronously, in the order they were written.\n */\n resume(): void {\n this._paused = false;\n const pending = this._pendingChunks.splice(0);\n for (const chunk of pending) {\n this._deliverChunk(chunk);\n }\n }\n\n /**\n * Fire the `onConnect` handler on this end.\n *\n * Call after `pair()` to simulate TCP connect completion. In production code,\n * `NetTransport` fires the handler automatically when the socket emits `'connect'`.\n *\n * @example\n * ```typescript\n * const [a] = InMemoryTransport.pair();\n * a.onConnect(() => console.log('connected'));\n * a.simulateConnect(); // fires immediately\n * ```\n */\n simulateConnect(): void {\n if (this._onConnectFn !== null) {\n try {\n this._onConnectFn();\n } catch {\n /* swallow */\n }\n }\n }\n\n private _deliverChunk(buf: Buffer): void {\n if (this._onDataFn === null) return;\n\n // Re-entrancy guard (D-03): writing from inside an onData handler would cause\n // infinite recursion or corrupt frame ordering. Detect and throw.\n if (this._writeDepth > 0) {\n throw new Error(\"InMemoryTransport: re-entrant write detected\");\n }\n\n if (this._splitBytes > 0) {\n let offset = 0;\n while (offset < buf.length) {\n const end = Math.min(offset + this._splitBytes, buf.length);\n const chunk = buf.subarray(offset, end);\n this._writeDepth++;\n try {\n this._onDataFn(chunk);\n } finally {\n this._writeDepth--;\n }\n offset = end;\n }\n } else {\n this._writeDepth++;\n try {\n this._onDataFn(buf);\n } finally {\n this._writeDepth--;\n }\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosyte/mllp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Production-grade MLLP client and server for Node.js — transport-only sibling to @cosyte/hl7",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.0.0"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Cosyte <hello@cosyte.com> (https://cosyte.com)",
|
|
11
|
+
"homepage": "https://github.com/cosyte/mllp#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cosyte/mllp/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/cosyte/mllp.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"hl7",
|
|
21
|
+
"mllp",
|
|
22
|
+
"healthcare",
|
|
23
|
+
"hl7v2",
|
|
24
|
+
"tcp"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.cjs",
|
|
34
|
+
"module": "./dist/index.mjs",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"default": "./dist/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/index.d.cts",
|
|
44
|
+
"default": "./dist/index.cjs"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./testing": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/testing/index.d.ts",
|
|
50
|
+
"default": "./dist/testing/index.mjs"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/testing/index.d.cts",
|
|
54
|
+
"default": "./dist/testing/index.cjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"./ack-from-hl7": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/ack-from-hl7/index.d.ts",
|
|
60
|
+
"default": "./dist/ack-from-hl7/index.mjs"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/ack-from-hl7/index.d.cts",
|
|
64
|
+
"default": "./dist/ack-from-hl7/index.cjs"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"./package.json": "./package.json"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public",
|
|
71
|
+
"provenance": true
|
|
72
|
+
},
|
|
73
|
+
"prettier": "@cosyte/prettier-config",
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@cosyte/hl7": ">=0.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"@cosyte/hl7": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@arethetypeswrong/cli": "0.18.4",
|
|
84
|
+
"@changesets/cli": "2.31.0",
|
|
85
|
+
"@cosyte/eslint-config": "^0.0.3",
|
|
86
|
+
"@cosyte/hl7": "file:vendor/cosyte-hl7-0.0.0.tgz",
|
|
87
|
+
"@cosyte/prettier-config": "^0.0.2",
|
|
88
|
+
"@cosyte/test-utils": "^0.0.1",
|
|
89
|
+
"@cosyte/tsconfig": "^0.0.2",
|
|
90
|
+
"@cosyte/tsup-config": "^0.0.1",
|
|
91
|
+
"@cosyte/vitest-config": "^0.0.1",
|
|
92
|
+
"@types/node": "^22.0.0",
|
|
93
|
+
"@types/node-forge": "1.3.14",
|
|
94
|
+
"@vitest/coverage-v8": "4.1.4",
|
|
95
|
+
"eslint": "10.5.0",
|
|
96
|
+
"fast-check": "3.23.2",
|
|
97
|
+
"node-forge": "1.4.0",
|
|
98
|
+
"prettier": "3.8.3",
|
|
99
|
+
"selfsigned": "2.4.1",
|
|
100
|
+
"tsup": "8.5.1",
|
|
101
|
+
"typescript": "5.9.3",
|
|
102
|
+
"vite": "7.3.6",
|
|
103
|
+
"vitest": "4.1.4"
|
|
104
|
+
},
|
|
105
|
+
"scripts": {
|
|
106
|
+
"build": "tsup",
|
|
107
|
+
"changeset": "changeset",
|
|
108
|
+
"typecheck": "tsc --noEmit",
|
|
109
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings=0",
|
|
110
|
+
"lint:fix": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix",
|
|
111
|
+
"format": "prettier --write \"src/**/*.{ts,md}\" \"test/**/*.ts\" \"*.{json,md,yml}\"",
|
|
112
|
+
"format:check": "prettier --check \"src/**/*.{ts,md}\" \"test/**/*.ts\" \"*.{json,md,yml}\"",
|
|
113
|
+
"test": "vitest run",
|
|
114
|
+
"test:watch": "vitest",
|
|
115
|
+
"test:coverage": "vitest run --coverage",
|
|
116
|
+
"certs:gen": "node scripts/generate-test-certs.mjs",
|
|
117
|
+
"clean": "rm -rf dist coverage",
|
|
118
|
+
"attw": "attw --pack . --profile node16",
|
|
119
|
+
"pack:docs": "bash scripts/build-docs-artifacts.sh",
|
|
120
|
+
"version": "changeset version && node scripts/sync-version.mjs && prettier --write package.json src/index.ts",
|
|
121
|
+
"publish:dry": "pnpm publish --dry-run --no-git-checks",
|
|
122
|
+
"release": "changeset publish"
|
|
123
|
+
}
|
|
124
|
+
}
|