@lessonkit/xapi 0.4.0 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@lessonkit/xapi`
2
2
 
3
- [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml)
3
+ [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@lessonkit/xapi.svg)](https://www.npmjs.com/package/@lessonkit/xapi)
5
5
  [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
6
 
@@ -18,8 +18,8 @@ npm install @lessonkit/xapi
18
18
  import { createXAPIClient } from "@lessonkit/xapi";
19
19
 
20
20
  const xapi = createXAPIClient({
21
+ courseId: "cyber-basics",
21
22
  transport: (statement) => {
22
- // Send to your LRS (or queue offline).
23
23
  console.log(statement);
24
24
  },
25
25
  });
@@ -27,8 +27,13 @@ const xapi = createXAPIClient({
27
27
  xapi.completeLesson({ lessonId: "phishing-101", durationMs: 1500, success: true, score: 7, maxScore: 10 });
28
28
  ```
29
29
 
30
- ## Notes (0.4.0)
30
+ Prefer mapping from telemetry: `telemetryEventToXAPIStatement(event)` (canonical object URNs).
31
31
 
32
+ ## Notes (0.6.0)
33
+
34
+ - `createXAPIClient` requires `courseId` for lifecycle helpers; React uses the mapper after each `track()`.
32
35
  - If the transport throws/rejects, statements are queued in-memory.
33
- - You can call `await xapi.flush()` to retry queued statements.
36
+ - Call `await xapi.flush()` to retry queued statements.
37
+
38
+ See [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md).
34
39
 
package/dist/index.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createInMemoryXAPIQueue: () => createInMemoryXAPIQueue,
24
- createXAPIClient: () => createXAPIClient
24
+ createXAPIClient: () => createXAPIClient,
25
+ telemetryEventToXAPIStatement: () => telemetryEventToXAPIStatement
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
@@ -48,6 +49,9 @@ function createInMemoryXAPIQueue() {
48
49
  }
49
50
 
50
51
  // src/client.ts
52
+ var import_core2 = require("@lessonkit/core");
53
+
54
+ // src/telemetryMap.ts
51
55
  var import_core = require("@lessonkit/core");
52
56
 
53
57
  // src/id.ts
@@ -65,24 +69,144 @@ function formatDurationMs(ms) {
65
69
  return `PT${fixed}S`;
66
70
  }
67
71
 
68
- // src/client.ts
72
+ // src/telemetryMap.ts
69
73
  var XAPIVerbs = {
70
- started: "http://adlnet.gov/expapi/verbs/initialized",
71
- completed: "http://adlnet.gov/expapi/verbs/completed"
74
+ initialized: "http://adlnet.gov/expapi/verbs/initialized",
75
+ completed: "http://adlnet.gov/expapi/verbs/completed",
76
+ answered: "http://adlnet.gov/expapi/verbs/answered",
77
+ experienced: "http://adlnet.gov/expapi/verbs/experienced"
72
78
  };
