discord-html-transcripts-fix 2.0.0 → 2.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 CHANGED
@@ -22,6 +22,31 @@ npm install discord-html-transcripts-fix
22
22
 
23
23
  `discord.js` is the only **required** peer dependency — React, Lit SSR, the markdown parser, etc. are installed automatically. `sharp` is an optional peer (image compression only).
24
24
 
25
+ <details>
26
+ <summary>Silencing the <code>node-domexception</code> deprecation warning on install</summary>
27
+
28
+ Lit SSR depends on `node-fetch`, which still pulls in the deprecated
29
+ `fetch-blob → node-domexception` chain, so `npm install` prints:
30
+
31
+ ```
32
+ npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
33
+ ```
34
+
35
+ It is cosmetic — install-time only, never at runtime, and `npm audit` reports nothing
36
+ for it. If you want it gone, swap `node-fetch` for a dependency-free drop-in in **your
37
+ own** `package.json` (npm only honours `overrides` in the root project, so this cannot
38
+ be shipped from here):
39
+
40
+ ```json
41
+ {
42
+ "overrides": {
43
+ "node-fetch": "npm:node-fetch-native@^1.6.7"
44
+ }
45
+ }
46
+ ```
47
+
48
+ </details>
49
+
25
50
  ## Quick start
26
51
 
27
52
  ```js
@@ -63,6 +88,8 @@ const stream = await createTranscript(channel, {
63
88
  | `saveImages` | `boolean` | `false` | Download images and inline them as base64 data URLs. |
64
89
  | `favicon` | `'guild'` \| `string` | `'guild'` | Page favicon — `'guild'` uses the server icon, or pass a URL. |
65
90
  | `hydrate` | `boolean` | `false` | Server-side hydrate via `@lit-labs/ssr` (slower; usually leave off). |
91
+ | `dateFormat` | `'dd/mm/yyyy'` \| `'mm/dd/yyyy'` | `'dd/mm/yyyy'` | Date order for message timestamps older than yesterday. |
92
+ | `timeFormat` | `'24h'` \| `'12h'` | `'24h'` | Clock format for message timestamps — `07:16` vs `07:16 AM`. |
66
93
  | `language` | `'en'` \| `'de'` | `'en'` | UI language for participant labels, filter strings, etc. |
67
94
  | `i18n` | `Partial<Record<lang, Record<key,string>>>` | — | Override individual strings per language. |
68
95
  | `statsFooter` | `false` \| `{ enabled?, template? }` | `{ enabled: true }` | Bottom stats line. See below. |
@@ -70,12 +97,37 @@ const stream = await createTranscript(channel, {
70
97
  | `poweredBy` | `boolean` | `false` | Show the original "Powered by discord-html-transcripts" credit link. Only renders when `statsFooter` is disabled. |
71
98
  | `callbacks` | `{ resolveUser, resolveRole, resolveChannel, resolveImageSrc }` | — | Custom resolvers for mentions / image URLs. |
72
99
 
100
+ ### Message timestamps
101
+
102
+ Timestamps are worded the way Discord words them, relative to when the transcript was
103
+ generated:
104
+
105
+ | When the message was sent | Rendered as |
106
+ | --- | --- |
107
+ | Today | `07:16` |
108
+ | Yesterday | `Yesterday at 07:16` (`Gestern um 07:16` with `language: 'de'`) |
109
+ | Anything older | `11/08/2026 07:16` |
110
+
111
+ The reference point is transcript creation time, so an archived transcript keeps saying
112
+ the same thing no matter when it is opened. The exact instant stays machine-readable in
113
+ `data-timestamp` (epoch ms) and `data-timestamp-iso` on every `<discord-message>`.
114
+
115
+ ```js
116
+ await createTranscript(channel, {
117
+ dateFormat: 'mm/dd/yyyy', // default 'dd/mm/yyyy'
118
+ timeFormat: '12h', // default '24h'
119
+ });
120
+ // → "07:16 AM", "Yesterday at 10:45 PM", "08/11/2026 02:30 PM"
121
+ ```
122
+
123
+ Times use the timezone of the machine generating the transcript.
124
+
73
125
  ### Configurable stats footer
74
126
 
75
127
  ```js
