From 90c7f903fd6f4efcacf3768781644d7bb107079e Mon Sep 17 00:00:00 2001 From: xHyroM Date: Sat, 8 Apr 2023 22:08:38 +0200 Subject: [PATCH] feat: things add serialize pkg make /setup cmd --- packages/bot/package.json | 1 + packages/bot/src/commands/setup.ts | 66 ++-- packages/bot/src/components/setup.ts | 181 ++++++++- packages/bot/src/index.ts | 31 +- packages/bot/src/modals/setup.ts | 120 +++++- packages/bot/src/structs/Modal.ts | 3 + .../src/structs/contexts/CommandContext.ts | 1 + .../src/structs/contexts/ComponentContext.ts | 6 + packages/bot/src/structs/contexts/Context.ts | 5 +- .../bot/src/structs/contexts/ModalContext.ts | 6 + packages/bot/src/types.d.ts | 14 + packages/bot/src/utils/returnRoleLpe.ts | 50 +++ packages/bot/tsconfig.json | 1 + packages/builders/package.json | 1 - packages/serialize/LICENSE | 202 ++++++++++ packages/serialize/README.md | 1 + packages/serialize/package.json | 16 + packages/serialize/scripts/build.mjs | 38 ++ packages/serialize/src/BitBuffer.ts | 93 +++++ packages/serialize/src/BitSerializer.ts | 42 +++ packages/serialize/src/custom-id.ts | 42 +++ packages/serialize/src/index.ts | 5 + packages/serialize/src/serializers.ts | 352 ++++++++++++++++++ packages/serialize/src/utils.ts | 18 + packages/serialize/tsconfig.json | 15 + pnpm-lock.yaml | 29 ++ 26 files changed, 1280 insertions(+), 59 deletions(-) create mode 100644 packages/bot/src/utils/returnRoleLpe.ts create mode 100644 packages/serialize/LICENSE create mode 100644 packages/serialize/README.md create mode 100644 packages/serialize/package.json create mode 100644 packages/serialize/scripts/build.mjs create mode 100644 packages/serialize/src/BitBuffer.ts create mode 100644 packages/serialize/src/BitSerializer.ts create mode 100644 packages/serialize/src/custom-id.ts create mode 100644 packages/serialize/src/index.ts create mode 100644 packages/serialize/src/serializers.ts create mode 100644 packages/serialize/src/utils.ts create mode 100644 packages/serialize/tsconfig.json diff --git a/packages/bot/package.json b/packages/bot/package.json index be35ea6..dd76474 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "builders": "workspace:*", + "serialize": "workspace:*", "discord-api-types": "^0.37.37" } } diff --git a/packages/bot/src/commands/setup.ts b/packages/bot/src/commands/setup.ts index e4c3212..6a1a5ef 100644 --- a/packages/bot/src/commands/setup.ts +++ b/packages/bot/src/commands/setup.ts @@ -1,45 +1,39 @@ import { Command } from "../structs/Command"; -import { MessageFlags, TextInputStyle } from "discord-api-types/v10"; -import { ActionRowBuilder, TextInputBuilder } from "builders"; +import { + ChannelType, + InteractionResponseType, + MessageFlags, +} from "discord-api-types/v10"; +import { ActionRowBuilder, ChannelSelectMenuBuilder } from "builders"; +import { serializers } from "serialize"; +// Part 1 ## select channel new Command({ name: "setup", - flags: MessageFlags.Ephemeral, acknowledge: false, run: async (ctx) => { - return ctx.returnModal({ - title: "Setup", - custom_id: "test", - components: [ - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setLabel("ASDSAD") - .setCustomId("prefix") - .setPlaceholder("Prefix") - .setStyle(TextInputStyle.Paragraph) - .setRequired(true), - ) - .toJSON(), - ], + return ctx.respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Select the channel to which the panel will be sent.", + components: [ + new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-channel", + }), + ) + .addChannelTypes( + ChannelType.GuildAnnouncement, + ChannelType.GuildText, + ), + ) + .toJSON(), + ], + flags: MessageFlags.Ephemeral, + }, }); - - /** - * await ctx.editReply({ - content: "Select the channel to which the panel will be sent.", - components: [ - new ActionRowBuilder() - .addComponents( - new ChannelSelectMenuBuilder() - .setCustomId("setup:part-channel") - .addChannelTypes( - ChannelType.GuildAnnouncement, - ChannelType.GuildText, - ), - ) - .toJSON(), - ], - }); - */ }, }); diff --git a/packages/bot/src/components/setup.ts b/packages/bot/src/components/setup.ts index 55062cd..71b2c2d 100644 --- a/packages/bot/src/components/setup.ts +++ b/packages/bot/src/components/setup.ts @@ -1,18 +1,85 @@ -import { ActionRowBuilder, RoleSelectMenuBuilder } from "builders"; +import { + ActionRowBuilder, + ButtonBuilder, + RoleSelectMenuBuilder, + TextInputBuilder, +} from "builders"; import { Component } from "../structs/Component"; -import { MessageFlags } from "discord-api-types/v10"; +import { + APIMessageComponentSelectMenuInteraction, + ButtonStyle, + InteractionResponseType, + MessageFlags, + TextInputStyle, +} from "discord-api-types/v10"; +import { serializers } from "serialize"; +import returnRoleLpe from "../utils/returnRoleLpe"; +// Part 2 Channels ## select button/dropdowns new Component({ id: "setup:part-channel", flags: MessageFlags.Ephemeral, run: async (ctx) => { + const interaction = + ctx.interaction as APIMessageComponentSelectMenuInteraction; + const channelId = interaction.data.values[0]; + + await ctx.editReply({ + content: + "Choose whether you want to use buttons or dropdown menu (select menu).", + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel("Buttons") + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-selecting", + data: { + channelId, + selecting: 1, + }, + }), + ) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setLabel("Dropdowns") + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-selecting", + data: { + channelId, + selecting: 2, + }, + }), + ) + .setStyle(ButtonStyle.Primary), + ) + .toJSON(), + ], + }); + }, +}); + +// Part 3 Selecting ## select roles +new Component({ + id: "setup:part-selecting", + flags: MessageFlags.Ephemeral, + run: async (ctx) => { + const data = ctx.decodedId.data; + await ctx.editReply({ content: "Select the roles that will be available in the menu.", components: [ new ActionRowBuilder() .addComponents( new RoleSelectMenuBuilder() - .setCustomId("setup:part-roles") + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-roles", + data, + }), + ) .setPlaceholder("Select roles") .setMinValues(1) .setMaxValues(25), @@ -23,13 +90,113 @@ new Component({ }, }); +// Part 4 Roles ## open modal for role lpe OR message preview new Component({ id: "setup:part-roles", - flags: MessageFlags.Ephemeral, + acknowledge: false, + run: (ctx) => { + const previousData = ctx.decodedId.data; + const interaction = + ctx.interaction as APIMessageComponentSelectMenuInteraction; + const rawRoleIds = + (previousData.rawRoleIds as string[]) ?? interaction.data.values; + + const data = { ...previousData, rawRoleIds }; + + return rawRoleIds.length > 0 + ? returnRoleLpe(data, ctx, rawRoleIds[0]) + : ctx.returnModal({ + title: "Message Preview", + custom_id: serializers.genericObject.encodeCustomId({ + type: "setup:part-messageContent", + data, + }), + components: [ + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Content") + .setCustomId("content") + .setPlaceholder("Select beautiful roles <3") + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(2000) + .setRequired(false), + ) + .toJSON(), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Embed Title") + .setCustomId("embedTitle") + .setPlaceholder("I love you") + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(256) + .setRequired(false), + ) + .toJSON(), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Embed Description") + .setCustomId("embedDescription") + .setPlaceholder("1. lol\n2. lol\n3. lol") + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(4000) + .setRequired(false), + ) + .toJSON(), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Embed Color") + .setCustomId("embedColor") + .setPlaceholder("#4287f5") + .setStyle(TextInputStyle.Short) + .setMaxLength(7) + .setRequired(false), + ) + .toJSON(), + ], + }); + }, +}); + +// Part 7 Send As ## finish +new Component({ + id: "setup:part-sendAs", + acknowledge: false, run: async (ctx) => { - await ctx.editReply({ - content: "done", - components: [], + const channelId = ctx.decodedId.data.channelId; + const selecting = + ctx.decodedId.data.selecting === 1 ? "buttons" : "dropdowns"; + const roleIds = ctx.decodedId.data.roleIds as { + label: string; + placeholder: string; + emoji: string; + }[]; + const message = ctx.decodedId.data.message; + const sendAs = ctx.decodedId.data.sendAs === 1 ? "webhook" : "bot"; + + console.log(channelId, roleIds, message, sendAs); + + // TODO: finish sending + const actionRow = new ActionRowBuilder(); + + switch (selecting) { + case "buttons": { + // TOOD: finish + } + case "dropdowns": { + // TODO: finish + } + } + + return ctx.respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Done!", + flags: MessageFlags.Ephemeral, + }, }); }, }); diff --git a/packages/bot/src/index.ts b/packages/bot/src/index.ts index edb429b..fc043af 100644 --- a/packages/bot/src/index.ts +++ b/packages/bot/src/index.ts @@ -16,6 +16,7 @@ import respond from "./utils/respond"; import { CommandContext } from "./structs/contexts/CommandContext"; import { ComponentContext } from "./structs/contexts/ComponentContext"; import { ModalContext } from "./structs/contexts/ModalContext"; +import { Env } from "./types"; export default { fetch: async (request: Request, env: Env) => { @@ -64,24 +65,31 @@ export default { break; } case InteractionType.ModalSubmit: { - const modal = MODALS.find((md) => md.id === interaction.data.custom_id); + const context = new ModalContext(interaction, env); + const modal = MODALS.find((md) => md.id === context.decodedId.type); if (!modal) return new Response("Unknown modal", { status: 404 }); try { - return respond({ - type: InteractionResponseType.DeferredChannelMessageWithSource, - data: { - flags: modal.flags, - }, - }); + if (modal.acknowledge) + return respond({ + type: InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: modal.flags, + }, + }); } finally { - modal.run(new ModalContext(interaction, env)); + if (modal.acknowledge) modal.run(context); + // rome-ignore lint/correctness/noUnsafeFinally: it works, must do better typings etc... + else return modal.run(context); } + + break; } case InteractionType.MessageComponent: { + const context = new ComponentContext(interaction, env); const component = COMPONENTS.find( - (cmp) => cmp.id === interaction.data.custom_id, + (cmp) => cmp.id === context.decodedId.type, ); if (!component) @@ -96,10 +104,9 @@ export default { }, }); } finally { - if (component.acknowledge) - component.run(new ComponentContext(interaction, env)); + if (component.acknowledge) component.run(context); // rome-ignore lint/correctness/noUnsafeFinally: it works, must do better typings etc... - else return component.run(new ComponentContext(interaction, env)); + else return component.run(context); } } } diff --git a/packages/bot/src/modals/setup.ts b/packages/bot/src/modals/setup.ts index 198e445..37fef5f 100644 --- a/packages/bot/src/modals/setup.ts +++ b/packages/bot/src/modals/setup.ts @@ -1,8 +1,124 @@ +import { + ButtonStyle, + InteractionResponseType, + MessageFlags, + TextInputStyle, +} from "discord-api-types/v10"; import { Modal } from "../structs/Modal"; +import { ActionRowBuilder, ButtonBuilder, TextInputBuilder } from "builders"; +import { serializers } from "serialize"; +// Part 5 Roles ## add label, placeholder, emoji OR message content new Modal({ - id: "test", + id: "setup:part-roles-lpe", + acknowledge: false, run: async (ctx) => { - // TODO: finish + const previousData = ctx.decodedId.data; + const rawRoleIds = previousData.rawRoleIds as string[]; + const roleIds = (previousData.roleIds ?? []) as { + label: string; + placeholder: string; + emoji: string; + }[]; + + const label = ctx.interaction.data.components[0].components[0].value; + const placeholder = ctx.interaction.data.components[1].components[0].value; + const emoji = ctx.interaction.data.components[2].components[0].value; + + roleIds.push({ label, placeholder, emoji }); + + rawRoleIds.shift(); + previousData.rawRoleIds = rawRoleIds; + const data = { ...previousData, roleIds }; + + return ctx.respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: + rawRoleIds.length > 0 + ? "Click the button to set the label, placeholder and emoji for next role." + : "Click the button to set message content or embed.", + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel( + rawRoleIds.length > 0 ? "Next Role" : "Message Content", + ) + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-roles", + data, + }), + ) + .setStyle(ButtonStyle.Primary), + ) + .toJSON(), + ], + }, + }); + }, +}); + +// Part 6 Message Content ## select send as webhook/as bot +new Modal({ + id: "setup:part-messageContent", + flags: MessageFlags.Ephemeral, + run: async (ctx) => { + const previousData = ctx.decodedId.data; + const content = ctx.interaction.data.components[0].components[0].value; + const embedTitle = ctx.interaction.data.components[1].components[0].value; + const embedDescription = + ctx.interaction.data.components[2].components[0].value; + const embedColor = ctx.interaction.data.components[3].components[0].value; + + if (!content && !embedTitle && !embedDescription) { + await ctx.editReply({ + content: "You must provide a message content or embed.", + components: [], + }); + + return; + } + + const data = { + ...previousData, + message: { content, embedTitle, embedDescription, embedColor }, + }; + + await ctx.editReply({ + content: + "Choose whether you want to send the message as a webhook or as a bot.", + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel("Webhook") + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-sendAs", + data: { + ...data, + sendAs: 1, + }, + }), + ) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setLabel("Bot") + .setCustomId( + serializers.genericObject.encodeCustomId({ + type: "setup:part-sendAs", + data: { + ...data, + sendAs: 2, + }, + }), + ) + .setStyle(ButtonStyle.Primary), + ) + .toJSON(), + ], + }); }, }); diff --git a/packages/bot/src/structs/Modal.ts b/packages/bot/src/structs/Modal.ts index 94657ad..807b65d 100644 --- a/packages/bot/src/structs/Modal.ts +++ b/packages/bot/src/structs/Modal.ts @@ -4,17 +4,20 @@ import { ModalContext } from "./contexts/ModalContext"; interface ModalOptions { id: string; + acknowledge?: boolean; flags?: MessageFlags; run: (interaction: ModalContext) => void; } export class Modal { public id: string; + public acknowledge: boolean; public flags: MessageFlags | undefined; public run: (interaction: ModalContext) => void; constructor(options: ModalOptions) { this.id = options.id; + this.acknowledge = options.acknowledge ?? true; this.flags = options.flags; this.run = options.run; diff --git a/packages/bot/src/structs/contexts/CommandContext.ts b/packages/bot/src/structs/contexts/CommandContext.ts index fc2cc6a..5d861ae 100644 --- a/packages/bot/src/structs/contexts/CommandContext.ts +++ b/packages/bot/src/structs/contexts/CommandContext.ts @@ -1,5 +1,6 @@ import { APIApplicationCommandInteraction } from "discord-api-types/v10"; import { Context } from "./Context"; +import { Env } from "../../types"; export class CommandContext extends Context { public interaction: APIApplicationCommandInteraction; diff --git a/packages/bot/src/structs/contexts/ComponentContext.ts b/packages/bot/src/structs/contexts/ComponentContext.ts index 5e1afd1..c2cd82e 100644 --- a/packages/bot/src/structs/contexts/ComponentContext.ts +++ b/packages/bot/src/structs/contexts/ComponentContext.ts @@ -1,12 +1,18 @@ import { APIMessageComponentInteraction } from "discord-api-types/v10"; +import { DeclaredId, Env } from "../../types"; +import { serializers } from "serialize"; import { Context } from "./Context"; export class ComponentContext extends Context { + public decodedId: DeclaredId; public interaction: APIMessageComponentInteraction; constructor(interaction: APIMessageComponentInteraction, env: Env) { super(interaction, env); + this.decodedId = serializers.genericObject.decodeCustomId( + interaction.data.custom_id, + ) as DeclaredId; this.interaction = interaction; } } diff --git a/packages/bot/src/structs/contexts/Context.ts b/packages/bot/src/structs/contexts/Context.ts index 52e48ea..a05f2be 100644 --- a/packages/bot/src/structs/contexts/Context.ts +++ b/packages/bot/src/structs/contexts/Context.ts @@ -7,6 +7,7 @@ import { Routes, } from "discord-api-types/v10"; import respond from "../../utils/respond"; +import { Env } from "../../types"; export class Context { public interaction: APIInteraction; @@ -17,10 +18,12 @@ export class Context { this.env = env; } + public respond = respond; + public async editReply(content: APIInteractionResponseCallbackData) { return await fetch( `${RouteBases.api}${Routes.webhookMessage( - this.interaction.id, + this.interaction.application_id, this.interaction.token, )}`, { diff --git a/packages/bot/src/structs/contexts/ModalContext.ts b/packages/bot/src/structs/contexts/ModalContext.ts index b5439b0..2851933 100644 --- a/packages/bot/src/structs/contexts/ModalContext.ts +++ b/packages/bot/src/structs/contexts/ModalContext.ts @@ -1,12 +1,18 @@ import { APIModalSubmitInteraction } from "discord-api-types/v10"; +import { DeclaredId, Env } from "../../types"; +import { serializers } from "serialize"; import { Context } from "./Context"; export class ModalContext extends Context { + public decodedId: DeclaredId; public interaction: APIModalSubmitInteraction; constructor(interaction: APIModalSubmitInteraction, env: Env) { super(interaction, env); + this.decodedId = serializers.genericObject.decodeCustomId( + interaction.data.custom_id, + ) as DeclaredId; this.interaction = interaction; } } diff --git a/packages/bot/src/types.d.ts b/packages/bot/src/types.d.ts index f7af31b..a09ff66 100644 --- a/packages/bot/src/types.d.ts +++ b/packages/bot/src/types.d.ts @@ -1,6 +1,20 @@ +import type { Generic } from "serialize"; + // secrets: wrangler secret put declare let MINIFLARE; // just check because algorithm is different +declare type DeclaredId = Record< + string, + | string + | { + [key: string]: Generic; + } +> & { + data: { + [key: string]: Generic; + }; +}; + declare interface Env { publicKey: string; token: string; diff --git a/packages/bot/src/utils/returnRoleLpe.ts b/packages/bot/src/utils/returnRoleLpe.ts new file mode 100644 index 0000000..9e7c34f --- /dev/null +++ b/packages/bot/src/utils/returnRoleLpe.ts @@ -0,0 +1,50 @@ +import { Generic, serializers } from "serialize"; +import { Context } from "../structs/contexts/Context"; +import { ActionRowBuilder, TextInputBuilder } from "builders"; +import { TextInputStyle } from "discord-api-types/v10"; + +export default function ( + data: Record, + ctx: Context, + rawRole: string, +) { + return ctx.returnModal({ + title: `Role Setup ${rawRole}`, + custom_id: serializers.genericObject.encodeCustomId({ + type: "setup:part-roles-lpe", + data, + }), + components: [ + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Label") + .setCustomId("label") + .setPlaceholder("Ping") + .setStyle(TextInputStyle.Short) + .setRequired(false), + ) + .toJSON(), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Placeholder") + .setCustomId("placeholder") + .setPlaceholder("pingping pong pong") + .setStyle(TextInputStyle.Short) + .setRequired(false), + ) + .toJSON(), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Emoji") + .setCustomId("emoji") + .setPlaceholder("emoji 💡") + .setStyle(TextInputStyle.Short) + .setRequired(false), + ) + .toJSON(), + ], + }); +} diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json index bcc3ab9..7f1f6e2 100644 --- a/packages/bot/tsconfig.json +++ b/packages/bot/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "esnext", + "downlevelIteration": true, "lib": ["esnext", "webworker"], "strict": true, "moduleResolution": "node", diff --git a/packages/builders/package.json b/packages/builders/package.json index 489013e..e48484c 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -1,7 +1,6 @@ { "name": "builders", "version": "0.0.1", - "private": true, "main": "dist/index.mjs", "types": "dist/index.d.ts", "scripts": { diff --git a/packages/serialize/LICENSE b/packages/serialize/LICENSE new file mode 100644 index 0000000..5adfeda --- /dev/null +++ b/packages/serialize/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021-2022 Dave Caruso + Copyright 2021-2022 CRBT Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/serialize/README.md b/packages/serialize/README.md new file mode 100644 index 0000000..1c68b91 --- /dev/null +++ b/packages/serialize/README.md @@ -0,0 +1 @@ +[https://github.com/CRBT-Team/Purplet/blob/main/packages/serialize](https://github.com/CRBT-Team/Purplet/blob/main/packages/serialize) diff --git a/packages/serialize/package.json b/packages/serialize/package.json new file mode 100644 index 0000000..0d240bc --- /dev/null +++ b/packages/serialize/package.json @@ -0,0 +1,16 @@ +{ + "name": "serialize", + "version": "0.0.1", + "main": "dist/index.mjs", + "types": "dist/index.d.ts", + "scripts": { + "build": "node scripts/build.mjs && tsc -p tsconfig.json" + }, + "devDependencies": { + "esbuild": "^0.15.11", + "typescript": "^4.8.4" + }, + "dependencies": { + "@paperdave/utils": "^1.6.1" + } +} diff --git a/packages/serialize/scripts/build.mjs b/packages/serialize/scripts/build.mjs new file mode 100644 index 0000000..0e93181 --- /dev/null +++ b/packages/serialize/scripts/build.mjs @@ -0,0 +1,38 @@ +import esbuild from "esbuild"; +import { rmSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +if (existsSync(join(__dirname, "..", "dist"))) + rmSync(join(__dirname, "..", "dist"), { recursive: true }); + +const watch = process.argv.includes("--watch"); +const dev = process.argv.includes("--dev"); + +Promise.all([ + esbuild.build({ + bundle: true, + logLevel: "info", + format: "esm", + mainFields: ["browser", "module", "main"], + platform: "neutral", + target: "es2020", + entryPoints: ["./src/index.ts"], + outfile: "./dist/index.mjs", + sourcemap: dev, + charset: "utf8", + minify: !dev, + watch: watch, + }), +]) + .catch((err) => { + console.error("Builders failed to build"); + console.error(err.message); + }) + .then(() => { + console.log( + watch ? "Waiting for your changes..." : "Builders has been built", + ); + }); diff --git a/packages/serialize/src/BitBuffer.ts b/packages/serialize/src/BitBuffer.ts new file mode 100644 index 0000000..4226aba --- /dev/null +++ b/packages/serialize/src/BitBuffer.ts @@ -0,0 +1,93 @@ +export class BitBuffer { + private array: Uint8Array; + private byteIndex = 0; + private bitIndex = 0; + + get index() { + return this.byteIndex * 8 + this.bitIndex; + } + + set index(value: number) { + this.byteIndex = Math.floor(value / 8); + this.bitIndex = value % 8; + } + + get buffer() { + return this.array.buffer; + } + + set buffer(v: ArrayBufferLike) { + this.array = new Uint8Array(v); + } + + constructor( + input: Uint8Array | ArrayBufferLike | ArrayLike | number = 256, + ) { + this.array = new Uint8Array( + input instanceof Uint8Array ? input.buffer : (input as ArrayBufferLike), + ); + } + + seek(index: number) { + this.index = index; + return this; + } + + read(length = 1) { + let result = 0; + for (let i = 0; i < length; i++) { + const bit = (this.array[this.byteIndex] >> this.bitIndex) & 1; + result |= bit << i; + this.bitIndex++; + if (this.bitIndex >= 8) { + this.bitIndex = 0; + this.byteIndex++; + } + } + return result; + } + + write(value: number, length = 1) { + for (let i = 0; i < length; i++) { + const bit = (value >> i) & 1; + this.array[this.byteIndex] |= bit << this.bitIndex; + this.bitIndex++; + if (this.bitIndex >= 8) { + this.bitIndex = 0; + this.byteIndex++; + } + } + return this; + } + + readBI(length = 1) { + let result = 0n; + for (let i = 0n; i < length; i++) { + const bit = (this.array[this.byteIndex] >> this.bitIndex) & 1; + if (bit) { + result |= 1n << i; + } + this.bitIndex++; + if (this.bitIndex >= 8) { + this.bitIndex = 0; + this.byteIndex++; + } + } + return result; + } + + writeBI(value: bigint, length = 1) { + for (let i = 0n; i < length; i++) { + const bit = (value >> i) & 1n; + if (bit) { + this.array[this.byteIndex] |= 1 << this.bitIndex; + } + this.bitIndex++; + if (this.bitIndex >= 8) { + this.bitIndex = 0; + this.byteIndex++; + } + } + return this; + } +} diff --git a/packages/serialize/src/BitSerializer.ts b/packages/serialize/src/BitSerializer.ts new file mode 100644 index 0000000..5b8f263 --- /dev/null +++ b/packages/serialize/src/BitSerializer.ts @@ -0,0 +1,42 @@ +import { BitBuffer } from "./BitBuffer"; +import { decodeCustomId, encodeCustomId } from "./custom-id"; + +export interface BitSerializerOptions { + write(value: T, buffer: BitBuffer): void; + read(buffer: BitBuffer): T; + check?(value: unknown): value is T; +} + +export class BitSerializer { + constructor(private options: BitSerializerOptions) {} + + write(value: T, buffer: BitBuffer): void { + this.options.write(value, buffer); + } + + read(buffer: BitBuffer): T { + return this.options.read(buffer); + } + + check(value: unknown): value is T { + return this.options.check?.(value) ?? true; + } + + encode(value: T, bufferLength = 256) { + const buffer = new BitBuffer(bufferLength); + this.options.write(value, buffer); + return new Uint8Array(buffer.buffer.slice(0, Math.ceil(buffer.index / 8))); + } + + decode(value: Uint8Array): T { + return this.options.read(new BitBuffer(value)); + } + + encodeCustomId(value: T, bufferLength = 256) { + return encodeCustomId(this.encode(value, bufferLength)); + } + + decodeCustomId(value: string): T { + return this.decode(decodeCustomId(value)); + } +} diff --git a/packages/serialize/src/custom-id.ts b/packages/serialize/src/custom-id.ts new file mode 100644 index 0000000..6ae8148 --- /dev/null +++ b/packages/serialize/src/custom-id.ts @@ -0,0 +1,42 @@ +// using all unicode characters from range 0 to 0x10FFFF, encode two and a half bytes per character +// This is essentially "base1114111" +export function encodeCustomId(data: Uint8Array) { + let output = ""; + for (let i = 0; i < data.length; i += 5) { + const one = data[i]; + const two = data[i + 1]; + const three = data[i + 2]; + const four = data[i + 3]; + const five = data[i + 4]; + + // byte one contains one + two + the least significant bits of three + const codePoint1 = (one << 12) | (two << 4) | (three & 0b1111); + // byte two contains four + five + the most significant bits of three + const codePoint2 = (four << 12) | (five << 4) | (three >> 4); + + output += + String.fromCodePoint(codePoint1) + String.fromCodePoint(codePoint2); + } + + return output; +} + +export function decodeCustomId(data: string) { + const codePoints = [...data]; + const output = new Uint8Array(Math.ceil(codePoints.length * 2.5)); + let j = 0; + + for (let i = 0; i < codePoints.length; i += 2, j += 5) { + // rome-ignore lint/style/noNonNullAssertion: + const codePoint1 = codePoints[i].codePointAt(0)!; + // rome-ignore lint/style/noNonNullAssertion: + const codePoint2 = codePoints[i + 1].codePointAt(0)!; + + output[j] = (codePoint1 >>> 12) & 0xff; + output[j + 1] = (codePoint1 >>> 4) & 0xff; + output[j + 2] = (codePoint1 & 0xf) | ((codePoint2 & 0xf) << 4); + output[j + 3] = (codePoint2 >>> 12) & 0xff; + output[j + 4] = (codePoint2 >>> 4) & 0xff; + } + return output; +} diff --git a/packages/serialize/src/index.ts b/packages/serialize/src/index.ts new file mode 100644 index 0000000..6faa6de --- /dev/null +++ b/packages/serialize/src/index.ts @@ -0,0 +1,5 @@ +export { BitSerializer, type BitSerializerOptions } from "./BitSerializer"; +export * as serializers from "./serializers"; +export { BitBuffer } from "./BitBuffer"; +export { type Generic } from "./utils"; +export { decodeCustomId, encodeCustomId } from "./custom-id"; diff --git a/packages/serialize/src/serializers.ts b/packages/serialize/src/serializers.ts new file mode 100644 index 0000000..d118615 --- /dev/null +++ b/packages/serialize/src/serializers.ts @@ -0,0 +1,352 @@ +import type { Dict } from "@paperdave/utils"; +import type { BitBuffer } from "./BitBuffer"; +import { BitSerializer } from "./BitSerializer"; +import type { Generic } from "./utils"; +import { fillArray } from "./utils"; + +/** Creates a serializer that does not read/write any data, but instead return a predefined constant. */ +export function constant(input: T): BitSerializer { + return new BitSerializer({ + read: () => input, + write: () => {}, + check(value): value is T { + return value === input; + }, + }); +} + +const nullSerializer = constant(null); + +/** Serializer for a boolean value. */ +export const boolean = new BitSerializer({ + read(buffer) { + return buffer.read() === 1; + }, + write(value, buffer) { + buffer.write(value ? 1 : 0); + }, + check(value): value is boolean { + return typeof value === "boolean"; + }, +}); + +/** Creates a serializer for unsigned integers, at the given precision. */ +function unsignedInt(bits: number) { + return new BitSerializer({ + read(buffer) { + return buffer.read(bits); + }, + write(value, buffer) { + buffer.write(value, bits); + }, + check(value): value is number { + return ( + typeof value === "number" && + Math.floor(value) === value && + value >= 0 && + value < 1 << bits + ); + }, + }); +} + +/** Creates a serializer for signed integers, at the given precision. */ +function signedInt(bits: number) { + const signBit = -1 << (bits - 1); + return new BitSerializer({ + read(buffer) { + const value = buffer.read(bits); + const sign = buffer.read(); + return value + (sign ? signBit : 0); + }, + write(value, buffer) { + const sign = value < 0; + if (sign) { + value += signBit; + } + buffer.write(value, bits); + buffer.write(sign ? 1 : 0); + }, + check(value): value is number { + return ( + typeof value === "number" && + Math.floor(value) === value && + value >= -(1 << (bits - 1)) && + value < 1 << (bits - 1) + ); + }, + }); +} + +/** Creates a serializer for unsigned BigInts, at the given precision. */ +function unsignedBigInt(bits: number) { + return new BitSerializer({ + read(buffer) { + return buffer.readBI(bits); + }, + write(value, buffer) { + buffer.writeBI(value, bits); + }, + check(value): value is bigint { + return typeof value === "bigint" && value >= 0; + }, + }); +} + +/** Creates a serializer for unsigned floats, at the given precision. */ +function signedBigInt(bits: number) { + const signBit = -1n << (BigInt(bits) - 1n); + return new BitSerializer({ + read(buffer: BitBuffer) { + const value = buffer.readBI(bits); + const sign = buffer.readBI(); + return value + (sign ? signBit : 0n); + }, + write(value, buffer) { + const sign = value < 0; + if (sign) { + value += signBit; + } + buffer.writeBI(value, bits); + buffer.write(sign ? 1 : 0); + }, + check(value): value is bigint { + return typeof value === "bigint"; + }, + }); +} + +/* Serializer for 8-bit unsigned integers. 0 <= n <= 255. */ +export const u8 = unsignedInt(8); +/* Serializer for 16-bit unsigned integers. 0 <= n <= 65535. */ +export const u16 = unsignedInt(16); +/* Serializer for 32-bit unsigned integers. 0 <= n <= 4294967295. */ +export const u32 = unsignedInt(32); +/* Serializer for 8-bit signed integers. -128 <= n <= 127. */ +export const s8 = signedInt(8); +/* Serializer for 16-bit signed integers. -32768 <= n <= 32767. */ +export const s16 = signedInt(16); +/* Serializer for 32-bit signed integers. -2147483648 <= n <= 2147483647. */ +export const s32 = signedInt(32); + +/* Serializer for 8-bit unsigned BigInts. 0n <= n <= 255n. */ +export const u8bi = unsignedBigInt(8); +/* Serializer for 16-bit unsigned BigInts. 0n <= n <= 65535n. */ +export const u16bi = unsignedBigInt(16); +/* Serializer for 32-bit unsigned BigInts. 0n <= n <= 4294967295n. */ +export const u32bi = unsignedBigInt(32); +/* Serializer for 64-bit unsigned BigInts. 0n <= n <= 18446744073709551615n. */ +export const u64bi = unsignedBigInt(64); +/* Serializer for 128-bit unsigned BigInts. 0n <= n <= (39 digits). */ +export const u128bi = unsignedBigInt(128); +/* Serializer for 8-bit signed BigInts. -128n <= n <= 127n. */ +export const s8bi = signedBigInt(8); +/* Serializer for 16-bit signed BigInts. -32768n <= n <= 32767n. */ +export const s16bi = signedBigInt(16); +/* Serializer for 32-bit signed BigInts. -2147483648n <= n <= 2147483647n. */ +export const s32bi = signedBigInt(32); +/* Serializer for 64-bit signed BigInts. -9223372036854775808n <= n <= 9223372036854775807n. */ +export const s64bi = signedBigInt(64); +/* Serializer for 128-bit signed BigInts. -170141183460469231731687303715884105728n <= n <= 170141183460469231731687303715884105727n. */ +export const s128bi = signedBigInt(128); + +/* Serializer for Discord snowflakes, which are u64bi strings. If you have a bigint value, use the `u64bi` serializer instead. */ +export const snowflake = new BitSerializer({ + read(buffer) { + return buffer.readBI(64).toString(); + }, + write(value, buffer) { + buffer.writeBI(BigInt(value), 64); + }, + check(value): value is string { + return typeof value === "string" && /^[0-9]{18,20}$/.test(value); + }, +}); + +/** Serializer for a JavaScript number (IEEE-754/float64) value. */ +export const float = new BitSerializer({ + read(buffer) { + return new Float64Array( + new Uint8Array(fillArray(8, () => buffer.read(8))).buffer, + )[0]; + }, + write(value, buffer) { + const view = new Uint8Array(new Float64Array([value]).buffer); + for (let i = 0; i < 8; i++) { + buffer.write(view[i], 8); + } + }, + check(value): value is number { + return typeof value === "number"; + }, +}); + +/** Serializer for a `Date` value. */ +export const date = new BitSerializer({ + read(buffer) { + return new Date(Number(buffer.readBI(52))); + }, + write(value, buffer) { + buffer.writeBI(BigInt(value.getTime()), 52); + }, + check(value): value is Date { + return value instanceof Date; + }, +}); + +/** Serializer for a string value. Uses a length + content approach, so it does not support lengths above 255. */ +export const string = new BitSerializer({ + read(buffer) { + const length = buffer.read(8); + const bytes = fillArray(length, () => buffer.read(8)); + return new TextDecoder().decode(new Uint8Array(bytes)); + }, + write(value, buffer) { + const bytes = new TextEncoder().encode(value); + if (bytes.length > 255) { + throw new Error( + "String serializer does not support strings over 255 bytes.", + ); + } + buffer.write(bytes.length, 8); + bytes.forEach((byte) => buffer.write(byte, 8)); + }, + check(value): value is string { + return typeof value === "string"; + }, +}); + +/** + * Creates a serializer for an array of values. Uses a length + content approach, so it does not + * support lengths above 255. + */ +export function arrayOf(serializer: BitSerializer) { + return new BitSerializer({ + read(buffer) { + const length = buffer.read(8); + const array = []; + for (let i = 0; i < length; i++) { + array.push(serializer.read(buffer)); + } + return array; + }, + write(value, buffer) { + buffer.write(value.length, 8); + value.forEach((item) => serializer.write(item, buffer)); + }, + // rome-ignore lint/suspicious/noExplicitAny: + check(value): value is any[] { + return ( + Array.isArray(value) && value.every((item) => serializer.check(item)) + ); + }, + }); +} + +/** Creates a serializer out of two other serializers. Uses one bit to tell them apart. */ +export function or(a: BitSerializer, b: BitSerializer) { + return new BitSerializer({ + read(buffer) { + return buffer.read() ? a.read(buffer) : b.read(buffer); + }, + write(value, buffer) { + const isA = a.check(value); + buffer.write(isA ? 1 : 0, 1); + if (isA) { + a.write(value, buffer); + } else { + b.write(value, buffer); + } + }, + check(value): value is A | B { + return a.check(value) || b.check(value); + }, + }); +} + +/** Serializer for numbers, using an extra bit to allow a short s16 when possible, otherwise float64. */ +export const number = or(s16, float); + +export function nullable(serializer: BitSerializer) { + return or(serializer, nullSerializer); +} + +export function object>( + definition: { + [K in keyof T]: BitSerializer; + }, +): BitSerializer { + const keys = Object.keys(definition).sort(); + return new BitSerializer({ + read(buffer) { + const obj = {} as T; + keys.forEach((key) => { + // rome-ignore lint/suspicious/noExplicitAny: + (obj as any)[key] = definition[key].read(buffer) as any; + }); + return obj; + }, + write(value, buffer) { + keys.forEach((key) => { + // rome-ignore lint/suspicious/noExplicitAny: + definition[key].write(value[key] as any, buffer); + }); + }, + check(value): value is T { + return ( + typeof value === "object" && + // rome-ignore lint/suspicious/noExplicitAny: + keys.every((key) => definition[key].check((value as any)[key])) + ); + }, + }); +} + +/** Serializer for an object of `generic` values. */ +export const genericObject = new BitSerializer>({ + read(buffer) { + const obj: Dict = {}; + let key: string = string.read(buffer); + while (key !== "") { + obj[key] = generic.read(buffer); + key = string.read(buffer); + } + return obj; + }, + write(value, buffer) { + Object.keys(value).forEach((key) => { + string.write(key, buffer); + generic.write(value[key], buffer); + }); + string.write("", buffer); + }, + check(value): value is Record { + return ( + value != null && + typeof value === "object" && + Object.keys(value).every( + // rome-ignore lint/suspicious/noExplicitAny: + (key) => string.check(key) && generic.check((value as any)[key]), + ) + ); + }, +}); + +export const genericArray = { + __proto__: BitSerializer.prototype, +} as unknown as BitSerializer; + +/** + * Serializer for the `generic` type, which is all JSON-compatible types, but also including Dates, + * undefined, and bigints up to 128 bits. Discord snowflakes have special treatment here as well, to + * reduce the number of serialized bytes from ~19 to 4 per snowflake. + */ +export const generic: BitSerializer = or( + or(or(snowflake, string), or(constant(null), constant(undefined))), + or( + or(or(or(date, s128bi), boolean), or(genericArray, genericObject)), + number, + ), +); + +Object.assign(genericArray, arrayOf(generic)); diff --git a/packages/serialize/src/utils.ts b/packages/serialize/src/utils.ts new file mode 100644 index 0000000..b7cca8e --- /dev/null +++ b/packages/serialize/src/utils.ts @@ -0,0 +1,18 @@ +export function fillArray(length: number, cb: (index: number) => T): T[] { + const arr = new Array(length); + for (let i = 0; i < length; i++) { + arr[i] = cb(i); + } + return arr; +} + +export type Generic = + | string + | number + | boolean + | bigint + | Date + | null + | undefined + | Generic[] + | { [key: string]: Generic }; diff --git a/packages/serialize/tsconfig.json b/packages/serialize/tsconfig.json new file mode 100644 index 0000000..218f228 --- /dev/null +++ b/packages/serialize/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "target": "ESNext", + "lib": ["ESNext"], + "module": "ESNext", + "strict": true, + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true, + "emitDeclarationOnly": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00c00fc..e8854e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: discord-api-types: specifier: ^0.37.37 version: 0.37.37 + serialize: + specifier: workspace:* + version: link:../serialize devDependencies: '@cloudflare/workers-types': specifier: ^3.17.0 @@ -55,6 +58,19 @@ importers: specifier: ^4.8.4 version: 4.8.4 + packages/serialize: + dependencies: + '@paperdave/utils': + specifier: ^1.6.1 + version: 1.6.1 + devDependencies: + esbuild: + specifier: ^0.15.11 + version: 0.15.11 + typescript: + specifier: ^4.8.4 + version: 4.8.4 + packages/website: dependencies: '@astrojs/prefetch': @@ -1297,6 +1313,14 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@paperdave/utils@1.6.1: + resolution: {integrity: sha512-LA9itR/tlnqztz3Scz/fFpQOaQMBrCylEoC5yM2cweUnvxRypCAOkB9TR9EcTI8sx7TlhpzFOFGCzTL5FnWBtQ==} + engines: {bun: '>=0.1.9', node: '>=16'} + dependencies: + utility-types: 3.10.0 + yaml: 2.2.1 + dev: false + /@pkgr/utils@2.3.1: resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -6139,6 +6163,11 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false + /utility-types@3.10.0: + resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} + engines: {node: '>= 4'} + dev: false + /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'}