@chatsift/discord-utils 0.6.0 → 0.6.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=embed.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/embed.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sortChannels.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sortChannels.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sortChannels.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,73 @@
1
+ import type { APIEmbed, APIEmbedField } from 'discord-api-types/v10';
2
+ /**
3
+ * Limits commonly encountered with Discord's API
4
+ */
5
+ export declare const MESSAGE_LIMITS: {
6
+ /**
7
+ * How long a message can be in characters
8
+ */
9
+ readonly CONTENT: 4000;
10
+ /**
11
+ * How many embeds can be in a message
12
+ */
13
+ readonly EMBED_COUNT: 10;
14
+ /**
15
+ * Embed specific limits
16
+ */
17
+ readonly EMBEDS: {
18
+ /**
19
+ * How long an embed title can be in characters
20
+ */
21
+ readonly TITLE: 256;
22
+ /**
23
+ * How long an embed description can be in characters
24
+ */
25
+ readonly DESCRIPTION: 4096;
26
+ /**
27
+ * How long an embed footer can be in characters
28
+ */
29
+ readonly FOOTER: 2048;
30
+ /**
31
+ * How long an embed author can be in characters
32
+ */
33
+ readonly AUTHOR: 256;
34
+ /**
35
+ * How many fields an embed can have
36
+ */
37
+ readonly FIELD_COUNT: 25;
38
+ /**
39
+ * Field specific limits
40
+ */
41
+ readonly FIELDS: {
42
+ /**
43
+ * How long a field name can be in characters
44
+ */
45
+ readonly NAME: 256;
46
+ /**
47
+ * How long a field value can be in characters
48
+ */
49
+ readonly VALUE: 1024;
50
+ };
51
+ };
52
+ };
53
+ /**
54
+ * Adds the given fields to an embed - mutating it
55
+ *
56
+ * @param embed - The embed to add fields to
57
+ * @param fields - The fields to add
58
+ */
59
+ export declare function addFields(embed: APIEmbed, ...fields: APIEmbedField[]): APIEmbed;
60
+ /**
61
+ * Cuts off text after the given length - appending "..." at the end
62
+ *
63
+ * @param text - The text to cut off
64
+ * @param total - The maximum length of the text
65
+ */
66
+ export declare function ellipsis(text: string, total: number): string;
67
+ /**
68
+ * Returns a fully truncated embed - safe to use with Discord's API - does not mutate the given embed
69
+ *
70
+ * @param embed - The embed to truncate
71
+ */
72
+ export declare function truncateEmbed(embed: APIEmbed): APIEmbed;
73
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,eAAO,MAAM,cAAc;IAC1B;;OAEG;;IAEH;;OAEG;;IAEH;;OAEG;;QAEF;;WAEG;;QAEH;;WAEG;;QAEH;;WAEG;;QAEH;;WAEG;;QAEH;;WAEG;;QAEH;;WAEG;;YAEF;;eAEG;;YAEH;;eAEG;;;;CAII,CAAC;AAEX;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,aAAa,EAAE,GAAG,QAAQ,CAG/E;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAW5D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CA2BvD"}
@@ -0,0 +1,3 @@
1
+ export * from './embed.js';
2
+ export * from './sortChannels.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -133,13 +133,13 @@ function sortChannels(unsorted) {
133
133
  if (a.type === import_v10.ChannelType.GuildCategory && b.type === import_v10.ChannelType.GuildText) {
134
134
  return 1;
135
135
  }
136
- return a.position - b.position;
136
+ return (a.position ?? 0) - (b.position ?? 0);
137
137
  });
138
138
  const channels = [];
139
139
  for (const top of sortedTopLevel ?? []) {
140
140
  channels.push(top);
141
141
  if (top.type === import_v10.ChannelType.GuildCategory) {
142
- const categoryChannels = (grouped[top.id] ?? []).sort((a, b) => a.position - b.position);
142
+ const categoryChannels = (grouped[top.id] ?? []).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
143
143
  channels.push(...categoryChannels);
144
144
  for (const channel of categoryChannels) {
145
145
  const channelThreads = threads.filter((thread) => thread.parent_id === channel.id).sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));
@@ -150,11 +150,7 @@ function sortChannels(unsorted) {
150
150
  channels.push(...channelThreads);
151
151
  }
152
152
  }
153
- return channels.length ? channels : filtered.sort((a, b) => {
154
- const aPos = "position" in a ? a.position ?? 0 : 0;
155
- const bPos = "position" in b ? b.position ?? 0 : 0;
156
- return aPos - bPos;
157
- });
153
+ return channels.length ? channels : filtered.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
158
154
  }
159
155
  __name(sortChannels, "sortChannels");