76
128
  await createTranscript(channel, {
77
129
  statsFooter: {
78
- template: '{messages} Nachrichten · {participants} Teilnehmer · {images} Bilder · {from} → {to} · {span}',
130
+ template: '{messages} messages from {participants} people · {images} images · {from} → {to}',
79
131
  },
80
132
  });
81
133
 
@@ -199,6 +251,9 @@ In addition to plain text, replies, embeds, and attachments, the viewer supports
199
251
 
200
252
  - **`react`, `react-dom`, `debug` moved into regular dependencies** so users don't install them manually (`debug` was actually a missing runtime dep in the original — `images.js` requires it)
201
253
  - **`sharp` declared as an optional peer dependency** — needed only for `.withCompression()`, no longer a hidden requirement
254
+ - **Discord-style message timestamps** — today shows the bare time (`07:16`), yesterday reads `Yesterday at 07:16`, anything older gets `11/08/2026 07:16`. Configurable via `dateFormat` and `timeFormat`. Previously the raw ISO string (`2026-08-14T07:16:07.422Z`) was rendered, because the web component only formats real `Date` objects and an HTML attribute always arrives as a string
255
+ - **`hydrate: true` actually works** — the markup was handed to Lit as a plain string, which Lit HTML-escapes, so the option emitted a page of visible `&lt;!DOCTYPE html&gt;…` source text instead of a transcript
256
+ - **`lit` added as an explicit dependency** — it was previously only reachable as a transitive dependency of `@lit-labs/ssr`, which does not resolve under pnpm's strict layout
202
257
  - **TypeScript declarations match runtime** — `ExportReturnType.Stream`, `language`, `i18n`, `stream`, and `withConcurrency()` are now exposed in the types
203
258
  - `discord.js` remains the only **required** peer dependency
204
259
 
@@ -27,6 +27,18 @@ const styles_1 = require("./renderers/components/styles");
27
27
  const DiscordImage_1 = require("./renderers/components/DiscordImage");
28
28
  const DiscordHighlightedCode_1 = require("./renderers/components/DiscordHighlightedCode");
29
29
 
30
+ // Warn at most once per process when hydration is requested without @lit-labs/ssr.
31
+ let warnedMissingLitSsr = false;
32
+
33
+ // Moves <!DOCTYPE html> back to the very front of the document. Lit SSR emits
34
+ // <!--lit-part--> markers around its result, and a comment before the doctype
35
+ // makes browsers fall back to quirks mode.
36
+ function hoistDoctype(markup) {
37
+ const match = markup.match(/<!DOCTYPE\s+html[^>]*>/i);
38
+ if (!match || markup.startsWith(match[0])) return markup;
39
+ return match[0] + markup.slice(0, match.index) + markup.slice(match.index + match[0].length);
40
+ }
41
+
30
42
  // Lock to an exact resolved version — semver ranges in src= aren't cacheable by CDNs.
31
43
  let discordComponentsVersion = '4.0.2';