79
+ function telemetryEventToXAPIStatement(event) {
80
+ const { courseId } = event;
81
+ switch (event.name) {
82
+ case "course_started":
83
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId }), XAPIVerbs.initialized, event.timestamp);
84
+ case "course_completed":
85
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId }), XAPIVerbs.completed, event.timestamp);
86
+ case "lesson_started": {
87
+ const lessonId = event.lessonId;
88
+ return statementFor(
89
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId }),
90
+ XAPIVerbs.initialized,
91
+ event.timestamp
92
+ );
93
+ }
94
+ case "lesson_completed": {
95
+ const lessonId = event.lessonId;
96
+ const data = event.data;
97
+ const result = {};
98
+ if (typeof data?.durationMs === "number") {
99
+ result.duration = formatDurationMs(data.durationMs);
100
+ }
101
+ if (typeof data?.success === "boolean") result.success = data.success;
102
+ if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
103
+ const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
104
+ const raw = typeof data.score === "number" ? data.score : void 0;
105
+ result.score = {
106
+ raw,
107
+ max,
108
+ min: 0,
109
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
110
+ };
111
+ }
112
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
113
+ result: Object.keys(result).length ? result : void 0
114
+ });
115
+ }
116
+ case "lesson_time_on_task":
117
+ return null;
118
+ case "quiz_answered": {
119
+ const lessonId = event.lessonId;
120
+ const checkId = event.data.checkId;
121
+ return statementFor(
122
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, checkId }),
123
+ XAPIVerbs.answered,
124
+ event.timestamp
125
+ );
126
+ }
127
+ case "quiz_completed": {
128
+ const lessonId = event.lessonId;
129
+ const checkId = event.data.checkId;
130
+ const { score, maxScore } = event.data;
131
+ const result = {};
132
+ if (typeof score === "number" || typeof maxScore === "number") {
133
+ const max = typeof maxScore === "number" ? maxScore : void 0;
134
+ const raw = typeof score === "number" ? score : void 0;
135
+ result.score = {
136
+ raw,
137
+ max,
138
+ min: 0,
139
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
140
+ };
141
+ }
142
+ return statementFor(
143
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, checkId }),
144
+ XAPIVerbs.completed,
145
+ event.timestamp,
146
+ { result: Object.keys(result).length ? result : void 0 }
147
+ );
148
+ }
149
+ case "interaction": {
150
+ const lessonId = event.lessonId;
151
+ const blockId = event.data?.blockId;
152
+ if (!lessonId || !blockId) return null;
153
+ return statementFor(
154
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, blockId }),
155
+ XAPIVerbs.experienced,
156
+ event.timestamp
157
+ );
158
+ }
159
+ default:
160
+ return null;
161
+ }
162
+ }
163
+ function statementFor(objectId, verb, timestamp, extra) {
164
+ return {
165
+ id: cryptoRandomId(),
166
+ timestamp,
167
+ verb,
168
+ object: { id: objectId },
169
+ result: extra?.result,
170
+ context: extra?.context
171
+ };
172
+ }
173
+
174
+ // src/client.ts
175
+ function isDevEnvironment() {
176
+ const g = globalThis;
177
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
178
+ }
73
179
  function createXAPIClient(opts) {
74
180
  const transport = opts?.transport;
75
- const baseId = opts?.baseId ?? "urn:lessonkit";
181
+ const courseId = opts?.courseId;
76
182
  const queue = opts?.queue ?? createInMemoryXAPIQueue();
183
+ let warnedNoTransport = false;
184
+ let warnedTransportFailure = false;
77
185
  const sendOrQueue = (statement) => {
78
186
  if (!transport) {
79
187
  queue.enqueue(statement);
188
+ if (isDevEnvironment() && !warnedNoTransport) {
189
+ warnedNoTransport = true;
190
+ console.warn(
191
+ "[lessonkit] xAPI statements are queued but no transport is configured; pass config.xapi.transport or config.xapi.client"
192
+ );
193
+ }
80
194
  return;
81
195
  }
82
196
  void Promise.resolve().then(() => transport(statement)).catch(() => {
83
197
  queue.enqueue(statement);
198
+ if (isDevEnvironment() && !warnedTransportFailure) {
199
+ warnedTransportFailure = true;
200
+ console.warn(
201
+ "[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
202
+ );
203
+ }
84
204
  });
85
205
  };
206
+ const emit = (event) => {
207
+ const statement = telemetryEventToXAPIStatement(event);
208
+ if (statement) sendOrQueue(statement);
209
+ };
86
210
  return {
87
211
  send: (statement) => {
88
212
  sendOrQueue(statement);
@@ -93,46 +217,44 @@ function createXAPIClient(opts) {
93
217
  await queue.flush(transport);
94
218
  },
95
219
  startedLesson: ({ lessonId }) => {
96
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.started);
97
- sendOrQueue(statement);
220
+ if (!courseId) return;
221
+ emit({
222
+ name: "lesson_started",
223
+ timestamp: (0, import_core2.nowIso)(),
224
+ courseId,
225
+ lessonId,
226
+ data: { lessonId }
227
+ });
98
228
  },
99
- completeLesson: ({ lessonId, durationMs, score, maxScore, success }) => {
100
- const result = {};
101
- if (typeof durationMs === "number") result.duration = formatDurationMs(durationMs);
102
- if (typeof success === "boolean") result.success = success;
103
- if (typeof score === "number" || typeof maxScore === "number") {
104
- const max = typeof maxScore === "number" ? maxScore : void 0;
105
- const raw = typeof score === "number" ? score : void 0;
106
- result.score = {
107
- raw,
108
- max,
109
- min: 0,
110
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
111
- };
112
- }
113
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
114
- result: Object.keys(result).length ? result : void 0
229
+ completeLesson: ({
230
+ lessonId,
231
+ durationMs,
232
+ score,
233
+ maxScore,
234
+ success
235
+ }) => {
236
+ if (!courseId) return;
237
+ emit({
238
+ name: "lesson_completed",
239
+ timestamp: (0, import_core2.nowIso)(),
240
+ courseId,
241
+ lessonId,
242
+ data: { lessonId, durationMs, score, maxScore, success }
115
243
  });
116
- sendOrQueue(statement);
117
244
  },
118
245
  completeCourse: () => {
119
- const statement = statementFor(`${baseId}:course`, XAPIVerbs.completed);
120
- sendOrQueue(statement);
246
+ if (!courseId) return;
247
+ emit({
248
+ name: "course_completed",
249
+ timestamp: (0, import_core2.nowIso)(),
250
+ courseId
251
+ });
121
252
  }
122
253
  };
123
254
  }
124
- function statementFor(objectId, verb, extra) {
125
- return {
126
- id: cryptoRandomId(),
127
- timestamp: (0, import_core.nowIso)(),
128
- verb,
129
- object: { id: objectId },
130
- result: extra?.result,
131
- context: extra?.context
132
- };
133
- }
134
255
  // Annotate the CommonJS export names for ESM import in node:
135
256
  0 && (module.exports = {
136
257
  createInMemoryXAPIQueue,
137
- createXAPIClient
258
+ createXAPIClient,
259
+ telemetryEventToXAPIStatement
138
260
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { LessonId } from '@lessonkit/core';
1
+ import { LessonId, CourseId, TelemetryEvent } from '@lessonkit/core';
2
2
 
3
3
  type XAPIStatement = {
4
4
  id: string;
@@ -38,8 +38,14 @@ declare function createInMemoryXAPIQueue(): XAPIQueue;
38
38
 
39
39
  declare function createXAPIClient(opts?: {
40
40
  transport?: XAPITransport;
41
- baseId?: string;
41
+ courseId?: CourseId;
42
42
  queue?: XAPIQueue;
43
43
  }): XAPIClient;
44
44
 
45
- export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient };
45
+ /**
46
+ * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
47
+ * `lesson_time_on_task` returns null (companion metric; lesson_completed carries duration).
48
+ */
49
+ declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
50
+
51
+ export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LessonId } from '@lessonkit/core';
1
+ import { LessonId, CourseId, TelemetryEvent } from '@lessonkit/core';
2
2
 
3
3
  type XAPIStatement = {
4
4
  id: string;
@@ -38,8 +38,14 @@ declare function createInMemoryXAPIQueue(): XAPIQueue;
38
38
 
39
39
  declare function createXAPIClient(opts?: {
40
40
  transport?: XAPITransport;
41
- baseId?: string;
41
+ courseId?: CourseId;
42
42
  queue?: XAPIQueue;
43
43
  }): XAPIClient;
44
44
 
45
- export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient };
45
+ /**
46
+ * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
47
+ * `lesson_time_on_task` returns null (companion metric; lesson_completed carries duration).
48
+ */
49
+ declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
50
+
51
+ export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ function createInMemoryXAPIQueue() {
23
23
  // src/client.ts
24
24
  import { nowIso } from "@lessonkit/core";
25
25
 
26
+ // src/telemetryMap.ts
27
+ import { buildLessonkitUrn } from "@lessonkit/core";
28
+
26
29
  // src/id.ts
27
30
  function cryptoRandomId() {
28
31
  const g = globalThis;
@@ -38,24 +41,144 @@ function formatDurationMs(ms) {
38
41
  return `PT${fixed}S`;
39
42
  }
40
43
 
41
- // src/client.ts
44
+ // src/telemetryMap.ts
42
45
  var XAPIVerbs = {
43
- started: "http://adlnet.gov/expapi/verbs/initialized",
44
- completed: "http://adlnet.gov/expapi/verbs/completed"
46
+ initialized: "http://adlnet.gov/expapi/verbs/initialized",
47
+ completed: "http://adlnet.gov/expapi/verbs/completed",
48
+ answered: "http://adlnet.gov/expapi/verbs/answered",
49
+ experienced: "http://adlnet.gov/expapi/verbs/experienced"
45
50
  };
51
+ function telemetryEventToXAPIStatement(event) {
52
+ const { courseId } = event;
53
+ switch (event.name) {
54
+ case "course_started":
55
+ return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.initialized, event.timestamp);
56
+ case "course_completed":
57
+ return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.completed, event.timestamp);
58
+ case "lesson_started": {
59
+ const lessonId = event.lessonId;
60
+ return statementFor(
61
+ buildLessonkitUrn({ courseId, lessonId }),
62
+ XAPIVerbs.initialized,
63
+ event.timestamp
64
+ );
65
+ }
66
+ case "lesson_completed": {
67
+ const lessonId = event.lessonId;
68
+ const data = event.data;
69
+ const result = {};
70
+ if (typeof data?.durationMs === "number") {
71
+ result.duration = formatDurationMs(data.durationMs);
72
+ }
73
+ if (typeof data?.success === "boolean") result.success = data.success;
74
+ if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
75
+ const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
76
+ const raw = typeof data.score === "number" ? data.score : void 0;
77
+ result.score = {
78
+ raw,
79
+ max,
80
+ min: 0,
81
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
82
+ };
83
+ }
84
+ return statementFor(buildLessonkitUrn({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
85
+ result: Object.keys(result).length ? result : void 0
86
+ });
87
+ }
88
+ case "lesson_time_on_task":
89
+ return null;
90
+ case "quiz_answered": {
91
+ const lessonId = event.lessonId;
92
+ const checkId = event.data.checkId;
93
+ return statementFor(
94
+ buildLessonkitUrn({ courseId, lessonId, checkId }),
95
+ XAPIVerbs.answered,
96
+ event.timestamp
97
+ );
98
+ }
99
+ case "quiz_completed": {
100
+ const lessonId = event.lessonId;
101
+ const checkId = event.data.checkId;
102
+ const { score, maxScore } = event.data;
103
+ const result = {};
104
+ if (typeof score === "number" || typeof maxScore === "number") {
105
+ const max = typeof maxScore === "number" ? maxScore : void 0;
106
+ const raw = typeof score === "number" ? score : void 0;
107
+ result.score = {
108
+ raw,
109
+ max,
110
+ min: 0,
111
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
112
+ };
113
+ }
114
+ return statementFor(
115
+ buildLessonkitUrn({ courseId, lessonId, checkId }),
116
+ XAPIVerbs.completed,
117
+ event.timestamp,
118
+ { result: Object.keys(result).length ? result : void 0 }
119
+ );
120
+ }
121
+ case "interaction": {
122
+ const lessonId = event.lessonId;
123
+ const blockId = event.data?.blockId;
124
+ if (!lessonId || !blockId) return null;
125
+ return statementFor(
126
+ buildLessonkitUrn({ courseId, lessonId, blockId }),
127
+ XAPIVerbs.experienced,
128
+ event.timestamp
129
+ );
130
+ }
131
+ default:
132
+ return null;
133
+ }
134
+ }
135
+ function statementFor(objectId, verb, timestamp, extra) {
136
+ return {
137
+ id: cryptoRandomId(),
138
+ timestamp,
139
+ verb,
140
+ object: { id: objectId },
141
+ result: extra?.result,
142
+ context: extra?.context
143
+ };
144
+ }
145
+
146
+ // src/client.ts
147
+ function isDevEnvironment() {
148
+ const g = globalThis;
149
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
150
+ }
46
151
  function createXAPIClient(opts) {
47
152
  const transport = opts?.transport;
48
- const baseId = opts?.baseId ?? "urn:lessonkit";
153
+ const courseId = opts?.courseId;
49
154
  const queue = opts?.queue ?? createInMemoryXAPIQueue();
155
+ let warnedNoTransport = false;
156
+ let warnedTransportFailure = false;
50
157
  const sendOrQueue = (statement) => {
51
158
  if (!transport) {
52
159
  queue.enqueue(statement);
160
+ if (isDevEnvironment() && !warnedNoTransport) {
161
+ warnedNoTransport = true;
162
+ console.warn(
163
+ "[lessonkit] xAPI statements are queued but no transport is configured; pass config.xapi.transport or config.xapi.client"
164
+ );
165
+ }
53
166
  return;
54
167
  }
55
168
  void Promise.resolve().then(() => transport(statement)).catch(() => {
56
169
  queue.enqueue(statement);
170
+ if (isDevEnvironment() && !warnedTransportFailure) {
171
+ warnedTransportFailure = true;
172
+ console.warn(
173
+ "[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
174
+ );
175
+ }
57
176
  });
58
177
  };
178
+ const emit = (event) => {
179
+ const statement = telemetryEventToXAPIStatement(event);
180
+ if (statement) sendOrQueue(statement);
181
+ };
59
182
  return {
60
183
  send: (statement) => {
61
184
  sendOrQueue(statement);
@@ -66,45 +189,43 @@ function createXAPIClient(opts) {
66
189
  await queue.flush(transport);
67
190
  },
68
191
  startedLesson: ({ lessonId }) => {
69
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.started);
70
- sendOrQueue(statement);
192
+ if (!courseId) return;
193
+ emit({
194
+ name: "lesson_started",
195
+ timestamp: nowIso(),
196
+ courseId,
197
+ lessonId,
198
+ data: { lessonId }
199
+ });
71
200
  },
72
- completeLesson: ({ lessonId, durationMs, score, maxScore, success }) => {
73
- const result = {};
74
- if (typeof durationMs === "number") result.duration = formatDurationMs(durationMs);
75
- if (typeof success === "boolean") result.success = success;
76
- if (typeof score === "number" || typeof maxScore === "number") {
77
- const max = typeof maxScore === "number" ? maxScore : void 0;
78
- const raw = typeof score === "number" ? score : void 0;
79
- result.score = {
80
- raw,
81
- max,
82
- min: 0,
83
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
84
- };
85
- }
86
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
87
- result: Object.keys(result).length ? result : void 0
201
+ completeLesson: ({
202
+ lessonId,
203
+ durationMs,
204
+ score,
205
+ maxScore,
206
+ success
207
+ }) => {
208
+ if (!courseId) return;
209
+ emit({
210
+ name: "lesson_completed",
211
+ timestamp: nowIso(),
212
+ courseId,
213
+ lessonId,
214
+ data: { lessonId, durationMs, score, maxScore, success }
88
215
  });
89
- sendOrQueue(statement);
90
216
  },
91
217
  completeCourse: () => {
92
- const statement = statementFor(`${baseId}:course`, XAPIVerbs.completed);
93
- sendOrQueue(statement);
218
+ if (!courseId) return;
219
+ emit({
220
+ name: "course_completed",
221
+ timestamp: nowIso(),
222
+ courseId
223
+ });
94
224
  }
95
225
  };
96
226
  }
97
- function statementFor(objectId, verb, extra) {
98
- return {
99
- id: cryptoRandomId(),
100
- timestamp: nowIso(),
101
- verb,
102
- object: { id: objectId },
103
- result: extra?.result,
104
- context: extra?.context
105
- };
106
- }
107
227
  export {
108
228
  createInMemoryXAPIQueue,
109
- createXAPIClient
229
+ createXAPIClient,
230
+ telemetryEventToXAPIStatement
110
231
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/xapi",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "xAPI statement generation primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -45,7 +45,7 @@
45
45
  "lint": "echo \"(no lint configured yet)\""
46
46
  },
47
47
  "dependencies": {
48
- "@lessonkit/core": "0.4.0"
48
+ "@lessonkit/core": "0.6.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsup": "^8.5.0",