160
156
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/embed.ts","../src/sortChannels.ts"],"sourcesContent":["export * from './embed.js';\nexport * from './sortChannels.js';\n","import type { APIEmbed, APIEmbedField } from 'discord-api-types/v10';\n\n/**\n * Limits commonly encountered with Discord's API\n */\nexport const MESSAGE_LIMITS = {\n\t/**\n\t * How long a message can be in characters\n\t */\n\tCONTENT: 4_000,\n\t/**\n\t * How many embeds can be in a message\n\t */\n\tEMBED_COUNT: 10,\n\t/**\n\t * Embed specific limits\n\t */\n\tEMBEDS: {\n\t\t/**\n\t\t * How long an embed title can be in characters\n\t\t */\n\t\tTITLE: 256,\n\t\t/**\n\t\t * How long an embed description can be in characters\n\t\t */\n\t\tDESCRIPTION: 4_096,\n\t\t/**\n\t\t * How long an embed footer can be in characters\n\t\t */\n\t\tFOOTER: 2_048,\n\t\t/**\n\t\t * How long an embed author can be in characters\n\t\t */\n\t\tAUTHOR: 256,\n\t\t/**\n\t\t * How many fields an embed can have\n\t\t */\n\t\tFIELD_COUNT: 25,\n\t\t/**\n\t\t * Field specific limits\n\t\t */\n\t\tFIELDS: {\n\t\t\t/**\n\t\t\t * How long a field name can be in characters\n\t\t\t */\n\t\t\tNAME: 256,\n\t\t\t/**\n\t\t\t * How long a field value can be in characters\n\t\t\t */\n\t\t\tVALUE: 1_024,\n\t\t},\n\t},\n} as const;\n\n/**\n * Adds the given fields to an embed - mutating it\n *\n * @param embed - The embed to add fields to\n * @param fields - The fields to add\n */\nexport function addFields(embed: APIEmbed, ...fields: APIEmbedField[]): APIEmbed {\n\t(embed.fields ??= []).push(...fields);\n\treturn embed;\n}\n\n/**\n * Cuts off text after the given length - appending \"...\" at the end\n *\n * @param text - The text to cut off\n * @param total - The maximum length of the text\n */\nexport function ellipsis(text: string, total: number): string {\n\tif (text.length <= total) {\n\t\treturn text;\n\t}\n\n\tconst keep = total - 3;\n\tif (keep < 1) {\n\t\treturn text.slice(0, total);\n\t}\n\n\treturn `${text.slice(0, keep)}...`;\n}\n\n/**\n * Returns a fully truncated embed - safe to use with Discord's API - does not mutate the given embed\n *\n * @param embed - The embed to truncate\n */\nexport function truncateEmbed(embed: APIEmbed): APIEmbed {\n\t// @ts-expect-error - Because of the explicit undefined\n\treturn {\n\t\t...embed,\n\t\tdescription: embed.description ? ellipsis(embed.description, MESSAGE_LIMITS.EMBEDS.DESCRIPTION) : undefined,\n\t\ttitle: embed.title ? ellipsis(embed.title, MESSAGE_LIMITS.EMBEDS.TITLE) : undefined,\n\t\tauthor: embed.author\n\t\t\t? {\n\t\t\t\t\t...embed.author,\n\t\t\t\t\tname: ellipsis(embed.author.name, MESSAGE_LIMITS.EMBEDS.AUTHOR),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfooter: embed.footer\n\t\t\t? {\n\t\t\t\t\t...embed.footer,\n\t\t\t\t\ttext: ellipsis(embed.footer.text, MESSAGE_LIMITS.EMBEDS.FOOTER),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfields: embed.fields\n\t\t\t? embed.fields\n\t\t\t\t\t.map((field) => ({\n\t\t\t\t\t\tname: ellipsis(field.name, MESSAGE_LIMITS.EMBEDS.FIELDS.NAME),\n\t\t\t\t\t\tvalue: ellipsis(field.value, MESSAGE_LIMITS.EMBEDS.FIELDS.VALUE),\n\t\t\t\t\t}))\n\t\t\t\t\t.slice(0, MESSAGE_LIMITS.EMBEDS.FIELD_COUNT)\n\t\t\t: [],\n\t};\n}\n","import type { APIChannel, APIGuildCategoryChannel, APITextChannel, APIThreadChannel } from 'discord-api-types/v10';\nimport { ChannelType } from 'discord-api-types/v10';\n\nconst GUILD_TEXT_TYPES = [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.GuildForum];\nconst THREAD_TYPES = [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread];\n\ntype SortableChannel = APIGuildCategoryChannel | APITextChannel | APIThreadChannel;\n\n/**\n * Sorts an array of text, category channels, and threads\n */\nexport function sortChannels(unsorted: APIChannel[]): SortableChannel[] {\n\tconst filtered = unsorted.filter(\n\t\t(channel): channel is SortableChannel =>\n\t\t\tGUILD_TEXT_TYPES.includes(channel.type) ||\n\t\t\tchannel.type === ChannelType.GuildCategory ||\n\t\t\tTHREAD_TYPES.includes(channel.type),\n\t);\n\n\t// Separate threads from other channels\n\tconst threads = filtered.filter((channel): channel is APIThreadChannel => THREAD_TYPES.includes(channel.type));\n\tconst nonThreads = filtered.filter((channel) => !THREAD_TYPES.includes(channel.type)) as (\n\t\tAPIGuildCategoryChannel | APITextChannel\n\t)[];\n\n\t// Group the non-thread channels by their category - or \"top\" if they aren't in one\n\tconst grouped = Object.groupBy(nonThreads, (channel) => channel.parent_id ?? 'top');\n\n\t// Sort the top level channels - text channels are above category channels, otherwise use their position\n\tconst sortedTopLevel = grouped['top']\n\t\t?.filter((channel) => !channel.parent_id)\n\t\t.sort((a, b) => {\n\t\t\tif (a.type === ChannelType.GuildText && b.type === ChannelType.GuildCategory) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif (a.type === ChannelType.GuildCategory && b.type === ChannelType.GuildText) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn a.position - b.position;\n\t\t});\n\n\tconst channels: SortableChannel[] = [];\n\tfor (const top of sortedTopLevel ?? []) {\n\t\tchannels.push(top);\n\n\t\tif (top.type === ChannelType.GuildCategory) {\n\t\t\t// Add channels in this category\n\t\t\tconst categoryChannels = (grouped[top.id] ?? []).sort((a, b) => a.position! - b.position!);\n\t\t\tchannels.push(...categoryChannels);\n\n\t\t\t// Add threads for each channel in the category\n\t\t\tfor (const channel of categoryChannels) {\n\t\t\t\tconst channelThreads = threads\n\t\t\t\t\t.filter((thread) => thread.parent_id === channel.id)\n\t\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\t\tchannels.push(...channelThreads);\n\t\t\t}\n\t\t} else {\n\t\t\t// Add threads for top-level channels\n\t\t\tconst channelThreads = threads\n\t\t\t\t.filter((thread) => thread.parent_id === top.id)\n\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\tchannels.push(...channelThreads);\n\t\t}\n\t}\n\n\treturn channels.length\n\t\t? channels\n\t\t: filtered.sort((a, b) => {\n\t\t\t\tconst aPos = 'position' in a ? (a.position ?? 0) : 0;\n\t\t\t\tconst bPos = 'position' in b ? (b.position ?? 0) : 0;\n\t\t\t\treturn aPos - bPos;\n\t\t\t});\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/embed.ts","../src/sortChannels.ts"],"sourcesContent":["export * from './embed.js';\nexport * from './sortChannels.js';\n","import type { APIEmbed, APIEmbedField } from 'discord-api-types/v10';\n\n/**\n * Limits commonly encountered with Discord's API\n */\nexport const MESSAGE_LIMITS = {\n\t/**\n\t * How long a message can be in characters\n\t */\n\tCONTENT: 4_000,\n\t/**\n\t * How many embeds can be in a message\n\t */\n\tEMBED_COUNT: 10,\n\t/**\n\t * Embed specific limits\n\t */\n\tEMBEDS: {\n\t\t/**\n\t\t * How long an embed title can be in characters\n\t\t */\n\t\tTITLE: 256,\n\t\t/**\n\t\t * How long an embed description can be in characters\n\t\t */\n\t\tDESCRIPTION: 4_096,\n\t\t/**\n\t\t * How long an embed footer can be in characters\n\t\t */\n\t\tFOOTER: 2_048,\n\t\t/**\n\t\t * How long an embed author can be in characters\n\t\t */\n\t\tAUTHOR: 256,\n\t\t/**\n\t\t * How many fields an embed can have\n\t\t */\n\t\tFIELD_COUNT: 25,\n\t\t/**\n\t\t * Field specific limits\n\t\t */\n\t\tFIELDS: {\n\t\t\t/**\n\t\t\t * How long a field name can be in characters\n\t\t\t */\n\t\t\tNAME: 256,\n\t\t\t/**\n\t\t\t * How long a field value can be in characters\n\t\t\t */\n\t\t\tVALUE: 1_024,\n\t\t},\n\t},\n} as const;\n\n/**\n * Adds the given fields to an embed - mutating it\n *\n * @param embed - The embed to add fields to\n * @param fields - The fields to add\n */\nexport function addFields(embed: APIEmbed, ...fields: APIEmbedField[]): APIEmbed {\n\t(embed.fields ??= []).push(...fields);\n\treturn embed;\n}\n\n/**\n * Cuts off text after the given length - appending \"...\" at the end\n *\n * @param text - The text to cut off\n * @param total - The maximum length of the text\n */\nexport function ellipsis(text: string, total: number): string {\n\tif (text.length <= total) {\n\t\treturn text;\n\t}\n\n\tconst keep = total - 3;\n\tif (keep < 1) {\n\t\treturn text.slice(0, total);\n\t}\n\n\treturn `${text.slice(0, keep)}...`;\n}\n\n/**\n * Returns a fully truncated embed - safe to use with Discord's API - does not mutate the given embed\n *\n * @param embed - The embed to truncate\n */\nexport function truncateEmbed(embed: APIEmbed): APIEmbed {\n\t// @ts-expect-error - Because of the explicit undefined\n\treturn {\n\t\t...embed,\n\t\tdescription: embed.description ? ellipsis(embed.description, MESSAGE_LIMITS.EMBEDS.DESCRIPTION) : undefined,\n\t\ttitle: embed.title ? ellipsis(embed.title, MESSAGE_LIMITS.EMBEDS.TITLE) : undefined,\n\t\tauthor: embed.author\n\t\t\t? {\n\t\t\t\t\t...embed.author,\n\t\t\t\t\tname: ellipsis(embed.author.name, MESSAGE_LIMITS.EMBEDS.AUTHOR),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfooter: embed.footer\n\t\t\t? {\n\t\t\t\t\t...embed.footer,\n\t\t\t\t\ttext: ellipsis(embed.footer.text, MESSAGE_LIMITS.EMBEDS.FOOTER),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfields: embed.fields\n\t\t\t? embed.fields\n\t\t\t\t\t.map((field) => ({\n\t\t\t\t\t\tname: ellipsis(field.name, MESSAGE_LIMITS.EMBEDS.FIELDS.NAME),\n\t\t\t\t\t\tvalue: ellipsis(field.value, MESSAGE_LIMITS.EMBEDS.FIELDS.VALUE),\n\t\t\t\t\t}))\n\t\t\t\t\t.slice(0, MESSAGE_LIMITS.EMBEDS.FIELD_COUNT)\n\t\t\t: [],\n\t};\n}\n","import { ChannelType } from 'discord-api-types/v10';\n\nconst GUILD_TEXT_TYPES = [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.GuildForum];\nconst THREAD_TYPES = [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread];\n\n/**\n * The minimal channel shape `sortChannels` actually reads from -- callers aren't required to hand it a full\n * over-the-wire `APIChannel`, just anything with these fields (e.g. a dashboard's slimmer cached projection).\n */\nexport interface SortableChannelLike {\n\tid: string;\n\tparent_id?: string | null;\n\tposition?: number;\n\ttype: ChannelType;\n}\n\n/**\n * Sorts an array of text, category channels, and threads. Generic over `TChannel` so the input shape is\n * preserved on the way out instead of being widened/narrowed to a specific Discord API union.\n */\nexport function sortChannels<TChannel extends SortableChannelLike>(unsorted: TChannel[]): TChannel[] {\n\tconst filtered = unsorted.filter(\n\t\t(channel) =>\n\t\t\tGUILD_TEXT_TYPES.includes(channel.type) ||\n\t\t\tchannel.type === ChannelType.GuildCategory ||\n\t\t\tTHREAD_TYPES.includes(channel.type),\n\t);\n\n\t// Separate threads from other channels\n\tconst threads = filtered.filter((channel) => THREAD_TYPES.includes(channel.type));\n\tconst nonThreads = filtered.filter((channel) => !THREAD_TYPES.includes(channel.type));\n\n\t// Group the non-thread channels by their category - or \"top\" if they aren't in one\n\tconst grouped = Object.groupBy(nonThreads, (channel) => channel.parent_id ?? 'top');\n\n\t// Sort the top level channels - text channels are above category channels, otherwise use their position\n\tconst sortedTopLevel = grouped['top']\n\t\t?.filter((channel) => !channel.parent_id)\n\t\t.sort((a, b) => {\n\t\t\tif (a.type === ChannelType.GuildText && b.type === ChannelType.GuildCategory) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif (a.type === ChannelType.GuildCategory && b.type === ChannelType.GuildText) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn (a.position ?? 0) - (b.position ?? 0);\n\t\t});\n\n\tconst channels: TChannel[] = [];\n\tfor (const top of sortedTopLevel ?? []) {\n\t\tchannels.push(top);\n\n\t\tif (top.type === ChannelType.GuildCategory) {\n\t\t\t// Add channels in this category\n\t\t\tconst categoryChannels = (grouped[top.id] ?? []).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));\n\t\t\tchannels.push(...categoryChannels);\n\n\t\t\t// Add threads for each channel in the category\n\t\t\tfor (const channel of categoryChannels) {\n\t\t\t\tconst channelThreads = threads\n\t\t\t\t\t.filter((thread) => thread.parent_id === channel.id)\n\t\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\t\tchannels.push(...channelThreads);\n\t\t\t}\n\t\t} else {\n\t\t\t// Add threads for top-level channels\n\t\t\tconst channelThreads = threads\n\t\t\t\t.filter((thread) => thread.parent_id === top.id)\n\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\tchannels.push(...channelThreads);\n\t\t}\n\t}\n\n\treturn channels.length ? channels : filtered.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -105,13 +105,13 @@ function sortChannels(unsorted) {
105
105
  if (a.type === ChannelType.GuildCategory && b.type === ChannelType.GuildText) {
106
106
  return 1;
107
107
  }
108
- return a.position - b.position;
108
+ return (a.position ?? 0) - (b.position ?? 0);
109
109
  });