32
44
  try {
@@ -119,7 +131,14 @@ async function render(_a) {
119
131
  stats,
120
132
  };
121
133
 
122
- const renderContext = Object.assign({ lang, i18n, dateSeps, stats }, callbacks ? { callbacks } : {});
134
+ // Resolved once so every message in this transcript is measured against the
135
+ // same "now" and shares one date/time format.
136
+ const timestampFormat = (0, utils_1.resolveTimestampFormat)({
137
+ dateFormat: options.dateFormat,
138
+ timeFormat: options.timeFormat,
139
+ });
140
+
141
+ const renderContext = Object.assign({ lang, i18n, dateSeps, stats, timestampFormat }, callbacks ? { callbacks } : {});
123
142
 
124
143
  const docTree = (0, jsx_runtime_1.jsxs)("html", { lang, children: [
125
144
  (0, jsx_runtime_1.jsxs)("head", { children: [
@@ -175,10 +194,39 @@ async function render(_a) {
175
194
 
176
195
  if (options.hydrate) {
177
196
  const markup = await (0, utils_1.streamToString)(prelude);
178
- const { render: renderLit } = await import('@lit-labs/ssr');
179
- const result = renderLit(markup);
180
- const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
181
- return await collectResult(result);
197
+ // @lit-labs/ssr and lit are regular dependencies, so this normally always
198
+ // resolves. The guard only covers a broken or partial install: instead of
199
+ // failing the whole export we fall back to the non-SSR'd markup, which is
200
+ // still a fully working transcript because the <discord-*> definitions are
201
+ // loaded from the CDN module tag above regardless of this branch.
202
+ try {
203
+ const { render: renderLit } = await import('@lit-labs/ssr');
204
+ const { html: litHtml } = await import('lit');
205
+ const { unsafeHTML } = await import('lit/directives/unsafe-html.js');
206
+ const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
207
+ // `markup` must go through unsafeHTML: handing lit a bare string makes
208
+ // it a text value, which lit HTML-escapes — that turned the whole
209
+ // transcript into visible &lt;!DOCTYPE html&gt;… source text.
210
+ const result = renderLit(litHtml`${unsafeHTML(markup)}`);
211
+ const rendered = await collectResult(result);
212
+ // lit wraps its output in <!--lit-part--> markers. Any comment ahead of
213
+ // the doctype pushes browsers into quirks mode and wrecks the layout,
214
+ // so the doctype has to lead the document again.
215
+ return hoistDoctype(rendered);
216
+ }
217
+ catch (err) {
218
+ // Only a missing module triggers the fallback — a genuine render
219
+ // failure must still surface instead of being silently swallowed.
220
+ const code = err && err.code;
221
+ if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') throw err;
222
+ if (!warnedMissingLitSsr) {
223
+ warnedMissingLitSsr = true;
224
+ console.warn('[discord-html-transcripts-fix] `hydrate: true` could not load @lit-labs/ssr / lit, ' +
225
+ 'although both ship as dependencies of this package. Returning non-hydrated markup — the ' +
226
+ 'transcript still renders correctly. Reinstalling your dependencies should fix this.');
227
+ }
228
+ return markup;
229
+ }
182
230
  }
183
231
 
184
232
  if (options.returnType === 'stream' || options.stream) {
@@ -10,6 +10,11 @@ const embeds_1 = require("../../utils/embeds");
10
10
  const content_1 = __importStar(require("./content"));
11
11
  const utils_1 = require("../../utils/utils");
12
12
 
13
+ function t(context, key, fallback) {
14
+ const dict = context?.i18n?.[context?.lang] || context?.i18n?.en || {};
15
+ return dict[key] || fallback || key;
16
+ }
17
+
13
18
  async function DiscordEmbed({ embed, context }) {
14
19
  const type = embed.type || 'rich';
15
20
  const url = embed.url ? (0, utils_1.safeHref)(embed.url) : undefined;
@@ -71,7 +76,7 @@ async function DiscordEmbed({ embed, context }) {
71
76
  embed.footer && ((0, jsx_runtime_1.jsx)("discord-embed-footer", {
72
77
  slot: "footer",
73
78
  footerImage: embed.footer.proxyIconURL ?? embed.footer.iconURL,
74
- timestamp: embed.timestamp ?? undefined,
79
+ timestamp: (0, utils_1.formatMessageTimestamp)(embed.timestamp, context?.timestampFormat, t(context, 'yesterdayAt', 'Yesterday at {time}')),
75
80
  children: embed.footer.text
76
81
  }))
77
82
  ]
@@ -110,6 +110,13 @@ async function DiscordMessage({ message, context }) {
110
110
  const snapshots = Array.isArray(message.messageSnapshots) ? message.messageSnapshots : [];
111
111
 
112
112
  const editedAtIso = message.editedAt instanceof Date ? message.editedAt.toISOString() : null;
113
+ // Hover text for the "(edited)" marker — the ISO string stays in
114
+ // data-edit-iso for tooling, but humans get the same wording as the
115
+ // message timestamps instead of "2026-08-14T07:16:07.422Z".
116
+ const editedAtLabel = (0, utils_1.formatMessageTimestamp)(message.editedAt, context?.timestampFormat, t(context, 'yesterdayAt', 'Yesterday at {time}'));
117
+ const editedAtTitle = editedAtLabel
118
+ ? t(context, 'editedAt', 'Edited at {time}').replace('{time}', editedAtLabel)
119
+ : undefined;
113
120
  const pinned = !!message.pinned;
114
121
 
115
122
  // Forum applied tags — only on the first message of a forum thread (the post itself)
@@ -138,17 +145,20 @@ async function DiscordMessage({ message, context }) {
138
145
  // no longer render a separate "/cmd opt:val by user" line.
139
146
  const slashData = buildSlashCommandData(message);
140
147
 
141
- // FIX: pass timestamp as ISO string. React would otherwise stringify a
142
- // Date via toString() (locale-dependent like "Tue May 26 2026 …"),
143
- // which skyra's <discord-message> often fails to parse — falling back
144
- // to new Date() == transcript creation time. ISO is round-trip safe.
148
+ // The timestamp is formatted here rather than handed to <discord-message>
149
+ // as a Date or ISO string. Skyra's converter only formats real Date
150
+ // objects — an attribute always arrives as a string, so it passed the raw
151
+ // "2026-08-14T07:16:07.422Z" straight through to the page. Preformatting
152
+ // also lets us match Discord's own today/yesterday/date wording and honour
153
+ // the dateFormat + timeFormat options.
145
154
  const createdAtIso = message.createdAt instanceof Date
146
155
  ? message.createdAt.toISOString()
147
156
  : (typeof message.createdAt === 'string' ? message.createdAt : undefined);
157
+ const timestampLabel = (0, utils_1.formatMessageTimestamp)(message.createdAt, context?.timestampFormat, t(context, 'yesterdayAt', 'Yesterday at {time}'));
148
158
 
149
159
  return ((0, jsx_runtime_1.jsxs)("discord-message", {
150
160
  id: `m-${message.id}`,
151
- timestamp: createdAtIso,
161
+ timestamp: timestampLabel,
152
162
  edited: message.editedAt !== null,
153
163
  server: (isCrossGuildReply || isCrosspost || isCrossposted) ? true : undefined,
154
164
  highlight: message.mentions.everyone || pinned,
@@ -158,6 +168,7 @@ async function DiscordMessage({ message, context }) {
158
168
  "data-author-name": authorName,
159
169
  "data-roles": memberRoleIds,
160
170
  "data-timestamp": ts ? String(ts) : undefined,
171
+ "data-timestamp-iso": createdAtIso,
161
172
  "data-text": lowerText,
162
173
  "data-pinned": pinned ? 'true' : undefined,
163
174
  "data-has-image": hasImage ? 'true' : undefined,
@@ -184,7 +195,7 @@ async function DiscordMessage({ message, context }) {
184
195
  (0, jsx_runtime_1.jsx)("span", { className: "dht-activity-icon", children: '🎮' }),
185
196
  (0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${activity.partyId.slice(-6)}` : ''] })
186
197
  ] }),
187
- editedAtIso && (0, jsx_runtime_1.jsx)("span", { className: "dht-edit-marker", title: editedAtIso, "data-edit-iso": editedAtIso, "data-i18n": "edited", "data-i18n-params": JSON.stringify({ time: editedAtIso }), children: '(' + t(context, 'edited', 'edited') + ')' }),
198
+ editedAtIso && (0, jsx_runtime_1.jsx)("span", { className: "dht-edit-marker", title: editedAtTitle || editedAtIso, "data-edit-iso": editedAtIso, "data-i18n": "edited", "data-i18n-params": JSON.stringify({ time: editedAtLabel || editedAtIso }), children: '(' + t(context, 'edited', 'edited') + ')' }),
188
199
  Array.isArray(message.editHistory) && message.editHistory.length > 0 && renderEditHistory(message.editHistory, context),
189
200
  snapshots.length > 0 && renderSnapshots(snapshots, context),
190
201
  message.content && ((0, jsx_runtime_1.jsx)(content_1.default, { content: message.content, context: Object.assign({}, context, { type: message.webhookId ? content_1.RenderType.WEBHOOK : content_1.RenderType.NORMAL }) })),
@@ -18,11 +18,11 @@ async function SystemMessage({ message, context }) {
18
18
  const color = message.member?.roles?.color?.hexColor;
19
19
  const authorId = author?.id;
20
20
  const ctx = context || {};
21
- // FIX: pass message.createdAt as ISO string so skyra renders the original
22
- // timestamp instead of falling back to "now" (= transcript creation time).
23
- const ts = message.createdAt instanceof Date
24
- ? message.createdAt.toISOString()
25
- : (typeof message.createdAt === 'string' ? message.createdAt : undefined);
21
+ // Preformatted for the same reason as in message.js: skyra only formats real
22
+ // Date objects, and an attribute always arrives as a string — so an ISO value
23
+ // was rendered verbatim. This also keeps system messages worded like the
24
+ // regular ones (today / yesterday / full date).
25
+ const ts = (0, utils_1.formatMessageTimestamp)(message.createdAt, ctx.timestampFormat, t(ctx, 'yesterdayAt', 'Yesterday at {time}'));
26
26
 
27
27
  switch (message.type) {
28
28
  case discord_js_1.MessageType.RecipientAdd:
@@ -61,6 +61,7 @@ const defaultDicts = {
61
61
  en: {
62
62
  edited: 'edited',
63
63
  editedAt: 'Edited at {time}',
64
+ yesterdayAt: 'Yesterday at {time}',
64
65
  joined: 'joined the server',
65
66
  pinned: 'pinned {message} to this channel.',
66
67
  pinnedLink: 'a message',
@@ -114,6 +115,7 @@ const defaultDicts = {
114
115
  de: {
115
116
  edited: 'bearbeitet',
116
117
  editedAt: 'Bearbeitet am {time}',
118
+ yesterdayAt: 'Gestern um {time}',
117
119
  joined: 'ist dem Server beigetreten',
118
120
  pinned: 'hat {message} in diesem Channel angepinnt.',
119
121
  pinnedLink: 'eine Nachricht',
package/dist/types.d.ts CHANGED
@@ -61,7 +61,7 @@ export type GenerateFromMessagesOptions<T extends ExportReturnType> = Partial<{
61
61
  * Stats footer rendered at the bottom (e.g. "12 messages · 3 participants · 2 images · …").
62
62
  * - `false` to disable entirely
63
63
  * - `{ enabled: false }` to disable
64
- * - `{ template: '{messages} Nachrichten · {participants} Teilnehmer · {images} Bilder · {from} → {to} · {span}' }`
64
+ * - `{ template: '{messages} messages from {participants} people · {images} images · {from} → {to}' }`
65
65
  * to render with a custom string. Supported placeholders:
66
66
  * `{messages}` `{participants}` `{images}` `{from}` `{to}` `{span}`
67
67
  *
@@ -71,6 +71,18 @@ export type GenerateFromMessagesOptions<T extends ExportReturnType> = Partial<{
71
71
  enabled?: boolean;
72
72
  template?: string;
73
73
  };
74
+ /**
75
+ * Date order used whenever a message timestamp is older than yesterday
76
+ * (e.g. `11/08/2026 07:16`).
77
+ * @default 'dd/mm/yyyy'
78
+ */
79
+ dateFormat: 'dd/mm/yyyy' | 'mm/dd/yyyy';
80
+ /**
81
+ * Clock format for message timestamps — `'24h'` renders `07:16`,
82
+ * `'12h'` renders `07:16 AM`.
83
+ * @default '24h'
84
+ */
85
+ timeFormat: '24h' | '12h';
74
86
  /**
75
87
  * UI language for the built-in strings (participant labels, filter UI, stats footer, …).
76
88
  * @default 'en'
@@ -12,8 +12,69 @@ exports.safeHref = safeHref;
12
12
  exports.safeColor = safeColor;
13
13
  exports.safeImageMime = safeImageMime;
14
14
  exports.escapeHtml = escapeHtml;
15
+ exports.resolveTimestampFormat = resolveTimestampFormat;
16
+ exports.formatMessageTimestamp = formatMessageTimestamp;
15
17
  const twemoji_1 = __importDefault(require("twemoji"));
16
18
 
19
+ const DEFAULT_TIMESTAMP_FORMAT = { dateFormat: 'dd/mm/yyyy', timeFormat: '24h' };
20
+
21
+ function pad2(n) {
22
+ return n < 10 ? '0' + n : String(n);
23
+ }
24
+
25
+ // Normalizes the user-facing options once, at render start, so every renderer
26
+ // shares one reference "now" — otherwise a transcript rendered across midnight
27
+ // could label the same day both "today" and with a full date.
28
+ function resolveTimestampFormat(options) {
29
+ const o = options || {};
30
+ return {
31
+ dateFormat: o.dateFormat === 'mm/dd/yyyy' ? 'mm/dd/yyyy' : DEFAULT_TIMESTAMP_FORMAT.dateFormat,
32
+ timeFormat: o.timeFormat === '12h' ? '12h' : DEFAULT_TIMESTAMP_FORMAT.timeFormat,
33
+ now: o.now instanceof Date ? o.now : new Date(),
34
+ };
35
+ }
36
+
37
+ function formatClock(d, timeFormat) {
38
+ const minutes = pad2(d.getMinutes());
39
+ if (timeFormat === '12h') {
40
+ const h = d.getHours();
41
+ const hour12 = h % 12 === 0 ? 12 : h % 12;
42
+ return `${pad2(hour12)}:${minutes} ${h < 12 ? 'AM' : 'PM'}`;
43
+ }
44
+ return `${pad2(d.getHours())}:${minutes}`;
45
+ }
46
+
47
+ function formatCalendarDate(d, dateFormat) {
48
+ const day = pad2(d.getDate());
49
+ const month = pad2(d.getMonth() + 1);
50
+ return dateFormat === 'mm/dd/yyyy'
51
+ ? `${month}/${day}/${d.getFullYear()}`
52
+ : `${day}/${month}/${d.getFullYear()}`;
53
+ }
54
+
55
+ function startOfDay(d) {
56
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
57
+ }
58
+
59
+ // Mirrors how Discord labels message timestamps: today shows the bare time,
60
+ // yesterday is spelled out, anything older gets the full date. The reference
61
+ // point is transcript creation time, so an archived transcript keeps saying the
62
+ // same thing no matter when it is opened.
63
+ // Returns undefined for unusable input so callers can omit the attribute
64
+ // entirely rather than render an empty timestamp.
65
+ function formatMessageTimestamp(value, format, yesterdayTemplate) {
66
+ const d = value instanceof Date ? value : (typeof value === 'string' || typeof value === 'number' ? new Date(value) : null);
67
+ if (!d || !Number.isFinite(d.getTime())) return undefined;
68
+
69
+ const fmt = format && format.now instanceof Date ? format : resolveTimestampFormat(format);
70
+ const time = formatClock(d, fmt.timeFormat);
71
+ const dayDiff = Math.round((startOfDay(fmt.now) - startOfDay(d)) / 86400000);
72
+
73
+ if (dayDiff === 0) return time;
74
+ if (dayDiff === 1) return (yesterdayTemplate || 'Yesterday at {time}').replace('{time}', time);
75
+ return `${formatCalendarDate(d, fmt.dateFormat)} ${time}`;
76
+ }
77
+
17
78
  function isDefined(value) {
18
79
  return value !== undefined && value !== null;
19
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-html-transcripts-fix",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A nicely formatted html transcript generator for discord.js. Bugfix fork with support for the latest discord.js and Components v2.",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,6 +33,7 @@
33
33
  "dependencies": {
34
34
  "@lit-labs/ssr": "^3.3.1",
35
35
  "@skyra/discord-components-core": "^4.0.2",
36
+ "lit": "^3.3.1",
36
37
  "debug": "^4.4.3",
37
38
  "discord-markdown-parser": "~1.3.0",
38
39
  "highlight.js": "^11.11.1",
@@ -40,7 +41,7 @@
40
41
  "react-dom": "^19.2.3",
41
42
  "simple-markdown": "^0.7.3",
42
43
  "twemoji": "^14.0.2",
43
- "undici": "^7.16.0"
44
+ "undici": "^7.29.0"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "discord.js": "^14.0.0 || ^15.0.0",