110
110
  const channels = [];
111
111
  for (const top of sortedTopLevel ?? []) {
112
112
  channels.push(top);
113
113
  if (top.type === ChannelType.GuildCategory) {
114
- const categoryChannels = (grouped[top.id] ?? []).sort((a, b) => a.position - b.position);
114
+ const categoryChannels = (grouped[top.id] ?? []).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
115
115
  channels.push(...categoryChannels);
116
116
  for (const channel of categoryChannels) {
117
117
  const channelThreads = threads.filter((thread) => thread.parent_id === channel.id).sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));
@@ -122,11 +122,7 @@ function sortChannels(unsorted) {
122
122
  channels.push(...channelThreads);
123
123
  }
124
124
  }
125
- return channels.length ? channels : filtered.sort((a, b) => {
126
- const aPos = "position" in a ? a.position ?? 0 : 0;
127
- const bPos = "position" in b ? b.position ?? 0 : 0;
128
- return aPos - bPos;
129
- });
125
+ return channels.length ? channels : filtered.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
130
126
  }
131
127
  __name(sortChannels, "sortChannels");
132
128
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/embed.ts","../src/sortChannels.ts"],"sourcesContent":["import type { APIEmbed, APIEmbedField } from 'discord-api-types/v10';\n\n/**\n * Limits commonly encountered with Discord's API\n */\nexport const MESSAGE_LIMITS = {\n\t/**\n\t * How long a message can be in characters\n\t */\n\tCONTENT: 4_000,\n\t/**\n\t * How many embeds can be in a message\n\t */\n\tEMBED_COUNT: 10,\n\t/**\n\t * Embed specific limits\n\t */\n\tEMBEDS: {\n\t\t/**\n\t\t * How long an embed title can be in characters\n\t\t */\n\t\tTITLE: 256,\n\t\t/**\n\t\t * How long an embed description can be in characters\n\t\t */\n\t\tDESCRIPTION: 4_096,\n\t\t/**\n\t\t * How long an embed footer can be in characters\n\t\t */\n\t\tFOOTER: 2_048,\n\t\t/**\n\t\t * How long an embed author can be in characters\n\t\t */\n\t\tAUTHOR: 256,\n\t\t/**\n\t\t * How many fields an embed can have\n\t\t */\n\t\tFIELD_COUNT: 25,\n\t\t/**\n\t\t * Field specific limits\n\t\t */\n\t\tFIELDS: {\n\t\t\t/**\n\t\t\t * How long a field name can be in characters\n\t\t\t */\n\t\t\tNAME: 256,\n\t\t\t/**\n\t\t\t * How long a field value can be in characters\n\t\t\t */\n\t\t\tVALUE: 1_024,\n\t\t},\n\t},\n} as const;\n\n/**\n * Adds the given fields to an embed - mutating it\n *\n * @param embed - The embed to add fields to\n * @param fields - The fields to add\n */\nexport function addFields(embed: APIEmbed, ...fields: APIEmbedField[]): APIEmbed {\n\t(embed.fields ??= []).push(...fields);\n\treturn embed;\n}\n\n/**\n * Cuts off text after the given length - appending \"...\" at the end\n *\n * @param text - The text to cut off\n * @param total - The maximum length of the text\n */\nexport function ellipsis(text: string, total: number): string {\n\tif (text.length <= total) {\n\t\treturn text;\n\t}\n\n\tconst keep = total - 3;\n\tif (keep < 1) {\n\t\treturn text.slice(0, total);\n\t}\n\n\treturn `${text.slice(0, keep)}...`;\n}\n\n/**\n * Returns a fully truncated embed - safe to use with Discord's API - does not mutate the given embed\n *\n * @param embed - The embed to truncate\n */\nexport function truncateEmbed(embed: APIEmbed): APIEmbed {\n\t// @ts-expect-error - Because of the explicit undefined\n\treturn {\n\t\t...embed,\n\t\tdescription: embed.description ? ellipsis(embed.description, MESSAGE_LIMITS.EMBEDS.DESCRIPTION) : undefined,\n\t\ttitle: embed.title ? ellipsis(embed.title, MESSAGE_LIMITS.EMBEDS.TITLE) : undefined,\n\t\tauthor: embed.author\n\t\t\t? {\n\t\t\t\t\t...embed.author,\n\t\t\t\t\tname: ellipsis(embed.author.name, MESSAGE_LIMITS.EMBEDS.AUTHOR),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfooter: embed.footer\n\t\t\t? {\n\t\t\t\t\t...embed.footer,\n\t\t\t\t\ttext: ellipsis(embed.footer.text, MESSAGE_LIMITS.EMBEDS.FOOTER),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfields: embed.fields\n\t\t\t? embed.fields\n\t\t\t\t\t.map((field) => ({\n\t\t\t\t\t\tname: ellipsis(field.name, MESSAGE_LIMITS.EMBEDS.FIELDS.NAME),\n\t\t\t\t\t\tvalue: ellipsis(field.value, MESSAGE_LIMITS.EMBEDS.FIELDS.VALUE),\n\t\t\t\t\t}))\n\t\t\t\t\t.slice(0, MESSAGE_LIMITS.EMBEDS.FIELD_COUNT)\n\t\t\t: [],\n\t};\n}\n","import type { APIChannel, APIGuildCategoryChannel, APITextChannel, APIThreadChannel } from 'discord-api-types/v10';\nimport { ChannelType } from 'discord-api-types/v10';\n\nconst GUILD_TEXT_TYPES = [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.GuildForum];\nconst THREAD_TYPES = [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread];\n\ntype SortableChannel = APIGuildCategoryChannel | APITextChannel | APIThreadChannel;\n\n/**\n * Sorts an array of text, category channels, and threads\n */\nexport function sortChannels(unsorted: APIChannel[]): SortableChannel[] {\n\tconst filtered = unsorted.filter(\n\t\t(channel): channel is SortableChannel =>\n\t\t\tGUILD_TEXT_TYPES.includes(channel.type) ||\n\t\t\tchannel.type === ChannelType.GuildCategory ||\n\t\t\tTHREAD_TYPES.includes(channel.type),\n\t);\n\n\t// Separate threads from other channels\n\tconst threads = filtered.filter((channel): channel is APIThreadChannel => THREAD_TYPES.includes(channel.type));\n\tconst nonThreads = filtered.filter((channel) => !THREAD_TYPES.includes(channel.type)) as (\n\t\tAPIGuildCategoryChannel | APITextChannel\n\t)[];\n\n\t// Group the non-thread channels by their category - or \"top\" if they aren't in one\n\tconst grouped = Object.groupBy(nonThreads, (channel) => channel.parent_id ?? 'top');\n\n\t// Sort the top level channels - text channels are above category channels, otherwise use their position\n\tconst sortedTopLevel = grouped['top']\n\t\t?.filter((channel) => !channel.parent_id)\n\t\t.sort((a, b) => {\n\t\t\tif (a.type === ChannelType.GuildText && b.type === ChannelType.GuildCategory) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif (a.type === ChannelType.GuildCategory && b.type === ChannelType.GuildText) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn a.position - b.position;\n\t\t});\n\n\tconst channels: SortableChannel[] = [];\n\tfor (const top of sortedTopLevel ?? []) {\n\t\tchannels.push(top);\n\n\t\tif (top.type === ChannelType.GuildCategory) {\n\t\t\t// Add channels in this category\n\t\t\tconst categoryChannels = (grouped[top.id] ?? []).sort((a, b) => a.position! - b.position!);\n\t\t\tchannels.push(...categoryChannels);\n\n\t\t\t// Add threads for each channel in the category\n\t\t\tfor (const channel of categoryChannels) {\n\t\t\t\tconst channelThreads = threads\n\t\t\t\t\t.filter((thread) => thread.parent_id === channel.id)\n\t\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\t\tchannels.push(...channelThreads);\n\t\t\t}\n\t\t} else {\n\t\t\t// Add threads for top-level channels\n\t\t\tconst channelThreads = threads\n\t\t\t\t.filter((thread) => thread.parent_id === top.id)\n\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\tchannels.push(...channelThreads);\n\t\t}\n\t}\n\n\treturn channels.length\n\t\t? channels\n\t\t: filtered.sort((a, b) => {\n\t\t\t\tconst aPos = 'position' in a ? (a.position ?? 0) : 0;\n\t\t\t\tconst bPos = 'position' in b ? (b.position ?? 0) : 0;\n\t\t\t\treturn aPos - bPos;\n\t\t\t});\n}\n"],"mappings":";;;;AAKO,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAI7B,SAAS;AAAA;AAAA;AAAA;AAAA,EAIT,aAAa;AAAA;AAAA;AAAA;AAAA,EAIb,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIP,OAAO;AAAA;AAAA;AAAA;AAAA,IAIP,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIR,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIR,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,QAAQ;AAAA;AAAA;AAAA;AAAA,MAIP,MAAM;AAAA;AAAA;AAAA;AAAA,MAIN,OAAO;AAAA,IACR;AAAA,EACD;AACD;AAQO,SAAS,UAAU,UAAoB,QAAmC;AAChF,GAAC,MAAM,WAAW,CAAC,GAAG,KAAK,GAAG,MAAM;AACpC,SAAO;AACR;AAHgB;AAWT,SAAS,SAAS,MAAc,OAAuB;AAC7D,MAAI,KAAK,UAAU,OAAO;AACzB,WAAO;AAAA,EACR;AAEA,QAAM,OAAO,QAAQ;AACrB,MAAI,OAAO,GAAG;AACb,WAAO,KAAK,MAAM,GAAG,KAAK;AAAA,EAC3B;AAEA,SAAO,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC;AAC9B;AAXgB;AAkBT,SAAS,cAAc,OAA2B;AAExD,SAAO;AAAA,IACN,GAAG;AAAA,IACH,aAAa,MAAM,cAAc,SAAS,MAAM,aAAa,eAAe,OAAO,WAAW,IAAI;AAAA,IAClG,OAAO,MAAM,QAAQ,SAAS,MAAM,OAAO,eAAe,OAAO,KAAK,IAAI;AAAA,IAC1E,QAAQ,MAAM,SACX;AAAA,MACA,GAAG,MAAM;AAAA,MACT,MAAM,SAAS,MAAM,OAAO,MAAM,eAAe,OAAO,MAAM;AAAA,IAC/D,IACC;AAAA,IACH,QAAQ,MAAM,SACX;AAAA,MACA,GAAG,MAAM;AAAA,MACT,MAAM,SAAS,MAAM,OAAO,MAAM,eAAe,OAAO,MAAM;AAAA,IAC/D,IACC;AAAA,IACH,QAAQ,MAAM,SACX,MAAM,OACL,IAAI,CAAC,WAAW;AAAA,MAChB,MAAM,SAAS,MAAM,MAAM,eAAe,OAAO,OAAO,IAAI;AAAA,MAC5D,OAAO,SAAS,MAAM,OAAO,eAAe,OAAO,OAAO,KAAK;AAAA,IAChE,EAAE,EACD,MAAM,GAAG,eAAe,OAAO,WAAW,IAC3C,CAAC;AAAA,EACL;AACD;AA3BgB;;;ACxFhB,SAAS,mBAAmB;AAE5B,IAAM,mBAAmB,CAAC,YAAY,WAAW,YAAY,WAAW,YAAY,UAAU;AAC9F,IAAM,eAAe,CAAC,YAAY,cAAc,YAAY,eAAe,YAAY,kBAAkB;AAOlG,SAAS,aAAa,UAA2C;AACvE,QAAM,WAAW,SAAS;AAAA,IACzB,CAAC,YACA,iBAAiB,SAAS,QAAQ,IAAI,KACtC,QAAQ,SAAS,YAAY,iBAC7B,aAAa,SAAS,QAAQ,IAAI;AAAA,EACpC;AAGA,QAAM,UAAU,SAAS,OAAO,CAAC,YAAyC,aAAa,SAAS,QAAQ,IAAI,CAAC;AAC7G,QAAM,aAAa,SAAS,OAAO,CAAC,YAAY,CAAC,aAAa,SAAS,QAAQ,IAAI,CAAC;AAKpF,QAAM,UAAU,OAAO,QAAQ,YAAY,CAAC,YAAY,QAAQ,aAAa,KAAK;AAGlF,QAAM,iBAAiB,QAAQ,KAAK,GACjC,OAAO,CAAC,YAAY,CAAC,QAAQ,SAAS,EACvC,KAAK,CAAC,GAAG,MAAM;AACf,QAAI,EAAE,SAAS,YAAY,aAAa,EAAE,SAAS,YAAY,eAAe;AAC7E,aAAO;AAAA,IACR;AAEA,QAAI,EAAE,SAAS,YAAY,iBAAiB,EAAE,SAAS,YAAY,WAAW;AAC7E,aAAO;AAAA,IACR;AAEA,WAAO,EAAE,WAAW,EAAE;AAAA,EACvB,CAAC;AAEF,QAAM,WAA8B,CAAC;AACrC,aAAW,OAAO,kBAAkB,CAAC,GAAG;AACvC,aAAS,KAAK,GAAG;AAEjB,QAAI,IAAI,SAAS,YAAY,eAAe;AAE3C,YAAM,oBAAoB,QAAQ,IAAI,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,WAAY,EAAE,QAAS;AACzF,eAAS,KAAK,GAAG,gBAAgB;AAGjC,iBAAW,WAAW,kBAAkB;AACvC,cAAM,iBAAiB,QACrB,OAAO,CAAC,WAAW,OAAO,cAAc,QAAQ,EAAE,EAClD,KAAK,CAAC,GAAG,MAAM,OAAO,OAAO,EAAE,EAAE,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC;AACpD,iBAAS,KAAK,GAAG,cAAc;AAAA,MAChC;AAAA,IACD,OAAO;AAEN,YAAM,iBAAiB,QACrB,OAAO,CAAC,WAAW,OAAO,cAAc,IAAI,EAAE,EAC9C,KAAK,CAAC,GAAG,MAAM,OAAO,OAAO,EAAE,EAAE,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC;AACpD,eAAS,KAAK,GAAG,cAAc;AAAA,IAChC;AAAA,EACD;AAEA,SAAO,SAAS,SACb,WACA,SAAS,KAAK,CAAC,GAAG,MAAM;AACxB,UAAM,OAAO,cAAc,IAAK,EAAE,YAAY,IAAK;AACnD,UAAM,OAAO,cAAc,IAAK,EAAE,YAAY,IAAK;AACnD,WAAO,OAAO;AAAA,EACf,CAAC;AACJ;AAhEgB;","names":[]}
1
+ {"version":3,"sources":["../src/embed.ts","../src/sortChannels.ts"],"sourcesContent":["import type { APIEmbed, APIEmbedField } from 'discord-api-types/v10';\n\n/**\n * Limits commonly encountered with Discord's API\n */\nexport const MESSAGE_LIMITS = {\n\t/**\n\t * How long a message can be in characters\n\t */\n\tCONTENT: 4_000,\n\t/**\n\t * How many embeds can be in a message\n\t */\n\tEMBED_COUNT: 10,\n\t/**\n\t * Embed specific limits\n\t */\n\tEMBEDS: {\n\t\t/**\n\t\t * How long an embed title can be in characters\n\t\t */\n\t\tTITLE: 256,\n\t\t/**\n\t\t * How long an embed description can be in characters\n\t\t */\n\t\tDESCRIPTION: 4_096,\n\t\t/**\n\t\t * How long an embed footer can be in characters\n\t\t */\n\t\tFOOTER: 2_048,\n\t\t/**\n\t\t * How long an embed author can be in characters\n\t\t */\n\t\tAUTHOR: 256,\n\t\t/**\n\t\t * How many fields an embed can have\n\t\t */\n\t\tFIELD_COUNT: 25,\n\t\t/**\n\t\t * Field specific limits\n\t\t */\n\t\tFIELDS: {\n\t\t\t/**\n\t\t\t * How long a field name can be in characters\n\t\t\t */\n\t\t\tNAME: 256,\n\t\t\t/**\n\t\t\t * How long a field value can be in characters\n\t\t\t */\n\t\t\tVALUE: 1_024,\n\t\t},\n\t},\n} as const;\n\n/**\n * Adds the given fields to an embed - mutating it\n *\n * @param embed - The embed to add fields to\n * @param fields - The fields to add\n */\nexport function addFields(embed: APIEmbed, ...fields: APIEmbedField[]): APIEmbed {\n\t(embed.fields ??= []).push(...fields);\n\treturn embed;\n}\n\n/**\n * Cuts off text after the given length - appending \"...\" at the end\n *\n * @param text - The text to cut off\n * @param total - The maximum length of the text\n */\nexport function ellipsis(text: string, total: number): string {\n\tif (text.length <= total) {\n\t\treturn text;\n\t}\n\n\tconst keep = total - 3;\n\tif (keep < 1) {\n\t\treturn text.slice(0, total);\n\t}\n\n\treturn `${text.slice(0, keep)}...`;\n}\n\n/**\n * Returns a fully truncated embed - safe to use with Discord's API - does not mutate the given embed\n *\n * @param embed - The embed to truncate\n */\nexport function truncateEmbed(embed: APIEmbed): APIEmbed {\n\t// @ts-expect-error - Because of the explicit undefined\n\treturn {\n\t\t...embed,\n\t\tdescription: embed.description ? ellipsis(embed.description, MESSAGE_LIMITS.EMBEDS.DESCRIPTION) : undefined,\n\t\ttitle: embed.title ? ellipsis(embed.title, MESSAGE_LIMITS.EMBEDS.TITLE) : undefined,\n\t\tauthor: embed.author\n\t\t\t? {\n\t\t\t\t\t...embed.author,\n\t\t\t\t\tname: ellipsis(embed.author.name, MESSAGE_LIMITS.EMBEDS.AUTHOR),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfooter: embed.footer\n\t\t\t? {\n\t\t\t\t\t...embed.footer,\n\t\t\t\t\ttext: ellipsis(embed.footer.text, MESSAGE_LIMITS.EMBEDS.FOOTER),\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tfields: embed.fields\n\t\t\t? embed.fields\n\t\t\t\t\t.map((field) => ({\n\t\t\t\t\t\tname: ellipsis(field.name, MESSAGE_LIMITS.EMBEDS.FIELDS.NAME),\n\t\t\t\t\t\tvalue: ellipsis(field.value, MESSAGE_LIMITS.EMBEDS.FIELDS.VALUE),\n\t\t\t\t\t}))\n\t\t\t\t\t.slice(0, MESSAGE_LIMITS.EMBEDS.FIELD_COUNT)\n\t\t\t: [],\n\t};\n}\n","import { ChannelType } from 'discord-api-types/v10';\n\nconst GUILD_TEXT_TYPES = [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.GuildForum];\nconst THREAD_TYPES = [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread];\n\n/**\n * The minimal channel shape `sortChannels` actually reads from -- callers aren't required to hand it a full\n * over-the-wire `APIChannel`, just anything with these fields (e.g. a dashboard's slimmer cached projection).\n */\nexport interface SortableChannelLike {\n\tid: string;\n\tparent_id?: string | null;\n\tposition?: number;\n\ttype: ChannelType;\n}\n\n/**\n * Sorts an array of text, category channels, and threads. Generic over `TChannel` so the input shape is\n * preserved on the way out instead of being widened/narrowed to a specific Discord API union.\n */\nexport function sortChannels<TChannel extends SortableChannelLike>(unsorted: TChannel[]): TChannel[] {\n\tconst filtered = unsorted.filter(\n\t\t(channel) =>\n\t\t\tGUILD_TEXT_TYPES.includes(channel.type) ||\n\t\t\tchannel.type === ChannelType.GuildCategory ||\n\t\t\tTHREAD_TYPES.includes(channel.type),\n\t);\n\n\t// Separate threads from other channels\n\tconst threads = filtered.filter((channel) => THREAD_TYPES.includes(channel.type));\n\tconst nonThreads = filtered.filter((channel) => !THREAD_TYPES.includes(channel.type));\n\n\t// Group the non-thread channels by their category - or \"top\" if they aren't in one\n\tconst grouped = Object.groupBy(nonThreads, (channel) => channel.parent_id ?? 'top');\n\n\t// Sort the top level channels - text channels are above category channels, otherwise use their position\n\tconst sortedTopLevel = grouped['top']\n\t\t?.filter((channel) => !channel.parent_id)\n\t\t.sort((a, b) => {\n\t\t\tif (a.type === ChannelType.GuildText && b.type === ChannelType.GuildCategory) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif (a.type === ChannelType.GuildCategory && b.type === ChannelType.GuildText) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn (a.position ?? 0) - (b.position ?? 0);\n\t\t});\n\n\tconst channels: TChannel[] = [];\n\tfor (const top of sortedTopLevel ?? []) {\n\t\tchannels.push(top);\n\n\t\tif (top.type === ChannelType.GuildCategory) {\n\t\t\t// Add channels in this category\n\t\t\tconst categoryChannels = (grouped[top.id] ?? []).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));\n\t\t\tchannels.push(...categoryChannels);\n\n\t\t\t// Add threads for each channel in the category\n\t\t\tfor (const channel of categoryChannels) {\n\t\t\t\tconst channelThreads = threads\n\t\t\t\t\t.filter((thread) => thread.parent_id === channel.id)\n\t\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\t\tchannels.push(...channelThreads);\n\t\t\t}\n\t\t} else {\n\t\t\t// Add threads for top-level channels\n\t\t\tconst channelThreads = threads\n\t\t\t\t.filter((thread) => thread.parent_id === top.id)\n\t\t\t\t.sort((a, b) => Number(BigInt(b.id) - BigInt(a.id)));\n\t\t\tchannels.push(...channelThreads);\n\t\t}\n\t}\n\n\treturn channels.length ? channels : filtered.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,17 @@
1
+ import { ChannelType } from 'discord-api-types/v10';
2
+ /**
3
+ * The minimal channel shape `sortChannels` actually reads from -- callers aren't required to hand it a full
4
+ * over-the-wire `APIChannel`, just anything with these fields (e.g. a dashboard's slimmer cached projection).
5
+ */
6
+ export interface SortableChannelLike {
7
+ id: string;
8
+ parent_id?: string | null;
9
+ position?: number;
10
+ type: ChannelType;
11
+ }
12
+ /**
13
+ * Sorts an array of text, category channels, and threads. Generic over `TChannel` so the input shape is
14
+ * preserved on the way out instead of being widened/narrowed to a specific Discord API union.
15
+ */
16
+ export declare function sortChannels<TChannel extends SortableChannelLike>(unsorted: TChannel[]): TChannel[];
17
+ //# sourceMappingURL=sortChannels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sortChannels.d.ts","sourceRoot":"","sources":["../src/sortChannels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKpD;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,SAAS,mBAAmB,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAwDnG"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatsift/discord-utils",
3
3
  "description": "Niche utilities for working with Discord's raw API",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {